ソースを参照

上传公开代码

龙腾猫跃 4 年 前
親
コミット
b0b02291cc
100 ファイル変更、20343 行追加、0 行削除
  1. 4 0
      .editorconfig
  2. 47 0
      Plain Craft Launcher 2.sln
  3. 33 0
      Plain Craft Launcher 2/App.config
  4. 542 0
      Plain Craft Launcher 2/Application.xaml
  5. 238 0
      Plain Craft Launcher 2/Application.xaml.vb
  6. 4 0
      Plain Craft Launcher 2/Controls/IMyRadio.vb
  7. 16 0
      Plain Craft Launcher 2/Controls/MyButton.xaml
  8. 196 0
      Plain Craft Launcher 2/Controls/MyButton.xaml.vb
  9. 300 0
      Plain Craft Launcher 2/Controls/MyCard.vb
  10. 14 0
      Plain Craft Launcher 2/Controls/MyCheckBox.xaml
  11. 202 0
      Plain Craft Launcher 2/Controls/MyCheckBox.xaml.vb
  12. 135 0
      Plain Craft Launcher 2/Controls/MyComboBox.vb
  13. 67 0
      Plain Craft Launcher 2/Controls/MyComboBoxItem.vb
  14. 25 0
      Plain Craft Launcher 2/Controls/MyExtraButton.xaml
  15. 191 0
      Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb
  16. 24 0
      Plain Craft Launcher 2/Controls/MyHint.xaml
  17. 82 0
      Plain Craft Launcher 2/Controls/MyHint.xaml.vb
  18. 15 0
      Plain Craft Launcher 2/Controls/MyIconButton.xaml
  19. 168 0
      Plain Craft Launcher 2/Controls/MyIconButton.xaml.vb
  20. 27 0
      Plain Craft Launcher 2/Controls/MyListItem.xaml
  21. 668 0
      Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
  22. 37 0
      Plain Craft Launcher 2/Controls/MyLoading.xaml
  23. 313 0
      Plain Craft Launcher 2/Controls/MyLoading.xaml.vb
  24. 55 0
      Plain Craft Launcher 2/Controls/MyMenuItem.vb
  25. 23 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml
  26. 134 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.vb
  27. 27 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml
  28. 138 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.vb
  29. 27 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml
  30. 125 0
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.vb
  31. 100 0
      Plain Craft Launcher 2/Controls/MyPageLeft.vb
  32. 376 0
      Plain Craft Launcher 2/Controls/MyPageRight.vb
  33. 9 0
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml
  34. 231 0
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb
  35. 9 0
      Plain Craft Launcher 2/Controls/MyRadioButton.xaml
  36. 255 0
      Plain Craft Launcher 2/Controls/MyRadioButton.xaml.vb
  37. 315 0
      Plain Craft Launcher 2/Controls/MyResizer.vb
  38. 51 0
      Plain Craft Launcher 2/Controls/MyScrollBar.vb
  39. 27 0
      Plain Craft Launcher 2/Controls/MyScrollViewer.vb
  40. 15 0
      Plain Craft Launcher 2/Controls/MySearchBox.xaml
  41. 42 0
      Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb
  42. 27 0
      Plain Craft Launcher 2/Controls/MySlider.xaml
  43. 177 0
      Plain Craft Launcher 2/Controls/MySlider.xaml.vb
  44. 19 0
      Plain Craft Launcher 2/Controls/MySliderDot.xaml
  45. 169 0
      Plain Craft Launcher 2/Controls/MySliderDot.xaml.vb
  46. 277 0
      Plain Craft Launcher 2/Controls/MyTextBox.vb
  47. 110 0
      Plain Craft Launcher 2/Controls/MyTextButton.vb
  48. 179 0
      Plain Craft Launcher 2/FormMain.xaml
  49. 1509 0
      Plain Craft Launcher 2/FormMain.xaml.vb
  50. 12 0
      Plain Craft Launcher 2/GlobalSuppressions.vb
  51. BIN
      Plain Craft Launcher 2/Images/Blocks/Anvil.png
  52. BIN
      Plain Craft Launcher 2/Images/Blocks/CobbleStone.png
  53. BIN
      Plain Craft Launcher 2/Images/Blocks/CommandBlock.png
  54. BIN
      Plain Craft Launcher 2/Images/Blocks/Egg.png
  55. BIN
      Plain Craft Launcher 2/Images/Blocks/Fabric.png
  56. BIN
      Plain Craft Launcher 2/Images/Blocks/GoldBlock.png
  57. BIN
      Plain Craft Launcher 2/Images/Blocks/Grass.png
  58. BIN
      Plain Craft Launcher 2/Images/Blocks/GrassPath.png
  59. BIN
      Plain Craft Launcher 2/Images/Blocks/OptiFabric.png
  60. BIN
      Plain Craft Launcher 2/Images/Blocks/RedstoneBlock.png
  61. BIN
      Plain Craft Launcher 2/Images/Blocks/RedstoneLampOff.png
  62. BIN
      Plain Craft Launcher 2/Images/Blocks/RedstoneLampOn.png
  63. BIN
      Plain Craft Launcher 2/Images/Heads/00ll00.png
  64. BIN
      Plain Craft Launcher 2/Images/Heads/DoodleHuang.png
  65. BIN
      Plain Craft Launcher 2/Images/Heads/Hao_Tian.jpg
  66. BIN
      Plain Craft Launcher 2/Images/Heads/LTCat.jpg
  67. BIN
      Plain Craft Launcher 2/Images/Heads/Logo.png
  68. BIN
      Plain Craft Launcher 2/Images/Heads/MCBBS.png
  69. BIN
      Plain Craft Launcher 2/Images/Heads/PCL2.png
  70. BIN
      Plain Craft Launcher 2/Images/Heads/Patrick.png
  71. BIN
      Plain Craft Launcher 2/Images/Heads/bangbang93.png
  72. BIN
      Plain Craft Launcher 2/Images/Heads/noin.png
  73. BIN
      Plain Craft Launcher 2/Images/Heads/wiki.png
  74. BIN
      Plain Craft Launcher 2/Images/Icons/A.png
  75. BIN
      Plain Craft Launcher 2/Images/Icons/B.png
  76. BIN
      Plain Craft Launcher 2/Images/Icons/NoIcon.png
  77. BIN
      Plain Craft Launcher 2/Images/Icons/R.png
  78. BIN
      Plain Craft Launcher 2/Images/Skins/Alex.png
  79. BIN
      Plain Craft Launcher 2/Images/Skins/Steve.png
  80. BIN
      Plain Craft Launcher 2/Images/Themes/10.png
  81. BIN
      Plain Craft Launcher 2/Images/Themes/11.png
  82. BIN
      Plain Craft Launcher 2/Images/Themes/12.png
  83. BIN
      Plain Craft Launcher 2/Images/Themes/13.png
  84. BIN
      Plain Craft Launcher 2/Images/Themes/42.png
  85. BIN
      Plain Craft Launcher 2/Images/Themes/5.png
  86. BIN
      Plain Craft Launcher 2/Images/Themes/6.png
  87. BIN
      Plain Craft Launcher 2/Images/Themes/7.png
  88. BIN
      Plain Craft Launcher 2/Images/Themes/8.png
  89. BIN
      Plain Craft Launcher 2/Images/Themes/9.png
  90. BIN
      Plain Craft Launcher 2/Images/icon.ico
  91. 1003 0
      Plain Craft Launcher 2/Modules/Base/ModAnimation.vb
  92. 109 0
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  93. 700 0
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  94. 2054 0
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  95. 315 0
      Plain Craft Launcher 2/Modules/Base/ModValidate.vb
  96. 158 0
      Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
  97. 887 0
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  98. 1670 0
      Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
  99. 1826 0
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  100. 3835 0
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

+ 4 - 0
.editorconfig

@@ -0,0 +1,4 @@
+[*.{cs,vb}]
+
+# IDE0028: 简化集合初始化
+dotnet_style_collection_initializer = true:silent

+ 47 - 0
Plain Craft Launcher 2.sln

@@ -0,0 +1,47 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29728.190
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Plain Craft Launcher 2", "Plain Craft Launcher 2\Plain Craft Launcher 2.vbproj", "{4499F2D2-18EF-4238-B1F0-7BD547AFF925}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Releaser", "Releaser", "{0FB7B07C-83E1-477E-B05B-C298304748BC}"
+	ProjectSection(SolutionItems) = preProject
+		E:\工具\de4dot\de4dot.exe = E:\工具\de4dot\de4dot.exe
+		E:\软件\dnSpy\dnSpy.exe = E:\软件\dnSpy\dnSpy.exe
+		Releaser\Release.nrproj = Releaser\Release.nrproj
+		Releaser\Releaser.bat = Releaser\Releaser.bat
+		Releaser\Snapshot.nrproj = Releaser\Snapshot.nrproj
+		Releaser\Snapshot.vmp = Releaser\Snapshot.vmp
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Beta|Any CPU = Beta|Any CPU
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+		ReleaseUpdate|Any CPU = ReleaseUpdate|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Beta|Any CPU.ActiveCfg = BETA|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Beta|Any CPU.Build.0 = BETA|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.ReleaseUpdate|Any CPU.ActiveCfg = ReleaseUpdate|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.ReleaseUpdate|Any CPU.Build.0 = ReleaseUpdate|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {838A572E-025C-4A2B-B682-68732EC0FE76}
+	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
+EndGlobal

+ 33 - 0
Plain Craft Launcher 2/App.config

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <system.net>
+    <settings>
+      <httpWebRequest useUnsafeHeaderParsing="true"/>
+    </settings>
+  </system.net>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/>
+    </startup>
+  <runtime>
+	<AppContextSwitchOverrides value="Switch.System.Net.DontEnableSchUseStrongCrypto=false" />
+    <generatePublisherEvidence enabled="false"/>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
+        <bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0"/>
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
+        <bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0"/>
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
+        <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
+      </dependentAssembly>
+      <dependentAssembly>
+        <assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
+        <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+</configuration>

+ 542 - 0
Plain Craft Launcher 2/Application.xaml

@@ -0,0 +1,542 @@
+<Application x:Class="Application"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:local="clr-namespace:PCL"
+    xmlns:sys="clr-namespace:System;assembly=mscorlib"
+    StartupUri="FormMain.xaml"
+    ShutdownMode="OnExplicitShutdown">
+    <Application.Resources>
+        <ResourceDictionary>
+            
+            <!-- 本地化 -->
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceDictionary Source="pack://application:,,,/Resources/Language/zh_CN.xaml" />
+                <ResourceDictionary Source="pack://application:,,,/Resources/Language/zh_CN.xaml" />
+            </ResourceDictionary.MergedDictionaries>
+
+            <!-- 颜色表 -->
+            <SolidColorBrush x:Key="ColorBrush1">#343d4a</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush2">#0b5bcb</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush3">#1370f3</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush4">#4890f5</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush5">#96c0f9</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush6">#d5e6fd</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush7">#e0eafd</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush8">#eaf2fe</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrush9">#80e0eafd</SolidColorBrush>
+            <Color x:Key="ColorObject1">#343d4a</Color>
+            <Color x:Key="ColorObject2">#0b5bcb</Color>
+            <Color x:Key="ColorObject3">#1370f3</Color>
+            <Color x:Key="ColorObject4">#4890f5</Color>
+            <Color x:Key="ColorObject5">#96c0f9</Color>
+            <Color x:Key="ColorObject6">#d5e6fd</Color>
+            <Color x:Key="ColorObject7">#e0eafd</Color>
+            <Color x:Key="ColorObject8">#eaf2fe</Color>
+            <Color x:Key="ColorObject9">#40eaf2fe</Color>
+            <SolidColorBrush x:Key="ColorBrushGray1">#404040</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray2">#737373</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray3">#8c8c8c</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray4">#a6a6a6</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray5">#cccccc</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray6">#ebebeb</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray7">#f0f0f0</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushGray8">#f5f5f5</SolidColorBrush>
+            <Color x:Key="ColorObjectGray1">#404040</Color>
+            <Color x:Key="ColorObjectGray2">#737373</Color>
+            <Color x:Key="ColorObjectGray3">#8c8c8c</Color>
+            <Color x:Key="ColorObjectGray4">#a6a6a6</Color>
+            <Color x:Key="ColorObjectGray5">#cccccc</Color>
+            <Color x:Key="ColorObjectGray6">#ebebeb</Color>
+            <Color x:Key="ColorObjectGray7">#f0f0f0</Color>
+            <Color x:Key="ColorObjectGray8">#f5f5f5</Color>
+            <SolidColorBrush x:Key="ColorBrushRedBack">#80fbdddd</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushRedLight">#ff4c4c</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushRedDark">#ce2111</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushHalfWhite" Color="#44ffffff" />
+            <SolidColorBrush x:Key="ColorBrushTransparent" Color="Transparent" />
+            <SolidColorBrush x:Key="ColorBrushSemiTransparent">#01eaf2fe</SolidColorBrush>
+            <SolidColorBrush x:Key="ColorBrushBackgroundTransparentSidebar" Color="#D2FFFFFF" />
+
+            <!-- Window -->
+            <Style TargetType="{x:Type Window}" x:Key="MyWindow">
+                <Setter Property="OverridesDefaultStyle" Value="True"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="{x:Type Window}">
+                            <ContentPresenter/>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+
+            <!-- Label -->
+            <Style TargetType="Label">
+                <Setter Property="OverridesDefaultStyle" Value="True"/>
+                <Setter Property="Padding" Value="0"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="Label">
+                            <Border Background="{TemplateBinding Background}">
+                                <ContentPresenter 
+                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
+                                    Margin="{TemplateBinding Padding}" 
+                                    RecognizesAccessKey="False" 
+                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
+                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="local:MyTextButton">
+                <Setter Property="OverridesDefaultStyle" Value="True"/>
+                <Setter Property="Padding" Value="0"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="Label">
+                            <Border Background="{TemplateBinding Background}">
+                                <TextBlock 
+                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
+                                    Margin="{TemplateBinding Padding}" 
+                                    Foreground="{TemplateBinding Foreground}"
+                                    Text="{TemplateBinding Content}"
+                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
+                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            
+            <!-- TextBlock -->
+            <Style TargetType="TextBlock">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="FontSize" Value="13" />
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="TextTrimming" Value="CharacterEllipsis" />
+                <Setter Property="FontFamily" Value="Resources/#PCL English, Microsoft YaHei UI" />
+            </Style>
+            <Style TargetType="TextBlock" x:Key="BasedOnTextBlock">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="FontSize" Value="13" />
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="TextTrimming" Value="CharacterEllipsis" />
+                <Setter Property="FontFamily" Value="Resources/#PCL English, Microsoft YaHei UI" />
+            </Style>
+
+            <!-- ToolTip -->
+            <Style TargetType="ToolTip">
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate>
+                            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1.5" CornerRadius="3" MaxWidth="500" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                                <Border.Effect>
+                                    <DropShadowEffect Opacity="0.4" BlurRadius="4" ShadowDepth="2" />
+                                </Border.Effect>
+                                <TextBlock Text="{TemplateBinding ContentControl.Content}" TextWrapping="Wrap" Foreground="{DynamicResource ColorBrush1}" Margin="7,5" FontSize="12"/>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style x:Key="ExtendTextToolTip" TargetType="ToolTip">
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate>
+                            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1" CornerRadius="3" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                                <TextBlock Text="{TemplateBinding ContentControl.Content}" TextWrapping="Wrap" Foreground="{DynamicResource ColorBrush1}" Margin="7,5" FontSize="12"/>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            
+            <!-- ScrollViewer -->
+            <Style x:Key="MyThumb" TargetType="Thumb">
+                <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="Thumb">
+                            <Border Background="{StaticResource ColorBrushSemiTransparent}" SnapsToDevicePixels="True">
+                                <Border Background="{TemplateBinding Foreground}" CornerRadius="3" Margin="2" />
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="local:MyScrollBar">
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Stylus.IsPressAndHoldEnabled" Value="False"/>
+                <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="SnapsToDevicePixels" Value="True" />
+                <Setter Property="UseLayoutRounding" Value="True" />
+                <Setter Property="MinWidth" Value="8"/>
+                <Setter Property="Width" Value="8"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ScrollBar">
+                            <Border CornerRadius="2">
+                                <Track x:Name="PART_Track" IsDirectionReversed="true" IsEnabled="{TemplateBinding IsMouseOver}">
+                                    <Track.Resources>
+                                        <sys:Double x:Key="{x:Static SystemParameters.VerticalScrollBarButtonHeightKey}">
+                                            100
+                                        </sys:Double>
+                                    </Track.Resources>
+                                    <Track.DecreaseRepeatButton>
+                                        <RepeatButton Command="{x:Static ScrollBar.PageUpCommand}" Opacity="0" />
+                                    </Track.DecreaseRepeatButton>
+                                    <Track.IncreaseRepeatButton>
+                                        <RepeatButton Command="{x:Static ScrollBar.PageDownCommand}" Opacity="0" />
+                                    </Track.IncreaseRepeatButton> <!-- 不能删,删了点击 Thumb 以外的部分就不能翻页了 -->
+                                    <Track.Thumb>
+                                        <Thumb Style="{StaticResource MyThumb}" Foreground="{TemplateBinding Foreground}" />
+                                    </Track.Thumb>
+                                </Track>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="local:MyScrollViewer">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="CanContentScroll" Value="False" />
+                <Setter Property="PanningMode" Value="VerticalOnly" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ScrollViewer">
+                            <Grid>
+                                <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" Margin="{TemplateBinding Padding}" Grid.Row="0"/>
+                                <local:MyScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}" Foreground="{TemplateBinding Foreground}" Background="{TemplateBinding Background}" HorizontalAlignment="Right" />
+                            </Grid>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="ScrollViewer" x:Key="ScrollViewerFullMargin">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="CanContentScroll" Value="False" />
+                <Setter Property="PanningMode" Value="VerticalOnly" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ScrollViewer">
+                            <Grid SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Margin="{TemplateBinding Padding}">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="*"/>
+                                    <ColumnDefinition Width="Auto"/>
+                                </Grid.ColumnDefinitions>
+                                <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}"/>
+                                <local:MyScrollBar Margin="2,1,1,1" x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}" Foreground="{TemplateBinding Foreground}" Background="{TemplateBinding Background}" />
+                            </Grid>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="ScrollViewer" x:Key="ScrollViewerNoMargin">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="CanContentScroll" Value="False" />
+                <Setter Property="PanningMode" Value="VerticalOnly" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ScrollViewer">
+                            <Grid SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Margin="{TemplateBinding Padding}" Cursor="Arrow">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="*"/>
+                                    <ColumnDefinition Width="Auto"/>
+                                </Grid.ColumnDefinitions>
+                                <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Cursor="{TemplateBinding Cursor}" />
+                                <local:MyScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}" Foreground="{TemplateBinding Foreground}" Background="{TemplateBinding Background}" />
+                            </Grid>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+
+            <!-- TextBox -->
+            <Style TargetType="local:MyTextBox">
+                <Setter Property="BorderThickness" Value="1" />
+                <Setter Property="Background" Value="{StaticResource ColorBrushHalfWhite}" />
+                <Setter Property="FontSize" Value="13" />
+                <Setter Property="SelectionBrush" Value="{DynamicResource ColorBrush3}" />
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="FontFamily" Value="Resources/#PCL English, Microsoft YaHei UI" />
+                <Setter Property="VerticalContentAlignment" Value="Center" />
+                <Setter Property="SnapsToDevicePixels" Value="True" />
+                <Setter Property="IsUndoEnabled" Value="True" />
+                <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
+                <Setter Property="HorizontalContentAlignment" Value="Left"/>
+                <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
+                <Setter Property="AllowDrop" Value="True"/>
+                <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
+                <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
+                <Setter Property="UndoLimit" Value="15" />
+                <Setter Property="MaxLength" Value="10000" />
+                <Setter Property="BorderBrush" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="TextBox">
+                            <Grid SnapsToDevicePixels="True" ToolTip="{TemplateBinding ToolTip}">
+                                <Grid.RowDefinitions>
+                                    <RowDefinition MinHeight="28" />
+                                    <RowDefinition Height="Auto" />
+                                </Grid.RowDefinitions>
+                                <Border Margin="{TemplateBinding Padding}" IsHitTestVisible="False" VerticalAlignment="Center" BorderThickness="0">
+                                    <TextBlock x:Name="labHint" Foreground="{StaticResource ColorBrushGray3}" Margin="7,0,0,0" Padding="{TemplateBinding BorderThickness}" FontSize="{TemplateBinding FontSize}" FontFamily="Resources/#PCL English, Microsoft YaHei UI" />
+                                </Border>
+                                <Border x:Name="border" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" CornerRadius="3">
+                                    <ScrollViewer x:Name="PART_ContentHost" Style="{StaticResource ScrollViewerNoMargin}" RenderOptions.ClearTypeHint="Enabled" BorderThickness="0"
+                                           Margin="5,0,0,0" Cursor="IBeam" />
+                                    <!-- 由于未知原因,TextView 会有 2,0 的 Margin -->
+                                </Border>
+                                <TextBlock Grid.Row="1" x:Name="labWrong" Height="0" Foreground="{TemplateBinding BorderBrush}" Padding="0,4,0,0" />
+                            </Grid>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="PasswordBox">
+                <Setter Property="FontSize" Value="13" />
+                <Setter Property="VerticalContentAlignment" Value="Top" />
+                <Setter Property="SelectionBrush" Value="{DynamicResource ColorBrush3}" />
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="Background" Value="{StaticResource ColorBrushHalfWhite}" />
+                <Setter Property="BorderBrush" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
+                <Setter Property="HorizontalContentAlignment" Value="Left"/>
+                <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
+                <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="PasswordBox">
+                            <Border x:Name="border" BorderThickness="1" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Padding="0" SnapsToDevicePixels="True" CornerRadius="3">
+                                <local:MyScrollViewer x:Name="PART_ContentHost" Margin="4.5,4.5,0,4.5" HorizontalScrollBarVisibility="Hidden" RenderOptions.ClearTypeHint="Enabled" SnapsToDevicePixels="True"/>
+                            </Border>
+                            <ControlTemplate.Triggers>
+                                <Trigger Property="IsEnabled" Value="False">
+                                    <Trigger.Setters>
+                                        <Setter Property="Foreground" Value="{StaticResource ColorBrushGray4}" />
+                                        <Setter Property="BorderBrush" Value="{StaticResource ColorBrushGray4}" />
+                                    </Trigger.Setters>
+                                </Trigger>
+                                <Trigger Property="IsMouseOver" Value="True">
+                                    <Trigger.Setters>
+                                        <Setter Property="BorderBrush" Value="{DynamicResource ColorBrush3}" />
+                                    </Trigger.Setters>
+                                </Trigger>
+                                <Trigger Property="IsFocused" Value="True">
+                                    <Trigger.Setters>
+                                        <Setter Property="Background" Value="{DynamicResource ColorBrush9}" />
+                                        <Setter Property="BorderBrush" Value="{DynamicResource ColorBrush4}" />
+                                    </Trigger.Setters>
+                                </Trigger>
+                            </ControlTemplate.Triggers>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+
+            <!-- ComboBox -->
+            <DataTemplate x:Key="ComboBoxItemTemplateWithDelete">
+                <local:MyListItem Title="{Binding}" FontSize="13" PaddingRight="40" MaxHeight="26" IsScaleAnimationEnabled="False" Margin="-1,0,0,0">
+                    <local:MyListItem.Buttons>
+                        <x:Array Type="local:MyIconButton">
+                            <local:MyIconButton Grid.Column="1" Height="20" Width="20" VerticalAlignment="Center" Tag="{Binding}" Click="MyIconButton_Click" Logo="F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z" />
+                        </x:Array>
+                    </local:MyListItem.Buttons>
+                </local:MyListItem>
+            </DataTemplate>
+            <!--<DataTemplate x:Key="ComboBoxItemTemplateNormal">
+                <local:MyListItem Title="{Binding}" FontSize="13" PaddingRight="40" Height="26" Margin="-1,0,0,0" />
+            </DataTemplate>-->
+            <Style TargetType="local:MyComboBox">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="SnapsToDevicePixels" Value="True" />
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="Background" Value="{StaticResource ColorBrushHalfWhite}" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <!--<Setter Property="ItemTemplate" Value="{StaticResource ComboBoxItemTemplateNormal}"/>-->
+                <Setter Property="FontSize" Value="13" />
+                <Setter Property="MaxDropDownHeight" Value="160" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ComboBox">
+                            <Grid>
+                                <Border x:Name="border" Background="{TemplateBinding Background}" CornerRadius="3" BorderThickness="1" BorderBrush="{TemplateBinding Foreground}" />
+                                <ToggleButton Margin="0,0,1.5,0" Foreground="{TemplateBinding Foreground}" IsTabStop="False" SnapsToDevicePixels="False" Focusable="False" IsChecked="{Binding IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" />
+                                <ContentPresenter Margin="8.5,0,21.5,0" IsHitTestVisible="False" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" VerticalAlignment="Center" />
+                                <Popup x:Name="PART_Popup" SnapsToDevicePixels="True" VerticalOffset="-1.5" AllowsTransparency="True" IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Placement="Bottom" PopupAnimation="Fade">
+                                    <Grid x:Name="PanPopup">
+                                        <Border Background="White" CornerRadius="3" />
+                                        <Border x:Name="dropDownBorder" CornerRadius="3" BorderBrush="{TemplateBinding Foreground}" BorderThickness="1" Width="{TemplateBinding Width}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
+                                            <local:MyScrollViewer x:Name="DropDownScrollViewer" PanningMode="VerticalOnly" SnapsToDevicePixels="False" Style="{StaticResource ScrollViewerFullMargin}" ScrollViewer.CanContentScroll="False" VerticalScrollBarVisibility="Auto">
+                                                <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" />
+                                            </local:MyScrollViewer>
+                                        </Border>
+                                    </Grid>
+                                </Popup>
+                            </Grid>
+                            <ControlTemplate.Triggers>
+                                <Trigger Property="HasItems" Value="false">
+                                    <Setter Property="Height" TargetName="dropDownBorder" Value="26"/>
+                                </Trigger>
+                            </ControlTemplate.Triggers>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+                <Style.Triggers>
+                    <Trigger Property="IsEditable" Value="True">
+                        <Setter Property="IsTabStop" Value="False"/>
+                        <Setter Property="ItemTemplate" Value="{StaticResource ComboBoxItemTemplateWithDelete}"/>
+                        <Setter Property="Template">
+                            <Setter.Value>
+                                <ControlTemplate TargetType="ComboBox">
+                                    <Grid>
+                                        <Border x:Name="border" Background="{TemplateBinding Background}" CornerRadius="3" BorderThickness="1" BorderBrush="{TemplateBinding Foreground}" />
+                                        <ToggleButton Margin="0,0,1.5,0" Foreground="{TemplateBinding Foreground}" IsTabStop="False" SnapsToDevicePixels="False" Focusable="False" IsChecked="{Binding IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" />
+                                        <local:MyTextBox x:Name="PART_EditableTextBox" BorderThickness="0" Margin="1.5,0,21.5,0" Padding="0" Background="{x:Null}" />
+                                        <Popup x:Name="PART_Popup" SnapsToDevicePixels="True" VerticalOffset="-1.5" AllowsTransparency="True" IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Placement="Bottom" PopupAnimation="Fade">
+                                            <Grid x:Name="PanPopup">
+                                                <Border Background="White" CornerRadius="3" />
+                                                <Border x:Name="dropDownBorder" SnapsToDevicePixels="False" CornerRadius="3" BorderBrush="{TemplateBinding Foreground}" BorderThickness="1" Width="{TemplateBinding Width}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
+                                                    <local:MyScrollViewer x:Name="DropDownScrollViewer" Style="{StaticResource ScrollViewerFullMargin}" ScrollViewer.CanContentScroll="False" VerticalScrollBarVisibility="Auto">
+                                                        <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" />
+                                                    </local:MyScrollViewer>
+                                                </Border>
+                                            </Grid>
+                                        </Popup>
+                                    </Grid>
+                                    <ControlTemplate.Triggers>
+                                        <Trigger Property="HasItems" Value="false">
+                                            <Setter Property="Height" TargetName="dropDownBorder" Value="26"/>
+                                        </Trigger>
+                                    </ControlTemplate.Triggers>
+                                </ControlTemplate>
+                            </Setter.Value>
+                        </Setter>
+                    </Trigger>
+                </Style.Triggers>
+            </Style>
+            <Style TargetType="ToggleButton">
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ToggleButton">
+                            <Border Background="{StaticResource ColorBrushSemiTransparent}">
+                                <Path x:Name="path" Width="15" RenderTransformOrigin="0.3,0.5" HorizontalAlignment="Right" Data="M1,1L4.5,4.5L8,1" StrokeThickness="1.5" Stroke="{TemplateBinding Foreground}" VerticalAlignment="Center">
+                                    <Path.RenderTransform>
+                                        <RotateTransform />
+                                    </Path.RenderTransform>
+                                </Path>
+                            </Border>
+                            <ControlTemplate.Triggers>
+                                <Trigger Property="IsChecked" Value="True">
+                                    <Trigger.EnterActions>
+                                        <BeginStoryboard>
+                                            <Storyboard Storyboard.TargetName="path" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)">
+                                                <DoubleAnimation To="180" Duration="0:0:0.2">
+                                                    <DoubleAnimation.EasingFunction>
+                                                        <CircleEase EasingMode="EaseOut" />
+                                                    </DoubleAnimation.EasingFunction>
+                                                </DoubleAnimation>
+                                            </Storyboard>
+                                        </BeginStoryboard>
+                                    </Trigger.EnterActions>
+                                    <Trigger.ExitActions>
+                                        <BeginStoryboard>
+                                            <Storyboard  Storyboard.TargetName="path" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)">
+                                                <DoubleAnimation To="0" Duration="0:0:0.2">
+                                                    <DoubleAnimation.EasingFunction>
+                                                        <CircleEase EasingMode="EaseOut" />
+                                                    </DoubleAnimation.EasingFunction>
+                                                </DoubleAnimation>
+                                            </Storyboard>
+                                        </BeginStoryboard>
+                                    </Trigger.ExitActions>
+                                </Trigger>
+                            </ControlTemplate.Triggers>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="ComboBoxItem" x:Key="MyComboBoxItem">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Background" Value="{StaticResource ColorBrushTransparent}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ComboBoxItem">
+                            <Border x:Name="ItemBorder" Padding="6,4" SnapsToDevicePixels="True" Background="{TemplateBinding Background}">
+                                <ContentPresenter />
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <Style TargetType="ComboBoxItem">
+                <Setter Property="OverridesDefaultStyle" Value="True" />
+                <Setter Property="FocusVisualStyle" Value="{x:Null}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ComboBoxItem">
+                            <Border x:Name="ItemBorder" SnapsToDevicePixels="True" Background="{TemplateBinding Background}">
+                                <ContentPresenter />
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+
+            <!-- ContextMenu -->
+            <Style TargetType="ContextMenu">
+                <Setter Property="SnapsToDevicePixels" Value="True"/>
+                <Setter Property="OverridesDefaultStyle" Value="True"/>
+                <Setter Property="Grid.IsSharedSizeScope" Value="True"/>
+                <Setter Property="Focusable" Value="False"/>
+                <Setter Property="HasDropShadow" Value="False"/>
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="ContextMenu">
+                            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1" CornerRadius="3" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                                <Border.Effect>
+                                    <DropShadowEffect Opacity="0.4" BlurRadius="4" ShadowDepth="2" />
+                                </Border.Effect>
+                                <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" Margin="0.5"/>
+                            </Border>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            <!-- MenuItem -->
+            <Style TargetType="local:MyMenuItem">
+                <Setter Property="Foreground" Value="{DynamicResource ColorBrush1}" />
+                <Setter Property="Template">
+                    <Setter.Value>
+                        <ControlTemplate TargetType="MenuItem">
+                            <Border Name="PanBack" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True" Height="26">
+                                <Grid SnapsToDevicePixels="False" UseLayoutRounding="False">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="Auto" SharedSizeGroup="Icon"/>
+                                        <ColumnDefinition />
+                                        <!--  <ColumnDefinition Width="Auto" SharedSizeGroup="Shortcut"/>
+                                    <ColumnDefinition Width="13"/>  -->
+                                    </Grid.ColumnDefinitions>
+                                    <Path Name="Icon" VerticalAlignment="Center" Stretch="Uniform" Margin="8,0" Fill="{TemplateBinding Foreground}" Height="12" Width="15" />
+                                    <TextBlock Name="HeaderHost" Grid.Column="1" Text="{TemplateBinding Header}" Foreground="{TemplateBinding Foreground}" Margin="0,0,40,0" VerticalAlignment="Center" Height="16" />
+                                    <!--  <TextBlock x:Name="InputGestureText" Grid.Column="2" Text="{TemplateBinding InputGestureText}" Margin="5,2,0,2" DockPanel.Dock="Right" /> -->
+                                </Grid>
+                            </Border>
+                            <ControlTemplate.Triggers>
+                                <Trigger Property="Icon" Value="{x:Null}">
+                                    <Setter TargetName="Icon" Property="Visibility" Value="Hidden"/>
+                                </Trigger>
+                            </ControlTemplate.Triggers>
+                        </ControlTemplate>
+                    </Setter.Value>
+                </Setter>
+            </Style>
+            
+        </ResourceDictionary>
+    </Application.Resources>
+</Application>

+ 238 - 0
Plain Craft Launcher 2/Application.xaml.vb

@@ -0,0 +1,238 @@
+Imports System.Net
+Imports System.Reflection
+Imports System.Windows.Threading
+
+Public Class Application
+
+#If DEBUG Then
+    ''' <summary>
+    ''' 用于开始程序时的一些测试。
+    ''' </summary>
+    Private Sub Test()
+        Try
+            ModDevelop.Start()
+        Catch ex As Exception
+            Log(ex, "开发者模式测试出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End If
+
+    '开始
+    Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
+        ApplicationStartTick = GetTimeTick()
+        SecretOnApplicationStart()
+        Try
+            '检查参数调用
+            If e.Args.Length > 0 Then
+                If e.Args(0) = "--update" Then
+                    '自动更新
+                    UpdateReplace(e.Args(1), e.Args(2).Trim(""""), e.Args(3).Trim(""""), e.Args(4))
+                    Environment.[Exit](Result.Cancel)
+                    Exit Sub
+                ElseIf e.Args(0) = "--link" Then
+                    '稍作等待后切换到联机页面
+                    Thread.Sleep(1000)
+                    FormMain.IsLinkRestart = True
+#If DEBUG Then
+                ElseIf e.Args(0) = "--make" Then
+                    '制作更新包
+                    UpdateMake(e.Args(1))
+                    Environment.[Exit](Result.Cancel)
+                    Exit Sub
+#End If
+                End If
+            End If
+            '初始化文件结构
+            Directory.CreateDirectory(Path & "PCL\Pictures")
+            Directory.CreateDirectory(Path & "PCL\Musics")
+            Try
+                Directory.CreateDirectory(PathTemp)
+                If Not CheckPermission(PathTemp) Then Throw New Exception("PCL2 没有对 " & PathTemp & " 的访问权限")
+            Catch ex As Exception
+                MyMsgBox("手动设置的缓存文件夹不可用,PCL2 将使用默认缓存文件夹。" & vbCrLf & "错误原因:" & GetString(ex, False), "缓存文件夹不可用")
+                Setup.Set("SystemSystemCache", "")
+                PathTemp = IO.Path.GetTempPath() & "PCL\"
+            End Try
+            Directory.CreateDirectory(PathTemp & "Cache")
+            Directory.CreateDirectory(PathTemp & "Download")
+            '检测单例
+#If Not DEBUG Then
+            Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher 2 ")
+            If WindowHwnd <> IntPtr.Zero Then
+                '将已有的 PCL2 窗口拖出来
+                ShowWindowToTop(WindowHwnd)
+                '播放提示音并退出
+                Beep()
+                Environment.[Exit](Result.Cancel)
+                Exit Sub
+            End If
+#End If
+            '设置初始窗口
+            If Setup.Get("UiLauncherLogo") AndAlso Not FormMain.IsLinkRestart Then
+                FrmStart = New SplashScreen("Images\icon.ico")
+                FrmStart.Show(False, True)
+            End If
+            '动态 DLL 调用
+            AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf AssemblyResolve
+            '日志初始化
+            LogStart()
+            '添加日志
+            Log("[Start] 程序版本:" & VersionDisplayName & "(" & VersionCode & ")")
+            Log("[Start] 识别码:" & UniqueAddress & If(ThemeCheckOne(9), ",已解锁反馈主题", ""))
+            Log("[Start] 程序路径:" & PathWithName)
+            '检测压缩包运行
+            If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
+                MyMsgBox("PCL2 正在临时文件夹运行,设置、游戏存档等很可能无法保存,且部分功能会无法使用或出错。" & vbCrLf & "请将 PCL2 从压缩文件中解压,或是更换文件夹后再继续使用!", "环境警告", "我知道了", IsWarn:=True)
+            End If
+            '设置初始化
+            Setup.Load("SystemDebugMode")
+            Setup.Load("SystemDebugAnim")
+            Setup.Load("ToolDownloadThread")
+            '网络配置初始化
+            ServicePointManager.Expect100Continue = True
+            ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 Or SecurityProtocolType.Tls Or SecurityProtocolType.Tls11 Or SecurityProtocolType.Tls12
+            ServicePointManager.DefaultConnectionLimit = 1024
+            ServicePointManager.ServerCertificateValidationCallback = New Security.RemoteCertificateValidationCallback(Function() As Boolean
+                                                                                                                           Return True
+                                                                                                                       End Function)
+            '计时
+            Log("[Start] 第一阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")
+            ApplicationStartTick = GetTimeTick()
+            '执行测试
+#If DEBUG Then
+            Test()
+#End If
+            AniControlEnabled += 1
+        Catch ex As Exception
+            MsgBox(GetString(ex, False, True), MsgBoxStyle.Critical, "PCL2 初始化错误")
+            FormMain.EndProgramForce(Result.Exception)
+        End Try
+    End Sub
+
+    '结束
+    Private Sub Application_SessionEnding(sender As Object, e As SessionEndingCancelEventArgs) Handles Me.SessionEnding
+        FrmMain.EndProgram(False)
+    End Sub
+
+    '异常
+    Private IsCritErrored As Boolean = False
+    Private Sub Application_DispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs) Handles Me.DispatcherUnhandledException
+        On Error Resume Next
+        e.Handled = True
+        If IsProgramEnded Then Exit Sub
+        If IsCritErrored Then
+            '在汇报错误后继续引发错误,知道这次压不住了
+            FormMain.EndProgramForce(Result.Exception)
+            Exit Sub
+        End If
+        IsCritErrored = True
+        Dim ExceptionString As String = GetString(e.Exception, False, True)
+        If ExceptionString.Contains("System.Windows.Threading.Dispatcher.Invoke") OrElse
+           ExceptionString.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") OrElse
+           ExceptionString.Contains(".Net Framework") OrElse ' “自动错误判断” 的结果分析
+           ExceptionString.Contains("未能加载文件或程序集") Then
+            OpenWebsite("https://dotnet.microsoft.com/zh-cn/download/dotnet-framework/thank-you/net462-offline-installer")
+            MsgBox("你的 .Net Framework 版本过低或损坏,请在打开的网页中重新下载并安装 .NET Framework 4.6.2 后重试!", MsgBoxStyle.Information, "运行环境错误")
+            FormMain.EndProgramForce(Result.Cancel)
+        Else
+            FeedbackInfo()
+            Log(e.Exception, "程序出现未知错误", LogLevel.Assert, "锟斤拷烫烫烫")
+        End If
+    End Sub
+
+    '动态 DLL 调用
+    Private Shared AssemblyStun As Assembly
+    Private Shared AssemblyNAudio As Assembly
+    Private Shared AssemblyJson As Assembly
+    Private Shared AssemblyDialog As Assembly
+    Private Shared ReadOnly AssemblyStunLock As New Object
+    Private Shared ReadOnly AssemblyNAudioLock As New Object
+    Private Shared ReadOnly AssemblyJsonLock As New Object
+    Private Shared ReadOnly AssemblyDialogLock As New Object
+    Public Shared Function AssemblyResolve(sender As Object, args As ResolveEventArgs) As Assembly
+        If args.Name.StartsWith("NAudio") Then
+            SyncLock AssemblyNAudioLock
+                If AssemblyNAudio Is Nothing Then
+                    Log("[Start] 加载 DLL:NAudio")
+                    AssemblyNAudio = Assembly.Load(GetResources("NAudio"))
+                End If
+                Return AssemblyNAudio
+            End SyncLock
+        ElseIf args.Name.StartsWith("Newtonsoft.Json") Then
+            SyncLock AssemblyJsonLock
+                If AssemblyJson Is Nothing Then
+                    Log("[Start] 加载 DLL:Json")
+                    AssemblyJson = Assembly.Load(GetResources("Json"))
+                End If
+                Return AssemblyJson
+            End SyncLock
+        ElseIf args.Name.StartsWith("Ookii.Dialogs.Wpf") Then
+            SyncLock AssemblyDialogLock
+                If AssemblyDialog Is Nothing Then
+                    Log("[Start] 加载 DLL:Dialogs")
+                    AssemblyDialog = Assembly.Load(GetResources("Dialogs"))
+                End If
+                Return AssemblyDialog
+            End SyncLock
+        ElseIf args.Name.StartsWith("STUN") Then
+            SyncLock AssemblyStunLock
+                If AssemblyStun Is Nothing Then
+                    Log("[Start] 加载 DLL:STUN")
+                    AssemblyStun = Assembly.Load(GetResources("STUN"))
+                End If
+                Return AssemblyStun
+            End SyncLock
+        Else
+            Return Nothing
+        End If
+    End Function
+
+    '切换窗口
+
+
+    '控件模板事件
+    Private Sub MyIconButton_Click(sender As Object, e As EventArgs)
+        If Setup.Get("LoginType") = McLoginType.Legacy Then
+            '离线
+            Dim Names As New List(Of String)
+            Names.AddRange(Setup.Get("LoginLegacyName").ToString.Split("¨"))
+            Names.Remove(sender.Tag)
+            Setup.Set("LoginLegacyName", Join(Names, "¨"))
+            FrmLoginLegacy.ComboName.ItemsSource = Names
+            FrmLoginLegacy.ComboName.Text = If(Names.Count > 0, Names(0), "")
+        Else
+            '非离线
+            Dim Token As String = GetStringFromEnum(Setup.Get("LoginType"))
+            Dim Dict As New Dictionary(Of String, String)
+            Dim Names As New List(Of String)
+            Dim Passs As New List(Of String)
+            If Not Setup.Get("Login" & Token & "Email") = "" Then Names.AddRange(Setup.Get("Login" & Token & "Email").ToString.Split("¨"))
+            If Not Setup.Get("Login" & Token & "Pass") = "" Then Passs.AddRange(Setup.Get("Login" & Token & "Pass").ToString.Split("¨"))
+            For i = 0 To Names.Count - 1
+                Dict.Add(Names(i), Passs(i))
+            Next
+            Dict.Remove(sender.Tag)
+            Setup.Set("Login" & Token & "Email", Join(Dict.Keys.ToArray, "¨"))
+            Setup.Set("Login" & Token & "Pass", Join(Dict.Values.ToArray, "¨"))
+            Select Case Token
+                Case "Mojang"
+                    FrmLoginMojang.ComboName.ItemsSource = Dict.Keys
+                    FrmLoginMojang.ComboName.Text = If(Dict.Keys.Count > 0, Dict.Keys(0), "")
+                    FrmLoginMojang.TextPass.Password = If(Dict.Values.Count > 0, Dict.Values(0), "")
+                Case "Nide"
+                    FrmLoginNide.ComboName.ItemsSource = Dict.Keys
+                    FrmLoginNide.ComboName.Text = If(Dict.Keys.Count > 0, Dict.Keys(0), "")
+                    FrmLoginNide.TextPass.Password = If(Dict.Values.Count > 0, Dict.Values(0), "")
+                Case "Auth"
+                    FrmLoginAuth.ComboName.ItemsSource = Dict.Keys
+                    FrmLoginAuth.ComboName.Text = If(Dict.Keys.Count > 0, Dict.Keys(0), "")
+                    FrmLoginAuth.TextPass.Password = If(Dict.Values.Count > 0, Dict.Values(0), "")
+                Case Else
+                    DebugAssert(True)
+                    Exit Sub
+            End Select
+        End If
+    End Sub
+
+End Class

+ 4 - 0
Plain Craft Launcher 2/Controls/IMyRadio.vb

@@ -0,0 +1,4 @@
+Public Interface IMyRadio
+    Event Check(sender As Object, e As RouteEventArgs)
+    Event Changed(sender As Object, e As RouteEventArgs)
+End Interface

+ 16 - 0
Plain Craft Launcher 2/Controls/MyButton.xaml

@@ -0,0 +1,16 @@
+<Border x:Class="MyButton"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             FocusVisualStyle="{x:Null}" x:Name="PanBack"
+             Focusable="True" UseLayoutRounding="False" SnapsToDevicePixels="False" Background="{StaticResource ColorBrushSemiTransparent}">
+    <Border CornerRadius="3" x:Name="PanFore" BorderThickness="1" BorderBrush="{DynamicResource ColorBrush1}" RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushHalfWhite}">
+        <Border.RenderTransform>
+            <ScaleTransform />
+        </Border.RenderTransform>
+        <TextBlock TextOptions.TextFormattingMode="Display" SnapsToDevicePixels="True" TextTrimming="None" Text="Button" HorizontalAlignment="Center" x:Name="LabText" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5" Foreground="{Binding Parent.BorderBrush, RelativeSource={RelativeSource Self}}" FontSize="13" IsHitTestVisible="False">
+            <TextBlock.RenderTransform>
+                <ScaleTransform />
+            </TextBlock.RenderTransform>
+        </TextBlock>
+    </Border>
+</Border>

+ 196 - 0
Plain Craft Launcher 2/Controls/MyButton.xaml.vb

@@ -0,0 +1,196 @@
+Public Class MyButton
+
+    '声明
+    Public Event Click(sender As Object, e As EventArgs) '自定义事件
+
+    '自定义属性
+    Public Uuid As Integer = GetUuid()
+    Public Property Text As String
+        Get
+            Return LabText.Text
+        End Get
+        Set(value As String)
+            LabText.Text = value
+        End Set
+    End Property '显示文本
+    Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyButton), New PropertyMetadata(New PropertyChangedCallback(
+                                                                                                                                                               Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+                                                                                                                                                                   If Not IsNothing(sender) Then CType(sender, MyButton).LabText.Text = e.NewValue
+                                                                                                                                                               End Sub)))
+    Public Property TextPadding As Thickness
+        Get
+            Return LabText.Padding
+        End Get
+        Set(value As Thickness)
+            LabText.Padding = value
+        End Set
+    End Property
+    Private _ColorType As ColorState = ColorState.Normal  '配色方案
+    Public Property ColorType As ColorState
+        Get
+            Return _ColorType
+        End Get
+        Set(value As ColorState)
+            _ColorType = value
+            RefreshColor()
+        End Set
+    End Property
+    Public Enum ColorState
+        Normal = 0
+        Highlight = 1
+        Red = 2
+    End Enum
+    '属性穿透
+    Public Shared Shadows ReadOnly PaddingProperty As DependencyProperty = DependencyProperty.Register("Padding", GetType(Thickness), GetType(MyButton), New PropertyMetadata(New PropertyChangedCallback(
+        Sub(sender As MyButton, e As DependencyPropertyChangedEventArgs) If sender IsNot Nothing Then sender.PanFore.Padding = e.NewValue)))
+    Public Overloads Property Padding As Thickness
+        Get
+            Return PanFore.Padding
+        End Get
+        Set(value As Thickness)
+            PanFore.Padding = value
+        End Set
+    End Property
+    Public Property RealRenderTransform As Transform
+        Get
+            Return PanFore.RenderTransform
+        End Get
+        Set(value As Transform)
+            PanFore.RenderTransform = value
+        End Set
+    End Property
+
+    '自定义事件
+    Private Const AnimationColorIn As Integer = 100
+    Private Const AnimationColorOut As Integer = 200
+    Private Sub RefreshColor(Optional obj = Nothing, Optional e = Nothing) Handles Me.MouseEnter, Me.MouseLeave, Me.Loaded, Me.IsEnabledChanged
+        Try
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+
+                If IsEnabled Then
+                    Select Case ColorType
+                        Case ColorState.Normal
+                            If IsMouseOver Then
+                                '指向(Main 3)
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrush3", AnimationColorIn)}, "MyButton Color " & Uuid)
+                            Else
+                                '普通(Main 1)
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrush1", AnimationColorOut)}, "MyButton Color " & Uuid)
+                            End If
+                        Case ColorState.Highlight
+                            If IsMouseOver Then
+                                '指向(Main 3)
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrush3", AnimationColorIn)}, "MyButton Color " & Uuid)
+                            Else
+                                '高亮(Main 2)
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrush2", AnimationColorOut)}, "MyButton Color " & Uuid)
+                            End If
+                        Case ColorState.Red
+                            If IsMouseOver Then
+                                '红色指向
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrushRedLight", AnimationColorIn)}, "MyButton Color " & Uuid)
+                            Else
+                                '红色
+                                AniStart({AaColor(PanFore, Border.BorderBrushProperty, "ColorBrushRedDark", AnimationColorOut)}, "MyButton Color " & Uuid)
+                            End If
+                    End Select
+                Else
+                    '不可用(Gray 4)
+                    AniStart({AaColor(PanFore, Border.BorderBrushProperty, ColorGray4 - PanFore.BorderBrush, AnimationColorOut)}, "MyButton Color " & Uuid)
+                End If
+            Else
+
+                AniStop("MyButton Color " & Uuid)
+                If IsEnabled Then
+                    Select Case ColorType
+                        Case ColorState.Normal
+                            If IsMouseOver Then
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrush3")
+                            Else
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrush1")
+                            End If
+                        Case ColorState.Highlight
+                            If IsMouseOver Then
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrush3")
+                            Else
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrush2")
+                            End If
+                        Case ColorState.Red
+                            If IsMouseOver Then
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrushRedLight")
+                            Else
+                                PanFore.SetResourceReference(Border.BorderBrushProperty, "ColorBrushRedDark")
+                            End If
+                    End Select
+                Else
+                    PanFore.BorderBrush = ColorGray4
+                End If
+
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新按钮颜色出错")
+        End Try
+    End Sub
+
+    '实现自定义事件
+    Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
+        If IsMouseDown Then
+            Log("[Control] 按下按钮:" & Text)
+            RaiseEvent Click(sender, e)
+            If Not String.IsNullOrEmpty(Tag) Then
+                If Tag.ToString.StartsWith("链接-") OrElse Tag.ToString.StartsWith("启动-") Then
+                    Hint("主页自定义按钮语法已更新,且不再兼容老版本语法,请查看新的自定义示例!")
+                End If
+            End If
+            ModEvent.TryStartEvent(EventType, EventData)
+        End If
+    End Sub
+    Public Property EventType As String
+        Get
+            Return GetValue(EventTypeProperty)
+        End Get
+
+        Set(ByVal value As String)
+            SetValue(EventTypeProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyButton), New PropertyMetadata(Nothing))
+    Public Property EventData As String
+        Get
+            Return GetValue(EventDataProperty)
+        End Get
+
+        Set(ByVal value As String)
+            SetValue(EventDataProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyButton), New PropertyMetadata(Nothing))
+
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsMouseDown As Boolean = False
+    Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
+        IsMouseDown = True
+        Focus()
+        AniStart({
+                 AaScaleTransform(PanFore, 0.955 - CType(PanFore.RenderTransform, ScaleTransform).ScaleX, 80,, New AniEaseOutFluent(AniEasePower.ExtraStrong)),
+                 AaScaleTransform(PanFore, -0.01, 700,, New AniEaseOutFluent(AniEasePower.Middle))
+                 }, "MyButton Scale " & Uuid)
+    End Sub
+    Private Sub Button_MouseEnter() Handles Me.MouseEnter
+        AniStart(AaColor(PanFore, BackgroundProperty, If(_ColorType = ColorState.Red, "ColorBrushRedBack", "ColorBrush9"), AnimationColorIn), "MyButton Background " & Uuid)
+    End Sub
+    Private Sub Button_MouseUp() Handles Me.MouseLeftButtonUp
+        If Not IsMouseDown Then Exit Sub
+        IsMouseDown = False
+        AniStart({
+               AaScaleTransform(PanFore, 1 - CType(PanFore.RenderTransform, ScaleTransform).ScaleX, 300, 10, New AniEaseOutFluent(AniEasePower.Middle))
+           }, "MyButton Scale " & Uuid)
+    End Sub
+    Private Sub Button_MouseLeave() Handles Me.MouseLeave
+        AniStart(AaColor(PanFore, BackgroundProperty, "ColorBrushHalfWhite", AnimationColorOut), "MyButton Background " & Uuid)
+        If Not IsMouseDown Then Exit Sub
+        IsMouseDown = False
+        AniStart(AaScaleTransform(PanFore, 1 - CType(PanFore.RenderTransform, ScaleTransform).ScaleX, 800,, New AniEaseOutFluent(AniEasePower.Strong)), "MyButton Scale " & Uuid)
+    End Sub
+
+End Class

+ 300 - 0
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -0,0 +1,300 @@
+Public Class MyCard
+
+    '控件
+    Inherits Grid
+    Private ReadOnly MainGrid As Grid
+    Private ReadOnly MainChrome As SystemDropShadowChrome
+    Private ReadOnly MainBorder As Border
+    Public MainTextBlock As TextBlock
+    Public MainSwap As Shapes.Path
+
+    '属性
+    Public Uuid As Integer = GetUuid()
+    Public Property Title As String
+        Get
+            Return GetValue(TitleProperty)
+        End Get
+        Set(value As String)
+            SetValue(TitleProperty, value)
+            If MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
+        End Set
+    End Property
+    Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyCard), New PropertyMetadata(""))
+
+    'UI 建立
+    Public Sub New()
+        MainChrome = New SystemDropShadowChrome With {.Margin = New Thickness(-9.5, -9, 0.5, -0.5), .Opacity = 0.1, .CornerRadius = New CornerRadius(6)}
+        MainChrome.SetResourceReference(SystemDropShadowChrome.ColorProperty, "ColorObject1")
+        Children.Add(MainChrome)
+        MainBorder = New Border With {.Background = New SolidColorBrush(Color.FromArgb(205, 255, 255, 255)), .CornerRadius = New CornerRadius(6), .IsHitTestVisible = False}
+        Children.Add(MainBorder)
+        MainGrid = New Grid
+        Children.Add(MainGrid)
+    End Sub
+    Private IsLoad As Boolean = False
+    Private Sub MyCard_Loaded() Handles Me.Loaded
+        '卡片的进入时动画已被页面通用切换动画替代
+        'If LoadAnimation AndAlso IsLoad Then
+        '    StartHeightAnimation(Math.Min(50, ActualHeight), ActualHeight - Math.Min(50, ActualHeight), True)
+        'End If
+        If IsLoad Then Exit Sub
+        '初次加载限定
+        If Not Title = "" AndAlso IsNothing(MainTextBlock) Then
+            MainTextBlock = New TextBlock With {.HorizontalAlignment = HorizontalAlignment.Left, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(15, 12, 0, 0), .FontWeight = FontWeights.Bold, .FontSize = 13, .IsHitTestVisible = False}
+            MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
+            MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
+            MainGrid.Children.Add(MainTextBlock)
+        End If
+        If CanSwap OrElse SwapControl IsNot Nothing Then
+            If SwapControl Is Nothing AndAlso Children.Count > 3 Then SwapControl = Children(3)
+            MainSwap = New Shapes.Path With {.HorizontalAlignment = HorizontalAlignment.Right, .Stretch = Stretch.Uniform, .Height = 6, .Width = 10, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(0, 17, 16, 0), .Data = New GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"), .RenderTransform = New RotateTransform(180), .RenderTransformOrigin = New Point(0.5, 0.5)}
+            MainSwap.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush1")
+            MainGrid.Children.Add(MainSwap)
+        End If
+        IsLoad = True
+        '改变默认的折叠
+        If IsSwaped AndAlso SwapControl IsNot Nothing Then
+            MainSwap.RenderTransform = New RotateTransform(If(SwapLogoRight, 270, 0))
+            '取消由于高度变化被迫触发的高度动画
+            Dim RawUseAnimation As Boolean = UseAnimation
+            UseAnimation = False
+            Height = SwapedHeight
+            AniStop("MyCard Height " & Uuid)
+            IsHeightAnimating = False
+            RunInUi(Sub() UseAnimation = RawUseAnimation, True)
+        End If
+    End Sub
+    Public Sub StackInstall()
+        StackInstall(SwapControl, SwapType, Title)
+        TriggerForceResize()
+    End Sub
+    Public Shared Sub StackInstall(ByRef Stack As StackPanel, Type As Integer, Optional CardTitle As String = "")
+        '这一部分的代码是好几年前留下的究极屎坑,当时还不知道该咋正确调用这种方法,就写了这么一坨屎
+        '但是现在……反正勉强能用……懒得改了就这样吧.jpg
+        '别骂了别骂了.jpg
+        If IsNothing(Stack.Tag) Then Exit Sub
+        '排序
+        Select Case Type
+            Case 3
+                Stack.Tag = Sort(CType(Stack.Tag, List(Of DlOptiFineListEntry)), Function(Left As DlOptiFineListEntry, Right As DlOptiFineListEntry) As Boolean
+                                                                                     Return VersionSortBoolean(Left.NameDisplay, Right.NameDisplay)
+                                                                                 End Function)
+            Case 4, 10
+                Stack.Tag = Sort(CType(Stack.Tag, List(Of DlLiteLoaderListEntry)), Function(Left As DlLiteLoaderListEntry, Right As DlLiteLoaderListEntry) As Boolean
+                                                                                       Return VersionSortBoolean(Left.Inherit, Right.Inherit)
+                                                                                   End Function)
+            Case 6
+                Stack.Tag = Sort(CType(Stack.Tag, List(Of DlForgeVersionEntry)), Function(Left As DlForgeVersionEntry, Right As DlForgeVersionEntry) As Boolean
+                                                                                     Return VersionSortBoolean(Left.Version, Right.Version)
+                                                                                 End Function)
+            Case 8, 9
+                Stack.Tag = Sort(CType(Stack.Tag, List(Of DlCfFile)), Function(Left As DlCfFile, Right As DlCfFile) As Boolean
+                                                                          Return Left.Date > Right.Date
+                                                                      End Function)
+        End Select
+        '控件转换
+        Select Case Type
+            Case 5
+                Dim LoadingPickaxe As New MyLoading With {.Text = "正在获取版本列表", .Margin = New Thickness(5)}
+                Dim Loader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion Main", AddressOf DlForgeVersionMain)
+                LoadingPickaxe.State = Loader
+                Loader.Start(Stack.Tag)
+                AddHandler LoadingPickaxe.StateChanged, AddressOf FrmDownloadForge.Forge_StateChanged
+                AddHandler LoadingPickaxe.Click, AddressOf FrmDownloadForge.Forge_Click
+                Stack.Children.Add(LoadingPickaxe)
+            Case 6
+                ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
+            Case 8
+                DlCfFilesPreload(Stack, Stack.Tag, AddressOf FrmDownloadMod.ProjectClick)
+        End Select
+        '实现控件虚拟化
+        For Each Data As Object In Stack.Tag
+            Select Case Type
+                Case 0
+                    Stack.Children.Add(PageSelectRight.McVersionListItem(Data))
+                Case 1
+                    Stack.Children.Add(PageOtherFeedback.FeedbackListItem(Data, CardTitle))
+                Case 2
+                    Stack.Children.Add(McDownloadListItem(Data, AddressOf McDownloadMenuSave, True))
+                Case 3
+                    Stack.Children.Add(OptiFineDownloadListItem(Data, AddressOf OptiFineSave_Click, True))
+                Case 4
+                    Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf FrmDownloadLiteLoader.DownloadStart, False))
+                Case 5
+                Case 6
+                    Stack.Children.Add(ForgeDownloadListItem(Data, AddressOf ForgeSave_Click, True))
+                Case 7
+                    Stack.Children.Add(McDownloadListItem(Data, AddressOf FrmDownloadInstall.MinecraftSelected, False))
+                Case 8
+                    Stack.Children.Add(CType(Data, DlCfFile).ToListItem(AddressOf FrmDownloadCfDetail.Save_Click))
+                Case 9
+                    Stack.Children.Add(CType(Data, DlCfFile).ToListItem(AddressOf FrmDownloadCfDetail.Install_Click, AddressOf FrmDownloadCfDetail.Save_Click))
+                Case 10
+                    Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf LiteLoaderSave_Click, True))
+                Case 11
+                    Stack.Children.Add(CType(Data, HelpEntry).ToListItem)
+                Case Else
+                    Log("未知的虚拟化种类:" & Type, LogLevel.Feedback)
+            End Select
+        Next
+        Stack.Children.Add(New FrameworkElement With {.Height = 18}) '下边距,同时适应折叠
+        Stack.Tag = Nothing
+    End Sub
+
+    '事件
+    Private Sub MyCard_MouseEnter(sender As Object, e As MouseEventArgs) Handles Me.MouseEnter
+        Dim AniList As New List(Of AniData)
+        If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 150))
+        If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush2", 150))
+        AniList.AddRange({
+                         AaColor(MainChrome, SystemDropShadowChrome.ColorProperty, "ColorObject2", 180),
+                         AaColor(MainBorder, Border.BackgroundProperty, New MyColor(230, 255, 255, 255) - MainBorder.Background, 180),
+                         AaOpacity(MainChrome, 0.3 - MainChrome.Opacity, 180)
+                     })
+        AniStart(AniList, "MyCard Mouse " & Uuid)
+    End Sub
+    Private Sub MyCard_MouseLeave(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave
+        Dim AniList As New List(Of AniData)
+        If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 250))
+        If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush1", 250))
+        AniList.AddRange({
+                         AaColor(MainChrome, SystemDropShadowChrome.ColorProperty, "ColorObject1", 300),
+                         AaColor(MainBorder, Border.BackgroundProperty, New MyColor(205, 255, 255, 255) - MainBorder.Background, 300),
+                         AaOpacity(MainChrome, 0.1 - MainChrome.Opacity, 300)
+                     })
+        AniStart(AniList, "MyCard Mouse " & Uuid)
+    End Sub
+
+    '高度改变动画
+    Public Property UseAnimation As Boolean = True
+    Private IsHeightAnimating As Boolean = False
+    Private ActualUsedHeight As Double '回滚实际高度(例如 NaN)
+    Private Sub MySizeChanged(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged
+        If Not UseAnimation Then Exit Sub
+        Dim DeltaHeight As Double = If(IsSwaped, SwapedHeight, e.NewSize.Height) - e.PreviousSize.Height
+        '卡片的进入时动画已被页面通用切换动画替代
+        If e.PreviousSize.Height = 0 OrElse IsHeightAnimating OrElse Math.Abs(DeltaHeight) < 1 OrElse ActualHeight = 0 Then Exit Sub
+        StartHeightAnimation(DeltaHeight, e.PreviousSize.Height, False)
+    End Sub
+    Private Sub StartHeightAnimation(DeltaHeight As Double, PreviousHeight As Double, IsLoadAnimation As Boolean)
+        If IsHeightAnimating OrElse FrmMain Is Nothing Then Exit Sub '避免 XAML 设计器出错
+
+        Dim AnimList As New List(Of AniData)
+        If DeltaHeight > 10 OrElse (DeltaHeight < -10 AndAlso Not IsNothing(SwapControl)) Then '如果不是需要折叠的卡片,高度减小时的弹跳会吞掉按钮下边框
+            '高度增加较大,使用弹起动画
+            Dim Delta As Double = MathRange(Math.Abs(DeltaHeight) * 0.05, 3, 10) * Math.Sign(DeltaHeight)
+            AnimList.AddRange({
+                     AaHeight(Me, DeltaHeight + Delta, 300, If(IsLoadAnimation, 30, 0), If(DeltaHeight > FrmMain.Height, New AniEaseInFluent(AniEasePower.ExtraStrong), New AniEaseOutFluent(AniEasePower.ExtraStrong))),
+                     AaHeight(Me, -Delta, 150, 260, Ease:=New AniEaseOutFluent(AniEasePower.Strong))
+                })
+        Else
+            '普通的改变就行啦
+            AnimList.AddRange({
+                         AaHeight(Me, DeltaHeight, MathRange(Math.Abs(DeltaHeight) * 4, 150, 250),, New AniEaseOutFluent)
+                    })
+        End If
+        AnimList.Add(AaCode(Sub()
+                                IsHeightAnimating = False
+                                Height = ActualUsedHeight
+                                If IsSwaped Then SwapControl.Visibility = Visibility.Collapsed
+                            End Sub,, True))
+        AniStart(AnimList, "MyCard Height " & Uuid)
+
+        IsHeightAnimating = True
+        ActualUsedHeight = If(IsSwaped, SwapedHeight, Height)
+        Height = PreviousHeight
+    End Sub
+    ''' <summary>
+    ''' 通知 MyCard,控件内容已改变,需要中断动画并更新高度。
+    ''' </summary>
+    Public Sub TriggerForceResize()
+        Height = If(IsSwaped, SwapedHeight, Double.NaN)
+        AniStop("MyCard Height " & Uuid)
+        IsHeightAnimating = False
+    End Sub
+
+    '折叠
+    '若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠
+    '这是因为不能直接在 XAML 中设置 SwapControl
+    Public SwapControl As Object
+    Public Property CanSwap As Boolean = False
+    ''' <summary>
+    ''' 被折叠的种类,用于控件虚拟化。
+    ''' </summary>
+    Public Property SwapType As Integer
+    ''' <summary>
+    ''' 是否已被折叠。
+    ''' </summary>
+    Public Property IsSwaped As Boolean
+        Get
+            Return _IsSwaped
+        End Get
+        Set(ByVal value As Boolean)
+            If _IsSwaped = value Then Exit Property
+            _IsSwaped = value
+            If SwapControl Is Nothing Then Exit Property
+            '展开
+            If Not IsSwaped AndAlso TypeOf SwapControl Is StackPanel Then StackInstall(SwapControl, SwapType, Title)
+            '若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
+            If Not IsLoaded Then Exit Property
+            '更新高度
+            SwapControl.Visibility = Visibility.Visible
+            TriggerForceResize()
+            '改变箭头
+            AniStart(AaRotateTransform(MainSwap, If(_IsSwaped, If(SwapLogoRight, 270, 0), 180) - CType(MainSwap.RenderTransform, RotateTransform).Angle, 400,, New AniEaseOutBack(AniEasePower.Weak)), "MyCard Swap " & Uuid, True)
+        End Set
+    End Property
+    Private _IsSwaped As Boolean = False
+
+    Public Property SwapLogoRight As Boolean = False
+    Private IsMouseDown As Boolean = False
+    Public Event PreviewSwap(sender As Object, e As RouteEventArgs)
+    Public Event Swap(sender As Object, e As RouteEventArgs)
+    Public Const SwapedHeight As Integer = 40
+    Private Sub MyCard_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
+        Dim Pos As Double = Mouse.GetPosition(Me).Y
+        If Not IsSwaped AndAlso (IsNothing(SwapControl) OrElse Pos > SwapedHeight OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Exit Sub '检测点击位置;或已经不在可视树上的误判
+        IsMouseDown = True
+    End Sub
+    Private Sub MyCard_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
+        If IsMouseDown Then
+            IsMouseDown = False
+            Dim Pos As Double = Mouse.GetPosition(Me).Y
+            If Not IsSwaped AndAlso (IsNothing(SwapControl) OrElse Pos > SwapedHeight OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Exit Sub '检测点击位置;或已经不在可视树上的误判
+
+            Dim ee = New RouteEventArgs(True)
+            RaiseEvent PreviewSwap(Me, ee)
+            If ee.Handled Then
+                IsMouseDown = False
+                Exit Sub
+            End If
+
+            IsSwaped = Not IsSwaped
+            Log("[Control] " & If(IsSwaped, "折叠卡片", "展开卡片") & If(Title Is Nothing, "", ":" & Title))
+            RaiseEvent Swap(Me, ee)
+        End If
+    End Sub
+    Private Sub MyCard_MouseLeave_Swap(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave
+        IsMouseDown = False
+    End Sub
+
+End Class
+Partial Public Module ModAnimation
+    Public Sub AniDispose(Control As MyCard, RemoveFromChildren As Boolean, Optional CallBack As ParameterizedThreadStart = Nothing)
+        If Not Control.IsHitTestVisible Then Exit Sub
+        Control.IsHitTestVisible = False
+        AniStart({
+                     AaScaleTransform(Control, -0.08, 200,, New AniEaseInFluent),
+                     AaOpacity(Control, -1, 200,, New AniEaseOutFluent),
+                     AaHeight(Control, -Control.ActualHeight, 150, 100, New AniEaseOutFluent),
+                     AaCode(Sub()
+                                If RemoveFromChildren Then
+                                    CType(Control.Parent, Object).Children.Remove(Control)
+                                Else
+                                    Control.Visibility = Visibility.Collapsed
+                                End If
+                                If CallBack IsNot Nothing Then CallBack(Control)
+                            End Sub,, True)
+            }, "MyCard Dispose " & Control.Uuid)
+    End Sub
+End Module

+ 14 - 0
Plain Craft Launcher 2/Controls/MyCheckBox.xaml

@@ -0,0 +1,14 @@
+<Grid
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="MyCheckBox"
+             FocusVisualStyle="{x:Null}"
+            MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20" Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True" d:DesignWidth="126.4" d:DesignHeight="44.8">
+    <TextBlock IsHitTestVisible="False" FontSize="13" x:Name="LabText" Padding="0" Margin="26,0,0,0" Text="Checkbox" Foreground="{DynamicResource ColorBrush1}" HorizontalAlignment="Left" VerticalAlignment="Center" />
+    <Border IsHitTestVisible="False" x:Name="ShapeBorder" BorderThickness="1.1" HorizontalAlignment="Left" VerticalAlignment="Center" Width="18" Height="18" BorderBrush="{DynamicResource ColorBrush1}" CornerRadius="3" Margin="1,0,0,0" Background="{StaticResource ColorBrushHalfWhite}" />
+    <Path IsHitTestVisible="False" x:Name="ShapeCheck" HorizontalAlignment="Left" VerticalAlignment="Center" Fill="{Binding BorderBrush, ElementName=ShapeBorder}" Width="12" Height="12" Margin="4,0,0,0" Data="M0,6L1.5,4.5 4.5,7.5 10.5,1.5 12,3 4.5,10.5 0,6z">
+        <Path.RenderTransform>
+            <ScaleTransform CenterX="6" CenterY="6" ScaleX="0" ScaleY="0" />
+        </Path.RenderTransform>
+    </Path>
+</Grid>

+ 202 - 0
Plain Craft Launcher 2/Controls/MyCheckBox.xaml.vb

@@ -0,0 +1,202 @@
+Public Class MyCheckBox
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    ''' <summary>
+    ''' 复选框勾选状态改变。
+    ''' </summary>
+    ''' <param name="user">是否为用户手动改变的勾选状态。</param>
+    Public Event Change(sender As Object, user As Boolean)
+    Public Event PreviewChange(sender As Object, e As RouteEventArgs)
+    Public Sub RaiseChange()
+        RaiseEvent Change(Me, False)
+    End Sub '使外部程序引发本控件的 Change 事件
+
+    '自定义属性
+
+    Private _Checked As Boolean = False '是否选中
+    Public Property Checked As Boolean
+        Get
+            Return _Checked
+        End Get
+        Set(value As Boolean)
+            SetChecked(value, False, True)
+        End Set
+    End Property
+    Private Const AnimationTimeOfCheck As Integer = 150 '勾选状态变更动画长度
+    ''' <summary>
+    ''' 手动设置 Checked 属性。
+    ''' </summary>
+    ''' <param name="value">新的 Checked 属性。</param>
+    ''' <param name="user">是否由用户引发。</param>
+    ''' <param name="anime">是否执行动画。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+        Try
+            If value = _Checked Then Exit Sub
+
+            'Preview 事件
+
+            If value AndAlso user Then
+                Dim e = New RouteEventArgs(user)
+                RaiseEvent PreviewChange(Me, e)
+                If e.Handled Then
+                    MouseDowned = True
+                    Checkbox_MouseLeave()
+                    MouseDowned = False
+                    Exit Sub
+                End If
+            End If
+
+            _Checked = value
+            If IsLoaded Then RaiseEvent Change(Me, user)
+
+            '更改动画
+
+            If IsLoaded AndAlso AniControlEnabled = 0 AndAlso anime Then '防止默认属性变更触发动画
+                AllowMouseDown = False
+                If Checked Then
+                    '由无变有
+                    AniStart({
+                          AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True),
+                          AaScaleTransform(ShapeCheck, 1 - CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack(AniEasePower.Weak)),
+                          AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True)
+                     }, "MyCheckBox Scale " & Uuid)
+                    AniStart({
+                          AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush2"), "ColorBrushGray4"), AnimationTimeOfCheck)
+                     }, "MyCheckBox BorderColor " & Uuid)
+                    AniStart({
+                          AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2)
+                     }, "MyCheckBox AllowMouseDown " & Uuid)
+                Else
+                    '由有变无
+                    AniStart({
+                          AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True),
+                          AaScaleTransform(ShapeCheck, -CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 0.9, , New AniEaseInFluent(AniEasePower.Weak)),
+                          AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True)
+                     }, "MyCheckBox Scale " & Uuid)
+                    AniStart({
+                          AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush1"), "ColorBrushGray4"), AnimationTimeOfCheck)
+                     }, "MyCheckBox BorderColor " & Uuid)
+                    AniStart({
+                          AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2)
+                     }, "MyCheckBox AllowMouseDown " & Uuid)
+                End If
+            Else
+                '不使用动画
+                AniStop("MyCheckBox Scale " & Uuid)
+                AniStop("MyCheckBox BorderColor " & Uuid)
+                AniStop("MyCheckBox AllowMouseDown " & Uuid)
+                If Checked Then
+                    CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX = 1
+                    CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleY = 1
+                    ShapeBorder.SetResourceReference(Border.BorderBrushProperty, If(IsEnabled, "ColorBrush2", "ColorBrushGray4"))
+                Else
+                    CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX = 0
+                    CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleY = 0
+                    ShapeBorder.SetResourceReference(Border.BorderBrushProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray4"))
+                End If
+            End If
+
+        Catch ex As Exception
+            Log(ex, "设置 Checked 失败")
+        End Try
+    End Sub
+    Public Property Text As String
+        Get
+            Return GetValue(TextProperty)
+        End Get
+        Set(value As String)
+            SetValue(TextProperty, value)
+        End Set
+    End Property '内容
+    Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyCheckbox), New PropertyMetadata(New PropertyChangedCallback(
+                                                                                                                                                               Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+                                                                                                                                                                   If Not IsNothing(sender) Then CType(sender, MyCheckbox).LabText.Text = e.NewValue
+                                                                                                                                                               End Sub)))
+
+    '点击事件
+
+    Private MouseDowned As Boolean = False
+    Private AllowMouseDown As Boolean = True
+    Private Sub Checkbox_MouseUp() Handles Me.MouseLeftButtonUp
+        If Not MouseDowned Then Exit Sub
+        Log("[Control] 按下复选框(" & (Not Checked).ToString & "):" & Text)
+        MouseDowned = False
+        SetChecked(Not Checked, True, True)
+        AniStart(AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), "MyCheckBox Background " & Uuid)
+    End Sub
+    Private Sub Checkbox_MouseDown() Handles Me.MouseLeftButtonDown
+        If Not AllowMouseDown Then Exit Sub
+        MouseDowned = True
+        Focus()
+        AniStart(AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrush9", 100), "MyCheckBox Background " & Uuid)
+        If Checked Then
+            AniStart({
+                     AaScale(ShapeBorder, 16.5 - ShapeBorder.Width, 1000, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True),
+                     AaScaleTransform(ShapeCheck, 0.9 - CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, 1000, , New AniEaseOutFluent(AniEasePower.Strong))
+                 }, "MyCheckBox Scale " & Uuid)
+        Else
+            AniStart(AaScale(ShapeBorder, 16.5 - ShapeBorder.Width, 1000, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True), "MyCheckBox Scale " & Uuid)
+        End If
+    End Sub
+    Private Sub Checkbox_MouseLeave() Handles Me.MouseLeave
+        If Not MouseDowned Then Exit Sub
+        MouseDowned = False
+        AniStart(AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), "MyCheckBox Background " & Uuid)
+        If Checked Then
+            AniStart({
+                     AaScale(ShapeBorder, 18 - ShapeBorder.Width, 400, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True),
+                     AaScaleTransform(ShapeCheck, 1 - CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, 500, , New AniEaseOutFluent(AniEasePower.Strong))
+                 }, "MyCheckBox Scale " & Uuid)
+        Else
+            AniStart(AaScale(ShapeBorder, 18 - ShapeBorder.Width, 400, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True), "MyCheckBox Scale " & Uuid)
+        End If
+    End Sub
+
+    '指向动画
+
+    Private Const AnimationTimeOfMouseIn As Integer = 100
+    Private Const AnimationTimeOfMouseOut As Integer = 200
+    Private Sub Checkbox_IsEnabledChanged() Handles Me.IsEnabledChanged
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            If IsEnabled Then
+                '可用
+                Checkbox_MouseLeaveAnimation()
+            Else
+                '不可用
+                AniStart({
+                         AaColor(ShapeBorder, Border.BorderBrushProperty, ColorGray4 - ShapeBorder.BorderBrush, AnimationTimeOfMouseOut)
+                 }, "MyCheckBox BorderColor " & Uuid)
+                AniStart({
+                         AaColor(LabText, TextBlock.ForegroundProperty, ColorGray4 - LabText.Foreground, AnimationTimeOfMouseOut)
+                 }, "MyCheckBox TextColor " & Uuid)
+            End If
+        Else
+            '无动画
+            AniStop("MyCheckBox TextColor " & Uuid)
+            AniStop("MyCheckBox BorderColor " & Uuid)
+            LabText.SetResourceReference(TextBlock.ForegroundProperty, If(Me.IsEnabled, "ColorBrush1", "ColorBrushGray4"))
+            ShapeBorder.SetResourceReference(Border.BorderBrushProperty, If(Me.IsEnabled, If(Checked, "ColorBrush2", "ColorBrush1"), "ColorBrushGray4"))
+        End If
+    End Sub
+    Private Sub Checkbox_MouseEnterAnimation() Handles Me.MouseEnter
+        AniStart({
+                 AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn)
+         }, "MyCheckBox TextColor " & Uuid)
+        AniStart({
+                 AaColor(ShapeBorder, Border.BorderBrushProperty, "ColorBrush3", AnimationTimeOfMouseIn)
+         }, "MyCheckBox BorderColor " & Uuid)
+    End Sub
+    Private Sub Checkbox_MouseLeaveAnimation() Handles Me.MouseLeave
+        If Not IsEnabled Then Exit Sub 'MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误
+        AniStart({
+                 AaColor(LabText, TextBlock.ForegroundProperty, If(Me.IsEnabled, "ColorBrush1", "ColorBrushGray4"), AnimationTimeOfMouseOut)
+         }, "MyCheckBox TextColor " & Uuid)
+        AniStart({
+                 AaColor(ShapeBorder, Border.BorderBrushProperty, If(Me.IsEnabled, If(Checked, "ColorBrush2", "ColorBrush1"), "ColorBrushGray4"), AnimationTimeOfMouseOut)
+         }, "MyCheckBox BorderColor " & Uuid)
+    End Sub
+
+End Class

+ 135 - 0
Plain Craft Launcher 2/Controls/MyComboBox.vb

@@ -0,0 +1,135 @@
+Public Class MyComboBox
+    Inherits ComboBox
+    Public Event TextChanged(sender As Object, e As TextChangedEventArgs)
+
+    '基础
+    Public Uuid As Integer = GetUuid()
+    Private TextBox As MyTextBox
+    Public Property HintText As String = ""
+    Public Overrides Sub OnApplyTemplate()
+        MyBase.OnApplyTemplate()
+        If Not IsEditable Then Exit Sub
+        Try
+            TextBox = Template.FindName("PART_EditableTextBox", Me)
+            TextBox.AddHandler(LostFocusEvent, New RoutedEventHandler(AddressOf RefreshColor))
+            TextBox.ChangedEventList.Add(New RoutedEventHandler(Sub(sender, e) RaiseEvent TextChanged(sender, e)))
+            TextBox.Tag = Tag '有时需要用文本框的 Tag 来写入设置
+            If Text = "" Then
+                TextBox.Text = _Text
+            Else
+                RaiseEvent TextChanged(Me, Nothing)
+            End If
+            If HintText.Length > 0 Then TextBox.HintText = HintText
+        Catch ex As Exception
+            Log(ex, "初始化可编辑文本框失败(" & If(Name, "") & ")", LogLevel.Feedback)
+        End Try
+    End Sub
+    Private _Text As String = SelectedItem
+    Public Shadows Property Text As String
+        Get
+            If IsEditable Then
+                If TextBox Is Nothing Then
+                    Return If(_Text, "")
+                Else
+                    Return If(TextBox.Text, "")
+                End If
+            Else
+                Return If(SelectedItem, "").ToString
+            End If
+        End Get
+        Set(value As String)
+            If IsEditable Then
+                If IsNothing(TextBox) Then
+                    _Text = value
+                Else
+                    TextBox.Text = value
+                End If
+            Else
+                Throw New NotSupportedException("该 ComboBox 不支持修改文本。")
+            End If
+        End Set
+    End Property
+
+    '鼠标按下接口
+    Private IsMouseDown As Boolean = False
+    Private Sub MyComboBox_PreviewMouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonDown
+        IsMouseDown = True
+    End Sub
+    Private Sub MyComboBox_PreviewMouseLeftButtonUp(sender As Object, e As EventArgs) Handles Me.PreviewMouseLeftButtonUp, Me.MouseLeave
+        IsMouseDown = False
+    End Sub
+
+    '指向动画
+    Public Sub RefreshColor() Handles Me.IsEnabledChanged, Me.MouseEnter, Me.MouseLeave, Me.PreviewMouseLeftButtonDown, Me.PreviewMouseLeftButtonUp, Me.GotKeyboardFocus
+        '判断当前颜色
+        Dim ColorName As String
+        Dim BackName As String
+        Dim Time As Integer
+        If IsEnabled Then
+            If IsEditable AndAlso Template.FindName("PART_EditableTextBox", Me).IsFocused Then
+                ColorName = "ColorBrush4"
+                BackName = "ColorBrush9"
+                Time = 60
+            ElseIf IsMouseDown OrElse IsDropDownOpen Then
+                ColorName = "ColorBrush3"
+                BackName = "ColorBrush9"
+                Time = 60
+            ElseIf IsMouseOver Then
+                ColorName = "ColorBrush3"
+                BackName = "ColorBrushHalfWhite"
+                Time = 100
+            Else
+                ColorName = "ColorBrush1"
+                BackName = "ColorBrushHalfWhite"
+                Time = 200
+            End If
+        Else
+            ColorName = "ColorBrushGray4"
+            BackName = "ColorBrushHalfWhite"
+            Time = 200
+        End If
+        '触发颜色动画
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            AniStart({
+                     AaColor(Me, ForegroundProperty, ColorName, Time),
+                     AaColor(Me, BackgroundProperty, BackName, Time)
+                 }, "MyComboBox Color " & Uuid)
+        Else
+            '无动画
+            AniStop("MyComboBox Color " & Uuid)
+            SetResourceReference(ForegroundProperty, ColorName)
+            SetResourceReference(BackgroundProperty, BackName)
+        End If
+    End Sub
+
+    Private RealWidth As Double '由于下拉框 Popup 宽度与 Width 一致,故不能为 NaN(Auto)
+    Private Sub MyComboBox_DropDownOpened(sender As Object, e As EventArgs) Handles Me.DropDownOpened
+        RealWidth = Width
+        Width = ActualWidth
+        Try
+            CType(Template.FindName("PanPopup", Me), Grid).Opacity = FrmMain.Opacity
+        Catch ex As Exception
+            Log(ex, "设置下拉框透明度失败", LogLevel.Feedback)
+        End Try
+    End Sub
+    Private Sub MyComboBox_DropDownClosed(sender As Object, e As EventArgs) Handles Me.DropDownClosed
+        Width = RealWidth
+    End Sub
+
+    '修复 WPF Bug:下拉框文本修改后,依然误认为还选择着此前的选项,导致再次点击该选项时内容不变
+    Private IsTextChanging As Boolean = False
+    Private Sub MyComboBox_TextChanged(sender As Object, e As TextChangedEventArgs) Handles Me.TextChanged
+        If IsTextChanging OrElse Not IsEditable Then Exit Sub
+        If SelectedItem IsNot Nothing AndAlso Text <> SelectedItem.ToString Then
+            Dim RawText As String = Text
+            Dim RawSelectionStart As Integer = TextBox.SelectionStart
+            IsTextChanging = True
+            SelectedItem = Nothing
+            Text = RawText
+            TextBox.SelectionStart = RawSelectionStart
+            IsTextChanging = False
+        End If
+    End Sub
+
+End Class

+ 67 - 0
Plain Craft Launcher 2/Controls/MyComboBoxItem.vb

@@ -0,0 +1,67 @@
+Public Class MyComboBoxItem
+    Inherits ComboBoxItem
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Sub New()
+        Style = FindResource("MyComboBoxItem")
+    End Sub
+
+    '指向动画
+
+    Private Const AnimationTimeIn As Integer = 100
+    Private Const AnimationTimeOut As Integer = 300
+    Private BackColorName As String, FontOpacity As Double
+
+    Private Sub RefreshColor() Handles Me.Unselected, Me.MouseMove, Me.MouseLeave, Me.Selected, Me.IsEnabledChanged
+        '判断当前颜色
+        Dim NewBackColorName As String, NewFontOpacity As Double
+        Dim Time As Integer
+        If IsSelected Then
+            NewBackColorName = "ColorBrush6"
+            NewFontOpacity = 1
+            Time = AnimationTimeIn
+        ElseIf IsMouseOver Then
+            NewBackColorName = "ColorBrush8"
+            NewFontOpacity = 1
+            Time = AnimationTimeIn
+        ElseIf IsEnabled Then
+            NewBackColorName = "ColorBrushTransparent"
+            NewFontOpacity = 1
+            Time = AnimationTimeOut
+        Else
+            NewBackColorName = "ColorBrushTransparent"
+            NewFontOpacity = 0.4
+            Time = AnimationTimeOut
+        End If
+        If BackColorName = NewBackColorName AndAlso FontOpacity = NewFontOpacity Then Exit Sub
+        BackColorName = NewBackColorName
+        FontOpacity = NewFontOpacity
+        '触发颜色动画
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            AniStart({
+                AaColor(Me, BackgroundProperty, BackColorName, Time),
+                AaOpacity(Me, FontOpacity - Opacity, Time)
+            }, "ComboBoxItem Color " & Uuid)
+        Else
+            '无动画
+            AniStop("ComboBoxItem Color " & Uuid)
+            SetResourceReference(BackgroundProperty, BackColorName)
+            Opacity = FontOpacity
+        End If
+    End Sub
+
+    Public Overrides Function Tostring() As String
+        Return Content.ToString
+    End Function
+    Public Shared Widening Operator CType(Value As MyComboBoxItem) As String
+        Return Value.Content.ToString
+    End Operator
+
+    Private Sub MyComboBoxItem_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
+        Log("[Control] 选择下拉列表项:" & Tostring())
+    End Sub
+
+End Class

+ 25 - 0
Plain Craft Launcher 2/Controls/MyExtraButton.xaml

@@ -0,0 +1,25 @@
+<Grid
+            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d" x:Class="MyExtraButton"
+            Width="40" RenderTransformOrigin="0.5,0.5" ToolTipService.ShowDuration="2333333" ToolTipService.Placement="Left" ToolTipService.VerticalOffset="16" ToolTipService.HorizontalOffset="-8" Height="0">
+    <Grid.RenderTransform>
+        <ScaleTransform ScaleX="0" ScaleY="0" />
+    </Grid.RenderTransform>
+    <Border x:Name="PanClick" CornerRadius="1000" BorderThickness="0.001" Background="{StaticResource ColorBrushSemiTransparent}"/>
+    <Grid x:Name="PanScale" IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5" Margin="0,10,0,0">
+        <Grid.RenderTransform>
+            <ScaleTransform />
+        </Grid.RenderTransform>
+        <Border x:Name="PanColor" CornerRadius="1000" BorderThickness="0.001" Background="{DynamicResource ColorBrush3}">
+            <Border.Effect>
+                <DropShadowEffect ShadowDepth="0" BlurRadius="10" Opacity="0.2" />
+            </Border.Effect>
+        </Border>
+        <Path x:Name="Path" Stretch="Uniform" Margin="12" Fill="{DynamicResource ColorBrush8}" RenderTransformOrigin="0.5,0.5">
+            <Path.RenderTransform>
+                <ScaleTransform />
+            </Path.RenderTransform>
+        </Path>
+    </Grid>
+</Grid>

+ 191 - 0
Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb

@@ -0,0 +1,191 @@
+Public Class MyExtraButton
+
+    '声明
+    Public Event Click(sender As Object, e As MouseButtonEventArgs) '自定义事件
+    Public Event RightClick(sender As Object, e As MouseButtonEventArgs)
+
+    '自定义属性
+    Public Uuid As Integer = GetUuid()
+    Private _Logo As String = ""
+    Public Property Logo As String
+        Get
+            Return _Logo
+        End Get
+        Set(value As String)
+            If value = _Logo Then Exit Property
+            _Logo = value
+            Path.Data = (New GeometryConverter).ConvertFromString(value)
+        End Set
+    End Property
+    Private _LogoScale As Double = 1
+    Public Property LogoScale() As Double
+        Get
+            Return _LogoScale
+        End Get
+        Set(value As Double)
+            _LogoScale = value
+            If Not IsNothing(Path) Then Path.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale}
+        End Set
+    End Property
+    Private _Show As Boolean = False
+    Public Property Show As Boolean
+        Get
+            Return _Show
+        End Get
+        Set(value As Boolean)
+            If _Show = value Then Exit Property
+            _Show = value
+            RunInUi(Sub()
+                        If value Then
+                            '有了
+                            Visibility = Visibility.Visible
+                            AniStart({
+                                AaScaleTransform(Me, 0.3 - CType(RenderTransform, ScaleTransform).ScaleX, 500, 60, New AniEaseOutFluent(AniEasePower.Weak)),
+                                AaScaleTransform(Me, 0.7, 500, 60, New AniEaseOutBack(AniEasePower.Weak)),
+                                AaHeight(Me, 50 - Height, 200,, New AniEaseOutFluent(AniEasePower.Weak))
+                            }, "MyExtraButton MainScale " & Uuid)
+                        Else
+                            '没了
+                            AniStart({
+                                AaScaleTransform(Me, -CType(RenderTransform, ScaleTransform).ScaleX, 100,, New AniEaseInFluent(AniEasePower.Weak)),
+                                AaHeight(Me, -Height, 400, 100, New AniEaseOutFluent()),
+                                AaCode(Sub() Visibility = Visibility.Collapsed,, True)
+                            }, "MyExtraButton MainScale " & Uuid)
+                        End If
+                        IsHitTestVisible = value '防止缩放动画中依然可以点进去
+                    End Sub)
+        End Set
+    End Property
+    Public Delegate Function ShowCheckDelegate() As Boolean
+    Public ShowCheck As ShowCheckDelegate = Nothing
+    Public Sub ShowRefresh()
+        If ShowCheck IsNot Nothing Then Show = ShowCheck()
+    End Sub
+
+    '触发点击事件
+    Private Sub Button_LeftMouseUp(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseLeftButtonUp
+        If IsLeftMouseHeld Then
+            Log("[Control] 按下附加按钮" & If(ToolTip = "", "", ":" & ToolTip.ToString))
+            RaiseEvent Click(sender, e)
+            e.Handled = True
+            Button_LeftMouseUp()
+        End If
+    End Sub
+    Private Sub Button_RightMouseUp(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseRightButtonUp
+        If IsRightMouseHeld Then
+            Log("[Control] 右键按下附加按钮" & If(ToolTip = "", "", ":" & ToolTip.ToString))
+            RaiseEvent RightClick(sender, e)
+            e.Handled = True
+            Button_RightMouseUp()
+        End If
+    End Sub
+    Private _CanRightClick As Boolean = False
+    Public Property CanRightClick As Boolean
+        Get
+            Return _CanRightClick
+        End Get
+        Set(ByVal value As Boolean)
+            _CanRightClick = value
+        End Set
+    End Property
+
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsLeftMouseHeld As Boolean = False
+    Private IsRightMouseHeld As Boolean = False
+    Private Sub Button_LeftMouseDown(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseLeftButtonDown
+        If Not IsLeftMouseHeld AndAlso Not IsRightMouseHeld Then
+            AniStart({
+                     AaScaleTransform(PanScale, 0.85 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 800,, New AniEaseOutFluent(AniEasePower.Strong)),
+                     AaScaleTransform(PanScale, -0.05, 60,, New AniEaseOutFluent)
+                     }, "MyExtraButton Scale " & Uuid)
+        End If
+        IsLeftMouseHeld = True
+        Focus()
+    End Sub
+    Private Sub Button_RightMouseDown(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseRightButtonDown
+        If Not CanRightClick Then Exit Sub
+        If Not IsLeftMouseHeld AndAlso Not IsRightMouseHeld Then
+            AniStart({
+                     AaScaleTransform(PanScale, 0.85 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 800,, New AniEaseOutFluent(AniEasePower.Strong)),
+                     AaScaleTransform(PanScale, -0.05, 60,, New AniEaseOutFluent)
+                     }, "MyExtraButton Scale " & Uuid)
+        End If
+        IsRightMouseHeld = True
+        Focus()
+    End Sub
+    Private Sub Button_LeftMouseUp() Handles PanClick.MouseLeftButtonUp
+        If Not IsRightMouseHeld Then
+            AniStart({
+                         AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 300,, New AniEaseOutBack)
+                     }, "MyExtraButton Scale " & Uuid)
+        End If
+        IsLeftMouseHeld = False
+        RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+    Private Sub Button_RightMouseUp() Handles PanClick.MouseRightButtonUp
+        If Not CanRightClick Then Exit Sub
+        If Not IsLeftMouseHeld Then
+            AniStart({
+                         AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 300,, New AniEaseOutBack)
+                     }, "MyExtraButton Scale " & Uuid)
+        End If
+        IsRightMouseHeld = False
+        RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+    Private Sub Button_MouseLeave() Handles PanClick.MouseLeave
+        IsLeftMouseHeld = False
+        IsRightMouseHeld = False
+        AniStart({
+                     AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 500,, New AniEaseOutFluent)
+                 }, "MyExtraButton Scale " & Uuid)
+        RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+
+    '自定义事件
+    '务必放在 IsMouseDown 更新之后
+    Private Const AnimationColorIn As Integer = 120
+    Private Const AnimationColorOut As Integer = 150
+    Public Sub RefreshColor() Handles PanClick.MouseEnter, PanClick.MouseLeave, Me.Loaded
+        Try
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+
+                If IsMouseOver Then
+                    '指向
+                    AniStart(AaColor(PanColor, BackgroundProperty, "ColorBrush4", AnimationColorIn), "MyExtraButton Color " & Uuid)
+                Else
+                    '普通
+                    AniStart(AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut), "MyExtraButton Color " & Uuid)
+                End If
+
+            Else
+
+                AniStop("MyExtraButton Color " & Uuid)
+                If IsMouseOver Then
+                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush2")
+                Else
+                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3")
+                End If
+
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新图标按钮颜色出错")
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 发出一圈波浪效果提示。
+    ''' </summary>
+    Public Sub Ribble()
+        RunInUi(Sub()
+                    Dim Shape As New Border With {.CornerRadius = New CornerRadius(1000), .BorderThickness = New Thickness(0.001), .Opacity = 0.5, .RenderTransformOrigin = New Point(0.5, 0.5), .RenderTransform = New ScaleTransform()}
+                    Shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5")
+                    PanScale.Children.Insert(0, Shape)
+                    AniStart({
+                       AaScaleTransform(Shape, 13, 1000, Ease:=New AniEaseInoutFluent(AniEasePower.Strong, 0.3)),
+                       AaOpacity(Shape, -Shape.Opacity, 1000),
+                       AaCode(Sub() PanScale.Children.Remove(Shape), After:=True)
+                    }, "ExtraButton Ribble " & GetUuid())
+                End Sub)
+    End Sub
+
+End Class

+ 24 - 0
Plain Craft Launcher 2/Controls/MyHint.xaml

@@ -0,0 +1,24 @@
+<Border x:Class="MyHint" x:Name="PanBack"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:PCL"
+             FocusVisualStyle="{x:Null}"
+             UseLayoutRounding="True" SnapsToDevicePixels="True" HorizontalAlignment="Stretch" BorderThickness="1" BorderBrush="#99FF4444" CornerRadius="3">
+    <Border.Background>
+        <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
+            <GradientStop x:Name="Gradient1" Color="#88FFBBBB" Offset="0.3"/>
+            <GradientStop x:Name="Gradient2" Color="#88FF8888" Offset="0.9"/>
+        </LinearGradientBrush>
+    </Border.Background>
+    <Grid UseLayoutRounding="False" SnapsToDevicePixels="False">
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="Auto" />
+            <ColumnDefinition Width="1*" />
+            <ColumnDefinition Width="Auto" />
+        </Grid.ColumnDefinitions>
+        <Path IsHitTestVisible="False" Grid.Column="0" x:Name="Path" VerticalAlignment="Center" Fill="#BF0000" Width="14" Stretch="Uniform" Margin="10,0,2,0" Data="F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z " />
+        <TextBlock IsHitTestVisible="False" Grid.Column="1" x:Name="LabText" Margin="0,0.5,0,0" Padding="6,6,8,6" Text="提示文本" VerticalAlignment="Center" Foreground="#BF0000" TextWrapping="Wrap" />
+        <local:MyIconButton x:Name="BtnClose" SnapsToDevicePixels="True" Visibility="Collapsed" Foreground="#BF0000" Theme="Custom" Margin="0,0,6,0" Grid.Column="2" Height="20" Width="20" VerticalAlignment="Center" Logo="F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z"
+                           ToolTip="不再显示" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="29" ToolTipService.HorizontalOffset="2" />
+    </Grid>
+</Border>

+ 82 - 0
Plain Craft Launcher 2/Controls/MyHint.xaml.vb

@@ -0,0 +1,82 @@
+Public Class MyHint
+
+    Public Uuid As Integer = GetUuid()
+
+    Private _IsWarn As Boolean = True
+    Public Property IsWarn As Boolean
+        Get
+            Return _IsWarn
+        End Get
+        Set(value As Boolean)
+            If _IsWarn = value Then Exit Property
+            _IsWarn = value
+            If _IsWarn Then
+                BorderBrush = New MyColor("#99FF4444")
+                Gradient1.Color = New MyColor("#88FFBBBB")
+                Gradient2.Color = New MyColor("#88FF8888")
+                Path.Fill = New MyColor("#BF0000")
+                LabText.Foreground = New MyColor("#BF0000")
+                BtnClose.Foreground = New MyColor("#BF0000")
+                Path.Data = (New GeometryConverter).ConvertFromString("F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z ")
+            Else
+                BorderBrush = New MyColor("#994D76FF")
+                Gradient1.Color = New MyColor("#88B0D0FF")
+                Gradient2.Color = New MyColor("#889EBAFF")
+                Path.Fill = New MyColor("#0062BF")
+                LabText.Foreground = New MyColor("#0062BF")
+                BtnClose.Foreground = New MyColor("#0062BF")
+                Path.Data = (New GeometryConverter).ConvertFromString("F1M38,19C48.4934,19 57,27.5066 57,38 57,48.4934 48.4934,57 38,57 27.5066,57 19,48.4934 19,38 19,27.5066 27.5066,19 38,19z M33.25,33.25L33.25,36.4167 36.4166,36.4167 36.4166,47.5 33.25,47.5 33.25,50.6667 44.3333,50.6667 44.3333,47.5 41.1666,47.5 41.1666,36.4167 41.1666,33.25 33.25,33.25z M38.7917,25.3333C37.48,25.3333 36.4167,26.3967 36.4167,27.7083 36.4167,29.02 37.48,30.0833 38.7917,30.0833 40.1033,30.0833 41.1667,29.02 41.1667,27.7083 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333z")
+            End If
+        End Set
+    End Property
+
+    Public Property Text As String
+        Get
+            Return LabText.Text
+        End Get
+        Set(ByVal value As String)
+            LabText.Text = value
+        End Set
+    End Property
+
+    Public Property CanClose As Boolean
+        Get
+            Return BtnClose.Visibility = Visibility.Visible
+        End Get
+        Set(ByVal value As Boolean)
+            BtnClose.Visibility = If(value, Visibility.Visible, Visibility.Collapsed)
+        End Set
+    End Property
+
+    Public RelativeSetup As String = ""
+
+    Private Sub MyHint_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        If CanClose AndAlso Setup.Get(RelativeSetup) Then
+            Visibility = Visibility.Collapsed
+        End If
+    End Sub
+    Private Sub BtnClose_Click(sender As Object, e As EventArgs) Handles BtnClose.Click
+        Setup.Set(RelativeSetup, True)
+        AniDispose(Me, False)
+    End Sub
+
+End Class
+Partial Public Module ModAnimation
+    Public Sub AniDispose(Control As MyHint, RemoveFromChildren As Boolean, Optional CallBack As ParameterizedThreadStart = Nothing)
+        If Not Control.IsHitTestVisible Then Exit Sub
+        Control.IsHitTestVisible = False
+        AniStart({
+                     AaScaleTransform(Control, -0.08, 200,, New AniEaseInFluent),
+                     AaOpacity(Control, -1, 200,, New AniEaseOutFluent),
+                     AaHeight(Control, -Control.ActualHeight, 150, 100, New AniEaseOutFluent),
+                     AaCode(Sub()
+                                If RemoveFromChildren Then
+                                    CType(Control.Parent, Object).Children.Remove(Control)
+                                Else
+                                    Control.Visibility = Visibility.Collapsed
+                                End If
+                                If CallBack IsNot Nothing Then CallBack(Control)
+                            End Sub,, True)
+            }, "MyCard Dispose " & Control.Uuid)
+    End Sub
+End Module

+ 15 - 0
Plain Craft Launcher 2/Controls/MyIconButton.xaml

@@ -0,0 +1,15 @@
+<Border x:Class="MyIconButton"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+            CornerRadius="1000" Background="{StaticResource ColorBrushSemiTransparent}" BorderThickness="0.001">
+    <Border x:Name="PanBack" CornerRadius="1000"  RenderTransformOrigin="0.5,0.5" BorderThickness="0.001" IsHitTestVisible="False">
+        <Path x:Name="Path" Stretch="Uniform" Margin="5" RenderTransformOrigin="0.5,0.5">
+            <Path.RenderTransform>
+                <ScaleTransform />
+            </Path.RenderTransform>
+        </Path>
+        <Border.RenderTransform>
+            <ScaleTransform />
+        </Border.RenderTransform>
+    </Border>
+</Border>

+ 168 - 0
Plain Craft Launcher 2/Controls/MyIconButton.xaml.vb

@@ -0,0 +1,168 @@
+Public Class MyIconButton
+
+    '声明
+    Public Event Click(sender As Object, e As EventArgs) '自定义事件
+
+    '自定义属性
+
+    Public Uuid As Integer = GetUuid()
+    Public Property Logo As String
+        Get
+            Return Path.Data.ToString
+        End Get
+        Set(value As String)
+            Path.Data = (New GeometryConverter).ConvertFromString(value)
+        End Set
+    End Property
+
+    Private _LogoScale As Double = 1
+    Public Property LogoScale() As Double
+        Get
+            Return _LogoScale
+        End Get
+        Set(value As Double)
+            _LogoScale = value
+            If Not IsNothing(Path) Then Path.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale}
+        End Set
+    End Property
+
+    Public Enum Themes
+        Color
+        White
+        Black
+        Custom
+    End Enum
+    Public Property Theme As Themes = Themes.Color
+
+    Private _Foreground As New SolidColorBrush(Color.FromRgb(128, 128, 128))
+    Public Property Foreground As SolidColorBrush
+        Get
+            Return _Foreground
+        End Get
+        Set(value As SolidColorBrush)
+            _Foreground = value
+            AniControlEnabled += 1
+            RefreshAnim()
+            AniControlEnabled -= 1
+        End Set
+    End Property
+
+    '触发点击事件
+    Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
+        If IsMouseDown Then
+            Log("[Control] 按下图标按钮" & If(String.IsNullOrEmpty(Name), "", ":" & Name))
+            RaiseEvent Click(sender, e)
+            e.Handled = True
+            Button_MouseUp()
+        End If
+    End Sub
+
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsMouseDown As Boolean = False
+    Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
+        IsMouseDown = True
+        Focus()
+        '指向
+        AniStart(AaScaleTransform(PanBack, 0.8 - CType(PanBack.RenderTransform, ScaleTransform).ScaleX, 400,, New AniEaseOutFluent(AniEasePower.Strong)), "MyIconButton Scale " & Uuid)
+    End Sub
+    Private Sub Button_MouseUp() Handles Me.MouseLeftButtonUp
+        If IsMouseDown Then
+            IsMouseDown = False
+            AniStart({
+                     AaScaleTransform(PanBack, 1.05 - CType(PanBack.RenderTransform, ScaleTransform).ScaleX, 250,, New AniEaseOutBack(AniEasePower.Weak)),
+                     AaScaleTransform(PanBack, -0.05, 250,, New AniEaseOutFluent(AniEasePower.Strong))
+                 }, "MyIconButton Scale " & Uuid)
+        End If
+        RefreshAnim() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+    Private Sub Button_MouseLeave() Handles Me.MouseLeave
+        IsMouseDown = False
+        AniStart({
+                     AaScaleTransform(PanBack, 1 - CType(PanBack.RenderTransform, ScaleTransform).ScaleX, 250,, New AniEaseOutFluent)
+                 }, "MyIconButton Scale " & Uuid)
+        RefreshAnim() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+
+    '自定义事件
+    '务必放在 IsMouseDown 更新之后
+    Private Const AnimationColorIn As Integer = 120
+    Private Const AnimationColorOut As Integer = 150
+    Public Sub RefreshAnim() Handles Me.MouseEnter, Me.MouseLeave, Me.Loaded
+        Try
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+
+                If PanBack.Background Is Nothing Then PanBack.Background = New MyColor(0, 255, 255, 255)
+                If Path.Fill Is Nothing AndAlso Theme = Themes.Black Then Path.Fill = New MyColor(140, 0, 0, 0)
+                If IsMouseOver Then
+                    '指向
+                    Dim AnimList As New List(Of AniData)
+                    Select Case Theme
+                        Case Themes.Color
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, "ColorBrush2", AnimationColorIn))
+                        Case Themes.White
+                            AnimList.Add(AaColor(PanBack, BackgroundProperty, New MyColor(50, 255, 255, 255) - PanBack.Background, AnimationColorIn))
+                        Case Themes.Black
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, New MyColor(230, 0, 0, 0) - Path.Fill, AnimationColorIn))
+                        Case Themes.Custom
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, New MyColor(255, Foreground) - Path.Fill, AnimationColorIn))
+                    End Select
+                    AniStart(AnimList, "MyIconButton Color " & Uuid)
+                Else
+                    '普通
+                    Dim AnimList As New List(Of AniData)
+                    Select Case Theme
+                        Case Themes.Color
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, "ColorBrush4", AnimationColorOut))
+                            PanBack.Background = New MyColor(0, 255, 255, 255)
+                        Case Themes.White
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, "ColorBrush8", AnimationColorOut))
+                            AnimList.Add(AaColor(PanBack, BackgroundProperty, New MyColor(0, 255, 255, 255) - PanBack.Background, AnimationColorOut))
+                        Case Themes.Black
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, New MyColor(160, 0, 0, 0) - Path.Fill, AnimationColorOut))
+                            PanBack.Background = New MyColor(0, 255, 255, 255)
+                        Case Themes.Custom
+                            AnimList.Add(AaColor(Path, Shape.FillProperty, New MyColor(160, Foreground) - Path.Fill, AnimationColorOut))
+                            PanBack.Background = New MyColor(0, 255, 255, 255)
+                    End Select
+                    AniStart(AnimList, "MyIconButton Color " & Uuid)
+                End If
+
+            Else
+
+                AniStop("MyIconButton Color " & Uuid)
+                Select Case Theme
+                    Case Themes.Color
+                        Path.SetResourceReference(Shape.FillProperty, "ColorBrush5")
+                    Case Themes.White
+                        Path.SetResourceReference(Shape.FillProperty, "ColorBrush8")
+                    Case Themes.Black
+                        Path.Fill = New MyColor(160, 0, 0, 0)
+                    Case Themes.Custom
+                        Path.Fill = New MyColor(160, Foreground)
+                End Select
+                PanBack.Background = New MyColor(0, 255, 255, 255)
+
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新图标按钮动画状态出错")
+        End Try
+    End Sub
+
+End Class
+Partial Public Module ModAnimation
+    Public Sub AniDispose(Control As MyIconButton, RemoveFromChildren As Boolean, Optional CallBack As ParameterizedThreadStart = Nothing)
+        If Not Control.IsHitTestVisible Then Exit Sub
+        Control.IsHitTestVisible = False
+        AniStart({
+                 AaScaleTransform(Control, -1.5, 200,, New AniEaseInFluent),
+                 AaCode(Sub()
+                            If RemoveFromChildren Then
+                                CType(Control.Parent, Object).Children.Remove(Control)
+                            Else
+                                Control.Visibility = Visibility.Collapsed
+                            End If
+                            If CallBack IsNot Nothing Then CallBack(Control)
+                        End Sub,, True)
+        }, "MyIconButton Dispose " & Control.Uuid)
+    End Sub
+End Module

+ 27 - 0
Plain Craft Launcher 2/Controls/MyListItem.xaml

@@ -0,0 +1,27 @@
+<Grid x:Class="MyListItem"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Name="PanBack"
+            RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}" Height="42">
+    <Grid.RenderTransform>
+        <ScaleTransform ScaleX="1" ScaleY="1" />
+    </Grid.RenderTransform>
+    <Grid.ColumnDefinitions>
+        <ColumnDefinition x:Name="ColumnCheck" Width="2"/>
+        <ColumnDefinition x:Name="ColumnPaddingLeft" Width="0" />
+        <ColumnDefinition x:Name="ColumnLogo" Width="4" />
+        <ColumnDefinition Width="1*"/>
+        <ColumnDefinition x:Name="ColumnPaddingRight" Width="0" />
+    </Grid.ColumnDefinitions>
+    <Grid.RowDefinitions>
+        <RowDefinition Height="1*" />
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="1*" />
+    </Grid.RowDefinitions>
+    <!--<Border x:Name="RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True" IsHitTestVisible="False" Opacity="0" Grid.ColumnSpan="4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}" BorderThickness="1" />-->
+    <!--<StackPanel VerticalAlignment="Center">-->
+    <TextBlock Grid.Row="1" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left" x:Name="LabTitle" IsHitTestVisible="False" Grid.Column="3" Text="{Binding Title, ElementName=PanBack}" TextTrimming="CharacterEllipsis" FontSize="{Binding FontSize, ElementName=PanBack}" Foreground="{Binding Foreground, ElementName=PanBack}" Margin="4,0,0,0" />
+    <!--<TextBlock Grid.Row="2" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left" x:Name="LabInfo" IsHitTestVisible="False" Grid.Column="2" TextTrimming="CharacterEllipsis" Visibility="Collapsed" FontSize="12" Foreground="{StaticResource ColorBrushGray2}" Margin="4,0,0,0" />-->
+    <!--</StackPanel>-->
+</Grid>

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

@@ -0,0 +1,668 @@
+Public Class MyListItem
+    Implements IMyRadio
+
+    Public Event Click(sender As Object, e As MouseButtonEventArgs)
+    Public Event LogoClick(sender As Object, e As MouseButtonEventArgs)
+    Public Event Check(sender As Object, e As RouteEventArgs) Implements IMyRadio.Check
+    Public Event Changed(sender As Object, e As RouteEventArgs) Implements IMyRadio.Changed
+
+#Region "后加载控件"
+
+    '指向背景
+    Private _RectBack As Border = Nothing
+    Public ReadOnly Property RectBack As Border
+        Get
+            If _RectBack Is Nothing Then
+                Dim Rect As New Border With {
+                    .Name = "RectBack",
+                    .CornerRadius = New CornerRadius(If(IsScaleAnimationEnabled, 6, 0)),
+                    .RenderTransform = If(IsScaleAnimationEnabled, New ScaleTransform(0.8, 0.8), Nothing),
+                    .RenderTransformOrigin = New Point(0.5, 0.5),
+                    .BorderThickness = New Thickness(GetWPFSize(1)),
+                    .SnapsToDevicePixels = True,
+                    .IsHitTestVisible = False,
+                    .Opacity = 0
+                }
+                Rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7")
+                Rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6")
+                SetColumnSpan(Rect, 999)
+                SetRowSpan(Rect, 999)
+                Children.Insert(0, Rect)
+                _RectBack = Rect
+                '<!--<Border x:Name = "RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True" 
+                'IsHitTestVisible = "False" Opacity="0" BorderThickness="1" 
+                'Grid.ColumnSpan = "4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}"/>-->
+            End If
+            Return _RectBack
+        End Get
+    End Property
+
+    '按钮
+    Public ButtonStack As FrameworkElement
+
+    '图标
+    Public PathLogo As FrameworkElement
+
+    '勾选条
+    Public RectCheck As Border
+
+    '副文本
+    Private _LabInfo As TextBlock = Nothing
+    Public ReadOnly Property LabInfo As TextBlock
+        Get
+            If _LabInfo Is Nothing Then
+                Dim Lab As New TextBlock With {
+                    .Name = "LabInfo",
+                    .SnapsToDevicePixels = False,
+                    .UseLayoutRounding = False,
+                    .HorizontalAlignment = HorizontalAlignment.Left,
+                    .IsHitTestVisible = False,
+                    .TextTrimming = TextTrimming.CharacterEllipsis,
+                    .Visibility = Visibility.Collapsed,
+                    .FontSize = 12,
+                    .Margin = New Thickness(4, 0, 0, 0),
+                    .Opacity = 0.6
+                }
+                SetColumn(Lab, 3)
+                SetRow(Lab, 2)
+                Children.Add(Lab)
+                _LabInfo = Lab
+                '<TextBlock Grid.Row="2" SnapsToDevicePixels="False" UseLayoutRounding="False" HorizontalAlignment="Left" x:Name = "LabInfo" IsHitTestVisible="False" Grid.Column="2" 
+                'TextTrimming = "CharacterEllipsis" Visibility="Collapsed" FontSize="12" Foreground="{StaticResource ColorBrushGray2}" Margin="4,0,0,0" />
+            End If
+            Return _LabInfo
+        End Get
+    End Property
+
+#End Region
+
+#Region "自定义属性"
+
+    'Uuid
+    Public Uuid As Integer = GetUuid()
+
+    ''' <summary>
+    ''' 是否启用缩放动画。
+    ''' </summary>
+    Public Property IsScaleAnimationEnabled As Boolean
+        Get
+            Return _IsScaleAnimationEnabled
+        End Get
+        Set
+            _IsScaleAnimationEnabled = Value
+            If _RectBack IsNot Nothing Then RectBack.CornerRadius = New CornerRadius(If(Value, 6, 0))
+        End Set
+    End Property
+    Private _IsScaleAnimationEnabled As Boolean = True
+
+    '边距
+    Public Property PaddingLeft As Integer
+        Get
+            Return ColumnPaddingLeft.Width.Value
+        End Get
+        Set(value As Integer)
+            ColumnPaddingLeft.Width = New GridLength(value)
+        End Set
+    End Property
+    Public Property PaddingRight As Integer
+        Get
+            Return ColumnPaddingRight.Width.Value
+        End Get
+        Set(value As Integer)
+            ColumnPaddingRight.Width = New GridLength(value)
+        End Set
+    End Property
+
+    '按钮
+    Private _Buttons As IEnumerable
+    Public Property Buttons As IEnumerable
+        Get
+            Return _Buttons
+        End Get
+        Set(value As IEnumerable)
+            _Buttons = value
+            '没有特殊按钮,移除原 Stack
+            If Not IsNothing(ButtonStack) Then
+                Children.Remove(ButtonStack)
+                ButtonStack = Nothing
+            End If
+            '获取按钮数量
+            Dim Count As Integer = 0
+            For Each Counter In value
+                Count += 1
+            Next
+            '添加新 Stack
+            Select Case Count
+                Case 0
+                    '没有按钮,不添加新的
+                Case 1
+                    '只有一个按钮
+                    For Each Btn As MyIconButton In value
+                        If Btn.Height.Equals(Double.NaN) Then Btn.Height = 23
+                        If Btn.Width.Equals(Double.NaN) Then Btn.Width = 23
+                        With Btn
+                            .Opacity = 0
+                            .Margin = New Thickness(0, 0, 5, 0)
+                            .SnapsToDevicePixels = False
+                            .HorizontalAlignment = HorizontalAlignment.Right
+                            .VerticalAlignment = VerticalAlignment.Center
+                            .SnapsToDevicePixels = False
+                            .UseLayoutRounding = False
+                        End With
+                        SetColumn(Btn, 4)
+                        SetColumnSpan(Btn, 4)
+                        SetRowSpan(Btn, 4)
+                        Children.Add(Btn)
+                        ButtonStack = Btn
+                    Next
+                Case Else
+                    '有复数按钮,使用 StackPanel
+                    ButtonStack = New StackPanel With {.Opacity = 0, .Margin = New Thickness(0, 0, 5, 0), .SnapsToDevicePixels = False, .Orientation = Orientation.Horizontal, .HorizontalAlignment = HorizontalAlignment.Right, .VerticalAlignment = VerticalAlignment.Center, .UseLayoutRounding = False}
+                    SetColumn(ButtonStack, 4)
+                    SetColumnSpan(ButtonStack, 4)
+                    SetRowSpan(ButtonStack, 4)
+                    '构造按钮
+                    For Each Btn As MyIconButton In value
+                        If Btn.Height.Equals(Double.NaN) Then Btn.Height = 23
+                        If Btn.Width.Equals(Double.NaN) Then Btn.Width = 23
+                        CType(ButtonStack, StackPanel).Children.Add(Btn)
+                    Next
+                    Children.Add(ButtonStack)
+            End Select
+        End Set
+    End Property
+
+    '标题
+    Public Property Title As String
+        Get
+            Return GetValue(TitleProperty)
+        End Get
+        Set(value As String)
+            SetValue(TitleProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyListItem))
+
+    '字号
+    Public Property FontSize As Double
+        Get
+            Return GetValue(FontSizeProperty)
+        End Get
+        Set(value As Double)
+            SetValue(FontSizeProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly FontSizeProperty As DependencyProperty = DependencyProperty.Register("FontSize", GetType(Double), GetType(MyListItem), New PropertyMetadata(CType(14, Double)))
+
+    '信息
+    Private _Info As String = ""
+    Public Property Info As String
+        Get
+            Return _Info
+        End Get
+        Set(value As String)
+            If _Info = value Then Exit Property
+            value = value.Replace(vbCr, "").Replace(vbLf, "")
+            _Info = value
+            LabInfo.Text = value
+            LabInfo.Visibility = If(value = "", Visibility.Collapsed, Visibility.Visible)
+        End Set
+    End Property
+
+    '图片
+    Private _Logo As String = ""
+    Public Property Logo As String
+        Get
+            Return _Logo
+        End Get
+        Set(value As String)
+            If _Logo = value Then Exit Property
+            _Logo = value
+            '删除旧 Logo
+            If Not IsNothing(PathLogo) Then Children.Remove(PathLogo)
+            '添加新 Logo
+            If Not _Logo = "" Then
+                If _Logo.ToLower.StartsWith("http") Then
+                    '网络图片
+                    PathLogo = New Image With {
+                            .IsHitTestVisible = LogoClickable,
+                            .Source = New ImageSourceConverter().ConvertFromString(_Logo),
+                            .RenderTransformOrigin = New Point(0.5, 0.5),
+                            .RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale},
+                            .SnapsToDevicePixels = True, .UseLayoutRounding = False}
+                    RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.Linear)
+                ElseIf _Logo.ToLower.EndsWith(".png") Then
+                    '位图
+                    Dim Bitmap = New MyBitmap(_Logo)
+                    PathLogo = New Canvas With {
+                            .IsHitTestVisible = LogoClickable,
+                            .Background = Bitmap,
+                            .RenderTransformOrigin = New Point(0.5, 0.5),
+                            .RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale},
+                            .SnapsToDevicePixels = True, .UseLayoutRounding = False}
+                    'If Bitmap.Pic.Width = 16 AndAlso Bitmap.Pic.Height = 16 Then
+                    '    '使用最适合 16x16 物品图片显示的大小
+                    '    RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.NearestNeighbor)
+                    '    PathLogo.HorizontalAlignment = HorizontalAlignment.Center : PathLogo.VerticalAlignment = VerticalAlignment.Center
+                    '    PathLogo.Width = GetWPFSize(Math.Floor(GetPixelSize(32) / 16) * 16) : PathLogo.Height = PathLogo.Width
+                    'Else
+                    PathLogo.HorizontalAlignment = HorizontalAlignment.Stretch : PathLogo.VerticalAlignment = VerticalAlignment.Stretch
+                    RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.HighQuality)
+                    'End If
+                Else
+                    '矢量图
+                    PathLogo = New Shapes.Path With {
+                        .IsHitTestVisible = LogoClickable, .HorizontalAlignment = HorizontalAlignment.Center, .VerticalAlignment = VerticalAlignment.Center, .Stretch = Stretch.Uniform,
+                        .Data = (New GeometryConverter).ConvertFromString(_Logo),
+                        .RenderTransformOrigin = New Point(0.5, 0.5),
+                        .RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale},
+                        .SnapsToDevicePixels = False, .UseLayoutRounding = False}
+                    PathLogo.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = Me})
+                End If
+                SetColumn(PathLogo, 2)
+                SetRowSpan(PathLogo, 4)
+                OnSizeChanged() '设置边距
+                Children.Add(PathLogo)
+                '图标的点击事件
+                If LogoClickable Then
+                    AddHandler PathLogo.MouseLeave, Sub(sender, e) IsLogoDown = False
+                    AddHandler PathLogo.MouseLeftButtonDown, Sub(sender, e) IsLogoDown = True
+                    AddHandler PathLogo.MouseLeftButtonUp, Sub(sender, e) If IsLogoDown Then IsLogoDown = False : RaiseEvent LogoClick(sender, e)
+                End If
+            End If
+            '改变行距
+            ColumnLogo.Width = New GridLength(If(_Logo = "", 0, 34) + If(Height < 40, 0, 4))
+        End Set
+    End Property
+    Private _LogoScale As Double = 1
+    Public Property LogoScale() As Double
+        Get
+            Return _LogoScale
+        End Get
+        Set(value As Double)
+            _LogoScale = value
+            If Not IsNothing(PathLogo) Then PathLogo.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale}
+        End Set
+    End Property
+
+    '图标的点击
+    Public Property LogoClickable As Boolean = False
+    Private IsLogoDown As Boolean = False
+
+    '勾选选项
+    Public Enum CheckType
+        None
+        Clickable
+        RadioBox
+        CheckBox
+    End Enum
+    Private _Type As CheckType = CheckType.None
+    Public Property Type As CheckType
+        Get
+            Return _Type
+        End Get
+        Set(value As CheckType)
+            If _Type = value Then Exit Property
+            _Type = value
+            '切换左栏大小
+            ColumnCheck.Width = New GridLength(If(_Type = CheckType.None OrElse _Type = CheckType.Clickable, If(Height < 40, 4, 2), 6))
+            '切换竖条控件
+            If _Type = CheckType.None OrElse _Type = CheckType.Clickable Then
+                '移除竖条控件
+                If Not IsNothing(RectCheck) Then
+                    Children.Remove(RectCheck)
+                    RectCheck = Nothing
+                End If
+                SetChecked(False, False, False)
+            Else
+                '添加竖条控件
+                If IsNothing(RectCheck) Then
+                    RectCheck = New Border With {.Width = 5, .Height = If(Checked, Double.NaN, 0), .CornerRadius = New CornerRadius(2, 2, 2, 2),
+                        .VerticalAlignment = If(Checked, VerticalAlignment.Stretch, VerticalAlignment.Center), .HorizontalAlignment = HorizontalAlignment.Left, .UseLayoutRounding = False, .SnapsToDevicePixels = False,
+                        .Margin = If(Checked, New Thickness(-1, 6, 0, 6), New Thickness(-1, 0, 0, 0))}
+                    RectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3")
+                    SetRowSpan(RectCheck, 4)
+                    Children.Add(RectCheck)
+                End If
+            End If
+        End Set
+    End Property
+
+    '适应尺寸
+    Private Sub OnSizeChanged() Handles Me.SizeChanged
+        ColumnCheck.Width = New GridLength(If(_Type = CheckType.None OrElse _Type = CheckType.Clickable, If(Height < 40, 4, 2), 6))
+        ColumnLogo.Width = New GridLength(If(_Logo = "", 0, 34) + If(Height < 40, 0, 4))
+        If Not IsNothing(PathLogo) Then
+            If _Logo.EndsWith(".png") Then
+                PathLogo.Margin = New Thickness(4, 5, 3, 5)
+            Else
+                PathLogo.Margin = New Thickness(If(Height < 40, 6, 8), 8, If(Height < 40, 4, 6), 8)
+            End If
+        End If
+        LabTitle.Margin = New Thickness(4, 0, 0, If(Height < 40, 0, 2))
+    End Sub
+
+    '勾选状态
+    Private _Checked As Boolean = False
+    Public Property Checked As Boolean
+        Get
+            Return _Checked
+        End Get
+        Set(value As Boolean)
+            SetChecked(value, False, True)
+        End Set
+    End Property
+    ''' <summary>
+    ''' 手动设置 Checked 属性。
+    ''' </summary>
+    ''' <param name="value">新的 Checked 属性。</param>
+    ''' <param name="user">是否由用户引发。</param>
+    ''' <param name="anime">是否执行动画。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+        Try
+
+            '自定义属性基础
+
+            Dim ChangedEventArgs As New RouteEventArgs(user)
+            Dim RawValue = _Checked
+            If Type = CheckType.RadioBox Then
+                If IsInitialized AndAlso Not value = _Checked Then
+                    _Checked = value
+                    RaiseEvent Changed(Me, ChangedEventArgs)
+                    If ChangedEventArgs.Handled Then
+                        _Checked = RawValue
+                        Exit Sub
+                    End If
+                End If
+                _Checked = value
+            Else
+                If value = _Checked Then Exit Sub
+                _Checked = value
+                If IsInitialized Then
+                    RaiseEvent Changed(Me, ChangedEventArgs)
+                    If ChangedEventArgs.Handled Then
+                        _Checked = RawValue
+                        Exit Sub
+                    End If
+                End If
+            End If
+            If value Then
+                Dim CheckEventArgs As New RouteEventArgs(user)
+                RaiseEvent Check(Me, CheckEventArgs)
+                If CheckEventArgs.Handled Then Exit Sub
+            End If
+
+            '保证只有一个单选 ListItem 选中
+
+            If Type = CheckType.RadioBox Then
+                If IsNothing(Parent) Then Exit Sub
+                Dim RadioboxList As New List(Of MyListItem)
+                Dim CheckedCount As Integer = 0
+                '收集控件列表与选中个数
+                For Each Control In CType(Parent, Object).Children
+                    If TypeOf Control Is MyListItem AndAlso CType(Control, MyListItem).Type = CheckType.RadioBox Then
+                        RadioboxList.Add(Control)
+                        If Control.Checked Then CheckedCount += 1
+                    End If
+                Next
+                '判断选中情况
+                Select Case CheckedCount
+                    Case 0
+                        '没有任何单选框被选中,选择第一个
+                        RadioboxList(0).Checked = True
+                    Case Is > 1
+                        '选中项目多于 1 个
+                        If Me.Checked Then
+                            '如果本控件选中,则取消其他所有控件的选中
+                            For Each Control As MyListItem In RadioboxList
+                                If Control.Checked AndAlso Not Control.Equals(Me) Then Control.Checked = False
+                            Next
+                        Else
+                            '如果本控件未选中,则只保留第一个选中的控件
+                            Dim FirstChecked = False
+                            For Each Control As MyListItem In RadioboxList
+                                If Control.Checked Then
+                                    If FirstChecked Then
+                                        Control.Checked = False '修改 Checked 会自动触发 Change 事件,所以不用额外触发
+                                    Else
+                                        FirstChecked = True
+                                    End If
+                                End If
+                            Next
+                        End If
+                End Select
+            End If
+
+            '更改动画
+
+            If IsLoaded AndAlso AniControlEnabled = 0 AndAlso anime Then '防止默认属性变更触发动画
+                Dim Anim As New List(Of AniData)
+                If Checked Then
+                    '由无变有
+                    If Not IsNothing(RectCheck) Then
+                        Dim Delta = ActualHeight - RectCheck.ActualHeight - 12
+                        Anim.Add(AaHeight(RectCheck, Delta * 0.4, 200,, New AniEaseOutFluent(AniEasePower.Weak)))
+                        Anim.Add(AaHeight(RectCheck, Delta * 0.6, 300,, New AniEaseOutBack(AniEasePower.Weak)))
+                        Anim.Add(AaOpacity(RectCheck, 1 - RectCheck.Opacity, 30))
+                        RectCheck.VerticalAlignment = VerticalAlignment.Center
+                        RectCheck.Margin = New Thickness(-1, 0, 0, 0)
+                    End If
+                    Anim.Add(AaColor(Me, ForegroundProperty, "ColorBrush3", 200))
+                Else
+                    '由有变无
+                    If Not IsNothing(RectCheck) Then
+                        'Anim.Add(AaWidth(RectCheck, -RectCheck.Width, 120,, New AniEaseInFluent))
+                        Anim.Add(AaHeight(RectCheck, -RectCheck.ActualHeight, 120,, New AniEaseInFluent(AniEasePower.Weak)))
+                        Anim.Add(AaOpacity(RectCheck, -RectCheck.Opacity, 70, 40))
+                        RectCheck.VerticalAlignment = VerticalAlignment.Center
+                    End If
+                    Anim.Add(AaColor(Me, ForegroundProperty, "ColorBrush1", 120))
+                End If
+                AniStart(Anim, "MyListItem Checked " & Uuid)
+            Else
+                '不使用动画
+                If Checked Then
+                    'If Not IsNothing(RectCheck) Then RectCheck.Width = 4
+                    If Not IsNothing(RectCheck) Then
+                        RectCheck.Height = Double.NaN
+                        RectCheck.Margin = New Thickness(-1, 6, 0, 6)
+                        RectCheck.Opacity = 1
+                        RectCheck.VerticalAlignment = VerticalAlignment.Stretch
+                    End If
+                    SetResourceReference(ForegroundProperty, "ColorBrush3")
+                Else
+                    'If Not IsNothing(RectCheck) Then RectCheck.Width = 0
+                    If Not IsNothing(RectCheck) Then
+                        RectCheck.Height = 0
+                        RectCheck.Margin = New Thickness(-1, 0, 0, 0)
+                        RectCheck.Opacity = 0
+                        RectCheck.VerticalAlignment = VerticalAlignment.Center
+                    End If
+                    SetResourceReference(ForegroundProperty, "ColorBrush1")
+                End If
+            End If
+
+        Catch ex As Exception
+            Log(ex, "设置 Checked 失败")
+        End Try
+    End Sub
+
+    '前景色绑定
+    Public Property Foreground As Brush
+        Get
+            Return GetValue(ForegroundProperty)
+        End Get
+        Set(value As Brush)
+            SetValue(ForegroundProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly ForegroundProperty As DependencyProperty = DependencyProperty.Register("Foreground", GetType(Brush), GetType(MyListItem), New PropertyMetadata(CType(Color1, SolidColorBrush)))
+
+    '菜单与按钮绑定
+    Public ContentHandler As Action(Of MyListItem, EventArgs)
+
+#End Region
+
+#Region "点击"
+
+    '触发点击事件
+    Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonUp
+        If IsMouseDown Then
+            RaiseEvent Click(sender, e)
+            If e.Handled Then Exit Sub
+            '触发自定义事件
+            If Not String.IsNullOrEmpty(EventType) Then
+                ModEvent.TryStartEvent(EventType, EventData)
+                e.Handled = True
+            End If
+            If e.Handled Then Exit Sub
+            '实际的单击处理
+            Select Case Type
+                Case CheckType.Clickable
+                    Log("[Control] 按下单击列表项:" & Title)
+                Case CheckType.RadioBox
+                    Log("[Control] 按下单选列表项:" & Title)
+                    If Not Checked Then SetChecked(True, True, True)
+                Case CheckType.CheckBox
+                    Log("[Control] 按下复选列表项(" & (Not Checked).ToString & "):" & Title)
+                    SetChecked(Not Checked, True, True)
+            End Select
+        End If
+        'AniStart({
+        '       AaScaleTransform(Me, 1.004 - CType(RenderTransform, ScaleTransform).ScaleX, 150,, New AniEaseOutFluent),
+        '       AaScaleTransform(Me, -0.004, 100, 150, New AniEaseInFluent)
+        '   }, "MyListItem Scale " & Uuid)
+    End Sub
+
+    '鼠标点击判定
+    Private IsMouseDown As Boolean = False
+    Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonDown
+        If IsMouseDirectlyOver AndAlso Not Type = CheckType.None Then
+            IsMouseDown = True
+            If Not IsNothing(ButtonStack) Then ButtonStack.IsHitTestVisible = False
+        End If
+        'AniStart({
+        '       AaScaleTransform(Me, 0.985 - CType(RenderTransform, ScaleTransform).ScaleX, 50,, New AniEaseOutFluent)
+        '   }, "MyListItem Scale " & Uuid)
+    End Sub
+    Private Sub Button_MouseLeave(sender As Object, e As Object) Handles Me.MouseLeave, Me.PreviewMouseLeftButtonUp
+        IsMouseDown = False
+        If Not IsNothing(ButtonStack) Then ButtonStack.IsHitTestVisible = True
+    End Sub
+
+    '实现自定义事件
+    Public Property EventType As String
+        Get
+            Return GetValue(EventTypeProperty)
+        End Get
+        Set(value As String)
+            SetValue(EventTypeProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyListItem), New PropertyMetadata(Nothing))
+    Public Property EventData As String
+        Get
+            Return GetValue(EventDataProperty)
+        End Get
+        Set(value As String)
+            SetValue(EventDataProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyListItem), New PropertyMetadata(Nothing))
+
+#End Region
+
+    Private StateLast As String
+    Public IsMouseOverAnimationEnabled As Boolean = True
+    Public Sub RefreshColor(sender As Object, e As EventArgs) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp
+        '菜单虚拟化检测
+        If ContentHandler IsNot Nothing Then
+            ContentHandler(sender, e)
+            ContentHandler = Nothing
+        End If
+        '判断当前颜色
+        Dim StateNew As String, Time As Integer
+        If IsMouseDown AndAlso Not (Type = CheckType.RadioBox AndAlso Checked) Then
+            StateNew = "MouseDown"
+            Time = 120
+        Else
+            If IsMouseOver AndAlso IsMouseOverAnimationEnabled Then
+                StateNew = "MouseOver"
+                Time = 120
+            Else
+                StateNew = "Idle"
+                Time = 180
+            End If
+        End If
+        If StateLast = StateNew Then Exit Sub
+        StateLast = StateNew
+        '触发颜色动画
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            Dim Ani As New List(Of AniData)
+            If IsMouseOver AndAlso IsMouseOverAnimationEnabled Then
+                If ButtonStack IsNot Nothing Then Ani.Add(AaOpacity(ButtonStack, 1 - ButtonStack.Opacity, Time * 0.7, Time * 0.3))
+                Ani.AddRange({
+                             AaColor(RectBack, Border.BackgroundProperty, If(IsMouseDown, "ColorBrush6", "ColorBrush9"), Time),
+                             AaOpacity(RectBack, 1 - RectBack.Opacity, Time,, New AniEaseOutFluent)
+                         })
+                If IsScaleAnimationEnabled Then
+                    Ani.Add(AaScaleTransform(RectBack, 1 - CType(RectBack.RenderTransform, ScaleTransform).ScaleX, Time * 1.6,, New AniEaseOutFluent))
+                    If IsMouseDown Then
+                        Ani.Add(AaScaleTransform(Me, 0.98 - CType(Me.RenderTransform, ScaleTransform).ScaleX, Time * 0.9,, New AniEaseOutFluent))
+                    Else
+                        Ani.Add(AaScaleTransform(Me, 1 - CType(Me.RenderTransform, ScaleTransform).ScaleX, Time * 1.2,, New AniEaseOutFluent))
+                    End If
+                End If
+            Else
+                If ButtonStack IsNot Nothing Then Ani.Add(AaOpacity(ButtonStack, -ButtonStack.Opacity, Time * 0.5))
+                Ani.Add(AaOpacity(RectBack, -RectBack.Opacity, Time))
+                If IsScaleAnimationEnabled Then Ani.AddRange({
+                             AaColor(RectBack, Border.BackgroundProperty, If(IsMouseDown, "ColorBrush6", "ColorBrush7"), Time),
+                             AaScaleTransform(Me, 1 - CType(Me.RenderTransform, ScaleTransform).ScaleX, Time * 3,, New AniEaseOutFluent),
+                             AaScaleTransform(RectBack, 0.996 - CType(RectBack.RenderTransform, ScaleTransform).ScaleX, Time,, New AniEaseOutFluent),
+                             AaScaleTransform(RectBack, -0.246, 1,,, True)
+                         })
+            End If
+            AniStart(Ani, "ListItem Color " & Uuid)
+        Else
+            '无动画
+            If IsMouseOver AndAlso IsMouseOverAnimationEnabled Then
+                If ButtonStack IsNot Nothing Then ButtonStack.Opacity = 1
+                '由于鼠标已经移入,所以直接实例化 RectBack
+                RectBack.Background = Color9
+                RectBack.Opacity = 1
+                RectBack.RenderTransform = New ScaleTransform(1, 1)
+                Me.RenderTransform = New ScaleTransform(1, 1)
+            Else
+                If ButtonStack IsNot Nothing Then ButtonStack.Opacity = 0
+                Me.RenderTransform = New ScaleTransform(1, 1)
+                If _RectBack IsNot Nothing Then
+                    If IsScaleAnimationEnabled Then RectBack.Background = Color7
+                    RectBack.Opacity = 0
+                    RectBack.RenderTransform = New ScaleTransform(0.75, 0.75)
+                End If
+            End If
+            AniStop("ListItem Color " & Uuid)
+        End If
+    End Sub
+
+    Private Sub MyListItem_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        If Checked Then
+            SetResourceReference(ForegroundProperty, "ColorBrush3")
+        Else
+            SetResourceReference(ForegroundProperty, "ColorBrush1")
+        End If
+        If EventType = "打开帮助" Then
+            Try
+                Dim NewElem = New HelpEntry(GetEventAbsoluteUrls(EventData, EventType)(0)).SetToListItem(Me)
+            Catch ex As Exception
+                Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
+            End Try
+        End If
+    End Sub
+    Public Overrides Function ToString() As String
+        Return Title
+    End Function
+
+End Class

+ 37 - 0
Plain Craft Launcher 2/Controls/MyLoading.xaml

@@ -0,0 +1,37 @@
+<Grid
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d" x:Class="MyLoading"
+             MinWidth="50" MinHeight="50" Background="{StaticResource ColorBrushSemiTransparent}">
+    <Grid.RowDefinitions>
+        <RowDefinition Height="1*"/>
+        <RowDefinition Height="Auto"/>
+        <RowDefinition Height="Auto"/>
+        <RowDefinition Height="1*"/>
+    </Grid.RowDefinitions>
+    <Grid UseLayoutRounding="False" SnapsToDevicePixels="False" Width="60" Height="47" HorizontalAlignment="Center" VerticalAlignment="Top" Grid.Row="1">
+        <Path Name="PathPickaxe" Stretch="Uniform" Height="35" Margin="10,6,0,0" StrokeThickness="2"
+              Data="M 963.6 858.2 410.816 305.504 C 508.116 213.304 609.204 196.8 711.104 128.6 837.11367 49.573762 879.34045 50.334062 751.5 49.5 611.3 52 471.8 96.2 353.3 182.4 309.8 155.7 252.1 161.2 214.5 198.9 176.9 236.6 171.3 294.2 198 337.7 111.8 456.3 67.6 595.8 65.1 735.9 63.315254 883.82034 65.077966 837.29308 144.2 695.488 212.4 593.588 228.888 492.4 321.088 395.2 L 873.9 948 c 0.60001 0.59999 1.6 0.6 2.2 0 l 87.5 -87.5 c 0.6 -0.7 0.6 -1.6 0 -2.3 z"
+              Stroke="{Binding Foreground, ElementName=PanBack}" HorizontalAlignment="Left" VerticalAlignment="Top">
+            <Path.RenderTransform>
+                <RotateTransform Angle="55" CenterX="30" CenterY="30" />
+            </Path.RenderTransform>
+        </Path>
+        <Path Opacity="0" Name="PathLeft" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}" Width="3" Height="5" Data="M0,0L2,0L1,4" Margin="7,41,0,0" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" >
+            <Path.RenderTransform>
+                <RotateTransform Angle="-45"/>
+            </Path.RenderTransform>
+        </Path>
+        <Path Opacity="0" Name="PathRight" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}" Width="3" Height="5" Data="M0,0L2,0L1,4" Margin="14,41,0,0" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" >
+            <Path.RenderTransform>
+                <RotateTransform Angle="45"/>
+            </Path.RenderTransform>
+        </Path>
+        <Path Opacity="0" Name="PathError" Stretch="Uniform" Fill="{Binding Foreground, ElementName=PanBack}" Width="17" Height="17" Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" Margin="4,23,0,0" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" >
+            <Path.RenderTransform>
+                <ScaleTransform ScaleX="0.6" ScaleY="0.6" />
+            </Path.RenderTransform>
+        </Path>
+        <Rectangle Width="25" Height="2"  Fill="{Binding Foreground, ElementName=PanBack}" VerticalAlignment="Bottom" HorizontalAlignment="Left" />
+    </Grid>
+    <TextBlock x:Name="LabText" Text="Loading" HorizontalAlignment="Center" TextWrapping="Wrap" VerticalAlignment="Top" Foreground="{Binding Foreground, ElementName=PanBack}" FontSize="16" Margin="0,10,0,0" Grid.Row="2"/>
+</Grid>

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

@@ -0,0 +1,313 @@
+Imports PCL.MyLoading
+
+Public Class MyLoading
+
+    Public Event IsErrorChanged(sender As Object, isError As Boolean)
+    Public Event StateChanged(sender As Object, newState As MyLoadingState, oldState As MyLoadingState)
+    Public Event Click(sender As Object, e As MouseButtonEventArgs)
+
+    Public Property AutoRun As Boolean = True
+    Private Uuid As Integer = GetUuid()
+
+#Region "颜色"
+
+    Public Property Foreground As SolidColorBrush
+        Get
+            Return GetValue(ForegroundProperty)
+        End Get
+        Set(value As SolidColorBrush)
+            SetValue(ForegroundProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly ForegroundProperty As DependencyProperty = DependencyProperty.Register("Foreground", GetType(SolidColorBrush), GetType(MyLoading))
+    Public Sub New()
+        InitializeComponent()
+        SetResourceReference(ForegroundProperty, "ColorBrush3")
+    End Sub
+
+#End Region
+
+#Region "文本"
+
+    Private Property _ShowProgress As Boolean = False
+    Public Property ShowProgress As Boolean
+        Get
+            Return _ShowProgress
+        End Get
+        Set(value As Boolean)
+            If _ShowProgress = value Then Exit Property
+            _ShowProgress = value
+            RefreshText()
+        End Set
+    End Property
+
+    Private _Text As String = "加载中"
+    Public Property Text As String
+        Get
+            Return _Text
+        End Get
+        Set(ByVal value As String)
+            _Text = value
+            RefreshText()
+        End Set
+    End Property
+
+    Private _TextError As String = "加载失败"
+    Public Property TextError As String
+        Get
+            Return _TextError
+        End Get
+        Set(ByVal value As String)
+            _TextError = value
+            RefreshText()
+        End Set
+    End Property
+    ''' <summary>
+    ''' 是否在使用 Loader 时使用 Loader 的错误输出来替换默认的错误文本显示。
+    ''' </summary>
+    Public Property TextErrorInherit As Boolean = True
+
+    Private Sub RefreshText() Handles Me.IsErrorChanged, Me.Loaded
+        If InnerState = MyLoadingState.Error Then
+            If TextErrorInherit AndAlso State.IsLoader Then
+                Dim Ex As Exception = CType(State, Object).Error
+                If Ex Is Nothing Then
+                    LabText.Text = "未知错误"
+                Else
+                    Do While Ex.InnerException IsNot Nothing
+                        Ex = Ex.InnerException
+                    Loop
+                    LabText.Text = StrTrim(Ex.Message)
+                    If LabText.Text.Contains("远程主机强迫关闭了一个现有的连接") OrElse LabText.Text.Contains("操作已超时") OrElse LabText.Text.Contains("操作超时") OrElse LabText.Text.Contains("服务器超时") OrElse LabText.Text.Contains("连接超时") Then LabText.Text = "网络环境不佳,请重试或尝试使用 VPN"
+                End If
+            Else
+                LabText.Text = TextError
+            End If
+        Else
+            If ShowProgress AndAlso State.IsLoader Then
+                LabText.Text = Text & " - " & Math.Floor(CType(State, Object).Progress * 100) & "%"
+            Else
+                LabText.Text = Text
+            End If
+        End If
+    End Sub
+
+#End Region
+
+#Region "状态改变"
+
+    '状态枚举
+    Public Enum MyLoadingState
+        Unloaded = -1
+        Run = 0
+        [Stop] = 1
+        [Error] = 2
+    End Enum
+
+    '用于外部改变的公开状态
+    Private WithEvents _State As ILoadingTrigger
+    Public Property State As ILoadingTrigger
+        Get
+            InitState()
+            Return _State
+        End Get
+        Set(value As ILoadingTrigger)
+            _State = value
+            RefreshState()
+        End Set
+    End Property
+    Private Sub InitState() Handles Me.Loaded
+        If _State Is Nothing Then
+            _State = New MyLoadingStateSimulator
+            If AutoRun Then _State.LoadingState = MyLoadingState.Run
+        End If
+    End Sub
+    Private Sub RefreshState() Handles _State.LoadingStateChanged, Me.Loaded, Me.Unloaded
+        If _State.LoadingState = MyLoadingState.Run AndAlso Not IsLoaded Then InnerState = MyLoadingState.Stop
+        InnerState = _State.LoadingState
+        OuterState = _State.LoadingState
+        AniLoop()
+    End Sub
+
+    '用于引发外部事件的状态
+    Private Property _OuterState As MyLoadingState = MyLoadingState.Unloaded
+    Private Property OuterState As MyLoadingState
+        Get
+            Return _OuterState
+        End Get
+        Set(value As MyLoadingState)
+            If _OuterState = value Then Exit Property
+            Dim OldValue = _OuterState
+            _OuterState = value
+            '引发事件
+            RaiseEvent StateChanged(Me, value, OldValue)
+            If (OldValue = MyLoadingState.Error) <> (value = MyLoadingState.Error) Then RaiseEvent IsErrorChanged(Me, value = MyLoadingState.Error)
+        End Set
+    End Property
+
+
+    '用于引发内部动画事件的状态
+    Private Property _InnerState As MyLoadingState = MyLoadingState.Unloaded
+    Private Property InnerState As MyLoadingState
+        Get
+            Return _InnerState
+        End Get
+        Set(value As MyLoadingState)
+            If _InnerState = value Then Exit Property
+            Dim OldValue = _InnerState
+            _InnerState = value
+            '引发事件
+            AniLoop()
+            If (OldValue = MyLoadingState.Error) <> (value = MyLoadingState.Error) Then ErrorAnimation(Me, value = MyLoadingState.Error)
+        End Set
+    End Property
+
+#End Region
+
+#Region "动画"
+
+    ''' <summary>
+    ''' 是否需要动画。
+    ''' </summary>
+    Public Property HasAnimation As Boolean = True
+
+    ''' <summary>
+    ''' 主动画循环是否正在运行中。
+    ''' </summary>
+    Private IsLooping As Boolean = False
+    Private Sub AniLoop()
+        '这坨循环代码也是老屎坑了,救救.jpg
+        If Not HasAnimation OrElse IsLooping OrElse Not InnerState = MyLoadingState.Run OrElse AniSpeed > 10 OrElse Not IsLoaded Then Exit Sub
+        IsLooping = True
+        ErrorAnimationWaiting = True
+        If ShowProgress Then
+            RefreshText()
+            AniStart({
+                    AaRotateTransform(PathPickaxe, -20 - CType(PathPickaxe.RenderTransform, RotateTransform).Angle, 350, 250, New AniEaseInBack(AniEasePower.Weak)),
+                    AaCode(AddressOf RefreshText, 100), '这是为了省下一个 Timer 线程……
+                    AaCode(AddressOf RefreshText, 200),
+                    AaCode(AddressOf RefreshText, 300),
+                    AaCode(AddressOf RefreshText, 400),
+                    AaCode(AddressOf RefreshText, 500),
+                    AaRotateTransform(PathPickaxe, 50, 900,, New AniEaseOutFluent, True),
+                    AaRotateTransform(PathPickaxe, 25, 900,, New AniEaseOutElastic(AniEasePower.Weak)),
+                    AaCode(Sub()
+                               PathLeft.Opacity = 1
+                               PathLeft.Margin = New Thickness(7, 41, 0, 0)
+                               PathRight.Opacity = 1
+                               PathRight.Margin = New Thickness(14, 41, 0, 0)
+                               ErrorAnimationWaiting = False
+                               RefreshText()
+                           End Sub),
+                    AaCode(AddressOf RefreshText, 100),
+                    AaCode(AddressOf RefreshText, 200),
+                    AaCode(AddressOf RefreshText, 300),
+                    AaCode(AddressOf RefreshText, 400),
+                    AaCode(AddressOf RefreshText, 500),
+                    AaCode(AddressOf RefreshText, 600),
+                    AaCode(AddressOf RefreshText, 700),
+                    AaCode(AddressOf RefreshText, 800),
+                    AaOpacity(PathLeft, -1, 100, 70),
+                    AaX(PathLeft, -5, 180, 20, New AniEaseOutFluent),
+                    AaY(PathLeft, -6, 180, 20, New AniEaseOutFluent),
+                    AaOpacity(PathRight, -1, 100, 70),
+                    AaX(PathRight, 5, 180, 20, New AniEaseOutFluent),
+                    AaY(PathRight, -6, 180, 20, New AniEaseOutFluent),
+                    AaCode(Sub()
+                               IsLooping = False
+                               AniLoop()
+                           End Sub,, True)
+            }, "MyLoader Loop " & Uuid & "/" & GetUuid())
+        Else
+            AniStart({
+                    AaRotateTransform(PathPickaxe, -20 - CType(PathPickaxe.RenderTransform, RotateTransform).Angle, 350, 250, New AniEaseInBack(AniEasePower.Weak)),
+                    AaRotateTransform(PathPickaxe, 50, 900,, New AniEaseOutFluent, True),
+                    AaRotateTransform(PathPickaxe, 25, 900,, New AniEaseOutElastic(AniEasePower.Weak)),
+                    AaCode(Sub()
+                               PathLeft.Opacity = 1
+                               PathLeft.Margin = New Thickness(7, 41, 0, 0)
+                               PathRight.Opacity = 1
+                               PathRight.Margin = New Thickness(14, 41, 0, 0)
+                               ErrorAnimationWaiting = False
+                           End Sub),
+                    AaOpacity(PathLeft, -1, 100, 50),
+                    AaX(PathLeft, -5, 180,, New AniEaseOutFluent),
+                    AaY(PathLeft, -6, 180,, New AniEaseOutFluent),
+                    AaOpacity(PathRight, -1, 100, 50),
+                    AaX(PathRight, 5, 180,, New AniEaseOutFluent),
+                    AaY(PathRight, -6, 180,, New AniEaseOutFluent),
+                    AaCode(Sub()
+                               IsLooping = False
+                               AniLoop()
+                           End Sub,, True)
+            }, "MyLoader Loop " & Uuid & "/" & GetUuid())
+        End If
+    End Sub
+
+    ''' <summary>
+    ''' 镐子是否还没挥下去,要求错误动画等待。
+    ''' </summary>
+    Private ErrorAnimationWaiting As Boolean = False
+    Private Sub ErrorAnimation(sender As Object, isError As Boolean)
+        If isError Then
+            '非错误变为错误
+            Dim Wait As Integer = If(ErrorAnimationWaiting, 400, 0)
+            AniStart({
+                AaColor(PanBack, ForegroundProperty, "ColorBrushRedLight", 300),
+                AaOpacity(PathError, 1 - PathError.Opacity, 100, 300 + Wait),
+                AaScaleTransform(PathError, 1 - CType(PathError.RenderTransform, ScaleTransform).ScaleX, 400, 300 + Wait, New AniEaseOutBack)
+            }, "MyLoader Error " & Uuid)
+        Else
+            '错误变为非错误
+            AniStart({
+                AaOpacity(PathError, -PathError.Opacity, 100),
+                AaScaleTransform(PathError, 0.5 - CType(PathError.RenderTransform, ScaleTransform).ScaleX, 200),
+                AaColor(PanBack, ForegroundProperty, "ColorBrush3", 300)
+            }, "MyLoader Error " & Uuid)
+        End If
+    End Sub
+
+#End Region
+
+#Region "点击事件"
+
+    Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
+        RaiseEvent Click(sender, e)
+    End Sub
+    Private IsMouseDown As Boolean = False
+    Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
+        '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+        IsMouseDown = True
+    End Sub
+    Private Sub Button_MouseLeave(sender As Object, e As Object) Handles Me.MouseLeave, Me.MouseLeftButtonUp
+        IsMouseDown = False
+    End Sub
+
+#End Region
+
+End Class
+
+Public Interface ILoadingTrigger
+    ReadOnly Property IsLoader As Boolean
+    Property LoadingState As MyLoadingState
+    Event LoadingStateChanged(NewState As MyLoadingState, OldState As MyLoadingState)
+End Interface
+
+Public Class MyLoadingStateSimulator
+    Implements ILoadingTrigger
+    Private Property _LoadingState As MyLoadingState = MyLoadingState.Unloaded
+    Public Property LoadingState As MyLoadingState Implements ILoadingTrigger.LoadingState
+        Get
+            Return _LoadingState
+        End Get
+        Set(value As MyLoadingState)
+            If _LoadingState = value Then Exit Property
+            Dim OldState = _LoadingState
+            _LoadingState = value
+            RaiseEvent LoadingStateChanged(value, OldState)
+        End Set
+    End Property
+    Public ReadOnly Property IsLoader As Boolean = False Implements ILoadingTrigger.IsLoader
+
+    Public Event LoadingStateChanged(NewState As MyLoadingState, OldState As MyLoadingState) Implements ILoadingTrigger.LoadingStateChanged
+End Class

+ 55 - 0
Plain Craft Launcher 2/Controls/MyMenuItem.vb

@@ -0,0 +1,55 @@
+Public Class MyMenuItem
+    Inherits MenuItem
+
+    Private Sub MyMenuItem_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        If Icon IsNot Nothing Then
+            Dim IconControl As Shapes.Path = GetTemplateChild("Icon")
+            If IconControl IsNot Nothing Then IconControl.Data = (New GeometryConverter).ConvertFromString(Icon)
+        End If
+    End Sub
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+
+    '指向动画
+
+    Private Const AnimationTimeIn As Integer = 100
+    Private Const AnimationTimeOut As Integer = 200
+    Private ColorName As String
+    Private Sub RefreshColor() Handles Me.MouseEnter, Me.MouseLeave, Me.IsEnabledChanged
+        '判断当前颜色
+        Dim BackName As String, ForeName As String
+        Dim Time As Integer
+        If Not IsEnabled Then
+            BackName = "ColorBrushTransparent"
+            ForeName = "ColorBrushGray5"
+            Time = AnimationTimeOut
+        ElseIf IsMouseOver Then
+            BackName = "ColorBrush6"
+            ForeName = "ColorBrush2"
+            Time = AnimationTimeIn
+        Else
+            BackName = "ColorBrushTransparent"
+            ForeName = "ColorBrush1"
+            Time = AnimationTimeOut
+        End If
+        '重复性验证
+        If ColorName = BackName Then Exit Sub
+        ColorName = BackName
+        '触发颜色动画
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            AniStart({
+                     AaColor(Me, BackgroundProperty, BackName, Time),
+                     AaColor(Me, ForegroundProperty, ForeName, Time)
+                 }, "MyMenuItem Color " & Uuid)
+        Else
+            '无动画
+            AniStop("MyMenuItem Color " & Uuid)
+            SetResourceReference(BackgroundProperty, BackName)
+            SetResourceReference(ForegroundProperty, ForeName)
+        End If
+    End Sub
+
+End Class

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

@@ -0,0 +1,23 @@
+<Grid x:Class="MyMsgInput"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:my="clr-namespace:PCL"
+             UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="500" HorizontalAlignment="Center" VerticalAlignment="Center">
+    <Border Name="PanBorder" CornerRadius="5" Width="{Binding ActualWidth, ElementName=PanMain, Mode=OneWay}" Height="{Binding ActualHeight, ElementName=PanMain, Mode=OneWay}" Background="#00FBFBFB">
+        <Border.Effect>
+            <DropShadowEffect Color="{DynamicResource ColorObject1}" BlurRadius="15" ShadowDepth="0" Opacity="0" x:Name="EffectShadow" />
+        </Border.Effect>
+    </Border>
+    <StackPanel Name="PanMain" VerticalAlignment="Top">
+        <TextBlock FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}" HorizontalAlignment="Left" Name="LabTitle" Margin="27,19,28,0" Text="测试标题文本" VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
+        <Rectangle Height="2" Fill="{DynamicResource ColorBrush2}" Margin="20,9,20,15" Name="ShapeLine" Opacity="0" />
+        <Border Name="PanCaption" Padding="28,0,28,15">
+            <my:MyTextBox x:Name="TextCaption" UseLayoutRounding="False" MaxLength="1000" Width="450" Margin="1" />
+        </Border>
+        <StackPanel Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="150,0,22,20" Orientation="Horizontal">
+            <my:MyButton ColorType="Normal" Text="测试按钮1" x:Name="Btn1" Margin="15,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+            <my:MyButton ColorType="Normal" Text="测试按钮2" x:Name="Btn2" Margin="15,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+            <my:MyButton ColorType="Normal" Text="测试按钮3" x:Name="Btn3" Margin="15,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+        </StackPanel>
+    </StackPanel>
+</Grid>

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

@@ -0,0 +1,134 @@
+Public Class MyMsgInput
+
+    Private ReadOnly MyConverter As MyMsgBoxConverter
+    Private ReadOnly Uuid As Integer = GetUuid()
+
+    Public Sub New(Converter As MyMsgBoxConverter)
+        Try
+
+            InitializeComponent()
+            Btn1.Name = Btn1.Name & GetUuid()
+            Btn2.Name = Btn2.Name & GetUuid()
+            Btn3.Name = Btn3.Name & GetUuid()
+            MyConverter = Converter
+            LabTitle.Text = Converter.Title
+            TextCaption.Text = Converter.Content
+            TextCaption.HintText = Converter.HintText
+            TextCaption.ValidateRules = Converter.ValidateRules
+            Btn1.Text = Converter.Button1
+            If Converter.IsWarn Then
+                Btn1.ColorType = MyButton.ColorState.Red
+                LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight")
+                ShapeLine.SetResourceReference(Rectangle.FillProperty, "ColorBrushRedLight")
+            End If
+            Btn2.Text = Converter.Button2
+            Btn3.Text = Converter.Button3
+            Btn2.Visibility = If(Converter.Button2 = "", Visibility.Collapsed, Visibility.Visible)
+            Btn3.Visibility = If(Converter.Button3 = "", Visibility.Collapsed, Visibility.Visible)
+            'If Converter.Caption > 0 Then
+            '  TextCaption.Focus()
+            '  TextCaption.Select(Converter.Caption.Length - 1, 0)
+            'End If
+
+        Catch ex As Exception
+            Log(ex, "输入弹窗初始化失败", LogLevel.Hint)
+        End Try
+    End Sub
+
+    Private Sub Load(sender As Object, e As EventArgs) Handles MyBase.Loaded
+        Try
+
+            'UI 初始化
+            If Btn2.IsVisible AndAlso Not Btn1.ColorType = MyButton.ColorState.Red Then Btn1.ColorType = MyButton.ColorState.Highlight
+            ShapeLine.StrokeThickness = GetWPFSize(1)
+            Measure(New Size(FrmMain.Width, FrmMain.Height))
+            Arrange(New Rect(0, 0, FrmMain.Width, FrmMain.Height))
+            If FrmMain.Width - ActualWidth < 20 Then Width = FrmMain.Width - GetWPFSize(2)
+            TextCaption.Focus()
+            '动画
+            AniStart({
+                AaColor(FrmMain.PanMsg, Grid.BackgroundProperty, New MyColor(60, 0, 0, 0), 250, , New AniEaseInFluent),
+                AaColor(PanBorder, Border.BackgroundProperty, New MyColor(255, 0, 0, 0), 150, , New AniEaseInFluent),
+                AaOpacity(EffectShadow, 0.75, 400, 50),
+                AaWidth(ShapeLine, ShapeLine.ActualWidth, 250, 100, New AniEaseOutFluent),
+                AaOpacity(ShapeLine, 1, 200, 100),
+                AaWidth(LabTitle, LabTitle.ActualWidth, 200, 100, New AniEaseOutFluent),
+                AaOpacity(LabTitle, 1, 150, 150),
+                AaOpacity(PanCaption, 1, 150, 150),
+                AaOpacity(Btn1, 1, 150, 100),
+                AaOpacity(Btn2, 1, 150, 150),
+                AaOpacity(Btn3, 1, 150, 200),
+                AaCode(Sub()
+                           ShapeLine.MinWidth = ShapeLine.ActualWidth
+                           ShapeLine.HorizontalAlignment = HorizontalAlignment.Stretch
+                           ShapeLine.Width = Double.NaN
+                           LabTitle.Width = Double.NaN
+                           LabTitle.TextTrimming = TextTrimming.CharacterEllipsis
+                       End Sub, 350)
+            }, "MyMsgBox Start " & Uuid)
+            '动画初始化
+            ShapeLine.Width = 0
+            ShapeLine.HorizontalAlignment = HorizontalAlignment.Center
+            LabTitle.Width = 0
+            LabTitle.Opacity = 0
+            LabTitle.TextWrapping = TextWrapping.NoWrap
+            PanCaption.Opacity = 0
+            '记录日志
+            Log("[Control] 输入弹窗:" & LabTitle.Text)
+
+        Catch ex As Exception
+            Log(ex, "输入弹窗加载失败", LogLevel.Hint)
+        End Try
+    End Sub
+    Private Sub Close()
+        '结束线程阻塞
+        MyConverter.WaitFrame.Continue = False
+        Interop.ComponentDispatcher.PopModal()
+        '弹窗动画
+        LabTitle.TextTrimming = TextTrimming.None
+        LabTitle.TextWrapping = TextWrapping.NoWrap
+        AniStart({
+            AaColor(FrmMain.PanMsg, Grid.BackgroundProperty, New MyColor(-60, 0, 0, 0), 350, , New AniEaseInFluent),
+            AaColor(PanBorder, Border.BackgroundProperty, New MyColor(-255, 0, 0, 0), 300, 100, New AniEaseInFluent),
+            AaOpacity(EffectShadow, -0.75, 150),
+            AaWidth(ShapeLine, -ShapeLine.ActualWidth, 250, , New AniEaseInFluent),
+            AaOpacity(ShapeLine, -1, 150, 100),
+            AaWidth(LabTitle, -LabTitle.ActualWidth, 250),
+            AaOpacity(LabTitle, -1, 200),
+            AaOpacity(PanCaption, -1, 200),
+            AaOpacity(Btn1, -1, 150),
+            AaOpacity(Btn2, -1, 150, 50),
+            AaOpacity(Btn3, -1, 150, 100),
+            AaCode(Sub() CType(Parent, Grid).Children.Remove(Me), , True)
+            }, "MyMsgBox Close " & Uuid)
+    End Sub
+
+    Public Sub Btn1_Click() Handles Btn1.Click
+        If MyConverter.IsExited OrElse Not TextCaption.ValidateResult = "" Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = TextCaption.Text
+        Close()
+    End Sub
+    Private Sub Btn2_Click() Handles Btn2.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = Nothing
+        Close()
+    End Sub
+    Private Sub Btn3_Click() Handles Btn3.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = Nothing
+        Close()
+    End Sub
+
+    Private Sub TextCaption_ValidateChanged(sender As Object, e As EventArgs) Handles TextCaption.ValidateChanged
+        Btn1.IsEnabled = TextCaption.ValidateResult = ""
+    End Sub
+
+    Private Sub Drag(sender As Object, e As MouseButtonEventArgs) Handles PanBorder.MouseLeftButtonDown, LabTitle.MouseLeftButtonDown
+        On Error Resume Next
+        If e.GetPosition(ShapeLine).Y <= 2 Then FrmMain.DragMove()
+    End Sub
+
+End Class

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

@@ -0,0 +1,27 @@
+<Grid x:Name="PanBack" x:Class="MyMsgSelect"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:my="clr-namespace:PCL"
+             UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="450" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
+    <Grid.RowDefinitions>
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="1*" />
+        <RowDefinition Height="Auto" />
+    </Grid.RowDefinitions>
+    <Border Name="PanBorder" Grid.RowSpan="99" Width="{Binding ActualWidth, ElementName=PanBack, Mode=OneWay}" Height="{Binding ActualHeight, ElementName=PanBack, Mode=OneWay}" Background="#00FBFBFB" CornerRadius="5">
+        <Border.Effect>
+            <DropShadowEffect Color="{DynamicResource ColorObject1}" BlurRadius="15" ShadowDepth="0" Opacity="0" x:Name="EffectShadow" />
+        </Border.Effect>
+    </Border>
+    <TextBlock FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}" HorizontalAlignment="Left" Name="LabTitle" Margin="28,19,28,0" Text="测试标题文本" VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
+    <Rectangle Height="2" Grid.Row="1" Fill="{DynamicResource ColorBrush2}" Margin="20,9,20,13" Name="ShapeLine" Opacity="0" />
+    <my:MyScrollViewer x:Name="PanCaption" Grid.Row="2" Margin="19,0,19,20" Padding="9,0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" SnapsToDevicePixels="True" UseLayoutRounding="True" DeltaMuity="0.5">
+        <StackPanel x:Name="PanSelection" Orientation="Vertical" />
+    </my:MyScrollViewer>
+    <StackPanel Name="PanBtn" Grid.Row="3" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="150,0,22,20" Orientation="Horizontal">
+        <my:MyButton ColorType="Normal" Text="测试按钮1" x:Name="Btn1" Margin="15,0,0,0" TextPadding="7"  SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" IsEnabled="False" />
+        <my:MyButton ColorType="Normal" Text="测试按钮2" x:Name="Btn2" Margin="15,0,0,0" TextPadding="7" Visibility="Visible" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+        <my:MyButton ColorType="Normal" Text="测试按钮3" x:Name="Btn3" TextPadding="7" Margin="15,0,0,0" Visibility="Visible" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+    </StackPanel>
+</Grid>

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

@@ -0,0 +1,138 @@
+Public Class MyMsgSelect
+
+    Private ReadOnly MyConverter As MyMsgBoxConverter
+    Private ReadOnly Uuid As Integer = GetUuid()
+
+    Public Sub New(Converter As MyMsgBoxConverter)
+        Try
+
+            InitializeComponent()
+            Btn1.Name = Btn1.Name & GetUuid()
+            Btn2.Name = Btn2.Name & GetUuid()
+            Btn3.Name = Btn3.Name & GetUuid()
+            MyConverter = Converter
+            LabTitle.Text = Converter.Title
+            Btn1.Text = Converter.Button1
+            If Converter.IsWarn Then
+                Btn1.ColorType = MyButton.ColorState.Red
+                LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight")
+                ShapeLine.SetResourceReference(Rectangle.FillProperty, "ColorBrushRedLight")
+            End If
+            Btn2.Text = Converter.Button2
+            Btn3.Text = Converter.Button3
+            Btn2.Visibility = If(Converter.Button2 = "", Visibility.Collapsed, Visibility.Visible)
+            Btn3.Visibility = If(Converter.Button3 = "", Visibility.Collapsed, Visibility.Visible)
+            '添加选择控件
+            For Each Selection As IMyRadio In Converter.Content
+                PanSelection.Children.Add(Selection)
+                AddHandler Selection.Check, AddressOf OnChecked
+                If TypeOf Selection Is MyListItem Then
+                    CType(Selection, MyListItem).Type = MyListItem.CheckType.RadioBox
+                    CType(Selection, MyListItem).MinHeight = 24
+                Else
+                    CType(Selection, MyRadioBox).MinHeight = 26
+                End If
+            Next
+
+        Catch ex As Exception
+            Log(ex, "选择弹窗初始化失败", LogLevel.Hint)
+        End Try
+    End Sub
+
+    Private Sub Load(sender As Object, e As EventArgs) Handles MyBase.Loaded
+        Try
+
+            'UI 初始化
+            If Btn2.IsVisible AndAlso Not Btn1.ColorType = MyButton.ColorState.Red Then Btn1.ColorType = MyButton.ColorState.Highlight
+            ShapeLine.StrokeThickness = GetWPFSize(1)
+            Measure(New Size(FrmMain.Width, FrmMain.Height))
+            Arrange(New Rect(0, 0, FrmMain.Width, FrmMain.Height))
+            If FrmMain.Width - ActualWidth < 20 Then Width = FrmMain.Width - GetWPFSize(2)
+            '动画
+            AniStart({
+                AaColor(FrmMain.PanMsg, Grid.BackgroundProperty, New MyColor(60, 0, 0, 0), 250, , New AniEaseInFluent),
+                AaColor(PanBorder, Border.BackgroundProperty, New MyColor(255, 0, 0, 0), 150, , New AniEaseInFluent),
+                AaOpacity(EffectShadow, 0.75, 400, 50),
+                AaWidth(ShapeLine, ShapeLine.ActualWidth, 250, 100, New AniEaseOutFluent),
+                AaOpacity(ShapeLine, 1, 200, 100),
+                AaWidth(LabTitle, LabTitle.ActualWidth, 200, 100, New AniEaseOutFluent),
+                AaOpacity(LabTitle, 1, 150, 150),
+                AaOpacity(PanCaption, 1, 150, 150),
+                AaOpacity(Btn1, 1, 150, 100),
+                AaOpacity(Btn2, 1, 150, 150),
+                AaOpacity(Btn3, 1, 150, 200),
+                AaCode(Sub()
+                           ShapeLine.MinWidth = ShapeLine.ActualWidth
+                           ShapeLine.HorizontalAlignment = HorizontalAlignment.Stretch
+                           ShapeLine.Width = Double.NaN
+                           LabTitle.Width = Double.NaN
+                           LabTitle.TextTrimming = TextTrimming.CharacterEllipsis
+                       End Sub, 350)
+            }, "MyMsgBox Start " & Uuid)
+            '动画初始化
+            ShapeLine.Width = 0
+            ShapeLine.HorizontalAlignment = HorizontalAlignment.Center
+            LabTitle.Width = 0
+            LabTitle.Opacity = 0
+            LabTitle.TextWrapping = TextWrapping.NoWrap
+            PanCaption.Opacity = 0
+            '记录日志
+            Log("[Control] 选择弹窗:" & LabTitle.Text)
+
+        Catch ex As Exception
+            Log(ex, "选择弹窗加载失败", LogLevel.Hint)
+        End Try
+    End Sub
+    Private Sub Close()
+        '结束线程阻塞
+        MyConverter.WaitFrame.Continue = False
+        Interop.ComponentDispatcher.PopModal()
+        '弹窗动画
+        LabTitle.TextTrimming = TextTrimming.None
+        LabTitle.TextWrapping = TextWrapping.NoWrap
+        AniStart({
+            AaColor(FrmMain.PanMsg, Grid.BackgroundProperty, New MyColor(-60, 0, 0, 0), 350, , New AniEaseInFluent),
+            AaColor(PanBorder, Border.BackgroundProperty, New MyColor(-255, 0, 0, 0), 300, 100, New AniEaseInFluent),
+            AaOpacity(EffectShadow, -0.75, 150),
+            AaWidth(ShapeLine, -ShapeLine.ActualWidth, 250, , New AniEaseInFluent),
+            AaOpacity(ShapeLine, -1, 150, 100),
+            AaWidth(LabTitle, -LabTitle.ActualWidth, 250),
+            AaOpacity(LabTitle, -1, 200),
+            AaOpacity(PanCaption, -1, 200),
+            AaOpacity(Btn1, -1, 150),
+            AaOpacity(Btn2, -1, 150, 50),
+            AaOpacity(Btn3, -1, 150, 100),
+            AaCode(Sub() CType(Parent, Grid).Children.Remove(Me), , True)
+            }, "MyMsgBox Close " & Uuid)
+    End Sub
+
+    Public Sub Btn1_Click() Handles Btn1.Click
+        If MyConverter.IsExited OrElse SelectedIndex = -1 Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = SelectedIndex
+        Close()
+    End Sub
+    Private Sub Btn2_Click() Handles Btn2.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = Nothing
+        Close()
+    End Sub
+    Private Sub Btn3_Click() Handles Btn3.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = Nothing
+        Close()
+    End Sub
+
+    Private SelectedIndex As Integer = -1
+    Private Sub OnChecked(sender As IMyRadio, e As EventArgs)
+        Btn1.IsEnabled = True
+        SelectedIndex = PanSelection.Children.IndexOf(sender)
+    End Sub
+    Private Sub Drag(sender As Object, e As MouseButtonEventArgs) Handles PanBorder.MouseLeftButtonDown, LabTitle.MouseLeftButtonDown
+        On Error Resume Next
+        If e.GetPosition(ShapeLine).Y <= 2 Then FrmMain.DragMove()
+    End Sub
+
+End Class

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

@@ -0,0 +1,27 @@
+<Grid x:Name="PanBack" x:Class="MyMsgText"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:my="clr-namespace:PCL"
+             UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="450" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
+    <Grid.RowDefinitions>
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="Auto" />
+        <RowDefinition Height="1*" />
+        <RowDefinition Height="Auto" />
+    </Grid.RowDefinitions>
+    <Border Name="PanBorder" Grid.RowSpan="99" Background="#00FBFBFB" CornerRadius="5">
+        <Border.Effect>
+            <DropShadowEffect Color="{DynamicResource ColorObject1}" BlurRadius="15" ShadowDepth="0" Opacity="0" x:Name="EffectShadow" />
+        </Border.Effect>
+    </Border>
+    <TextBlock FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}" HorizontalAlignment="Left" Name="LabTitle" Margin="28,19,28,0" Text="测试标题文本" VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
+    <Rectangle Height="2" Grid.Row="1" Fill="{DynamicResource ColorBrush2}" Margin="20,9,20,13" Name="ShapeLine" Opacity="0" />
+    <my:MyScrollViewer VerticalAlignment="Top" x:Name="PanCaption" Grid.Row="2" Margin="19,0,19,20" Padding="9,0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" SnapsToDevicePixels="True" UseLayoutRounding="True" DeltaMuity="0.5">
+        <TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabCaption" FontSize="15" Foreground="#FF5C5C5C" FontWeight="Normal" Padding="1" />
+    </my:MyScrollViewer>
+    <StackPanel Name="PanBtn" Grid.Row="3" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="150,0,22,20" Orientation="Horizontal">
+        <my:MyButton ColorType="Normal" Text="测试按钮1" x:Name="Btn1" Margin="15,0,0,0" TextPadding="7"  SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+        <my:MyButton ColorType="Normal" Text="测试按钮2" x:Name="Btn2" Margin="15,0,0,0" TextPadding="7" Visibility="Visible" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+        <my:MyButton ColorType="Normal" Text="测试按钮3" x:Name="Btn3" TextPadding="7" Margin="15,0,0,0" Visibility="Visible" SnapsToDevicePixels="False" Padding="5,0" Opacity="0" UseLayoutRounding="False" />
+    </StackPanel>
+</Grid>

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

@@ -0,0 +1,125 @@
+Public Class MyMsgText
+
+    Private ReadOnly MyConverter As MyMsgBoxConverter
+    Private ReadOnly Uuid As Integer = GetUuid()
+
+    Public Sub New(Converter As MyMsgBoxConverter)
+        Try
+
+            InitializeComponent()
+            Btn1.Name = Btn1.Name & GetUuid()
+            Btn2.Name = Btn2.Name & GetUuid()
+            Btn3.Name = Btn3.Name & GetUuid()
+            MyConverter = Converter
+            LabTitle.Text = Converter.Title
+            LabCaption.Text = Converter.Content
+            Btn1.Text = Converter.Button1
+            If Converter.IsWarn Then
+                Btn1.ColorType = MyButton.ColorState.Red
+                LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight")
+                ShapeLine.SetResourceReference(Rectangle.FillProperty, "ColorBrushRedLight")
+            End If
+            Btn2.Text = Converter.Button2
+            Btn3.Text = Converter.Button3
+            Btn2.Visibility = If(Converter.Button2 = "", Visibility.Collapsed, Visibility.Visible)
+            Btn3.Visibility = If(Converter.Button3 = "", Visibility.Collapsed, Visibility.Visible)
+            ShapeLine.StrokeThickness = GetWPFSize(1)
+
+        Catch ex As Exception
+            Log(ex, "普通弹窗初始化失败", LogLevel.Hint)
+        End Try
+    End Sub
+
+    Private Sub Load(sender As Object, e As EventArgs) Handles MyBase.Loaded
+        Try
+
+            'UI 初始化
+            If Btn2.IsVisible AndAlso Not Btn1.ColorType = MyButton.ColorState.Red Then Btn1.ColorType = MyButton.ColorState.Highlight
+            Measure(New Size(FrmMain.Width - 20, FrmMain.Height - 20))
+            Arrange(New Rect(0, 0, FrmMain.Width - 20, FrmMain.Height - 20))
+            Btn1.Focus()
+            '动画
+            AniStart({
+                AaColor(FrmMain.PanMsg, BackgroundProperty, New MyColor(60, 0, 0, 0), 250, , New AniEaseInFluent),
+                AaColor(PanBorder, Border.BackgroundProperty, New MyColor(255, 0, 0, 0), 150, , New AniEaseInFluent),
+                AaOpacity(EffectShadow, 0.75, 400, 50),
+                AaWidth(ShapeLine, ShapeLine.ActualWidth, 250, 100, New AniEaseOutFluent),
+                AaOpacity(ShapeLine, 1, 200, 100),
+                AaWidth(LabTitle, LabTitle.ActualWidth, 200, 150, New AniEaseOutFluent),
+                AaOpacity(LabTitle, 1, 150, 150),
+                AaOpacity(PanCaption, 1, 150, 150),
+                AaOpacity(Btn1, 1, 150, 100),
+                AaOpacity(Btn2, 1, 150, 150),
+                AaOpacity(Btn3, 1, 150, 200),
+                AaCode(Sub()
+                           ShapeLine.MinWidth = ShapeLine.ActualWidth
+                           ShapeLine.HorizontalAlignment = HorizontalAlignment.Stretch
+                           ShapeLine.Width = Double.NaN
+                           LabTitle.Width = Double.NaN
+                           LabTitle.TextTrimming = TextTrimming.CharacterEllipsis
+                       End Sub, 350)
+            }, "MyMsgBox Start " & Uuid)
+            '动画初始化
+            ShapeLine.Width = 0
+            ShapeLine.HorizontalAlignment = HorizontalAlignment.Center
+            LabTitle.Width = 0
+            LabTitle.Opacity = 0
+            LabTitle.TextWrapping = TextWrapping.NoWrap
+            PanCaption.Opacity = 0
+            '记录日志
+            Log("[Control] 普通弹窗:" & LabTitle.Text & vbCrLf & LabCaption.Text)
+
+        Catch ex As Exception
+            Log(ex, "普通弹窗加载失败", LogLevel.Hint)
+        End Try
+    End Sub
+    Private Sub Close()
+        '结束线程阻塞
+        If MyConverter.ForceWait OrElse Not MyConverter.Button2 = "" Then MyConverter.WaitFrame.Continue = False
+        Interop.ComponentDispatcher.PopModal()
+        '弹窗动画
+        LabTitle.TextTrimming = TextTrimming.None
+        LabTitle.TextWrapping = TextWrapping.NoWrap
+        AniStart({
+            AaColor(FrmMain.PanMsg, Grid.BackgroundProperty, New MyColor(-60, 0, 0, 0), 350, , New AniEaseInFluent),
+            AaColor(PanBorder, Border.BackgroundProperty, New MyColor(-255, 0, 0, 0), 300, 100, New AniEaseInFluent),
+            AaOpacity(EffectShadow, -0.75, 150),
+            AaWidth(ShapeLine, -ShapeLine.ActualWidth, 250, , New AniEaseInFluent(AniEasePower.Weak)),
+            AaOpacity(ShapeLine, -1, 200),
+            AaWidth(LabTitle, -LabTitle.ActualWidth, 250),
+            AaOpacity(LabTitle, -1, 200),
+            AaOpacity(PanCaption, -1, 200),
+            AaOpacity(Btn1, -1, 150),
+            AaOpacity(Btn2, -1, 150, 50),
+            AaOpacity(Btn3, -1, 150, 100),
+            AaCode(Sub() CType(Parent, Grid).Children.Remove(Me), , True)
+        }, "MyMsgBox Close " & Uuid)
+        '动画初始化
+        ShapeLine.MinWidth = 0
+    End Sub
+
+    Public Sub Btn1_Click() Handles Btn1.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = 1
+        Close()
+    End Sub
+    Private Sub Btn2_Click() Handles Btn2.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = 2
+        Close()
+    End Sub
+    Private Sub Btn3_Click() Handles Btn3.Click
+        If MyConverter.IsExited Then Exit Sub
+        MyConverter.IsExited = True
+        MyConverter.Result = 3
+        Close()
+    End Sub
+
+    Private Sub Drag(sender As Object, e As MouseButtonEventArgs) Handles PanBorder.MouseLeftButtonDown, LabTitle.MouseLeftButtonDown
+        On Error Resume Next
+        If e.GetPosition(ShapeLine).Y <= 2 Then FrmMain.DragMove()
+    End Sub
+
+End Class

+ 100 - 0
Plain Craft Launcher 2/Controls/MyPageLeft.vb

@@ -0,0 +1,100 @@
+Public Class MyPageLeft
+    Inherits Grid
+    Private Uuid As Integer = GetUuid()
+
+    '执行逐个进入动画的控件
+    Public Property AnimatedControl As FrameworkElement
+        Get
+            Return GetValue(AnimatedControlProperty)
+        End Get
+        Set(value As FrameworkElement)
+            SetValue(AnimatedControlProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly AnimatedControlProperty As DependencyProperty = DependencyProperty.Register("AnimatedControl", GetType(FrameworkElement), GetType(MyPageLeft), New PropertyMetadata(Nothing))
+
+    Public Sub TriggerShowAnimation()
+        If AnimatedControl Is Nothing Then
+            '缩放动画
+            If TypeOf RenderTransform IsNot ScaleTransform Then
+                RenderTransform = New ScaleTransform(0.96, 0.96)
+                RenderTransformOrigin = New Point(0.5, 0.5)
+            End If
+            Opacity = 0
+            AniStart({
+                AaScaleTransform(Me, 1 - CType(RenderTransform, ScaleTransform).ScaleX, 400,, New AniEaseOutBack(2)),
+                AaOpacity(Me, 1, 100)
+            }, "PageLeft PageChange " & Uuid)
+        Else
+            '逐个进入动画
+            Dim AniList As New List(Of AniData)
+            Dim Id As Integer = 0, Delay As Integer = 0
+            Dim Controls = GetAllAnimControls()
+            For Each Element As FrameworkElement In Controls
+                Element.Opacity = 0
+                Element.RenderTransform = New TranslateTransform(-25, 0)
+                AniList.Add(AaOpacity(Element, If(TypeOf Element Is TextBlock, 0.6, 1), 200, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
+                AniList.Add(AaTranslateX(Element, 5, 200, Delay, New AniEaseOutFluent))
+                AniList.Add(AaTranslateX(Element, 20, 300, Delay, New AniEaseOutBack(AniEasePower.Weak)))
+                If TypeOf Element Is MyListItem Then
+                    AniList.Add(AaCode(Sub()
+                                           CType(Element, MyListItem).IsMouseOverAnimationEnabled = True
+                                           CType(Element, MyListItem).RefreshColor(Me, New EventArgs)
+                                       End Sub, Delay + 280))
+                End If
+                Delay += Math.Max(8, 20 - Id) * 2.5
+                Id += 1
+            Next
+            AniStart(AniList, "PageLeft PageChange " & Uuid)
+        End If
+    End Sub
+    Public Sub TriggerHideAnimation()
+        If AnimatedControl Is Nothing Then
+            '缩放动画
+            If TypeOf RenderTransform IsNot ScaleTransform Then
+                RenderTransform = New ScaleTransform(1, 1)
+                RenderTransformOrigin = New Point(0.5, 0.5)
+            End If
+            AniStart({
+                AaScaleTransform(Me, 0.95 - CType(RenderTransform, ScaleTransform).ScaleX, 130,, New AniEaseInFluent(AniEasePower.Weak)),
+                AaOpacity(Me, -Opacity, 100, 30)
+            }, "PageLeft PageChange " & Uuid)
+        Else
+            '逐个退出动画
+            Dim AniList As New List(Of AniData)
+            Dim Id As Integer = 0
+            Dim Controls = GetAllAnimControls()
+            For Each Element As FrameworkElement In Controls
+                AniList.Add(AaOpacity(Element, -Element.Opacity, 60, 80 / Controls.Count * Id))
+                AniList.Add(AaTranslateX(Element, -6, 60, 80 / Controls.Count * Id))
+                Id += 1
+            Next
+            AniStart(AniList, "PageLeft PageChange " & Uuid)
+        End If
+    End Sub
+
+    '遍历获取所有需要生成动画的控件
+    Private Function GetAllAnimControls() As List(Of FrameworkElement)
+        Dim AllControls As New List(Of FrameworkElement)
+        GetAllAnimControls(AnimatedControl, AllControls)
+        Return AllControls
+    End Function
+    Private Sub GetAllAnimControls(Element As FrameworkElement, ByRef AllControls As List(Of FrameworkElement))
+        If Element.Visibility = Visibility.Collapsed Then Exit Sub
+        If TypeOf Element Is MyTextButton Then
+            AllControls.Add(Element)
+        ElseIf TypeOf Element Is MyListItem Then
+            CType(Element, MyListItem).IsMouseOverAnimationEnabled = False
+            AllControls.Add(Element)
+        ElseIf TypeOf Element Is ContentControl Then
+            GetAllAnimControls(CType(Element, ContentControl).Content, AllControls)
+        ElseIf TypeOf Element Is Panel Then
+            For Each Element2 As FrameworkElement In CType(Element, Panel).Children
+                GetAllAnimControls(Element2, AllControls)
+            Next
+        Else
+            AllControls.Add(Element)
+        End If
+    End Sub
+
+End Class

+ 376 - 0
Plain Craft Launcher 2/Controls/MyPageRight.vb

@@ -0,0 +1,376 @@
+Public Class MyPageRight
+    Inherits AdornerDecorator
+    Public PageUuid As Integer = GetUuid()
+
+    '当前状态
+    Public Enum PageStates
+        Empty '默认状态,页面全空
+        LoaderWait '加载环初始等待
+        LoaderEnter '加载环进入动画
+        LoaderStayForce '加载环正常显示(强制等待)
+        LoaderStay '加载环正常显示
+        LoaderExit '加载环退出动画
+        ContentEnter '内容进入动画
+        ContentStay '内容正常显示
+        ReloadExit '刷新导致的全部退出动画
+        PageExit '切换页面导致的全部退出动画
+    End Enum
+    Private _PageState As PageStates = PageStates.Empty
+    Public Property PageState As PageStates
+        Get
+            Return _PageState
+        End Get
+        Set(value As PageStates)
+            If _PageState = value Then Exit Property
+            _PageState = value
+            If ModeDebug Then Log("[UI] 页面状态切换为 " & GetStringFromEnum(value))
+        End Set
+    End Property
+
+#Region "加载器"
+
+    Private PageLoader As LoaderBase
+    Private PageLoaderInputInvoke
+    Private PageLoaderUi As MyLoading
+    Private PanLoader As FrameworkElement
+    Private PanContent As FrameworkElement
+    Private PanAlways As FrameworkElement
+    Private PageLoaderAutoRun As Boolean
+
+    '初始化
+    ''' <summary>
+    ''' 表明页面存在需要在后台执行的加载器。
+    ''' </summary>
+    ''' <param name="LoaderUi">MyLoading 控件。</param>
+    ''' <param name="PanLoader">MyLoading 控件对应的卡片。</param>
+    ''' <param name="PanContent">加载结束后出现的内容容器。</param>
+    ''' <param name="PanAlways">无论是否在加载总是要显示的容器。可以为 Nothing。</param>
+    ''' <param name="RealLoader">在工作线程执行的加载器。</param>
+    ''' <param name="FinishedInvoke">当加载器执行完成,在 UI 线程触发的 UI 初始化事件。</param>
+    Public Sub PageLoaderInit(LoaderUi As MyLoading, PanLoader As FrameworkElement, PanContent As FrameworkElement, PanAlways As FrameworkElement,
+                              RealLoader As LoaderBase, FinishedInvoke As Action(Of LoaderBase), Optional InputInvoke As Func(Of Object) = Nothing,
+                              Optional AutoRun As Boolean = True)
+        '初始化参数
+        Me.PanLoader = PanLoader
+        Me.PanContent = PanContent
+        Me.PanAlways = PanAlways
+        Me.PageLoader = RealLoader
+        Me.PageLoaderUi = LoaderUi
+        Me.PageLoaderInputInvoke = InputInvoke
+        Me.PageLoaderAutoRun = AutoRun
+        '添加结束 Invoke
+        AddHandler RealLoader.PreviewFinish,
+            Sub()
+                Do While PageState = MyPageRight.PageStates.PageExit OrElse PageState = MyPageRight.PageStates.ReloadExit
+                    Thread.Sleep(10) '不在退出动画时执行 UI 线程操作,避免退出动画被重置
+                Loop
+                RunInUiWait(Sub() FinishedInvoke(RealLoader))
+                Thread.Sleep(20) '由于大量初始化控件会导致掉帧,延迟触发 State 改变事件
+            End Sub
+        AddHandler RealLoader.OnStateChangedUi, Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) RunInUi(Sub() PageLoaderState(Loader, NewState, OldState))
+        '隐藏 UI
+        PanLoader.Visibility = Visibility.Collapsed
+        PanContent.Visibility = Visibility.Collapsed
+        If PanAlways IsNot Nothing Then PanAlways.Visibility = Visibility.Collapsed
+        '初次运行加载器
+        If PageLoaderAutoRun Then
+            If PageLoader.GetType.Name.StartsWith("LoaderTask") Then
+                PageLoader.Start(CType(PageLoader, Object).StartGetInput(Nothing, PageLoaderInputInvoke))
+            Else
+                Dim Input = Nothing
+                If PageLoaderInputInvoke IsNot Nothing Then Input = PageLoaderInputInvoke()
+                PageLoader.Start(Input)
+            End If
+        End If
+        If PageLoader.State = LoadState.Finished Then
+            RunInUiWait(Sub() FinishedInvoke(RealLoader)) '加载器已提前完成,直接触发事件
+        End If
+        '设置加载环
+        PageLoaderUi.State = RealLoader
+        AddHandler PageLoaderUi.Click, Sub() If RealLoader.State = LoadState.Failed Then PageLoaderRestart() '点击重试事件
+    End Sub
+
+    '重试
+    Public Sub PageLoaderRestart(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = True) '由外部调用的重试
+        If Not PageLoaderAutoRun Then Exit Sub
+        If PageLoader.GetType.Name.StartsWith("LoaderTask") Then
+            PageLoader.Start(CType(PageLoader, Object).StartGetInput(Input, PageLoaderInputInvoke), IsForceRestart:=IsForceRestart)
+        Else
+            If Input Is Nothing AndAlso PageLoaderInputInvoke IsNot Nothing Then Input = PageLoaderInputInvoke()
+            PageLoader.Start(Input, IsForceRestart:=IsForceRestart)
+        End If
+    End Sub
+
+#End Region
+
+#Region "事件"
+
+    '外部触发的事件
+    ''' <summary>
+    ''' 需要切换到当前页面,并且原本的 Loaded 事件已执行完成。
+    ''' 需要根据加载器状态,从 Empty 切换到 ContentEnter、LoaderWait、LoaderEnter。
+    ''' </summary>
+    Public Sub PageOnEnter()
+        If ModeDebug Then Log("[UI] 已触发 PageOnEnter")
+        RaiseEvent OnPageEnter()
+
+        Select Case PageState
+            Case PageStates.Empty
+                If PageLoader Is Nothing OrElse PageLoader.State = LoadState.Finished Then
+                    PageState = PageStates.ContentEnter
+                    TriggerEnterAnimation(PanAlways, If(PanContent, Child))
+                ElseIf PageLoader.State = LoadState.Loading Then
+                    PageState = PageStates.LoaderWait
+                    AniStart(AaCode(AddressOf PageOnLoaderWaitFinished, 200), "PageRight PageChange " & PageUuid)
+                ElseIf PageLoader.State = LoadState.Failed Then
+                    PageState = PageStates.LoaderEnter
+                    TriggerEnterAnimation(PanAlways, PanLoader)
+                ElseIf PageLoader.State = LoadState.Waiting Then
+                    '由于 “页面加载器 Minecraft Version List 不应在 PageOnEnter 事件出现 Waiting 状态” 的神秘反馈添加的临时修复
+                    PageState = PageStates.LoaderWait
+                    AniStart(AaCode(AddressOf PageOnLoaderWaitFinished, 200), "PageRight PageChange " & PageUuid)
+                    Log("[Anim] 警告:页面加载器 " & PageLoader.Name & " 不应在 PageOnEnter 事件出现 " & GetStringFromEnum(PageLoader.State) & " 状态", LogLevel.Debug)
+                Else
+                    Throw New Exception("页面加载器 " & PageLoader.Name & " 不应在 PageOnEnter 事件出现 " & GetStringFromEnum(PageLoader.State) & " 状态。")
+                End If
+            Case Else
+                Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnter 事件。")
+        End Select
+    End Sub
+    Public Event OnPageEnter()
+    ''' <summary>
+    ''' 需要切换到其他页面。
+    ''' 需要立即切换至 PageExit 或 Empty。
+    ''' </summary>
+    Public Sub PageOnExit()
+        If ModeDebug Then Log("[UI] 已触发 PageOnExit")
+        Select Case PageState
+            Case PageStates.ContentEnter, PageStates.ContentStay
+                PageState = PageStates.PageExit
+                TriggerExitAnimation(PanAlways, If(PanContent, Child))
+            Case PageStates.LoaderEnter, PageStates.LoaderStayForce, PageStates.LoaderStay
+                PageState = PageStates.PageExit
+                TriggerExitAnimation(PanAlways, PanLoader)
+            Case PageStates.LoaderWait
+                PageState = PageStates.PageExit
+                TriggerExitAnimation(PanAlways)
+            Case PageStates.LoaderExit, PageStates.ReloadExit
+                PageState = PageStates.PageExit
+                If PanAlways IsNot Nothing Then TriggerExitAnimation(PanAlways, If(PanContent, Child))
+            Case PageStates.PageExit, PageStates.Empty
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 即将切换到其他页面,需要强制完成页面状态清理。
+    ''' 需要立即切换至 Empty。
+    ''' </summary>
+    Public Sub PageOnForceExit()
+        If PageState = PageStates.Empty Then Exit Sub
+        If ModeDebug Then Log("[UI] 已触发 PageOnForceExit")
+        PageState = PageStates.Empty
+        AniStop("PageRight PageChange " & PageUuid)
+        '由于动画会被强制中止,所以需要手动进行隐藏
+        If PageLoader Is Nothing Then
+            Child.Visibility = Visibility.Collapsed
+        Else
+            PanContent.Visibility = Visibility.Collapsed
+            PanLoader.Visibility = Visibility.Collapsed
+            If PanAlways IsNot Nothing Then PanAlways.Visibility = Visibility.Collapsed
+        End If
+    End Sub
+
+    '内部触发的事件
+    ''' <summary>
+    ''' 逐个进入动画已执行完成。
+    ''' 需要根据目前状态,从 ContentEnter 切换到 ContentStay,或从 LoaderEnter 切换到 LoaderStayForce。
+    ''' </summary>
+    Private Sub PageOnEnterAnimationFinished()
+        If ModeDebug Then Log("[UI] 已触发 PageOnEnterAnimationFinished")
+        Select Case PageState
+            Case PageStates.ContentEnter
+                PageState = PageStates.ContentStay
+            Case PageStates.LoaderEnter
+                PageState = PageStates.LoaderStayForce
+                AniStart(AaCode(AddressOf PageOnLoaderStayFinished, 400), "PageRight PageChange " & PageUuid)
+            Case Else
+                Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnterAnimationFinished 事件。")
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 逐个退出动画已执行完成。
+    ''' 需要根据目前状态,从 AllExit 切换到 Empty,或从 LoaderExit 切换到 ContentEnter,或从 ContentExit 重新触发 PageOnEnter。
+    ''' </summary>
+    Private Sub PageOnExitAnimationFinished()
+        If ModeDebug Then Log("[UI] 已触发 PageOnExitAnimationFinished")
+        Select Case PageState
+            Case PageStates.PageExit
+                PageState = PageStates.Empty
+            Case PageStates.ReloadExit
+                PageState = PageStates.Empty
+                PageOnEnter()
+            Case PageStates.LoaderExit
+                PageState = PageStates.ContentEnter
+                TriggerEnterAnimation(PanContent)
+            Case Else
+                Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnExitAnimationFinished 事件。")
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 加载环进入等待已结束。
+    ''' 需要从 LoaderWait 切换到 LoaderEnter。
+    ''' </summary>
+    Private Sub PageOnLoaderWaitFinished()
+        If ModeDebug Then Log("[UI] 已触发 PageOnLoaderWaitFinished")
+        Select Case PageState
+            Case PageStates.LoaderWait
+                PageState = PageStates.LoaderEnter
+                If PanAlways IsNot Nothing AndAlso PanAlways.Visibility = Visibility.Collapsed Then
+                    TriggerEnterAnimation(PanAlways, PanLoader)
+                Else
+                    TriggerEnterAnimation(PanLoader)
+                End If
+            Case Else
+                Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnLoaderWaitFinished 事件。")
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 加载环展示等待已结束。
+    ''' 需要从 LoaderStayForce 切换到 LoaderStay 或 LoaderExit。
+    ''' </summary>
+    Private Sub PageOnLoaderStayFinished()
+        If ModeDebug Then Log("[UI] 已触发 PageOnLoaderStayFinished")
+        Select Case PageState
+            Case PageStates.LoaderStayForce
+                If PageLoader.State = LoadState.Finished Then
+                    PageState = PageStates.LoaderExit
+                    TriggerExitAnimation(PanLoader)
+                Else
+                    PageState = PageStates.LoaderStay
+                End If
+            Case Else
+                Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnLoaderWaitFinished 事件。")
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' 全局加载状态已改变。
+    ''' </summary>
+    Private Sub PageLoaderState(sender As Object, NewState As LoadState, OldState As LoadState)
+        Select Case NewState
+            Case LoadState.Failed, LoadState.Loading
+                If Not OldState = LoadState.Finished Then Exit Sub
+                If ModeDebug Then Log("[UI] 已触发 PageLoaderState (Refresh)")
+                '重新开始运行
+                '需要从部分状态切换到 ReloadExit
+                Select Case PageState
+                    Case PageStates.ContentEnter, PageStates.ContentStay
+                        PageState = PageStates.ReloadExit
+                        TriggerExitAnimation(PanContent)
+                    Case PageStates.LoaderExit, PageStates.ReloadExit
+                        PageState = PageStates.ReloadExit
+                End Select
+            Case LoadState.Finished
+                If Not OldState = LoadState.Loading Then Exit Sub
+                If ModeDebug Then Log("[UI] 已触发 PageLoaderState (Stop)")
+                '运行结束
+                '需要从 LoaderWait 切换到 ContentEnter,或从 LoaderStay 切换到 LoaderExit
+                Select Case PageState
+                    Case PageStates.LoaderWait
+                        PageState = PageStates.ContentEnter
+                        If PanAlways IsNot Nothing AndAlso PanAlways.Visibility = Visibility.Collapsed Then
+                            TriggerEnterAnimation(PanAlways, PanContent)
+                        Else
+                            TriggerEnterAnimation(PanContent)
+                        End If
+                    Case PageStates.LoaderStay
+                        PageState = PageStates.LoaderExit
+                        TriggerExitAnimation(PanLoader)
+                End Select
+        End Select
+    End Sub
+
+#End Region
+
+#Region "动画"
+
+    '逐个进入动画
+    Public Sub TriggerEnterAnimation(ParamArray Elements As FrameworkElement())
+        '去除 Nothing 项
+        Dim RealElements As New List(Of FrameworkElement)
+        For Each Element In Elements
+            If Element IsNot Nothing Then RealElements.Add(Element)
+        Next
+
+        For Each Element In RealElements
+            Element.Visibility = Visibility.Visible '页面均处于默认的隐藏状态
+        Next
+        Dim AniList As New List(Of AniData)
+        Dim Delay As Integer = 0
+        For Each Element In RealElements
+            For Each Control As FrameworkElement In GetAllAnimControls(Element, True)
+                '还原被隐藏的卡片的消失动画
+                Control.IsHitTestVisible = True
+                If Control.RenderTransform IsNot Nothing AndAlso TypeOf Control.RenderTransform Is TranslateTransform Then Control.RenderTransform = Nothing
+            Next
+            For Each Control As FrameworkElement In GetAllAnimControls(Element)
+                Control.Opacity = 0
+                Control.RenderTransform = New TranslateTransform(0, -16)
+                AniList.Add(AaOpacity(Control, 1, 150, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
+                AniList.Add(AaTranslateY(Control, 5, 250, Delay, New AniEaseOutFluent))
+                AniList.Add(AaTranslateY(Control, 11, 350, Delay, New AniEaseOutBack))
+                Delay += 40
+            Next
+        Next
+        AniList.Add(AaCode(Sub() PageOnEnterAnimationFinished(),, True))
+        AniStart(AniList, "PageRight PageChange " & PageUuid)
+    End Sub
+
+    '逐个退出动画
+    Public Sub TriggerExitAnimation(ParamArray Elements As FrameworkElement())
+        '去除 Nothing 项
+        Dim RealElements As New List(Of FrameworkElement)
+        For Each Element In Elements
+            If Element IsNot Nothing Then RealElements.Add(Element)
+        Next
+
+        Dim AniList As New List(Of AniData)
+        Dim Delay As Integer = 0
+        For Each Element In RealElements
+            For Each Control As FrameworkElement In GetAllAnimControls(Element)
+                Control.IsHitTestVisible = False
+                AniList.Add(AaOpacity(Control, -1, 90, Delay))
+                AniList.Add(AaTranslateY(Control, -6, 90, Delay))
+                Delay += 20
+            Next
+        Next
+        AniList.Add(AaCode(Sub()
+                               For Each Element In RealElements
+                                   Element.Visibility = Visibility.Collapsed
+                               Next
+                               PageOnExitAnimationFinished()
+                           End Sub,, True))
+        AniStart(AniList, "PageRight PageChange " & PageUuid)
+    End Sub
+
+    '遍历获取所有需要生成动画的控件
+    Friend Function GetAllAnimControls(Element As FrameworkElement, Optional IgnoreInvisibility As Boolean = False) As List(Of FrameworkElement)
+        Dim AllControls As New List(Of FrameworkElement)
+        GetAllAnimControls(Element, AllControls, IgnoreInvisibility)
+        Return AllControls
+    End Function
+    Private Sub GetAllAnimControls(Element As FrameworkElement, ByRef AllControls As List(Of FrameworkElement), IgnoreInvisibility As Boolean)
+        If Not IgnoreInvisibility AndAlso Element.Visibility = Visibility.Collapsed Then Exit Sub
+        If TypeOf Element Is MyCard OrElse TypeOf Element Is MyHint Then
+            AllControls.Add(Element)
+        ElseIf TypeOf Element Is ContentControl Then
+            GetAllAnimControls(CType(Element, ContentControl).Content, AllControls, IgnoreInvisibility)
+        ElseIf TypeOf Element Is Panel Then
+            For Each Element2 As FrameworkElement In CType(Element, Panel).Children
+                GetAllAnimControls(Element2, AllControls, IgnoreInvisibility)
+            Next
+        End If
+    End Sub
+
+#End Region
+
+End Class

+ 9 - 0
Plain Craft Launcher 2/Controls/MyRadioBox.xaml

@@ -0,0 +1,9 @@
+<Grid x:Class="MyRadioBox"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             FocusVisualStyle="{x:Null}"
+             MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20" Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True">
+        <TextBlock IsHitTestVisible="False" x:Name="LabText" FontSize="13" Padding="0" Margin="26,0,0,0" Text="Radiobox" Foreground="{DynamicResource ColorBrush1}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
+    <Ellipse IsHitTestVisible="False" x:Name="ShapeBorder" StrokeThickness="1.1" HorizontalAlignment="Left" VerticalAlignment="Center" Width="18" Height="18" Stroke="{DynamicResource ColorBrush1}" Margin="1,0,0,0" Fill="{StaticResource ColorBrushHalfWhite}" />
+        <Ellipse IsHitTestVisible="False" x:Name="ShapeDot" HorizontalAlignment="Left" VerticalAlignment="Center" Fill="{Binding Stroke, ElementName=ShapeBorder}" Width="0" Height="0" Margin="10,0,0,0" StrokeThickness="0" />
+</Grid>

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

@@ -0,0 +1,231 @@
+Public Class MyRadioBox
+    Implements IMyRadio
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Event PreviewCheck(sender As Object, e As RouteEventArgs)
+    Public Event PreviewChange(sender As Object, e As RouteEventArgs)
+    Public Event Check(sender As Object, e As RouteEventArgs) Implements IMyRadio.Check
+    Public Event Changed(sender As Object, e As RouteEventArgs) Implements IMyRadio.Changed
+
+    '自定义属性
+
+    Private _Checked As Boolean = False '是否选中
+    Public Property Checked As Boolean
+        Get
+            Return _Checked
+        End Get
+        Set(value As Boolean)
+            SetChecked(value, False, True)
+        End Set
+    End Property
+    ''' <summary>
+    ''' 手动设置 Checked 属性。
+    ''' </summary>
+    ''' <param name="value">新的 Checked 属性。</param>
+    ''' <param name="user">是否由用户引发。</param>
+    ''' <param name="anime">是否执行动画。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+        Try
+
+            'Preview 事件
+
+            If value AndAlso user Then
+                Dim e = New RouteEventArgs(user)
+                RaiseEvent PreviewCheck(Me, e)
+                If e.Handled Then
+                    Radiobox_MouseLeave()
+                    Exit Sub
+                End If
+            End If
+
+            '自定义属性基础
+
+            Dim IsChanged As Boolean = False
+            If IsLoaded AndAlso Not value = _Checked Then RaiseEvent PreviewChange(Me, New RouteEventArgs(user))
+            If Not value = _Checked Then
+                _Checked = value
+                IsChanged = True
+            End If
+
+            '保证只有一个单选框选中
+
+            If Parent Is Nothing Then Exit Sub
+            Dim RadioboxList As New List(Of MyRadioBox)
+            Dim CheckedCount As Integer = 0
+            '收集控件列表与选中个数
+            For Each Control In CType(Parent, Object).Children
+                If TypeOf Control Is MyRadioBox Then
+                    RadioboxList.Add(Control)
+                    If Control.Checked Then CheckedCount += 1
+                End If
+            Next
+            '判断选中情况
+            Select Case CheckedCount
+                Case 0
+                    '没有任何单选框被选中,选择第一个
+                    RadioboxList(0).Checked = True
+                Case Is > 1
+                    '选中项目多于 1 个
+                    If Checked Then
+                        '如果本控件选中,则取消其他所有控件的选中
+                        For Each Control As MyRadioBox In RadioboxList
+                            If Control.Checked AndAlso Not Control.Equals(Me) Then Control.Checked = False
+                        Next
+                    Else
+                        '如果本控件未选中,则只保留第一个选中的控件
+                        Dim FirstChecked = False
+                        For Each Control As MyRadioBox In RadioboxList
+                            If Control.Checked Then
+                                If FirstChecked Then
+                                    Control.Checked = False '修改 Checked 会自动触发 Change 事件,所以不用额外触发
+                                Else
+                                    FirstChecked = True
+                                End If
+                            End If
+                        Next
+                    End If
+            End Select
+            'End If
+
+            '更改动画
+
+            If Not IsChanged Then Exit Sub
+            If IsLoaded AndAlso AniControlEnabled = 0 AndAlso anime Then '防止默认属性变更触发动画
+                If Checked Then
+                    '由无变有
+                    If ShapeDot.Opacity < 0.01 Then ShapeDot.Opacity = 1
+                    AniStart({
+                              AaScale(ShapeBorder, 10 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent(AniEasePower.Weak), , True),
+                              AaScale(ShapeBorder, 8, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.6, New AniEaseOutBack, , True)
+                         }, "MyRadioBox Border " & Uuid)
+                    AniStart({
+                              AaScale(ShapeDot, 9 - ShapeDot.Width, AnimationTimeOfCheck * 2.6,, New AniEaseOutBack(AniEasePower.Weak), , True),
+                              AaOpacity(ShapeDot, 1 - ShapeDot.Opacity, AnimationTimeOfCheck * 0.5, AnimationTimeOfCheck * 0.6)
+                         }, "MyRadioBox Dot " & Uuid)
+                    AniStart({
+                              AaColor(ShapeBorder, Ellipse.StrokeProperty, If(IsMouseOver, "ColorBrush3", If(IsEnabled, "ColorBrush2", "ColorBrushGray4")), AnimationTimeOfCheck)
+                         }, "MyRadioBox BorderColor " & Uuid)
+                Else
+                    '由有变无
+                    AniStart({
+                              AaScale(ShapeBorder, 18 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True)
+                         }, "MyRadioBox Border " & Uuid)
+                    AniStart({
+                              AaScale(ShapeDot, -ShapeDot.Width, AnimationTimeOfCheck, , New AniEaseInFluent, , True),
+                              AaOpacity(ShapeDot, -ShapeDot.Opacity, AnimationTimeOfCheck * 0.5, AnimationTimeOfCheck * 0.2)
+                         }, "MyRadioBox Dot " & Uuid)
+                    AniStart({
+                              AaColor(ShapeBorder, Ellipse.StrokeProperty, If(IsMouseOver, "ColorBrush3", If(IsEnabled, "ColorBrush1", "ColorBrushGray4")), AnimationTimeOfCheck)
+                         }, "MyRadioBox BorderColor " & Uuid)
+                End If
+            Else
+                '不使用动画
+                AniStop("MyRadioBox Border " & Uuid)
+                AniStop("MyRadioBox Dot " & Uuid)
+                AniStop("MyRadioBox BorderColor " & Uuid)
+                If Checked Then
+                    ShapeDot.Width = 9
+                    ShapeDot.Height = 9
+                    ShapeDot.Opacity = 1
+                    ShapeDot.Margin = New Thickness(5.5, 0, 0, 0)
+                    ShapeBorder.SetResourceReference(Ellipse.StrokeProperty, If(IsEnabled, "ColorBrush2", "ColorBrushGray4"))
+                Else
+                    ShapeDot.Width = 0
+                    ShapeDot.Height = 0
+                    ShapeDot.Opacity = 0
+                    ShapeDot.Margin = New Thickness(10, 0, 0, 0)
+                    ShapeBorder.SetResourceReference(Ellipse.StrokeProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray4"))
+                End If
+            End If
+
+            '触发事件
+            If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
+            RaiseEvent Changed(Me, New RouteEventArgs(user))
+
+        Catch ex As Exception
+            Log(ex, "单选框勾选改变错误", LogLevel.Hint)
+        End Try
+    End Sub
+    Public Property Text As String
+        Get
+            Return GetValue(TextProperty)
+        End Get
+        Set(value As String)
+            SetValue(TextProperty, value)
+        End Set
+    End Property '内容
+    Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyRadioBox), New PropertyMetadata(New PropertyChangedCallback(
+                                                                                                                                                               Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+                                                                                                                                                                   If Not IsNothing(sender) Then CType(sender, MyRadioBox).LabText.Text = e.NewValue
+                                                                                                                                                               End Sub)))
+
+    '点击事件
+
+    Private MouseDowned As Boolean = False
+    Private AllowMouseDown As Boolean = True
+    Private Sub Radiobox_MouseUp() Handles Me.MouseLeftButtonUp
+        If Not MouseDowned Then Exit Sub
+        MouseDowned = False
+        Log("[Control] 按下单选框:" & Text)
+        SetChecked(True, True, True)
+        AniStart(AaColor(ShapeBorder, Shapes.Ellipse.FillProperty, "ColorBrushHalfWhite", 100), "MyRadioBox Background " & Uuid)
+    End Sub
+    Private Sub Radiobox_MouseDown() Handles Me.MouseLeftButtonDown
+        MouseDowned = True
+        Focus()
+        AniStart(AaColor(ShapeBorder, Shapes.Ellipse.FillProperty, "ColorBrush9", 100), "MyRadioBox Background " & Uuid)
+        If Not Checked Then
+            AniStart(AaScale(ShapeBorder, 16.5 - ShapeBorder.Width, 1000, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True), "MyRadioBox Border " & Uuid)
+        End If
+    End Sub
+    Private Sub Radiobox_MouseLeave() Handles Me.MouseLeave
+        If Not MouseDowned Then Exit Sub
+        MouseDowned = False
+        AniStart(AaColor(ShapeBorder, Shapes.Ellipse.FillProperty, "ColorBrushHalfWhite", 100), "MyRadioBox Background " & Uuid)
+        If Not Checked Then
+            AniStart(AaScale(ShapeBorder, 18 - ShapeBorder.Width, 400, , New AniEaseOutFluent(AniEasePower.Strong), Absolute:=True), "MyRadioBox Border " & Uuid)
+        End If
+    End Sub
+
+    '指向动画
+
+    Private Const AnimationTimeOfMouseIn As Integer = 100 '鼠标指向动画长度
+    Private Const AnimationTimeOfMouseOut As Integer = 200 '鼠标指向动画长度
+    Private Const AnimationTimeOfCheck As Integer = 150 '勾选状态变更动画长度
+    Private Sub Radiobox_IsEnabledChanged() Handles Me.IsEnabledChanged
+        If Me.IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            If Me.IsEnabled Then
+                '可用
+                Radiobox_MouseLeaveAnimation()
+            Else
+                '不可用
+                AniStart(AaColor(ShapeBorder, Ellipse.StrokeProperty, ColorGray4 - ShapeBorder.Stroke, AnimationTimeOfMouseOut), "MyRadioBox BorderColor " & Uuid)
+                AniStart(AaColor(LabText, TextBlock.ForegroundProperty, ColorGray4 - LabText.Foreground, AnimationTimeOfMouseOut), "MyRadioBox TextColor " & Uuid)
+            End If
+        Else
+            '无动画
+            AniStop("MyRadioBox BorderColor " & Uuid)
+            AniStop("MyRadioBox TextColor " & Uuid)
+            LabText.SetResourceReference(TextBlock.ForegroundProperty, If(Me.IsEnabled, "ColorBrush1", "ColorBrushGray4"))
+            ShapeBorder.SetResourceReference(Ellipse.StrokeProperty, If(Me.IsEnabled, If(Checked, "ColorBrush2", "ColorBrush1"), "ColorBrushGray4"))
+        End If
+    End Sub
+    Private Sub Radiobox_MouseEnterAnimation() Handles Me.MouseEnter
+        AniStart(AaColor(ShapeBorder, Ellipse.StrokeProperty, "ColorBrush3", AnimationTimeOfMouseIn), "MyRadioBox BorderColor " & Uuid)
+        AniStart(AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn), "MyRadioBox TextColor " & Uuid)
+    End Sub
+    Private Sub Radiobox_MouseLeaveAnimation() Handles Me.MouseLeave
+        If Not Me.IsEnabled Then Exit Sub 'MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误
+        If IsLoaded AndAlso AniControlEnabled = 0 Then
+            AniStart(AaColor(ShapeBorder, Ellipse.StrokeProperty, If(IsEnabled, If(Checked, "ColorBrush2", "ColorBrush1"), "ColorBrushGray4"), AnimationTimeOfMouseOut), "MyRadioBox BorderColor " & Uuid)
+            AniStart(AaColor(LabText, TextBlock.ForegroundProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray4"), AnimationTimeOfMouseOut), "MyRadioBox TextColor " & Uuid)
+        Else
+            ShapeBorder.SetResourceReference(Ellipse.StrokeProperty, If(IsEnabled, If(Checked, "ColorBrush2", "ColorBrush1"), "ColorBrushGray4"))
+            LabText.SetResourceReference(TextBlock.ForegroundProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray4"))
+        End If
+    End Sub
+
+End Class

+ 9 - 0
Plain Craft Launcher 2/Controls/MyRadioButton.xaml

@@ -0,0 +1,9 @@
+<Border x:Name="PanBack" x:Class="MyRadioButton"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             HorizontalAlignment="Center" VerticalAlignment="Center" Background="{StaticResource ColorBrushSemiTransparent}" CornerRadius="13.5" MinHeight="27" MaxHeight="27">
+    <StackPanel Orientation="Horizontal">
+        <Path x:Name="ShapeLogo" HorizontalAlignment="Left" RenderTransformOrigin="0.5,0.5" Margin="12,0,0,0" Stretch="Uniform" Fill="White" MaxHeight="16" MaxWidth="16" VerticalAlignment="Center" />
+        <TextBlock x:Name="LabText" VerticalAlignment="Center" Grid.Column="1" Foreground="White" Margin="8,0,12,0" Grid.ColumnSpan="2" SnapsToDevicePixels="False" UseLayoutRounding="False" />
+    </StackPanel>
+</Border>

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

@@ -0,0 +1,255 @@
+Public Class MyRadioButton
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Event Check(sender As Object, raiseByMouse As Boolean)
+    Public Event Change(sender As Object, raiseByMouse As Boolean)
+    Public Sub RaiseChange()
+        RaiseEvent Change(Me, False)
+    End Sub '使外部程序可以引发本控件的 Change 事件
+
+    '自定义属性
+
+    Public Property Logo As String
+        Get
+            Return ShapeLogo.Data.ToString
+        End Get
+        Set(value As String)
+            ShapeLogo.Data = (New GeometryConverter).ConvertFromString(value)
+        End Set
+    End Property
+    Private _LogoScale As Double = 1
+    Public Property LogoScale() As Double
+        Get
+            Return _LogoScale
+        End Get
+        Set(value As Double)
+            _LogoScale = value
+            If Not IsNothing(ShapeLogo) Then ShapeLogo.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale}
+        End Set
+    End Property
+
+    Private _Checked As Boolean = False '是否选中
+    Public Property Checked As Boolean
+        Get
+            Return _Checked
+        End Get
+        Set(value As Boolean)
+            SetChecked(value, False, True)
+        End Set
+    End Property
+    ''' <summary>
+    ''' 手动设置 Checked 属性。
+    ''' </summary>
+    ''' <param name="value">新的 Checked 属性。</param>
+    ''' <param name="user">是否由用户引发。</param>
+    ''' <param name="anime">是否执行动画。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+        Try
+
+            '自定义属性基础
+
+            Dim IsChanged As Boolean = False
+            If IsLoaded AndAlso Not value = _Checked Then RaiseEvent Change(Me, user)
+            If Not value = _Checked Then
+                _Checked = value
+                IsChanged = True
+            End If
+
+            '保证只有一个单选框选中
+
+            If IsNothing(Parent) Then Exit Sub
+            Dim RadioboxList As New List(Of MyRadioButton)
+            Dim CheckedCount As Integer = 0
+            '收集控件列表与选中个数
+            For Each Control In CType(Parent, Object).Children
+                If TypeOf Control Is MyRadioButton Then
+                    RadioboxList.Add(Control)
+                    If Control.Checked Then CheckedCount += 1
+                End If
+            Next
+            '判断选中情况
+            Select Case CheckedCount
+                Case 0
+                    '没有任何单选框被选中,选择第一个
+                    RadioboxList(0).Checked = True
+                Case Is > 1
+                    '选中项目多于 1 个
+                    If Me.Checked Then
+                        '如果本控件选中,则取消其他所有控件的选中
+                        For Each Control As MyRadioButton In RadioboxList
+                            If Control.Checked AndAlso Not Control.Equals(Me) Then Control.Checked = False
+                        Next
+                    Else
+                        '如果本控件未选中,则只保留第一个选中的控件
+                        Dim FirstChecked = False
+                        For Each Control As MyRadioButton In RadioboxList
+                            If Control.Checked Then
+                                If FirstChecked Then
+                                    Control.Checked = False '修改 Checked 会自动触发 Change 事件,所以不用额外触发
+                                Else
+                                    FirstChecked = True
+                                End If
+                            End If
+                        Next
+                    End If
+            End Select
+
+            '更改动画
+
+            If Not IsChanged Then Exit Sub
+            RefreshColor(Nothing, anime)
+
+            '触发事件
+            If Checked Then RaiseEvent Check(Me, user)
+
+        Catch ex As Exception
+            Log(ex, "单选按钮勾选改变错误", LogLevel.Hint)
+        End Try
+    End Sub
+    Public Property Text As String
+        Get
+            Return GetValue(TextProperty)
+        End Get
+        Set(value As String)
+            SetValue(TextProperty, value)
+        End Set
+    End Property '内容
+    Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyRadioButton), New PropertyMetadata(New PropertyChangedCallback(
+                                                                                                                                                               Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+                                                                                                                                                                   If Not IsNothing(sender) Then CType(sender, MyRadioButton).LabText.Text = e.NewValue
+                                                                                                                                                               End Sub)))
+    Public Enum ColorState
+        White
+        Highlight
+    End Enum
+    Private _ColorType As ColorState = ColorState.White
+    Public Property ColorType As ColorState
+        Get
+            Return _ColorType
+        End Get
+        Set(value As ColorState)
+            _ColorType = value
+            RefreshColor()
+        End Set
+    End Property '颜色类别
+
+    '点击事件
+
+    Private IsMouseDown As Boolean = False
+    Private Sub Radiobox_MouseUp() Handles Me.MouseLeftButtonUp
+        If Checked Then Exit Sub
+        If Not IsMouseDown Then Exit Sub
+        Log("[Control] 按下单选按钮:" & Text)
+        IsMouseDown = False
+        SetChecked(True, True, True)
+    End Sub
+    Private Sub Radiobox_MouseDown() Handles Me.MouseLeftButtonDown
+        If Checked Then Exit Sub
+        IsMouseDown = True
+        RefreshColor()
+    End Sub
+    Private Sub Radiobox_MouseLeave() Handles Me.MouseLeave
+        IsMouseDown = False
+    End Sub
+
+    '动画
+
+    Private Const AnimationTimeOfMouseIn As Integer = 90 '鼠标指向动画长度
+    Private Const AnimationTimeOfMouseOut As Integer = 150 '鼠标移出动画长度
+    Private Const AnimationTimeOfCheck As Integer = 120 '勾选状态变更动画长度
+    Private Sub RefreshColor(Optional obj = Nothing, Optional e = Nothing) Handles Me.MouseEnter, Me.MouseLeave, Me.Loaded
+        Try
+            If IsLoaded AndAlso AniControlEnabled = 0 AndAlso Not False.Equals(e) Then '防止默认属性变更触发动画,若强制不执行动画,则 e 为 False
+
+                Select Case ColorType
+                    Case ColorState.White
+                        If Checked Then
+                            '勾选
+                            AniStart({
+                                         AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfCheck),
+                                         AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfCheck)
+                                    }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, New MyColor(255, 255, 255) - Background, AnimationTimeOfCheck), "MyRadioButton Color " & Uuid)
+                        ElseIf IsMouseDown Then
+                            '按下
+                            AniStart(AaColor(Me, BackgroundProperty, New MyColor(120, Color8) - Background, 60), "MyRadioButton Color " & Uuid)
+                        ElseIf IsMouseOver Then
+                            '指向
+                            AniStart({
+                                 AaColor(ShapeLogo, Shapes.Path.FillProperty, New MyColor(255, 255, 255) - ShapeLogo.Fill, AnimationTimeOfMouseIn),
+                                 AaColor(LabText, TextBlock.ForegroundProperty, New MyColor(255, 255, 255) - LabText.Foreground, AnimationTimeOfMouseIn)
+                            }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, New MyColor(50, Color8) - Background, AnimationTimeOfMouseIn), "MyRadioButton Color " & Uuid)
+                        Else
+                            '正常
+                            AniStart({
+                                 AaColor(ShapeLogo, Shapes.Path.FillProperty, New MyColor(255, 255, 255) - ShapeLogo.Fill, AnimationTimeOfMouseOut),
+                                 AaColor(LabText, TextBlock.ForegroundProperty, New MyColor(255, 255, 255) - LabText.Foreground, AnimationTimeOfMouseOut)
+                            }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyRadioButton Color " & Uuid)
+                        End If
+                    Case ColorState.Highlight
+                        If Checked Then
+                            '勾选
+                            AniStart({
+                                         AaColor(ShapeLogo, Shapes.Path.FillProperty, New MyColor(255, 255, 255) - ShapeLogo.Fill, AnimationTimeOfCheck),
+                                         AaColor(LabText, TextBlock.ForegroundProperty, New MyColor(255, 255, 255) - LabText.Foreground, AnimationTimeOfCheck)
+                                    }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, "ColorBrush3", AnimationTimeOfCheck), "MyRadioButton Color " & Uuid)
+                        ElseIf IsMouseDown Then
+                            '按下
+                            AniStart(AaColor(Me, BackgroundProperty, "ColorBrush6", AnimationTimeOfMouseIn), "MyRadioButton Color " & Uuid)
+                        ElseIf IsMouseOver Then
+                            '指向
+                            AniStart({
+                                         AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfMouseIn),
+                                         AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn)
+                                    }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, "ColorBrush7", AnimationTimeOfMouseIn), "MyRadioButton Color " & Uuid)
+                        Else
+                            '正常
+                            AniStart({
+                                         AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfMouseOut),
+                                         AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseOut)
+                                    }, "MyRadioButton Checked " & Uuid)
+                            AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyRadioButton Color " & Uuid)
+                        End If
+                End Select
+
+            Else
+
+                '不使用动画
+                AniStop("MyRadioButton Checked " & Uuid)
+                AniStop("MyRadioButton Color " & Uuid)
+                Select Case ColorType
+                    Case ColorState.White
+                        If Checked Then
+                            Background = New MyColor(255, 255, 255)
+                            ShapeLogo.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush3")
+                            LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3")
+                        Else
+                            Background = ColorSemiTransparent
+                            ShapeLogo.Fill = New MyColor(255, 255, 255)
+                            LabText.Foreground = New MyColor(255, 255, 255)
+                        End If
+                    Case ColorState.Highlight
+                        If Checked Then
+                            SetResourceReference(BackgroundProperty, "ColorBrush3")
+                            ShapeLogo.Fill = New MyColor(255, 255, 255)
+                            LabText.Foreground = New MyColor(255, 255, 255)
+                        Else
+                            Background = ColorSemiTransparent
+                            ShapeLogo.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush3")
+                            LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3")
+                        End If
+                End Select
+
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新按钮颜色出错")
+        End Try
+    End Sub
+
+End Class

+ 315 - 0
Plain Craft Launcher 2/Controls/MyResizer.vb

@@ -0,0 +1,315 @@
+Imports System.Runtime.InteropServices
+Imports System.Windows.Interop
+Imports System.Windows.Threading
+
+Public Class MyResizer
+
+    Private Delegate Sub RefreshDelegate()
+
+    Private Structure POINT
+        Public x As Integer
+        Public y As Integer
+        Public Sub New(x As Integer, y As Integer)
+            Me.x = x
+            Me.y = y
+        End Sub
+    End Structure
+    Private Structure MINMAXINFO
+        Public ptReserved As POINT
+        Public ptMaxSize As POINT
+        Public ptMaxPosition As POINT
+        Public ptMinTrackSize As POINT
+        Public ptMaxTrackSize As POINT
+    End Structure
+    Private Structure RECT
+        Public left As Integer
+        Public top As Integer
+        Public right As Integer
+        Public bottom As Integer
+        Public Shared Empty As RECT = Nothing
+
+        Public ReadOnly Property Width() As Integer
+            Get
+                Return Math.Abs(Me.right - Me.left)
+            End Get
+        End Property
+        Public ReadOnly Property Height() As Integer
+            Get
+                Return Me.bottom - Me.top
+            End Get
+        End Property
+
+        Public Sub New(left As Integer, top As Integer, right As Integer, bottom As Integer)
+            Me.left = left
+            Me.top = top
+            Me.right = right
+            Me.bottom = bottom
+        End Sub
+        Public Sub New(rcSrc As RECT)
+            Me.left = rcSrc.left
+            Me.top = rcSrc.top
+            Me.right = rcSrc.right
+            Me.bottom = rcSrc.bottom
+        End Sub
+    End Structure
+
+    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)>
+    Private Class MONITORINFO
+        Public cbSize As Integer = Marshal.SizeOf(GetType(MONITORINFO))
+        Public rcMonitor As RECT = Nothing
+        Public rcWork As RECT = Nothing
+        Public dwFlags As Integer = 0
+    End Class
+
+    Private Structure PointAPI
+        Public X As Integer
+        Public Y As Integer
+    End Structure
+
+    Private ReadOnly target As Window = Nothing
+
+    Private resizeRight As Boolean = False
+    Private resizeLeft As Boolean = False
+    Private resizeUp As Boolean = False
+    Private resizeDown As Boolean = False
+
+    Private ReadOnly leftElements As New Dictionary(Of UIElement, Short)()
+    Private ReadOnly rightElements As New Dictionary(Of UIElement, Short)()
+    Private ReadOnly upElements As New Dictionary(Of UIElement, Short)()
+    Private ReadOnly downElements As New Dictionary(Of UIElement, Short)()
+
+    Private startMousePoint As PointAPI = Nothing
+    Private startWindowSize As Size = Nothing
+    Private startWindowLeftUpPoint As POINT = Nothing
+
+    Private Shared workAreaMaxHeight As Integer = -1
+
+    Private hs As HwndSource
+
+    Public Sub New(target As Window)
+        Me.target = target
+        If IsNothing(target) Then Throw New Exception("Invalid Window handle")
+        AddHandler target.SourceInitialized, AddressOf Me.MyMacClass_SourceInitialized
+    End Sub
+
+    Private Sub MyMacClass_SourceInitialized(sender As Object, e As EventArgs)
+        Me.hs = (TryCast(PresentationSource.FromVisual(CType(sender, Visual)), HwndSource))
+        Me.hs.AddHook(AddressOf Me.WndProc)
+    End Sub
+
+    Private Function WndProc(hwnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr, ByRef handled As Boolean) As IntPtr
+        If msg = 36 Then
+            WmGetMinMaxInfo(hwnd, lParam)
+            handled = True
+        End If
+        Return CType(0, IntPtr)
+    End Function
+
+    Private Shared Sub WmGetMinMaxInfo(hwnd As IntPtr, lParam As IntPtr)
+        Dim mINMAXINFO As MINMAXINFO = CType(Marshal.PtrToStructure(lParam, GetType(MINMAXINFO)), MINMAXINFO)
+        Dim flags As Integer = 2
+        Dim intPtr As IntPtr = MonitorFromWindow(hwnd, flags)
+        Dim flag As Boolean = intPtr <> IntPtr.Zero
+        If flag Then
+            Dim mONITORINFO As New MONITORINFO()
+            GetMonitorInfo(intPtr, mONITORINFO)
+            Dim rcWork As RECT = mONITORINFO.rcWork
+            Dim rcMonitor As RECT = mONITORINFO.rcMonitor
+            mINMAXINFO.ptMaxPosition.x = Math.Abs(rcWork.left - rcMonitor.left)
+            mINMAXINFO.ptMaxPosition.y = Math.Abs(rcWork.top - rcMonitor.top)
+            mINMAXINFO.ptMaxSize.y = Math.Abs(rcWork.bottom - rcWork.top)
+            workAreaMaxHeight = mINMAXINFO.ptMaxSize.y
+            If rcWork.Height = rcMonitor.Height Then
+                mINMAXINFO.ptMaxSize.y -= 2
+            End If
+        End If
+        Marshal.StructureToPtr(mINMAXINFO, lParam, True)
+    End Sub
+
+    Private Declare Function GetMonitorInfo Lib "user32" (hMonitor As IntPtr, lpmi As MONITORINFO) As Boolean
+
+    Friend Declare Function MonitorFromWindow Lib "User32" (handle As IntPtr, flags As Integer) As IntPtr
+
+    Private Sub connectMouseHandlers(element As UIElement)
+        AddHandler element.MouseLeftButtonDown, AddressOf Me.element_MouseLeftButtonDown
+    End Sub
+
+    Public Sub addResizerRight(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.rightElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerLeft(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.leftElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerUp(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.upElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerDown(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.downElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerRightDown(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.rightElements.Add(element, 0)
+        Me.downElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerLeftDown(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.leftElements.Add(element, 0)
+        Me.downElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerRightUp(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.rightElements.Add(element, 0)
+        Me.upElements.Add(element, 0)
+    End Sub
+
+    Public Sub addResizerLeftUp(element As UIElement)
+        Me.connectMouseHandlers(element)
+        Me.leftElements.Add(element, 0)
+        Me.upElements.Add(element, 0)
+    End Sub
+
+    Private Sub element_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs)
+        GetCursorPos(startMousePoint)
+        startMousePoint.X = GetWPFSize(startMousePoint.X)
+        startMousePoint.Y = GetWPFSize(startMousePoint.Y)
+        startWindowSize = New Size(target.Width, target.Height)
+        startWindowLeftUpPoint = New POINT(target.Left, target.Top)
+        Dim key As UIElement = CType(sender, UIElement)
+        If leftElements.ContainsKey(key) Then resizeLeft = True
+        If rightElements.ContainsKey(key) Then resizeRight = True
+        If upElements.ContainsKey(key) Then resizeUp = True
+        If downElements.ContainsKey(key) Then resizeDown = True
+        RunInNewThread(AddressOf updateSizeLoop, "窗口大小调整检测")
+    End Sub
+
+    Private Sub updateSizeLoop()
+        Try
+            While resizeDown OrElse resizeLeft OrElse resizeRight OrElse resizeUp
+                target.Dispatcher.Invoke(AddressOf updateSize, DispatcherPriority.Render)
+                target.Dispatcher.Invoke(AddressOf updateMouseDown, DispatcherPriority.Render)
+                Thread.Sleep(0)
+            End While
+        Catch
+        End Try
+    End Sub
+
+    Private Sub updateSize()
+        Dim pointAPI As PointAPI = Nothing
+        GetCursorPos(pointAPI)
+        pointAPI.X = GetWPFSize(pointAPI.X)
+        pointAPI.Y = GetWPFSize(pointAPI.Y)
+        Try
+
+            Dim NewWidth As Double = -1
+            Dim NewHeight As Double = -1
+            Dim NewLeft As Double = -10000
+            Dim NewTop As Double = -10000
+
+            If resizeRight Then
+                If target.Width = target.MinWidth Then
+                    If startMousePoint.X < pointAPI.X Then
+                        NewWidth = startWindowSize.Width - (startMousePoint.X - pointAPI.X)
+                    End If
+                Else
+                    If startWindowSize.Width - ((startMousePoint.X - pointAPI.X)) >= target.MinWidth Then
+                        NewWidth = startWindowSize.Width - (startMousePoint.X - pointAPI.X)
+                    Else
+                        NewWidth = target.MinWidth
+                    End If
+                End If
+            End If
+
+            If resizeDown Then
+                If target.Height = target.MinHeight Then
+                    If startMousePoint.Y < pointAPI.Y Then
+                        If workAreaMaxHeight > 0 Then
+                            NewHeight = If((startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + target.Top <= workAreaMaxHeight), (startWindowSize.Height - ((startMousePoint.Y - pointAPI.Y))), ((workAreaMaxHeight) - target.Top))
+                        Else
+                            NewHeight = startWindowSize.Height - (startMousePoint.Y - pointAPI.Y)
+                        End If
+                    End If
+                Else
+                    If startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) >= target.MinHeight Then
+                        If workAreaMaxHeight > 0 Then
+                            NewHeight = If((startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + target.Top <= (workAreaMaxHeight)), (startWindowSize.Height - (startMousePoint.Y - pointAPI.Y)), ((workAreaMaxHeight) - target.Top))
+                        Else
+                            NewHeight = startWindowSize.Height - (startMousePoint.Y - pointAPI.Y)
+                        End If
+                    Else
+                        NewHeight = target.MinHeight
+                    End If
+                End If
+            End If
+
+            If resizeLeft Then
+                If target.Width = target.MinWidth Then
+                    If startMousePoint.X > pointAPI.X Then
+                        NewWidth = startWindowSize.Width + startMousePoint.X - pointAPI.X
+                        NewLeft = startWindowLeftUpPoint.x - (startMousePoint.X - pointAPI.X)
+                    Else
+                        NewWidth = target.MinWidth
+                        NewLeft = startWindowLeftUpPoint.x + startWindowSize.Width - target.Width
+                    End If
+                Else
+                    If startWindowSize.Width + (startMousePoint.X - pointAPI.X) >= target.MinWidth Then
+                        NewWidth = startWindowSize.Width + (startMousePoint.X - pointAPI.X)
+                        NewLeft = startWindowLeftUpPoint.x - (startMousePoint.X - pointAPI.X)
+                    Else
+                        NewWidth = target.MinWidth
+                        NewLeft = startWindowLeftUpPoint.x + startWindowSize.Width - target.Width
+                    End If
+                End If
+            End If
+
+            If resizeUp Then
+                If target.Height = target.MinHeight Then
+                    If startMousePoint.Y > pointAPI.Y Then
+                        NewHeight = startWindowSize.Height + startMousePoint.Y - pointAPI.Y
+                        NewTop = startWindowLeftUpPoint.y - (startMousePoint.Y - pointAPI.Y)
+                    Else
+                        NewHeight = target.MinHeight
+                        NewTop = startWindowLeftUpPoint.y + startWindowSize.Height - target.Height
+                    End If
+                Else
+                    If startWindowSize.Height + (startMousePoint.Y - pointAPI.Y) >= target.MinHeight Then
+                        NewHeight = startWindowSize.Height + startMousePoint.Y - pointAPI.Y
+                        NewTop = startWindowLeftUpPoint.y - (startMousePoint.Y - pointAPI.Y)
+                    Else
+                        NewHeight = target.MinHeight
+                        NewTop = startWindowLeftUpPoint.y + startWindowSize.Height - target.Height
+                    End If
+                End If
+            End If
+
+            If NewWidth > 10 AndAlso Math.Abs(NewWidth - target.Width) > 0.7 Then target.Width = NewWidth
+            If NewHeight > 10 AndAlso Math.Abs(NewHeight - target.Height) > 0.7 Then target.Height = NewHeight
+            If NewLeft > -9999 AndAlso Math.Abs(NewLeft - target.Left) > 0.7 Then target.Left = NewLeft
+            If NewTop > -9999 AndAlso Math.Abs(NewTop - target.Top) > 0.7 Then target.Top = NewTop
+
+        Catch
+        End Try
+    End Sub
+
+    Private Sub updateMouseDown()
+        Dim flag As Boolean = Mouse.LeftButton = MouseButtonState.Released
+        If flag Then
+            resizeRight = False
+            resizeLeft = False
+            resizeUp = False
+            resizeDown = False
+        End If
+    End Sub
+
+    Private Declare Function GetCursorPos Lib "user32.dll" (<Out()> ByRef lpPoint As PointAPI) As Boolean
+
+End Class

+ 51 - 0
Plain Craft Launcher 2/Controls/MyScrollBar.vb

@@ -0,0 +1,51 @@
+Public Class MyScrollBar
+    Inherits Primitives.ScrollBar
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+
+    '指向动画
+
+    Private Sub RefreshColor() Handles Me.IsEnabledChanged, Me.GotMouseCapture, Me.LostMouseCapture, Me.MouseEnter, Me.MouseLeave, Me.IsVisibleChanged
+        Try
+
+            '判断当前颜色
+            Dim NewOpacity As Double, NewColor As String, Time As Integer
+            If Not IsVisible Then
+                NewOpacity = 0
+                Time = 20 '防止错误的尺寸判断导致闪烁
+                NewColor = "ColorBrush4"
+            ElseIf IsMouseCaptureWithin Then
+                NewOpacity = 1
+                NewColor = "ColorBrush4"
+                Time = 50
+            ElseIf IsMouseOver Then
+                NewOpacity = 0.9
+                NewColor = "ColorBrush3"
+                Time = 130
+            Else
+                NewOpacity = 0.5
+                NewColor = "ColorBrush4"
+                Time = 180
+            End If
+            '触发颜色动画
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+                '有动画
+                AniStart({
+                         AaColor(Me, ForegroundProperty, NewColor, Time),
+                         AaOpacity(Me, NewOpacity - Opacity, Time)
+                 }, "MyScrollBar Color " & Uuid)
+            Else
+                '无动画
+                AniStop("MyScrollBar Color " & Uuid)
+                SetResourceReference(ForegroundProperty, NewColor)
+                Opacity = NewOpacity
+            End If
+
+        Catch ex As Exception
+            Log(ex, "滚动条颜色改变出错")
+        End Try
+    End Sub
+
+End Class

+ 27 - 0
Plain Craft Launcher 2/Controls/MyScrollViewer.vb

@@ -0,0 +1,27 @@
+Public Class MyScrollViewer
+    Inherits ScrollViewer
+
+    Public Property DeltaMuity As Double = 1
+    Private RealOffset As Double
+    Private Sub MyScrollViewer_PreviewMouseWheel(sender As Object, e As MouseWheelEventArgs) Handles Me.PreviewMouseWheel
+        If e.Delta = 0 OrElse ActualHeight = 0 OrElse ScrollableHeight = 0 Then Exit Sub
+        Dim SourceTypeName As String = e.Source.GetType.Name
+        If Content.TemplatedParent Is Nothing AndAlso (SourceTypeName.Contains("Combo") OrElse SourceTypeName = "MyTextBox") Then Exit Sub '确定可视树,防止滚动问题
+        PerformVerticalOffsetDelta(-e.Delta)
+        e.Handled = True
+    End Sub
+    Public Sub PerformVerticalOffsetDelta(Delta As Double)
+        AniStart(AaDouble(Sub(AnimDelta As Double)
+                              RealOffset = MathRange(RealOffset + AnimDelta, 0, ExtentHeight - ActualHeight)
+                              ScrollToVerticalOffset(RealOffset)
+                          End Sub, Delta * DeltaMuity, 300,, New AniEaseOutFluent(6)))
+    End Sub
+    Private Sub MyScrollViewer_ScrollChanged(sender As Object, e As ScrollChangedEventArgs) Handles Me.ScrollChanged
+        RealOffset = VerticalOffset
+        If FrmMain IsNot Nothing AndAlso (e.VerticalChange OrElse e.ViewportHeightChange) Then FrmMain.BtnExtraBack.ShowRefresh()
+    End Sub
+    Private Sub MyScrollViewer_IsVisibleChanged(sender As Object, e As DependencyPropertyChangedEventArgs) Handles Me.IsVisibleChanged
+        FrmMain.BtnExtraBack.ShowRefresh()
+    End Sub
+
+End Class

+ 15 - 0
Plain Craft Launcher 2/Controls/MySearchBox.xaml

@@ -0,0 +1,15 @@
+<local:MyCard x:Class="MySearchBox"
+      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+      xmlns:local="clr-namespace:PCL"
+      mc:Ignorable="d" 
+      d:DesignHeight="450" d:DesignWidth="800"
+      Height="40">
+    <Grid>
+        <local:MyTextBox HasAnimation="False" x:Name="TextBox" BorderThickness="0" Padding="32,0,40,0" MaxLength="50" FontSize="13.5" UseLayoutRounding="False" SnapsToDevicePixels="False" VerticalContentAlignment="Center" />
+        <Path Width="14" Margin="14,0,0,0" IsHitTestVisible="False" Height="14" Fill="{DynamicResource ColorBrush1}" Stretch="Uniform" Data="M675.9168 242.816C556.3008 123.2 362.4448 123.2 242.8288 242.816 123.2128 362.432 123.2128 556.288 242.8288 675.904 362.4448 795.52 556.3008 795.52 675.9168 675.904 795.5328 556.288 795.5328 362.432 675.9168 242.816L675.9168 242.816ZM1000.7168 892.48C1030.6048 922.368 1030.6048 970.816 1000.7168 1000.704 970.8288 1030.656 922.3808 1030.656 892.4288 1000.704L730.0608 838.336C728.1408 836.416 727.4368 833.92 725.7728 831.872 546.3168 960.448 295.7568 945.408 134.5408 784.192-44.8512 604.8-44.8512 313.92 134.5408 134.528 313.9328-44.864 604.8128-44.864 784.2048 134.528 945.4208 295.744 960.4608 546.304 831.8848 725.76 833.9328 727.424 836.4288 728.128 838.3488 730.048L1000.7168 892.48Z" HorizontalAlignment="Left" />
+        <local:MyIconButton x:Name="BtnClear" IsHitTestVisible="False" Opacity="0" Width="24" Margin="0,0,10,0" LogoScale="0.75" Height="24" 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" HorizontalAlignment="Right" Theme="Black" />
+    </Grid>
+</local:MyCard>

+ 42 - 0
Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb

@@ -0,0 +1,42 @@
+Class MySearchBox
+    Inherits MyCard
+
+    Public Event TextChanged(ByVal sender As Object, ByVal e As EventArgs)
+    Private Sub MySearchBox_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        TextBox.Focus()
+    End Sub
+
+    '属性
+    Public Property HintText() As String
+        Get
+            Return TextBox.HintText
+        End Get
+        Set(ByVal value As String)
+            TextBox.HintText = value
+        End Set
+    End Property
+    Public Property Text() As String
+        Get
+            Return TextBox.Text
+        End Get
+        Set(ByVal value As String)
+            TextBox.Text = value
+        End Set
+    End Property
+
+    Private Sub Text_TextChanged(sender As Object, e As TextChangedEventArgs) Handles TextBox.TextChanged
+        If String.IsNullOrEmpty(TextBox.Text) Then
+            AniStart(AaOpacity(BtnClear, -BtnClear.Opacity, 90), "MySearchBox ClearBtn " & Uuid)
+            BtnClear.IsHitTestVisible = False
+        Else
+            AniStart(AaOpacity(BtnClear, 1 - BtnClear.Opacity, 90), "MySearchBox ClearBtn " & Uuid)
+            BtnClear.IsHitTestVisible = True
+        End If
+        RaiseEvent TextChanged(sender, e)
+    End Sub
+    Private Sub BtnClear_Click(sender As Object, e As EventArgs) Handles BtnClear.Click
+        TextBox.Text = ""
+        TextBox.Focus()
+    End Sub
+
+End Class

+ 27 - 0
Plain Craft Launcher 2/Controls/MySlider.xaml

@@ -0,0 +1,27 @@
+<Border x:Class="MySlider"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             mc:Ignorable="d" 
+             d:DesignHeight="16" d:DesignWidth="162" 
+             FocusVisualStyle="{x:Null}"
+             MinHeight="16" UseLayoutRounding="False" SnapsToDevicePixels="False" BorderBrush="{DynamicResource ColorBrush1}" Name="PanBack" Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True">
+    <Grid x:Name="PanMain" MinHeight="16" MaxHeight="16" IsHitTestVisible="False">
+        <Line Height="4" Name="LineBack" Stroke="{StaticResource ColorBrushGray5}" StrokeThickness="1.1" X2="10000" Y1="2" Y2="2" HorizontalAlignment="Right" Width="0" SnapsToDevicePixels="True" Margin="0,0,1,0" />
+        <Line Height="4" Name="LineFore" Stroke="{Binding ElementName=PanBack, Path=BorderBrush}" StrokeThickness="1.1" X2="10000" Y1="2" Y2="2" Width="0" HorizontalAlignment="Left" SnapsToDevicePixels="True" Margin="1,0,0,0" />
+        <Ellipse HorizontalAlignment="Left" Width="16" Stroke="{Binding BorderBrush, ElementName=PanBack}" StrokeThickness="1.1" Fill="{StaticResource ColorBrushHalfWhite}" Name="ShapeDot" Height="16" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
+            <Ellipse.RenderTransform>
+                <ScaleTransform />
+            </Ellipse.RenderTransform>
+        </Ellipse>
+        <Popup Name="Popup" AllowsTransparency="True" IsHitTestVisible="False" PopupAnimation="Fade" VerticalOffset="38" HorizontalOffset="{Binding Margin.Left, ElementName=ShapeDot}" Placement="Center" PlacementRectangle="8,0,0,0">
+            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1.5" CornerRadius="3" MaxWidth="500" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                <Border.Effect>
+                    <DropShadowEffect Opacity="0.4" BlurRadius="4" ShadowDepth="2" />
+                </Border.Effect>
+                <TextBlock Name="TextHint" TextWrapping="Wrap" Foreground="{DynamicResource ColorBrush1}" Margin="7,5" FontSize="12"/>
+            </Border>
+        </Popup>
+    </Grid>
+</Border>

+ 177 - 0
Plain Craft Launcher 2/Controls/MySlider.xaml.vb

@@ -0,0 +1,177 @@
+Public Class MySlider
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Event Change(sender As Object, user As Boolean)
+    Public Event PreviewChange(sender As Object, e As RouteEventArgs)
+
+    '自定义属性
+
+    Private _MaxValue As Integer = 100
+    Public Property MaxValue As Integer
+        Get
+            Return _MaxValue
+        End Get
+        Set(value As Integer)
+            If value = _MaxValue Then Exit Property
+            _MaxValue = value
+            RefreshWidth(Nothing, Nothing)
+        End Set
+    End Property
+    Private ChangeByKey As Boolean = False
+    Private _Value As Integer = 0
+    Public Property Value As Integer
+        Get
+            Return _Value
+        End Get
+        Set(newValue As Integer)
+            Try
+
+                newValue = MathRange(newValue, 0, MaxValue)
+                If _Value = newValue Then Exit Property
+
+                '触发 Preview 事件,修改新值
+                Dim OldValue = _Value
+                _Value = newValue
+                If AniControlEnabled = 0 Then
+                    Dim e = New RouteEventArgs(False)
+                    RaiseEvent PreviewChange(Me, e)
+                    If e.Handled Then
+                        _Value = OldValue
+                        DragStop()
+                        Exit Property
+                    End If
+                End If
+
+                If IsLoaded AndAlso AniControlEnabled = 0 Then
+                    If ActualWidth < 16 Then Exit Property
+                    Dim NewWidth As Double = _Value / MaxValue * (ActualWidth - 16)
+                    Dim DeltaProcess As Double = Math.Abs(LineFore.Width / (ActualWidth - 16) - _Value / MaxValue)
+                    Dim Time As Double = (1 - Math.Pow(1 - DeltaProcess, 3)) * 300 + If(ChangeByKey, 100, 0)
+                    AniStart({
+                            AaWidth(LineFore, Math.Max(0, NewWidth + If(NewWidth < 0.5, 0, 0.5)) - LineFore.Width, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear)),
+                            AaWidth(LineBack, Math.Max(0, ActualWidth - 16 - NewWidth + If(ActualWidth - 16 - NewWidth < 0.5, 0, 0.5)) - LineBack.Width, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear)),
+                            AaX(ShapeDot, NewWidth - ShapeDot.Margin.Left, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear))
+                         }, "MySlider Progress " & Uuid)
+                Else
+                    RefreshWidth(Nothing, Nothing)
+                End If
+                If AniControlEnabled = 0 Then RaiseEvent Change(Me, False)
+
+            Catch ex As Exception
+                Log(ex, "滑动条进度改变出错", LogLevel.Hint)
+            End Try
+        End Set
+    End Property
+    Private Sub RefreshWidth(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged
+        If Not IsNothing(e) Then PanMain.Width = e.NewSize.Width
+        AniStop("MySlider Progress " & Uuid)
+        Dim NewWidth As Double = _Value / MaxValue * (ActualWidth - 16)
+        LineFore.Width = Math.Max(0, NewWidth + If(NewWidth < 0.5, 0, 0.5))
+        LineBack.Width = Math.Max(0, ActualWidth - 16 - NewWidth + If(ActualWidth - 16 - NewWidth < 0.5, 0, 0.5))
+        SetLeft(ShapeDot, NewWidth)
+    End Sub
+
+    '拖动
+
+    Public GetHintText As [Delegate]
+    Private Sub DragStart(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
+        e.Handled = True '防止 ScrollViewer 失焦问题
+        DragControl = Me
+        FrmMain.DragDoing()
+        AniStart({
+                 AaScaleTransform(ShapeDot, 0.8 - CType(ShapeDot.RenderTransform, ScaleTransform).ScaleX, 75,, New AniEaseOutFluent)
+            }, "MySlider Scale " & Uuid)
+        '更新 Popup
+        If GetHintText IsNot Nothing Then
+            TextHint.Text = GetHintText.DynamicInvoke(Value)
+            Popup.IsOpen = True
+            AniStop("MySlider KeyPopup " & Uuid)
+        End If
+    End Sub
+    Public Sub DragDoing()
+        Dim Percent As Double = MathRange((Mouse.GetPosition(PanMain).X - 8) / (ActualWidth - 16), 0, 1)
+        Dim NewValue As Integer = Percent * MaxValue
+        If Not NewValue = Value Then
+            Value = NewValue
+        End If
+        '更新 Popup
+        If GetHintText IsNot Nothing Then TextHint.Text = GetHintText.DynamicInvoke(NewValue)
+    End Sub
+    Public Sub DragStop()
+        RefreshColor()
+        AniStart({
+                 AaScaleTransform(ShapeDot, 1 - CType(ShapeDot.RenderTransform, ScaleTransform).ScaleX, 200,, New AniEaseOutFluent)
+            }, "MySlider Scale " & Uuid)
+        If GetHintText IsNot Nothing Then Popup.IsOpen = False
+    End Sub
+
+    '指向动画
+
+    Private Sub RefreshColor() Handles Me.IsEnabledChanged, Me.MouseEnter, Me.MouseLeave
+        Try
+
+            '判断当前颜色
+            Dim ColorName As String
+            Dim AnimationTime As Integer
+            If IsEnabled Then
+                If IsMouseOver OrElse (Not IsNothing(DragControl) AndAlso DragControl.Equals(Me)) Then
+                    ColorName = "ColorBrush3"
+                    AnimationTime = 100
+                Else
+                    ColorName = "ColorBrush1"
+                    AnimationTime = 200
+                End If
+            Else
+                ColorName = "ColorBrushGray4"
+                AnimationTime = 200
+            End If
+            '触发颜色动画
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+                '有动画
+                AniStart({AaColor(Me, BorderBrushProperty, ColorName, AnimationTime)}, "MySlider Color " & Uuid)
+            Else
+                '无动画
+                AniStop("MySlider Color " & Uuid)
+                SetResourceReference(BorderBrushProperty, ColorName)
+            End If
+
+        Catch ex As Exception
+            Log(ex, "滑动条颜色改变出错")
+        End Try
+    End Sub
+
+    '按键改变
+
+    Public Property ValueByKey As UInteger = 1
+    Private Sub MySlider_MouseEnter() Handles Me.MouseEnter
+        Focus() '确保按键能改变值
+    End Sub
+    Private Sub MySlider_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
+        '拒绝一边拖动一边用按键改变
+        If ReferenceEquals(Me, DragControl) Then Exit Sub
+        '改变值
+        If e.Key = Key.Left Then
+            ChangeByKey = True
+            Value -= ValueByKey
+            ChangeByKey = False
+            e.Handled = True
+        ElseIf e.Key = Key.Right Then
+            ChangeByKey = True
+            Value += ValueByKey
+            ChangeByKey = False
+            e.Handled = True
+        Else
+            Exit Sub
+        End If
+        '更新 Popup
+        If GetHintText IsNot Nothing Then
+            TextHint.Text = GetHintText.DynamicInvoke(Value)
+            Popup.IsOpen = True
+            AniStop("MySlider KeyPopup " & Uuid)
+            AniStart(AaCode(Sub() Popup.IsOpen = False, 700 * AniSpeed), "MySlider KeyPopup " & Uuid)
+        End If
+    End Sub
+
+End Class

+ 19 - 0
Plain Craft Launcher 2/Controls/MySliderDot.xaml

@@ -0,0 +1,19 @@
+<Border x:Class="MySliderDot"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             mc:Ignorable="d" 
+             d:DesignWidth="16" d:DesignHeight="162" 
+             FocusVisualStyle="{x:Null}"
+             MinWidth="16" UseLayoutRounding="False" SnapsToDevicePixels="False" BorderBrush="{DynamicResource ColorBrush1}" Name="PanBack" Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True">
+    <Grid x:Name="PanMain" MinWidth="16" MaxWidth="16" RenderTransformOrigin="0.5,0.5">
+        <Line IsHitTestVisible="False" Width="0" Name="LineBack" Stroke="{StaticResource ColorBrushGray5}" StrokeThickness="1.5" Y2="10000" X1="2" X2="2" VerticalAlignment="Bottom" Height="0" SnapsToDevicePixels="True" Margin="0,0,0,1" />
+        <Line IsHitTestVisible="False" Width="0" Name="LineFore" Stroke="{Binding ElementName=PanBack, Path=BorderBrush}" StrokeThickness="1.5" Y2="10000" X1="2" X2="2" Height="0" VerticalAlignment="Top" SnapsToDevicePixels="True" Margin="0,1,0,0" />
+        <Ellipse IsHitTestVisible="True" VerticalAlignment="Top" Height="16" Stroke="White" StrokeThickness="1.5" Fill="{DynamicResource ColorBrush3}" Name="ShapeDot" Width="16" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5" Opacity="0.8">
+            <Ellipse.RenderTransform>
+                <ScaleTransform ScaleX="2.3" ScaleY="2.3" />
+            </Ellipse.RenderTransform>
+        </Ellipse>
+    </Grid>
+</Border>

+ 169 - 0
Plain Craft Launcher 2/Controls/MySliderDot.xaml.vb

@@ -0,0 +1,169 @@
+Public Class MySliderDot
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Event Change(sender As Object, user As Boolean)
+    Public Event PreviewChange(sender As Object, e As RouteEventArgs)
+
+    '自定义属性
+
+    Private _MaxValue As Integer = 100
+    Public Property MaxValue As Integer
+        Get
+            Return _MaxValue
+        End Get
+        Set(value As Integer)
+            If value = _MaxValue Then Exit Property
+            _MaxValue = value
+            RefreshHeight(Nothing, Nothing)
+        End Set
+    End Property
+    Private ChangeByKey As Boolean = False
+    Private _Value As Integer = 0
+    Public Property Value As Integer
+        Get
+            Return _Value
+        End Get
+        Set(newValue As Integer)
+            Try
+
+                newValue = MathRange(newValue, 0, MaxValue)
+                If _Value = newValue Then Exit Property
+
+                '触发前瞻事件,修改新值
+                Dim OldValue = _Value
+                _Value = newValue
+                If AniControlEnabled = 0 Then
+                    Dim e = New RouteEventArgs(False)
+                    RaiseEvent PreviewChange(Me, e)
+                    If e.Handled Then
+                        _Value = OldValue
+                        Exit Property
+                    End If
+                End If
+
+                If IsLoaded AndAlso AniControlEnabled = 0 Then
+                    If ActualHeight < 16 Then Exit Property
+                    Dim NewHeight As Double = _Value / MaxValue * (ActualHeight - 16)
+                    Dim DeltaProcess As Double = Math.Abs(LineFore.Height / (ActualHeight - 16) - _Value / MaxValue)
+                    Dim Time As Double = (1 - Math.Pow(1 - DeltaProcess, 3)) * 400 + If(ChangeByKey, 100, 0)
+                    AniStart({
+                            AaHeight(LineFore, Math.Max(0, NewHeight + If(NewHeight < 0.5, 0, 0.5)) - LineFore.Height, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear)),
+                            AaHeight(LineBack, Math.Max(0, ActualHeight - 16 - NewHeight + If(ActualHeight - 16 - NewHeight < 0.5, 0, 0.5)) - LineBack.Height, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear)),
+                            AaY(ShapeDot, NewHeight - ShapeDot.Margin.Top, Time,, If(Time > 50, New AniEaseOutFluent, New AniEaseLinear))
+                         }, "MySlider Progress " & Uuid)
+                Else
+                    RefreshHeight(Nothing, Nothing)
+                End If
+                If AniControlEnabled = 0 Then RaiseEvent Change(Me, False)
+
+            Catch ex As Exception
+                Log(ex, "滑动条进度改变出错", LogLevel.Hint)
+            End Try
+        End Set
+    End Property
+    Private Sub RefreshHeight(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged
+        If Not IsNothing(e) Then PanMain.Height = e.NewSize.Height
+        AniStop("MySlider Progress " & Uuid)
+        Dim NewHeight As Double = _Value / MaxValue * (ActualHeight - 16)
+        LineFore.Height = Math.Max(0, NewHeight + If(NewHeight < 0.5, 0, 0.5))
+        LineBack.Height = Math.Max(0, ActualHeight - 16 - NewHeight + If(ActualHeight - 16 - NewHeight < 0.5, 0, 0.5))
+        SetTop(ShapeDot, NewHeight)
+    End Sub
+
+    '拖动
+
+    Public GetHintText As [Delegate]
+    Private Sub DragStart(sender As Object, e As MouseButtonEventArgs) Handles ShapeDot.MouseLeftButtonDown
+        e.Handled = True '防止 ScrollViewer 失焦问题
+        DragControl = Me
+        AniStart({
+                 AaScaleTransform(ShapeDot, 2 - CType(ShapeDot.RenderTransform, ScaleTransform).ScaleX, 100,, New AniEaseOutFluent)
+            }, "MySlider Scale " & Uuid)
+    End Sub
+    Public Sub DragDoing()
+        Dim Percent As Double = MathRange((Mouse.GetPosition(PanMain).Y - 8) / (ActualHeight - 16), 0, 1)
+        Dim NewValue As Integer = Percent * MaxValue
+        If Not NewValue = Value Then Value = NewValue
+    End Sub
+    Public Sub DragStop()
+        RefreshColor()
+        AniStart({
+                 AaScaleTransform(ShapeDot, 2.3 - CType(ShapeDot.RenderTransform, ScaleTransform).ScaleX, 200,, New AniEaseOutFluent)
+            }, "MySlider Scale " & Uuid)
+        If Value > 0 Then
+            Value = MaxValue
+        Else
+            FrmMain.SliderDrag_Finish()
+        End If
+    End Sub
+
+    '指向动画
+
+    Private Sub RefreshColor() Handles Me.IsEnabledChanged, Me.MouseEnter, Me.MouseLeave
+        'Try
+
+        '    '判断当前颜色
+        '    Dim ColorName As String
+        '    Dim AnimationTime As Integer
+        '    If IsEnabled Then
+        '        If IsMouseOver OrElse (Not IsNothing(DragControl) AndAlso DragControl.Equals(Me)) Then
+        '            ColorName = "ColorBrush3"
+        '            AnimationTime = 100
+        '        Else
+        '            ColorName = "ColorBrush1"
+        '            AnimationTime = 200
+        '        End If
+        '    Else
+        '        ColorName = "ColorBrushGray4"
+        '        AnimationTime = 200
+        '    End If
+        '    '触发颜色动画
+        '    If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+        '        '有动画
+        '        AniStart({AaColor(Me, BorderBrushProperty, ColorName, AnimationTime)}, "MySlider Color " & Uuid)
+        '    Else
+        '        '无动画
+        '        AniStop("MySlider Color " & Uuid)
+        '        SetResourceReference(BorderBrushProperty, ColorName)
+        '    End If
+
+        'Catch ex As Exception
+        '    Log(ex, "滑动条颜色改变出错")
+        'End Try
+    End Sub
+
+    '按键改变
+
+    'Public Property ValueByKey As UInteger = 1
+    'Private Sub MySlider_MouseEnter() Handles Me.MouseEnter
+    '    Focus() '确保按键能改变值
+    'End Sub
+    'Private Sub MySlider_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
+    '    '拒绝一边拖动一边用按键改变
+    '    If ReferenceEquals(Me, DragControl) Then Exit Sub
+    '    '改变值
+    '    If e.Key = Key.Left Then
+    '        ChangeByKey = True
+    '        Value -= ValueByKey
+    '        ChangeByKey = False
+    '        e.Handled = True
+    '    ElseIf e.Key = Key.Right Then
+    '        ChangeByKey = True
+    '        Value += ValueByKey
+    '        ChangeByKey = False
+    '        e.Handled = True
+    '    Else
+    '        Exit Sub
+    '    End If
+    '    '更新 Popup
+    '    If GetHintText IsNot Nothing Then
+    '        TextHint.Text = GetHintText.DynamicInvoke(Value)
+    '        Popup.IsOpen = True
+    '        AniStop("MySlider KeyPopup " & Uuid)
+    '        AniStart(AaCode(Sub() Popup.IsOpen = False, 700 * AniSpeed), "MySlider KeyPopup " & Uuid)
+    '    End If
+    'End Sub
+
+End Class

+ 277 - 0
Plain Craft Launcher 2/Controls/MyTextBox.vb

@@ -0,0 +1,277 @@
+Public Class MyTextBox
+    Inherits TextBox
+
+    '自定义属性
+
+    Public Property HasAnimation As Boolean = True
+    Public Property ShowValidateResult As Boolean = True
+
+    '额外控件初始化
+
+    Private _labWrong As TextBlock = Nothing
+    Private ReadOnly Property labWrong As TextBlock
+        Get
+            If Template Is Nothing Then Return Nothing
+            If _labWrong Is Nothing Then _labWrong = Template.FindName("labWrong", Me)
+            Return _labWrong
+        End Get
+    End Property
+    Private _labHint As TextBlock = Nothing
+    Private ReadOnly Property labHint As TextBlock
+        Get
+            If Template Is Nothing Then Return Nothing
+            If _labHint Is Nothing Then _labHint = Template.FindName("labHint", Me)
+            Return _labHint
+        End Get
+    End Property
+    Public Overrides Sub OnApplyTemplate()
+        MyBase.OnApplyTemplate()
+        If HintText = "" OrElse labHint.Text <> "" Then Exit Sub
+        labHint.Text = If(Text = "", HintText, "")
+    End Sub
+
+    '事件
+
+    Public Uuid As Integer = GetUuid()
+    Public Shared Event ValidateChanged(sender As Object, e As EventArgs)
+    Public ChangedEventList As New List(Of RoutedEventHandler)
+    Public Custom Event ValidatedTextChanged As RoutedEventHandler
+        AddHandler(value As RoutedEventHandler)
+            ChangedEventList.Add(value)
+        End AddHandler
+        RemoveHandler(value As RoutedEventHandler)
+            ChangedEventList.Remove(value)
+        End RemoveHandler
+        RaiseEvent(sender As Object, e As TextChangedEventArgs)
+            For Each handler As RoutedEventHandler In ChangedEventList
+                If Not IsNothing(handler) Then handler.Invoke(sender, e)
+            Next
+        End RaiseEvent
+    End Event
+
+    '输入验证
+
+    ''' <summary>
+    ''' 输入验证结果。若为空字符串则无错误,否则为第一个错误原因。
+    ''' </summary>
+    Public Property ValidateResult As String
+        Get
+            Return GetValue(ValidateResultProperty)
+        End Get
+        Set(value As String)
+            SetValue(ValidateResultProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly ValidateResultProperty As DependencyProperty = DependencyProperty.Register("ValidateResult", GetType(String), GetType(MyTextBox), New PropertyMetadata(""))
+    ''' <summary>
+    ''' 输入验证的规则。
+    ''' </summary>
+    Public Property ValidateRules As ObjectModel.Collection(Of Validate)
+        Get
+            Return _ValidateRules
+        End Get
+        Set(Value As ObjectModel.Collection(Of Validate))
+            _ValidateRules = Value
+            Validate()
+        End Set
+    End Property
+    Private _ValidateRules As New ObjectModel.Collection(Of Validate)
+    ''' <summary>
+    ''' 进行输入验证。
+    ''' </summary>
+    Public Sub Validate() Handles Me.Loaded
+        '执行输入验证
+        ValidateResult = ""
+        For Each ValidateRule As Validate In ValidateRules
+            ValidateResult = ValidateRule.Validate(Text)
+            If IsNothing(ValidateResult) OrElse Not ValidateResult = "" Then Exit For
+        Next
+        Dim NewResult As Integer = If(ValidateResult = "", 0, 1)
+        '根据结果改变样式
+        If ShownValidateResult <> NewResult Then
+            If IsLoaded AndAlso labWrong IsNot Nothing Then
+                ChangeValidateResult(ValidateResult = "", True)
+            Else
+                RunInNewThread(Sub()
+                                   Thread.Sleep(30)
+                                   RunInUi(Sub() ChangeValidateResult(NewResult, False))
+                               End Sub, "DelayedValidate Change")
+            End If
+        End If
+        '更新错误信息
+        If ShowValidateResult AndAlso Not ValidateResult = "" Then
+            If IsLoaded AndAlso labWrong IsNot Nothing Then
+                labWrong.Text = ValidateResult
+            Else
+                RunInNewThread(Sub()
+                                   Dim IsFinished As Boolean = False
+                                   Do Until IsFinished
+                                       Thread.Sleep(20)
+                                       RunInUiWait(Sub()
+                                                       If labWrong IsNot Nothing Then
+                                                           labWrong.Text = ValidateResult
+                                                           IsFinished = True
+                                                       End If
+                                                       If Not IsLoaded Then IsFinished = True
+                                                   End Sub)
+                                   Loop
+                               End Sub, "DelayedValidate Text")
+            End If
+        End If
+    End Sub
+    Private ShownValidateResult As Integer = -1
+    Private Sub ChangeValidateResult(NewResult As Boolean, IsLoaded As Boolean)
+        If IsLoaded AndAlso AniControlEnabled = 0 AndAlso IsTextChanged AndAlso labWrong IsNot Nothing Then
+            If NewResult Then
+                '变为正确
+                ShownValidateResult = 0
+                AniStart({
+                         AaOpacity(labWrong, -labWrong.Opacity, 150),
+                         AaHeight(labWrong, -labWrong.Height, 150,, New AniEaseOutFluent),
+                         AaCode(Sub() labWrong.Visibility = Visibility.Collapsed,, True)
+                    }, "MyTextBox Validate " & Uuid)
+            ElseIf ShowValidateResult Then
+                '变为错误
+                ShownValidateResult = 1
+                labWrong.Visibility = Visibility.Visible
+                AniStart({
+                         AaOpacity(labWrong, 1 - labWrong.Opacity, 170),
+                         AaHeight(labWrong, 21 - labWrong.Height, 300,, New AniEaseOutBack(AniEasePower.Weak))
+                    }, "MyTextBox Validate " & Uuid)
+            Else
+                '变为错误,但不显示文本
+                ShownValidateResult = 2
+            End If
+        Else
+            ShownValidateResult = 3
+            'AniStop("MyTextBox Validate " & Uuid)
+            'If NewResult Then
+            '    '变为正确
+            '    labWrong.Opacity = 0
+            '    labWrong.Height = 0
+            '    labWrong.Visibility = Visibility.Collapsed
+            'Else
+            '    '变为错误
+            '    labWrong.Visibility = Visibility.Visible
+            '    labWrong.Text = ValidateResult
+            '    labWrong.Opacity = 1
+            '    labWrong.Height = 20.5
+            'End If
+            'Dim OriginalEnabled As Integer = AniControlEnabled
+            'AniControlEnabled += 1
+            'RefreshColor()
+            'AniControlEnabled = OriginalEnabled
+        End If
+        RefreshColor()
+        RaiseEvent ValidateChanged(Me, New EventArgs)
+    End Sub
+
+    '提示文本
+
+    ''' <summary>
+    ''' 是否已经由用户输入过文本,若尚未输入过,则不显示输入检查的失败。
+    ''' </summary>
+    Private IsTextChanged As Boolean = False
+    Private _HintText As String = ""
+    Public Property HintText As String
+        Get
+            Return _HintText
+        End Get
+        Set(value As String)
+            _HintText = value
+            If labHint IsNot Nothing Then labHint.Text = If(Text = "", HintText, "")
+        End Set
+    End Property
+    Private Sub MyTextBox_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles Me.TextChanged
+        Try
+            '改变提示文本
+            If labHint IsNot Nothing Then labHint.Text = If(Text = "", HintText, "")
+            '改变输入记录
+            IsTextChanged = IsLoaded
+            '进行输入验证
+            Validate()
+            If Not ValidateResult = "" Then Exit Sub
+            '改变文本
+            RaiseEvent ValidatedTextChanged(sender, e)
+        Catch ex As Exception
+            Log(ex, "进行输入验证时出错", LogLevel.Assert)
+        End Try
+    End Sub
+
+    '颜色
+
+    Private Sub RefreshColor() Handles Me.IsEnabledChanged, Me.MouseEnter, Me.MouseLeave, Me.GotFocus, Me.LostFocus
+        Try
+            '手动关闭动画
+            If Not HasAnimation Then Exit Sub
+            '不对 ComboBox 从属进行动画
+            If TemplatedParent IsNot Nothing AndAlso TypeOf TemplatedParent Is MyComboBox Then Exit Sub
+            '判断当前颜色
+            Dim ForeColorName As String, BackColorName As String
+            Dim AnimationTime As Integer
+            If IsEnabled Then
+                If ValidateResult = "" OrElse Not IsTextChanged Then
+                    If IsFocused Then
+                        ForeColorName = "ColorBrush4"
+                        BackColorName = "ColorBrush9"
+                        AnimationTime = 60
+                    ElseIf IsMouseOver Then
+                        ForeColorName = "ColorBrush3"
+                        BackColorName = "ColorBrushHalfWhite"
+                        AnimationTime = 100
+                    Else
+                        ForeColorName = "ColorBrush1"
+                        BackColorName = "ColorBrushHalfWhite"
+                        AnimationTime = 200
+                    End If
+                Else
+                    ForeColorName = "ColorBrushRedLight"
+                    BackColorName = "ColorBrushRedBack"
+                    AnimationTime = 200
+                    'If IsFocused OrElse IsMouseOver Then
+                    '    ForeColorName = "ColorBrushRedLight"
+                    '    BackColorName = "ColorBrushRedBack"
+                    '    AnimationTime = 100
+                    'Else
+                    '    ForeColorName = "ColorBrushRedDark"
+                    '    BackColorName = "ColorBrushHalfWhite"
+                    '    AnimationTime = 200
+                    'End If
+                End If
+            Else
+                ForeColorName = "ColorBrushGray4"
+                BackColorName = "ColorBrushHalfWhite"
+                AnimationTime = 200
+            End If
+            '触发颜色动画
+            If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+                '有动画
+                AniStart({
+                         AaColor(Me, BorderBrushProperty, ForeColorName, AnimationTime),
+                         AaColor(Me, BackgroundProperty, BackColorName, AnimationTime)
+                     }, "MyTextBox Color " & Uuid)
+            Else
+                '无动画
+                AniStop("MyTextBox Color " & Uuid)
+                SetResourceReference(BorderBrushProperty, ForeColorName)
+                SetResourceReference(BackgroundProperty, BackColorName)
+            End If
+
+        Catch ex As Exception
+            Log(ex, "文本框颜色改变出错")
+        End Try
+    End Sub
+    Private Sub RefreshTextColor() Handles Me.IsEnabledChanged
+        Dim NewColor As MyColor = If(IsEnabled, ColorGray1, ColorGray4)
+        If CType(Foreground, SolidColorBrush).Color.R = NewColor.R Then Exit Sub
+        If IsLoaded AndAlso AniControlEnabled = 0 AndAlso Not Text = "" Then
+            '有动画
+            AniStart({AaColor(Me, ForegroundProperty, If(IsEnabled, "ColorBrushGray1", "ColorBrushGray4"), 200)}, "MyTextBox TextColor " & Uuid)
+        Else
+            '无动画
+            AniStop("MyTextBox TextColor " & Uuid)
+            Foreground = NewColor
+        End If
+    End Sub
+
+End Class

+ 110 - 0
Plain Craft Launcher 2/Controls/MyTextButton.vb

@@ -0,0 +1,110 @@
+Public Class MyTextButton
+    Inherits Label
+
+    Public Event Click(sender As Object, e As EventArgs)
+
+    '基础
+
+    Public Uuid As Integer = GetUuid()
+    Public Sub New()
+        SetResourceReference(ForegroundProperty, "ColorBrush1")
+        Background = ColorSemiTransparent
+    End Sub
+
+    '文本
+
+    Public Property Text As String
+        Get
+            Return GetValue(TextProperty)
+        End Get
+        Set(value As String)
+            SetValue(TextProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly TextProperty As DependencyProperty =
+        DependencyProperty.Register("Text", GetType(String), GetType(MyTextButton), New PropertyMetadata("", Sub(sender As MyTextButton, e As DependencyPropertyChangedEventArgs)
+                                                                                                                 If Not e.OldValue = e.NewValue Then
+                                                                                                                     AniStart({
+                                                                                                                              AaOpacity(sender, -sender.Opacity, 50),
+                                                                                                                              AaCode(Sub() sender.Content = e.NewValue,, True),
+                                                                                                                              AaOpacity(sender, 1, 170)
+                                                                                                                        }, "MyTextButton Text " & sender.Uuid)
+                                                                                                                 End If
+                                                                                                             End Sub))
+
+    '鼠标事件
+
+    Public IsMouseDown As Boolean = False
+    Private Sub MyTextButton_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonDown
+        IsMouseDown = True
+        e.Handled = True
+    End Sub
+    Private Sub MyTextButton_MouseLeave() Handles Me.MouseLeave
+        IsMouseDown = False
+    End Sub
+    Private Sub MyTextButton_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonUp
+        If IsMouseDown Then
+            IsMouseDown = False
+            Log("[Control] 按下文本按钮:" & Text)
+            RaiseEvent Click(Me, Nothing)
+            ModEvent.TryStartEvent(EventType, EventData)
+            e.Handled = True
+        End If
+    End Sub
+
+    '指向动画
+
+    Private Const AnimationTimeIn As Integer = 100
+    Private Const AnimationTimeOut As Integer = 200
+    Private ColorName As String
+    Private Sub RefreshColor() Handles Me.MouseEnter, Me.MouseLeave, Me.IsEnabledChanged, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp
+        '判断当前颜色
+        Dim ForeName As String
+        Dim Time As Integer
+        If IsMouseDown Then
+            ForeName = "ColorBrush4"
+            Time = 30
+        ElseIf IsMouseOver Then
+            ForeName = "ColorBrush3"
+            Time = AnimationTimeIn
+        Else
+            ForeName = "ColorBrush1"
+            Time = AnimationTimeOut
+        End If
+        '重复性验证
+        If ColorName = ForeName Then Exit Sub
+        ColorName = ForeName
+        '触发颜色动画
+        If IsLoaded AndAlso AniControlEnabled = 0 Then '防止默认属性变更触发动画
+            '有动画
+            AniStart(AaColor(Me, ForegroundProperty, ForeName, Time), "MyTextButton Color " & Uuid)
+        Else
+            '无动画
+            AniStop("MyTextButton Color " & Uuid)
+            SetResourceReference(ForegroundProperty, ForeName)
+        End If
+    End Sub
+
+    '实现自定义事件
+    Public Property EventType As String
+        Get
+            Return GetValue(EventTypeProperty)
+        End Get
+
+        Set(ByVal value As String)
+            SetValue(EventTypeProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyTextButton), New PropertyMetadata(Nothing))
+    Public Property EventData As String
+        Get
+            Return GetValue(EventDataProperty)
+        End Get
+
+        Set(ByVal value As String)
+            SetValue(EventDataProperty, value)
+        End Set
+    End Property
+    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyTextButton), New PropertyMetadata(Nothing))
+
+End Class

+ 179 - 0
Plain Craft Launcher 2/FormMain.xaml

@@ -0,0 +1,179 @@
+<Window
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:local="clr-namespace:PCL"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
+        x:Name="WindMain" x:Class="FormMain" Style="{StaticResource MyWindow}"
+        AllowDrop="True" Topmost="True" Title="Plain Craft Launcher 2 " AllowsTransparency="True" MinHeight="450" MinWidth="760" ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico" Left="-10000" WindowStyle="None" ResizeMode="CanMinimize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" RenderTransformOrigin="0.5,0.5">
+    <Window.TaskbarItemInfo>
+        <TaskbarItemInfo ThumbnailClipMargin="{Binding Margin, ElementName=PanForm}" />
+    </Window.TaskbarItemInfo>
+    <Grid x:Name="PanBack" SnapsToDevicePixels="True" UseLayoutRounding="True" Margin="1" RenderTransformOrigin="0.5,0.5">
+        <Grid.RenderTransform>
+            <ScaleTransform ScaleX="0.95" ScaleY="0.95" />
+        </Grid.RenderTransform>
+        <Rectangle x:Name="ResizerT" Stroke="{x:Null}" StrokeThickness="0.0001" Height="8" VerticalAlignment="Top" Margin="13,0" Cursor="SizeNS">
+            <Rectangle.Fill>
+                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
+                    <GradientStop Color="#21000000" Offset="1" />
+                    <GradientStop Color="#11000000" Offset="0.7" />
+                    <GradientStop Color="#00000000" Offset="0" />
+                </LinearGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerB" Stroke="{x:Null}" StrokeThickness="0.0001" Height="8" VerticalAlignment="Bottom" Margin="13,0" Cursor="SizeNS">
+            <Rectangle.Fill>
+                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
+                    <GradientStop Color="#21000000" Offset="0" />
+                    <GradientStop Color="#11000000" Offset="0.3" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </LinearGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerR" HorizontalAlignment="Right" Margin="0,13" Stroke="{x:Null}" StrokeThickness="0.0001" Width="8" Cursor="SizeWE">
+            <Rectangle.Fill>
+                <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
+                    <GradientStop Color="#21000000" Offset="0" />
+                    <GradientStop Color="#11000000" Offset="0.3" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </LinearGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerL" HorizontalAlignment="Left" Stroke="{x:Null}" Width="8" Margin="0,13" StrokeThickness="0.0001" Cursor="SizeWE">
+            <Rectangle.Fill>
+                <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
+                    <GradientStop Color="#21000000" Offset="1" />
+                    <GradientStop Color="#11000000" Offset="0.7" />
+                    <GradientStop Color="#00000000" Offset="0" />
+                </LinearGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerLT" HorizontalAlignment="Left" Stroke="{x:Null}" Width="13" StrokeThickness="0.0001" VerticalAlignment="Top" Height="13" Cursor="SizeNWSE">
+            <Rectangle.Fill>
+                <RadialGradientBrush Center="1,1" GradientOrigin="1,1" RadiusX="1" RadiusY="1">
+                    <GradientStop Color="#21000000" Offset="0.2" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </RadialGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerLB" HorizontalAlignment="Left" Stroke="{x:Null}" Width="13" StrokeThickness="0.0001" VerticalAlignment="Bottom" Height="13" Cursor="SizeNESW">
+            <Rectangle.Fill>
+                <RadialGradientBrush Center="1,0" GradientOrigin="1,0" RadiusX="1" RadiusY="1">
+                    <GradientStop Color="#21000000" Offset="0.2" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </RadialGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerRB" HorizontalAlignment="Right" Stroke="{x:Null}" Width="13" StrokeThickness="0.0001" VerticalAlignment="Bottom" Height="13" Cursor="SizeNWSE">
+            <Rectangle.Fill>
+                <RadialGradientBrush Center="0,0" GradientOrigin="0,0" RadiusX="1" RadiusY="1">
+                    <GradientStop Color="#21000000" Offset="0.2" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </RadialGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Rectangle x:Name="ResizerRT" HorizontalAlignment="Right" Stroke="{x:Null}" Width="13" StrokeThickness="0.0001" VerticalAlignment="Top" Height="13" Cursor="SizeNESW">
+            <Rectangle.Fill>
+                <RadialGradientBrush Center="0,1" GradientOrigin="0,1" RadiusX="1" RadiusY="1">
+                    <GradientStop Color="#21000000" Offset="0.2" />
+                    <GradientStop Color="#00000000" Offset="1" />
+                </RadialGradientBrush>
+            </Rectangle.Fill>
+        </Rectangle>
+        <Border Margin="8" x:Name="BorderForm">
+            <Border.Clip>
+                <RectangleGeometry x:Name="RectForm" RadiusX="6" RadiusY="6" />
+            </Border.Clip>
+            <Grid x:Name="PanForm" SnapsToDevicePixels="False" UseLayoutRounding="False">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition />
+                </Grid.RowDefinitions>
+                <Canvas x:Name="ImgBack" IsHitTestVisible="False" Grid.Row="1" RenderOptions.BitmapScalingMode="LowQuality" />
+                <Grid x:Name="PanTitle" Height="48">
+                    <Grid.Background>
+                        <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
+                            <GradientStop Color="{StaticResource ColorObject3}" Offset="0"/>
+                            <GradientStop Color="{StaticResource ColorObject2}" Offset="0.5"/>
+                            <GradientStop Color="{StaticResource ColorObject3}" Offset="1"/>
+                        </LinearGradientBrush>
+                    </Grid.Background>
+                    <Image x:Name="ImgTitle" IsHitTestVisible="False" Stretch="UniformToFill" IsEnabled="False" RenderOptions.BitmapScalingMode="LowQuality" />
+                    <local:MyIconButton x:Name="BtnTitleClose" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,12,0" Theme="White" 
+                                       LogoScale="0.72" 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" />
+                    <local:MyIconButton x:Name="BtnTitleMin" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,44,0" Theme="White" 
+                                       LogoScale="0.72" Logo="F1 M0,0 h15 v2 h-15 v-2 Z" />
+                    <Grid x:Name="PanTitleMain" HorizontalAlignment="Left" Width="{Binding ActualWidth, ElementName=PanTitle, Mode=OneWay}">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="1*" />
+                        </Grid.ColumnDefinitions>
+                        <Path x:Name="ShapeTitleLogo" Data="M26,29 v-25 h5 a7,7 180 0 1 0,14 h-5 M61,6.5 a10,11.5 180 1 0 0,18 M68,2.5 v24.5 h12 M98,2 v27 M107,2 v27" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="19,15.5,0,15.5" Stretch="fill" Stroke="White" StrokeThickness="1.9" Width="53" />
+                        <TextBlock x:Name="LabTitleLogo" Visibility="Collapsed" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="18,0,0,0" Foreground="White" FontSize="17" VerticalAlignment="Center" />
+                        <Image x:Name="ImageTitleLogo" SnapsToDevicePixels="False" UseLayoutRounding="False" IsHitTestVisible="False" HorizontalAlignment="Left" Margin="7" VerticalAlignment="Center" Visibility="Collapsed" />
+                        <StackPanel Grid.Column="1" x:Name="PanTitleSelect" HorizontalAlignment="Left" Height="27" VerticalAlignment="Center" Orientation="Horizontal" Margin="13,0">
+                            <local:MyRadioButton x:Name="BtnTitleSelect0" Text="{DynamicResource LangTitleHome}" Tag="0" Margin="5,0" Checked="True" Padding="2,0"
+                                             LogoScale="0.95" Logo="M985.403 552.96C985.403 812.702 773.041 1024 512 1024S38.597 812.702 38.597 552.96c0-158.208 79.163-304.955 211.771-392.625 31.074-20.52 72.94-11.973 93.46 19.101s12.012 72.94-19.062 93.46c-94.681 62.622-151.237 167.306-151.237 280.064 0 185.344 151.788 336.108 338.471 336.108S850.471 738.304 850.471 552.96c0-114.649-57.974-220.278-155.096-282.585-31.35-20.125-40.448-61.833-20.362-93.184s61.794-40.448 93.184-20.4c135.995 87.236 217.206 235.362 217.206 396.169zM512 560.128c37.258 0 67.466-30.208 67.466-67.466V67.466C579.466 30.208 549.258 0 512 0s-67.466 30.208-67.466 67.466v425.196c0 37.258 30.208 67.466 67.466 67.466z" />
+                            <local:MyRadioButton x:Name="BtnTitleSelect1" Text="{DynamicResource LangTitleDownload}" Tag="1" Margin="5,0" Padding="2,0"
+                                             LogoScale="0.9" Logo="M955.076923 610.461538h-59.076923c-15.753846 0-29.538462 13.784615-29.538462 29.538462v196.923077c0 15.753846-13.784615 29.538462-29.538461 29.538461h-649.846154c-15.753846 0-29.538462-13.784615-29.538461-29.538461v-196.923077c0-15.753846-13.784615-29.538462-29.538462-29.538462h-59.076923c-15.753846 0-29.538462 13.784615-29.538462 29.538462V905.846154c0 43.323077 35.446154 78.769231 78.769231 78.769231h787.692308c43.323077 0 78.769231-35.446154 78.769231-78.769231V640c0-15.753846-13.784615-29.538462-29.538462-29.538462zM492.307692 740.430769c11.815385 11.815385 29.538462 11.815385 41.353846 0l265.846154-265.846154c11.815385-11.815385 11.815385-29.538462 0-41.353846l-41.353846-41.353846c-11.815385-11.815385-29.538462-11.815385-41.353846 0l-110.276923 110.276923c-11.815385 11.815385-33.476923 3.938462-33.476923-13.784615V68.923077C571.076923 53.169231 555.323077 39.384615 541.538462 39.384615h-59.076924c-15.753846 0-29.538462 13.784615-29.538461 29.538462v417.476923c0 17.723077-21.661538 25.6-33.476923 13.784615l-110.276923-110.276923c-11.815385-11.815385-29.538462-11.815385-41.353846 0L226.461538 433.230769c-11.815385 11.815385-11.815385 29.538462 0 41.353846L492.307692 740.430769z" />
+                            <local:MyRadioButton x:Name="BtnTitleSelect2" Text="联机" Tag="2" Margin="5,0" Padding="2,0"
+                                             LogoScale="1.05" Logo="M512 817c-48.601 0-88-39.399-88-88s39.399-88 88-88 88 39.399 88 88-39.399 88-88 88zM237.671 565.74C308.335 474.58 397.369 429 504.774 429c118.433 0 214.225 55.421 287.377 166.264l-53.407 30.369c-13.84 7.87-31.362 4.367-41.114-8.219-50.291-64.911-114.577-97.367-192.856-97.367-86.851 0-156.835 40.318-209.95 120.953l-47.995-28.02c-15.263-8.91-20.412-28.507-11.502-43.77a32 32 0 0 1 2.344-3.47zM107.691 419.47C205.24 278.491 337.805 208 505.379 208c178.77 0 317.694 80.224 416.772 240.672l-56.54 31.73c-13.686 7.68-30.922 4.303-40.697-7.975C735.581 360.213 629.07 303.009 505.38 300.815 373.997 298.485 261.637 362.88 168.3 494l-50.116-28.505c-15.362-8.738-20.732-28.275-11.994-43.637a32 32 0 0 1 1.5-2.387z"/>
+                            <local:MyRadioButton x:Name="BtnTitleSelect3" Text="{DynamicResource LangTitleSetup}" Tag="3" Margin="5,0" Padding="2,0"
+                                             LogoScale="1.1" Logo="M940.4 463.7L773.3 174.2c-17.3-30-49.2-48.4-83.8-48.4H340.2c-34.6 0-66.5 18.5-83.8 48.4L89.2 463.7c-17.3 30-17.3 66.9 0 96.8L256.4 850c17.3 30 49.2 48.4 83.8 48.4h349.2c34.6 0 66.5-18.5 83.8-48.4l167.2-289.5c17.3-29.9 17.3-66.8 0-96.8z m-94.6 96.8L725.9 768.1c-17.3 30-49.2 48.4-83.8 48.4H387.5c-34.6 0-66.5-18.5-83.8-48.4L183.9 560.5c-17.3-30-17.3-66.9 0-96.8l119.8-207.5c17.3-30 49.2-48.4 83.8-48.4h254.6c34.6 0 66.5 18.5 83.8 48.4l119.8 207.5c17.3 30 17.3 66.9 0.1 96.8z M522.3 321.2c-2.5-0.1-5-0.2-7.5-0.2-119.9 0-214 110.3-186.3 235 15.8 70.9 71.5 126.6 142.4 142.4 17.5 3.9 34.7 5.4 51.4 4.7 102.1-3.9 183.6-87.9 183.6-191 0.1-103-81.5-187-183.6-190.9z m68.6 269.1c-18.5 18-43 28.9-68.6 30.7l-6 0.3c-30.2 0.4-58.6-11.4-79.7-33-19.5-20.1-30.7-47-30.9-75-0.3-29.6 11.1-57.4 32-78.3 20.6-20.6 48-32 77.2-32 2.5 0 5 0.1 7.5 0.3 26.7 1.8 51.5 13.2 70.5 32.5 19.6 20 30.8 46.9 31.2 74.9 0.2 30.2-11.5 58.6-33.2 79.6z"/>
+                            <local:MyRadioButton x:Name="BtnTitleSelect4" Text="{DynamicResource LangTitleOther}" Tag="4" Margin="5,0" Padding="2,0"
+                                             LogoScale="0.93" Logo="M364.145778 0h-273.066667C40.846222 0 0 40.846222 0 91.022222v273.066667C0 414.264889 40.846222 455.111111 91.136 455.111111h273.066667C414.321778 455.111111 455.111111 414.264889 455.111111 364.032V91.022222C455.111111 40.846222 414.321778 0 364.145778 0zM341.333333 341.333333H113.777778V113.777778h227.555555v227.555555zM933.034667 0h-273.066667C609.792 0 568.888889 40.846222 568.888889 91.022222v273.066667c0 50.176 40.903111 91.022222 91.136 91.022222h273.066667C983.210667 455.111111 1024 414.264889 1024 364.032V91.022222c0-50.176-40.789333-91.022222-90.965333-91.022222zM910.222222 341.333333h-227.555555V113.777778h227.555555v227.555555zM364.145778 568.888889h-273.066667C40.846222 568.888889 0 609.735111 0 659.911111v273.066667c0 50.176 40.846222 91.022222 91.136 91.022222h273.066667C414.321778 1024 455.111111 983.153778 455.111111 932.920889v-273.066667C455.111111 609.735111 414.321778 568.888889 364.145778 568.888889zM341.333333 910.222222H113.777778v-227.555555h227.555555v227.555555zM933.034667 568.888889h-273.066667c-50.176 0-91.079111 40.846222-91.079111 91.022222v273.066667c0 50.176 40.903111 91.022222 91.136 91.022222h273.066667c50.119111 0 90.908444-40.846222 90.908444-91.079111v-273.066667c0-50.119111-40.789333-90.965333-90.965333-90.965333zM910.222222 910.222222h-227.555555v-227.555555h227.555555v227.555555z"/>
+                        </StackPanel>
+                    </Grid>
+                    <Grid x:Name="PanTitleInner" Visibility="Collapsed" Margin="-16,0,0,0" Opacity="0" HorizontalAlignment="Left" Width="{Binding ActualWidth, ElementName=PanTitle, Mode=OneWay}">
+                        <local:MyIconButton HorizontalAlignment="Left" Theme="White" x:Name="BtnTitleInner" Margin="12,0,0,0" Height="28" Width="28"
+                                             LogoScale="0.87" Logo="M1097.162313 584.707482 250.272424 584.707482 562.833701 896.95123C591.749274 925.859979 591.749274 972.659975 562.833701 1001.568724 533.91812 1030.477473 487.058315 1030.477473 458.142742 1001.568724L21.580946 565.451235C6.757449 550.631234-0.361728 531.131238 0.028357 511.679985L0.028357 511.631236 0.028357 511.582486C-0.361728 492.13124 6.757449 472.582488 21.580946 457.762487L458.142742 21.644997C487.058315-7.215002 533.91812-7.215002 562.833701 21.644997 591.749274 50.553746 591.749274 97.4025 562.833701 126.262499L250.272424 438.55499 1097.162313 438.55499C1137.585605 438.55499 1170.304578 471.266238 1170.304578 511.631236 1170.304578 551.947483 1137.585605 584.707482 1097.162313 584.707482L1097.162313 584.707482Z" />
+                        <TextBlock SnapsToDevicePixels="False" IsHitTestVisible="False" Name="LabTitleInner" VerticalAlignment="Center" Margin="47,1,0,0" HorizontalAlignment="Left" Foreground="{DynamicResource ColorBrush8}" FontSize="15" />
+                    </Grid>
+                </Grid>
+                <Grid x:Name="PanLeft" Grid.Row="1" HorizontalAlignment="Left" SnapsToDevicePixels="True">
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="Auto"/>
+                        <ColumnDefinition Width="1*"/>
+                    </Grid.ColumnDefinitions>
+                    <Rectangle x:Name="RectLeftBackground" Width="0" Fill="{DynamicResource ColorBrushBackgroundTransparentSidebar}" IsHitTestVisible="False" />
+                    <Rectangle x:Name="RectLeftShadow" Width="4" HorizontalAlignment="Left" IsHitTestVisible="False" Grid.Column="1" >
+                        <Rectangle.Fill>
+                            <LinearGradientBrush EndPoint="1,0" StartPoint="0,0" Opacity="0.085">
+                                <GradientStop Color="#000000" Offset="0"/>
+                                <GradientStop Color="#00000000" Offset="1"/>
+                            </LinearGradientBrush>
+                        </Rectangle.Fill>
+                    </Rectangle>
+                </Grid>
+                <Grid x:Name="PanMain" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top">
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="Auto"/>
+                        <ColumnDefinition Width="1*"/>
+                    </Grid.ColumnDefinitions>
+                    <Border Grid.Column="1" Grid.IsSharedSizeScope="True" UseLayoutRounding="True" SnapsToDevicePixels="True">
+                        <Border x:Name="PanMainRight" RenderOptions.BitmapScalingMode="NearestNeighbor" UseLayoutRounding="False" SnapsToDevicePixels="False">
+                        </Border>
+                    </Border>
+                    <Border Grid.IsSharedSizeScope="True" UseLayoutRounding="True" SnapsToDevicePixels="True">
+                        <Border x:Name="PanMainLeft" RenderOptions.BitmapScalingMode="NearestNeighbor" UseLayoutRounding="False" SnapsToDevicePixels="False">
+                        </Border>
+                    </Border>
+                </Grid>
+                <StackPanel x:Name="PanHint" UseLayoutRounding="True" SnapsToDevicePixels="True" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="0,0,0,25" IsHitTestVisible="False" Grid.RowSpan="2" />
+                <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="1" Margin="15">
+                    <local:MyExtraButton x:Name="BtnExtraBack" HorizontalAlignment="Right" VerticalAlignment="Center" ToolTip="返回顶部" Visibility="Collapsed"
+                                       Logo="M858.496 188.9024 173.1072 188.9024c-30.2848 0-54.8352-24.5504-54.8352-54.8352L118.272 106.6496c0-30.2848 24.5504-54.8352 54.8352-54.8352l685.3888 0c30.2848 0 54.8352 24.5504 54.8352 54.8352l0 27.4176C913.3312 164.352 888.7808 188.9024 858.496 188.9024L858.496 188.9024zM150.6048 550.8608c0 0 300.0064-240.3584 303.0272-243.328 13.9776-13.5936 31.1808-21.8624 48.8192-24.7552 1.7152-0.3072 3.4304-0.5888 5.1456-0.768 2.7392-0.3072 5.4528-0.3584 8.192-0.3328 2.7392-0.0256 5.4272 0.0256 8.1664 0.3328 1.7408 0.1792 3.4304 0.4864 5.1456 0.768 17.664 2.8928 34.8672 11.1616 48.8192 24.7552 3.0464 2.944 303.0016 243.328 303.0016 243.328 32.384 31.5136 29.6192 63.9744-2.7392 95.5136-32.3328 31.5392-75.648 2.9696-108.0064-28.544l-185.8816-147.1232 0 485.8368c0 30.3104-24.5248 54.8608-54.8352 54.8608l-27.392 0c-30.2848 0-54.8352-24.5504-54.8352-54.8608L447.232 470.7072l-185.8304 147.0976c-32.3584 31.5392-75.6992 60.1344-108.032 28.5696C121.0368 614.8352 118.272 582.3744 150.6048 550.8608L150.6048 550.8608zM150.6048 550.8608" />
+                    <local:MyExtraButton x:Name="BtnExtraDownload" HorizontalAlignment="Right" VerticalAlignment="Center" ToolTip="下载管理" Visibility="Collapsed"
+                                       Logo="M955.076923 610.461538h-59.076923c-15.753846 0-29.538462 13.784615-29.538462 29.538462v196.923077c0 15.753846-13.784615 29.538462-29.538461 29.538461h-649.846154c-15.753846 0-29.538462-13.784615-29.538461-29.538461v-196.923077c0-15.753846-13.784615-29.538462-29.538462-29.538462h-59.076923c-15.753846 0-29.538462 13.784615-29.538462 29.538462V905.846154c0 43.323077 35.446154 78.769231 78.769231 78.769231h787.692308c43.323077 0 78.769231-35.446154 78.769231-78.769231V640c0-15.753846-13.784615-29.538462-29.538462-29.538462zM492.307692 740.430769c11.815385 11.815385 29.538462 11.815385 41.353846 0l265.846154-265.846154c11.815385-11.815385 11.815385-29.538462 0-41.353846l-41.353846-41.353846c-11.815385-11.815385-29.538462-11.815385-41.353846 0l-110.276923 110.276923c-11.815385 11.815385-33.476923 3.938462-33.476923-13.784615V68.923077C571.076923 53.169231 555.323077 39.384615 541.538462 39.384615h-59.076924c-15.753846 0-29.538462 13.784615-29.538461 29.538462v417.476923c0 17.723077-21.661538 25.6-33.476923 13.784615l-110.276923-110.276923c-11.815385-11.815385-29.538462-11.815385-41.353846 0L226.461538 433.230769c-11.815385 11.815385-11.815385 29.538462 0 41.353846L492.307692 740.430769z" />
+                    <local:MyExtraButton x:Name="BtnExtraApril" HorizontalAlignment="Right" VerticalAlignment="Center" ToolTip="投降" Visibility="Collapsed"
+                                       Logo="M124.96 32c-28.64 0-60.064 23.2-60.064 51.904v856.192c0 28.576 30.688 51.904 59.296 51.904s60.064-23.328 60.064-51.904V83.904C184.256 55.2 153.536 32 124.96 32z m616.352 71.264c-168.416 0-168.416-69.024-336.8-69.024-97.216 0-172.256 45.344-172.256 45.344l-0.736 516.736s75.776-45.28 172.96-45.28c168.416 0 168.416 69.024 336.8 69.024 105.344 0 211.168-55.328 211.168-55.328V48.064c0.032 0-105.792 55.2-211.136 55.2z m0 0" />
+                    <local:MyExtraButton x:Name="BtnExtraShutdown" HorizontalAlignment="Right" VerticalAlignment="Center" ToolTip="关闭 Minecraft" Visibility="Collapsed"
+                                       Logo="M802.152 143.682c31.236 22.212 59.004 47.379 83.297 75.486 24.297 28.116 45.12 58.481 62.472 91.105 17.356 32.624 30.543 67.159 39.569 103.6 9.023 36.442 13.533 73.403 13.533 110.887 0 64.556-12.32 125.116-36.96 181.69-24.643 56.57-57.963 105.854-99.956 147.847-41.994 41.998-91.278 75.313-147.851 99.955-56.57 24.642-117.134 36.966-181.684 36.966-63.86 0-124.08-12.324-180.649-36.966-56.575-24.642-106.032-57.957-148.37-99.955-42.344-41.993-75.662-91.277-99.956-147.846C81.3 649.876 69.155 589.315 69.155 524.76c0-36.788 4.337-72.884 13.016-108.283 8.678-35.4 20.996-69.065 36.96-100.999 15.969-31.926 35.75-61.774 59.35-89.537 23.6-27.768 49.976-52.757 79.133-74.969 15.272-11.107 31.755-15.271 49.456-12.497 17.697 2.782 32.1 11.455 43.21 26.03 11.108 14.583 15.27 30.891 12.496 48.94-2.781 18.047-11.454 32.622-26.03 43.732-43.732 31.927-77.225 71.145-100.48 117.653-23.247 46.507-34.873 96.483-34.873 149.93 0 45.812 8.673 89.027 26.03 129.632 17.352 40.605 41.124 76.003 71.317 106.198 30.2 30.2 65.599 54.145 106.204 71.846 40.607 17.697 83.814 26.549 129.631 26.549 45.811 0 89.02-8.852 129.626-26.549 40.606-17.701 76.01-41.647 106.203-71.846 30.195-30.195 54.14-65.593 71.841-106.198 17.702-40.607 26.549-83.82 26.549-129.631 0-54.14-12.49-105.334-37.478-153.577-24.994-48.24-60.048-87.977-105.162-119.213-15.272-10.412-24.468-24.643-27.592-42.69-3.125-18.049 0.518-34.709 10.93-49.983 10.412-14.575 24.643-23.427 42.69-26.547 18.047-3.12 34.698 0.519 49.97 10.93zM534.57 527.888c-18.05 0-33.495-6.421-46.337-19.264-12.841-12.841-19.263-28.287-19.263-46.335V66.635c0-18.048 6.42-33.666 19.263-46.854C501.076 6.593 516.521 0 534.571 0c18.738 0 34.53 6.593 47.371 19.781 12.842 13.188 19.263 28.806 19.263 46.854v395.654c0 18.047-6.42 33.493-19.263 46.335-12.842 12.843-28.633 19.264-47.371 19.264z" />
+                    <local:MyExtraButton x:Name="BtnExtraMusic" HorizontalAlignment="Right" VerticalAlignment="Center" Visibility="Collapsed" CanRightClick="True" />
+                </StackPanel>
+                <Grid x:Name="PanMsg" Grid.RowSpan="2" Background="#00000000" Visibility="Collapsed" />
+            </Grid>
+        </Border>
+    </Grid>
+</Window>

+ 1509 - 0
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -0,0 +1,1509 @@
+Imports System.ComponentModel
+
+Public Class FormMain
+
+#Region "基础"
+
+    '更新日志
+    Private Sub ShowUpdateLog(LastVersion As Integer)
+        Dim FeatureCount As Integer = 0, BugCount As Integer = 0
+        Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
+        '统计更新日志条目
+#If BETA Then
+        If LastVersion < 257 Then 'Release 2.3.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复下载部分 Mod、整合包的 Bug"))
+            FeatureCount += 4
+            BugCount += 4
+        End If
+        If LastVersion < 255 Then 'Release 2.2.14
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法启动 Minecraft 1.19-Pre1 的 Bug"))
+            FeatureCount += 5
+            BugCount += 3
+        End If
+        If LastVersion < 253 Then 'Release 2.2.13
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "支持在安装菜单中直接安装 OptiFabric"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持调整 Mod 文件名中中文译名的位置"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "崩溃分析优化,支持分析更多 Forge 相关的崩溃"))
+            FeatureCount += 13
+            BugCount += 27
+        End If
+        If LastVersion < 250 Then 'Release 2.2.11
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持将 Mod 文件拖入窗口进行安装"))
+            If LastVersion = 246 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用中文搜索 Mod 的 Bug"))
+            FeatureCount += 8
+            BugCount += 33
+        End If
+        If LastVersion < 246 Then 'Release 2.2.9
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "Mod 下载支持筛选 Forge Mod 与 Fabric Mod"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "大幅修改联机页面布局(作为联机优化的第二个阶段)"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "Mod、整合包下载的综合优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复使用离线登录进入 Forge 的存档可能导致游戏崩溃"))
+            FeatureCount += 13
+            BugCount += 11
+        End If
+        If LastVersion < 242 Then 'Release 2.2.7
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复 MC 所包含的一个严重安全漏洞"))
+            If LastVersion >= 236 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复可能无法更改登录信息的 Bug"))
+        End If
+        If LastVersion < 238 Then 'Release 2.2.6
+            If LastVersion = 236 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用联机模块的 Bug"))
+            FeatureCount += 1
+            BugCount += 2
+        End If
+        If LastVersion < 236 Then 'Release 2.2.5
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "网络状态检测支持检测 Windows 防火墙与网络延迟"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "重制右侧页面的切换动画,卡片与提示条将逐个进入退出,并具有独特的位移动画"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "添加 MC 1.18 中要求 Java 17 的检测与提醒"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化页面切换、下载的性能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "允许拖拽按钮的设置第三方登录"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复网络状态检测的结果总为 A 级的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法下载新版本的 Forge 的 Bug"))
+            FeatureCount += 25
+            BugCount += 22
+        End If
+        If LastVersion < 233 Then 'Release 2.2.3
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "微软账号支持更换皮肤与披风"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持拖拽整合包文件到 PCL2 进行快捷安装"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "Mod 下载支持显示对应的 MC 版本与 Mod 加载器,且可以跳转到 MCBBS 介绍页"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持安装 1.17 OptiFine + Forge"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "使用 Mojang 账号登录将会显示迁移提醒"))
+            FeatureCount += 12
+            BugCount += 26
+        End If
+        If LastVersion < 231 Then 'Release 2.2.1, Also 229
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(9, "联机功能的早期测试(尚未完成,延迟很高,之后会优化的)"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "添加 1.18 实验性快照 5 的特供下载,且支持列表的联网更新"))
+            FeatureCount += 11
+            BugCount += 3
+        End If
+        If LastVersion < 226 Then 'Release 2.1.4
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持 1.17.1 Forge、Fabric 的安装与启动"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化 Forge 下载与安装速度,提高安装稳定性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "更新内置帮助库,添加数个指南页面与整合包、存档的安装教程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持将游戏文件夹压缩包作为游戏整合包导入"))
+            FeatureCount += 12
+            BugCount += 13
+        End If
+        If LastVersion < 224 Then 'Release 2.1.3
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "界面与动画综合优化,重新制作主题配色"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "添加 1.18 实验性快照的特供下载"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "更新内置帮助库,添加光影、数据包、Mod 等资源的安装教程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "游戏崩溃分析优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "若设置为自动更新,PCL2 会在关闭时才替换文件"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "追加大量千万████的可能性"))
+            'FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持 1.17 Forge 的安装与启动"))
+            FeatureCount += 33
+            BugCount += 20
+        End If
+        If LastVersion < 217 Then 'Release 2.0.5
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "可以根据游戏版本自动选择所需的 Java"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "允许在 Java 版本检测不通过时强制指定使用 Java"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "允许为不同版本独立设置 Java"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "Java 选择与搜索的综合优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用 Mod、整合包搜索功能的 Bug"))
+            FeatureCount += 8
+            BugCount += 20
+        End If
+        If LastVersion < 214 Then 'Release 2.0.2
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "添加一键关闭所有运行中的 Minecraft 的按钮"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "增加对 MC 1.17 需要 Java 16 的检测与说明"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Minecraft 崩溃分析优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法修改部分个性化设置的严重 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复 Windows 7 可能无法登录微软账号的严重 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复网络不稳定时安装整合包极易失败的严重 Bug"))
+            FeatureCount += 10
+            BugCount += 13
+        End If
+        If LastVersion < 212 Then 'Release 2.0.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(9, "添加帮助页面,且可以自行添加、删除其中的内容"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持安装 MCBBS 格式的整合包"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "自定义主页/帮助页面的 XAML 功能扩展"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "同时安装 OptiFine 与 Forge 时,OptiFine 将作为 Mod 安装"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化 CurseForge 搜索"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化多个报错提示,更加人性化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "适配适用于快照版 MC 的 OptiFine"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复进入 OptiFine 下载页面导致卡死的严重 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法启动部分 OptiFine+Forge 版本的 Bug"))
+            FeatureCount += 20
+            BugCount += 42
+        End If
+#Else
+        '9:大+
+        '8:中大+
+        '7:中+,大*
+        '6:中小型+,中大*
+        '5:小型+,中*
+        '4:中小*
+        '3:小*
+        '2:极度严重的 Bug
+        '1:严重的 Bug
+        If LastVersion < 259 Then 'Snapshot 2.3.1
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "解决了联机人数 ≥3 人时出现的频繁掉线或突发高延迟的问题"))
+            FeatureCount += 14
+        End If
+        If LastVersion < 258 Then 'Snapshot 2.3.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复下载部分 Mod、整合包的 Bug"))
+            FeatureCount += 4
+            BugCount += 4
+        End If
+        If LastVersion < 256 Then 'Snapshot 2.2.14
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法启动 Minecraft 1.19-Pre1 的 Bug"))
+            FeatureCount += 5
+            BugCount += 3
+        End If
+        If LastVersion < 254 Then 'Snapshot 2.2.13
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持调整 Mod 文件名中中文译名的位置"))
+            FeatureCount += 3
+            BugCount += 17
+        End If
+        If LastVersion < 252 Then 'Snapshot 2.2.12
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "支持在安装菜单中直接安装 OptiFabric"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "崩溃分析优化,支持分析更多 Forge 相关的崩溃"))
+            If LastVersion = 251 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复同时安装 Forge 和 OptiFine 时 OptiFine 无效的 Bug"))
+            FeatureCount += 10
+            BugCount += 10
+        End If
+        If LastVersion < 251 Then 'Snapshot 2.2.11
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持将 Mod 文件拖入窗口进行安装"))
+            FeatureCount += 2
+            BugCount += 16
+        End If
+        If LastVersion < 249 Then 'Snapshot 2.2.10
+            If LastVersion = 247 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用中文搜索 Mod 的 Bug"))
+            FeatureCount += 6
+            BugCount += 17
+        End If
+        If LastVersion < 247 Then 'Snapshot 2.2.9
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "Mod 下载支持筛选 Forge Mod 与 Fabric Mod"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "Mod、整合包下载的综合优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复使用离线登录进入 Forge 的存档可能导致游戏崩溃"))
+            FeatureCount += 8
+            BugCount += 4
+        End If
+        If LastVersion < 245 Then 'Snapshot 2.2.8
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "大幅修改联机页面布局(作为联机优化的第二个阶段)"))
+            FeatureCount += 5
+            BugCount += 7
+        End If
+        If LastVersion < 243 Then 'Snapshot 2.2.7
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复 MC 所包含的一个严重安全漏洞"))
+            If LastVersion >= 236 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复可能无法更改登录信息的 Bug"))
+        End If
+        If LastVersion < 239 Then 'Snapshot 2.2.6
+            If LastVersion >= 235 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复自动更新由于路径错误失败的 Bug"))
+            If LastVersion = 237 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用联机模块的 Bug"))
+            FeatureCount += 1
+            BugCount += 1
+        End If
+        If LastVersion < 237 Then 'Snapshot 2.2.5
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "允许拖拽按钮的设置第三方登录"))
+            If LastVersion = 235 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复在启动游戏时有可能卡死的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法下载新版本的 Forge 的 Bug"))
+            FeatureCount += 2
+            BugCount += 3
+        End If
+        If LastVersion < 235 Then 'Snapshot 2.2.4
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "网络状态检测支持检测 Windows 防火墙与网络延迟"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "重制右侧页面的切换动画,卡片与提示条将逐个进入退出,并具有独特的位移动画"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "添加 MC 1.18 中要求 Java 17 的检测与提醒"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化页面切换、下载的性能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复网络状态检测的结果总为 A 级的 Bug"))
+            FeatureCount += 23
+            BugCount += 19
+        End If
+        If LastVersion < 234 Then 'Snapshot 2.2.3
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持拖拽整合包文件到 PCL2 进行快捷安装"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持安装 1.17 OptiFine + Forge"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "Mod 详情页面支持跳转 MCBBS 介绍页"))
+            FeatureCount += 10
+            BugCount += 17
+        End If
+        If LastVersion < 232 Then 'Snapshot 2.2.2
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "微软账号支持更换皮肤与披风"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持显示 Mod 与整合包对应的 MC 版本与 Mod 加载器名称"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "使用 Mojang 账号登录将会显示迁移提醒"))
+            FeatureCount += 2
+            BugCount += 9
+        End If
+        If LastVersion < 230 Then 'Snapshot 2.2.1, Also 228
+            If LastVersion = 227 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "联机功能的小优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "添加 1.18 实验性快照 5 的特供下载,且支持列表的联网更新"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复 Win7 上使用联机功能导致日志文件极大,占用硬盘空间的 Bug"))
+            FeatureCount += 12
+            BugCount += 4
+        End If
+        If LastVersion < 227 Then 'Snapshot 2.2.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(9, "新增联机功能的早期测试版本"))
+            FeatureCount += 2
+            BugCount += 2
+        End If
+        If LastVersion < 225 Then 'Snapshot 2.1.4
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "支持 1.17.1 Forge、Fabric 的安装与启动"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化 Forge 下载与安装速度,提高安装稳定性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "更新内置帮助库,添加数个指南页面与整合包、存档的安装教程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持将游戏文件夹压缩包作为游戏整合包导入"))
+            FeatureCount += 12
+            BugCount += 13
+        End If
+        If LastVersion < 223 Then 'Snapshot 2.1.3
+            BugCount += 4
+        End If
+        If LastVersion < 222 Then 'Snapshot 2.1.2
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "更新内置帮助库,添加光影、数据包、Mod 的安装教程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "游戏崩溃分析优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复登录微软账号启动游戏时报错的 Bug"))
+            FeatureCount += 8
+            BugCount += 11
+        End If
+        If LastVersion < 221 Then 'Snapshot 2.1.1
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "界面与动画综合优化,重新制作主题配色"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "若识别码不变,更新密钥在输入一次后会一直有效"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "若设置为自动更新,PCL2 会在关闭时才替换文件"))
+            FeatureCount += 10
+            BugCount += 5
+        End If
+        If LastVersion < 220 Then 'Snapshot 2.1.0
+            'FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "界面与动画综合优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "追加大量千万████的可能性"))
+            FeatureCount += 15
+        End If
+        If LastVersion < 219 Then 'Snapshot 2.0.5
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复无法使用 Mod、整合包搜索功能的 Bug"))
+            BugCount += 2
+        End If
+        If LastVersion < 218 Then 'Snapshot 2.0.4
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "允许为不同版本独立设置 Java"))
+            BugCount += 12
+        End If
+        If LastVersion < 216 Then 'Snapshot 2.0.3
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(7, "可以根据游戏版本自动选择所需的 Java"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "允许在 Java 版本检测不通过时强制指定使用 Java"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "Java 选择与搜索的综合优化"))
+            FeatureCount += 8
+            BugCount += 7
+        End If
+        If LastVersion < 215 Then 'Snapshot 2.0.2
+            If LastVersion = 213 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复账号切换、版本选择下拉框无法展开的严重 Bug"))
+            If LastVersion = 213 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复误判部分客户端需要 Java 16,导致无法启动游戏的严重 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复 Windows 7 可能无法登录微软账号的严重 Bug"))
+            BugCount += 1
+        End If
+        If LastVersion < 213 Then 'Snapshot 2.0.1
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(6, "添加一键关闭所有运行中的 Minecraft 的按钮"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "增加对 MC 1.17 需要 Java 16 的检测与说明"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Minecraft 崩溃分析优化"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复网络不稳定时安装整合包极易失败的严重 Bug"))
+            FeatureCount += 10
+            BugCount += 12
+        End If
+        If LastVersion < 211 Then 'Snapshot 2.0.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "适配适用于快照版 MC 的 OptiFine"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复进入 OptiFine 下载页面导致卡死的严重 Bug"))
+            BugCount += 3
+        End If
+#End If
+        '整理更新日志文本
+        Dim ContentList As New List(Of String)
+        Dim SortedFeatures = Sort(FeatureList, Function(Left As KeyValuePair(Of Integer, String), Right As KeyValuePair(Of Integer, String)) As Boolean
+                                                   Return Left.Key > Right.Key
+                                               End Function)
+        If SortedFeatures.Count = 0 AndAlso FeatureCount = 0 AndAlso BugCount = 0 Then
+            ContentList.Add("龙猫忘记写更新日志啦!可以去提醒他一下……")
+        End If
+        For i = 0 To Math.Min(9, SortedFeatures.Count - 1) '最多取 10 项
+            ContentList.Add(SortedFeatures(i).Value)
+        Next
+        If SortedFeatures.Count > 10 Then FeatureCount += SortedFeatures.Count - 10
+        If FeatureCount > 0 OrElse BugCount > 0 Then
+            ContentList.Add(If(FeatureCount > 0, "其他 " & FeatureCount & " 项小调整与修改", "") &
+                        If(FeatureCount > 0 AndAlso BugCount > 0, ",", "") &
+                        If(BugCount > 0, "修复了 " & BugCount & " 个 Bug", "") &
+                        ",详见完整更新日志")
+        End If
+        Dim Content As String = "· " & Join(ContentList, vbCrLf & "· ")
+        '输出更新日志
+        RunInNewThread(Sub()
+                           If MyMsgBox(Content, "PCL2 已更新至 " & VersionDisplayName, "确定", "完整更新日志") = 2 Then
+                               OpenWebsite("https://afdian.net/@LTCat?tab=feed")
+                           End If
+                       End Sub, "UpdateLog Output")
+    End Sub
+
+    '窗口加载
+    Private IsWindowLoadFinished As Boolean = False
+    Public Shared IsLinkRestart As Boolean = False '是否为联机提权后自动重启
+    Public Sub New()
+        ApplicationStartTick = GetTimeTick()
+        '窗体参数初始化
+        FrmMain = Me
+        FrmLaunchLeft = New PageLaunchLeft
+        FrmLaunchRight = New PageLaunchRight
+        '版本号改变
+        Dim LastVersion As Integer = Setup.Get("SystemLastVersionReg")
+        If LastVersion < VersionCode Then
+            '触发升级
+            UpgradeSub(LastVersion)
+        ElseIf LastVersion > VersionCode Then
+            '触发降级
+            DowngradeSub(LastVersion)
+        End If
+        ''刷新语言
+        'Lang = ReadReg("Lang", "zh_CN")
+        'Application.Current.Resources.MergedDictionaries(1) = New ResourceDictionary With {.Source = New Uri("Resources\Language\" & Lang & ".xaml", UriKind.Relative)}
+        '刷新主题
+        ThemeCheckAll(False)
+        Setup.Load("UiLauncherTheme")
+        '加载 UI
+        InitializeComponent()
+        Opacity = 0
+        '切换到首页
+        If Not IsNothing(FrmLaunchLeft.Parent) Then FrmLaunchLeft.SetValue(ContentPresenter.ContentProperty, Nothing)
+        If Not IsNothing(FrmLaunchRight.Parent) Then FrmLaunchRight.SetValue(ContentPresenter.ContentProperty, Nothing)
+        PanMainLeft.Child = FrmLaunchLeft
+        PanMainRight.Child = FrmLaunchRight
+        FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay
+        If IsLinkRestart Then PageChange(PageType.Link)
+        '模式提醒
+#If DEBUG Then
+        Hint("[开发者模式] PCL 正以开发者模式运行,这可能会造成严重的性能下降,请务必立即向开发者反馈此问题!", HintType.Critical)
+#End If
+        If ModeDebug Then Hint("[调试模式] PCL 正以调试模式运行,这可能会造成性能的下降,若无必要请不要开启!")
+        '尽早执行的加载池
+        McFolderListLoader.Start(0) '为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid
+
+        Log("[Start] 第二阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")
+    End Sub
+    Private Sub FormMain_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        ApplicationStartTick = GetTimeTick()
+        Handle = New Interop.WindowInteropHelper(Me).Handle
+        '读取设置
+        Setup.Load("UiBackgroundOpacity")
+        Setup.Load("UiBackgroundBlur")
+        Setup.Load("UiLogoType")
+        Setup.Load("UiHiddenPageDownload")
+        BackgroundRefresh(False, True)
+        MusicRefreshPlay(False, True)
+        JavaListInit()
+        '扩展按钮
+        BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck
+        BtnExtraBack.ShowCheck = AddressOf BtnExtraBack_ShowCheck
+        BtnExtraApril.ShowCheck = AddressOf BtnExtraApril_ShowCheck
+        BtnExtraShutdown.ShowCheck = AddressOf BtnExtraShutdown_ShowCheck
+        BtnExtraApril.ShowRefresh()
+        '初始化尺寸改变
+        Dim Resizer As New MyResizer(Me)
+        Resizer.addResizerDown(ResizerB)
+        Resizer.addResizerLeft(ResizerL)
+        Resizer.addResizerLeftDown(ResizerLB)
+        Resizer.addResizerLeftUp(ResizerLT)
+        Resizer.addResizerRight(ResizerR)
+        Resizer.addResizerRightDown(ResizerRB)
+        Resizer.addResizerRightUp(ResizerRT)
+        Resizer.addResizerUp(ResizerT)
+        '#If DEBUG Then
+        '        MinWidth = 200
+        '        MinHeight = 150
+        '#End If
+        'PLC 彩蛋
+        If RandomInteger(1, 1000) = 23 Then
+            ShapeTitleLogo.Data = New GeometryConverter().ConvertFromString("M26,29 v-25 h5 a7,7 180 0 1 0,14 h-5 M80,6.5 a10,11.5 180 1 0 0,18   M47,2.5 v24.5 h12   M98,2 v27   M107,2 v27")
+        End If
+        '加载窗口
+        ThemeRefreshMain()
+        Height = ReadReg("WindowHeight", MinHeight + 50)
+        Width = ReadReg("WindowWidth", MinWidth + 50)
+        Topmost = False
+        If FrmStart IsNot Nothing Then FrmStart.Close(New TimeSpan(0, 0, 0, 0, 400 / AniSpeed))
+        '更改窗口
+        Top = (GetWPFSize(My.Computer.Screen.WorkingArea.Height) - Height) / 2
+        Left = (GetWPFSize(My.Computer.Screen.WorkingArea.Width) - Width) / 2
+        IsSizeSaveable = True
+        ShowWindowToTop()
+        Dim HwndSource As Interop.HwndSource = PresentationSource.FromVisual(Me)
+        HwndSource.AddHook(New Interop.HwndSourceHook(AddressOf WndProc))
+        AniStart({
+                     AaCode(Sub() AniControlEnabled -= 1, 50),
+                     AaOpacity(Me, Setup.Get("UiLauncherTransparent") / 1000 + 0.4, 300, 100),
+                     AaScaleTransform(PanBack, 0.05, 500, 100, New AniEaseOutBack(AniEasePower.Weak)),
+                     AaCode(Sub()
+                                PanBack.RenderTransform = Nothing
+                                'If OsVersion > New Version(10, 1, 0, 0) Then RectForm.RadiusX = 5 : RectForm.RadiusY = 5 'Win11 下圆角
+                                IsWindowLoadFinished = True
+                                Log("[System] DPI:" & DPI & ",工作区尺寸:" & My.Computer.Screen.WorkingArea.Width & " x " & My.Computer.Screen.WorkingArea.Height & ",系统版本:" & OsVersion.ToString)
+                            End Sub, , True)
+                 }, "Form Show")
+        'Timer 启动
+        AniStartRun()
+        TimerMainStartRun()
+        '加载池
+        RunInNewThread(Sub()
+                           If Not Setup.Get("SystemEula") Then
+Reopen:
+                               Select Case MyMsgBox("在使用 PCL2 前,请同意 PCL2 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "打开用户协议与免责声明页面")
+                                   Case 1
+                                       Setup.Set("SystemEula", True)
+                                   Case 2
+                                       EndProgram(False)
+                                   Case 3
+                                       OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW")
+                                       GoTo Reopen
+                               End Select
+                           End If
+                           Try
+                               Thread.Sleep(200)
+                               If Setup.Get("LinkAuto") Then PageLinkIoi.InitLoader.Start()
+                               If Not Setup.Get("HintFeedback") = "" Then FeedbackLoader.Start()
+                               DlClientListMojangLoader.Start(1)
+                               RunCountSub()
+                               ServerLoader.Start(1)
+                           Catch ex As Exception
+                               Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
+                           End Try
+                           Try
+                               If File.Exists(Path & "PCL\Plain Craft Launcher 2.exe") Then File.Delete(Path & "PCL\Plain Craft Launcher 2.exe")
+                           Catch ex As Exception
+                               Log(ex, "清理自动更新文件失败")
+                           End Try
+                       End Sub, "Start Loader", ThreadPriority.Lowest)
+
+        Log("[Start] 第三阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")
+    End Sub
+    '根据打开次数触发的事件
+    Private Sub RunCountSub()
+        Setup.Set("SystemCount", Setup.Get("SystemCount") + 1)
+#If Not BETA Then
+        Select Case Setup.Get("SystemCount")
+            Case 1
+                MyMsgBox("欢迎使用 PCL2 快照版!" & vbCrLf &
+                         "快照版包含尚未在正式版发布的测试性功能,仅用于赞助者本人尝鲜。所以请不要发给其他人或者用于制作整合包哦!" & vbCrLf &
+                         "如果你并非通过赞助或赞助者本人邀请进群获得的本程序,那么可能是有人在违规传播,记得提醒他一下啦。", "快照版使用说明")
+        End Select
+        If Setup.Get("SystemCount") >= 99 Then
+            If ThemeUnlock(6, False) Then
+                MyMsgBox("你已经使用了 99 次 PCL2 啦,感谢你长期以来的支持!" & vbCrLf &
+                         "隐藏主题 铁杆粉 已解锁!", "提示")
+            End If
+        End If
+#End If
+    End Sub
+    '升级与降级事件
+    Private Sub UpgradeSub(LastVersionCode As Integer)
+        Log("[Start] 版本号从 " & LastVersionCode & " 升高到 " & VersionCode)
+        Setup.Set("SystemLastVersionReg", VersionCode)
+        '检查有记录的最高版本号
+        Dim LowerVersionCode As Integer
+#If BETA Then
+        LowerVersionCode = Setup.Get("SystemHighestBetaVersionReg")
+        If LowerVersionCode < VersionCode Then
+            Setup.Set("SystemHighestBetaVersionReg", VersionCode)
+            Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
+        End If
+#Else
+        LowerVersionCode = Setup.Get("SystemHighestAlphaVersionReg")
+        If LowerVersionCode < VersionCode Then
+            Setup.Set("SystemHighestAlphaVersionReg", VersionCode)
+            Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
+        End If
+#End If
+        '修改主题设置项名称
+        If LowerVersionCode <= 207 Then
+            Dim UnlockedTheme As New List(Of String) From {"2"}
+            UnlockedTheme.AddRange(New List(Of String)(Setup.Get("UiLauncherThemeHide").ToString.Split("|")))
+            UnlockedTheme.AddRange(New List(Of String)(Setup.Get("UiLauncherThemeHide2").ToString.Split("|")))
+            Setup.Set("UiLauncherThemeHide2", Join(ArrayNoDouble(UnlockedTheme), "|"))
+        End If
+        '重置欧皇彩
+        If LastVersionCode <= 115 AndAlso Setup.Get("UiLauncherThemeHide2").ToString.Split("|").Contains("13") Then
+            Dim UnlockedTheme As New List(Of String)(Setup.Get("UiLauncherThemeHide2").ToString.Split("|"))
+            UnlockedTheme.Remove("13")
+            Setup.Set("UiLauncherThemeHide2", Join(UnlockedTheme, "|"))
+            MyMsgBox("由于新版 PCL2 修改了欧皇彩的解锁方式,你需要重新解锁欧皇彩。" & vbCrLf &
+                     "多谢各位的理解啦!", "重新解锁提醒")
+        End If
+        '重置滑稽彩
+        If LastVersionCode <= 152 AndAlso Setup.Get("UiLauncherThemeHide2").ToString.Split("|").Contains("12") Then
+            Dim UnlockedTheme As New List(Of String)(Setup.Get("UiLauncherThemeHide2").ToString.Split("|"))
+            UnlockedTheme.Remove("12")
+            Setup.Set("UiLauncherThemeHide2", Join(UnlockedTheme, "|"))
+            MyMsgBox("由于新版 PCL2 修改了滑稽彩的解锁方式,你需要重新解锁滑稽彩。" & vbCrLf &
+                     "多谢各位的理解啦!", "重新解锁提醒")
+        End If
+        '移动自定义皮肤
+        If LastVersionCode <= 161 AndAlso File.Exists(Path & "PCL\CustomSkin.png") AndAlso Not File.Exists(PathTemp & "CustomSkin.png") Then
+            File.Copy(Path & "PCL\CustomSkin.png", PathTemp & "CustomSkin.png")
+            Log("[Start] 已移动离线自定义皮肤")
+        End If
+        '解除帮助页面的隐藏
+        If LastVersionCode <= 205 Then
+            Setup.Set("UiHiddenOtherHelp", False)
+            Log("[Start] 已解除帮助页面的隐藏")
+        End If
+        '输出更新日志
+        If LastVersionCode = 0 Then Exit Sub
+        If LowerVersionCode >= VersionCode Then Exit Sub
+        ShowUpdateLog(LowerVersionCode)
+    End Sub
+    Private Sub DowngradeSub(LastVersionCode As Integer)
+        Log("[Start] 版本号从 " & LastVersionCode & " 降低到 " & VersionCode)
+        Setup.Set("SystemLastVersionReg", VersionCode)
+    End Sub
+
+#End Region
+
+#Region "自定义窗口"
+
+    '关闭
+    Private Sub FormMain_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
+        EndProgram(True)
+        e.Cancel = True
+    End Sub
+    ''' <summary>
+    ''' 正常关闭程序。程序将在执行此方法后约 0.3s 退出。
+    ''' </summary>
+    ''' <param name="SendWarning">是否在还有下载任务未完成时发出警告。</param>
+    Public Sub EndProgram(SendWarning As Boolean)
+        '发出警告
+        If SendWarning AndAlso HasDownloadingTask() Then
+            If MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 1 Then
+                '强行结束下载任务
+                RunInNewThread(Sub()
+                                   Log("[System] 正在强行停止任务")
+                                   For Each Task As LoaderBase In LoaderTaskbar.ToArray
+                                       Task.Abort()
+                                   Next
+                               End Sub, "强行停止下载任务")
+            Else
+                Exit Sub
+            End If
+        End If
+        '关闭
+        RunInUiWait(Sub()
+                        IsHitTestVisible = False
+                        If PanBack.RenderTransform Is Nothing Then
+                            PanBack.RenderTransform = New ScaleTransform
+                            AniStart({
+                                AaOpacity(Me, -Opacity, 100, 100),
+                                AaScaleTransform(PanBack, -0.05, 200,, New AniEaseInBack),
+                                AaCode(Sub()
+                                           IsHitTestVisible = False
+                                           Top = -10000
+                                           ShowInTaskbar = False
+                                       End Sub, 225),
+                                AaCode(AddressOf EndProgramForce, 250)
+                            }, "Form Close")
+                        Else
+                            EndProgramForce()
+                        End If
+                        Log("[System] 收到关闭指令")
+                    End Sub)
+    End Sub
+    Private Shared IsLogShown As Boolean = False
+    Public Shared Sub EndProgramForce(Optional ReturnCode As Result = Result.Success)
+        On Error Resume Next
+        IsProgramEnded = True
+        AniControlEnabled += 1
+        PageLinkIoi.IoiStop(False)
+        If IsUpdateWaitingRestart Then UpdateRestart(False)
+        If ReturnCode = Result.Exception Then
+            If Not IsLogShown Then
+                FeedbackInfo()
+                Log("请在 https://jinshuju.net/f/rP4b6E?x_field_1=crash 提交错误报告,以便于作者解决此问题!")
+                IsLogShown = True
+                ShellAndGetExitCode(Path & "PCL\Log1.txt")
+            End If
+            Thread.Sleep(500) '防止 PCL 在记事本打开前就被掐掉
+        End If
+        Log("[System] 程序已退出,返回值:" & GetStringFromEnum(CType(ReturnCode, [Enum])))
+        LogFlush()
+        If ReturnCode = Result.Success Then
+            Process.GetCurrentProcess.Kill()
+        Else
+            Environment.Exit(ReturnCode)
+        End If
+    End Sub
+    Private Sub BtnTitleClose_Click(sender As Object, e As RoutedEventArgs) Handles BtnTitleClose.Click
+        EndProgram(True)
+    End Sub
+
+    '移动
+    Private Sub FormDragMove(sender As Object, e As MouseButtonEventArgs) Handles PanTitle.MouseLeftButtonDown, PanMsg.MouseLeftButtonDown
+        On Error Resume Next
+        If sender.IsMouseDirectlyOver Then DragMove()
+    End Sub
+
+    '改变大小
+    ''' <summary>
+    ''' 是否可以向注册表储存尺寸改变信息。以此避免初始化时误储存。
+    ''' </summary>
+    Public IsSizeSaveable As Boolean = False
+    Private Sub FormMain_SizeChanged() Handles Me.SizeChanged, Me.Loaded
+        If IsSizeSaveable Then
+            WriteReg("WindowHeight", Height)
+            WriteReg("WindowWidth", Width)
+        End If
+        RectForm.Rect = New Rect(0, 0, BorderForm.ActualWidth, BorderForm.ActualHeight)
+        PanForm.Width = BorderForm.ActualWidth + 0.001
+        PanForm.Height = BorderForm.ActualHeight + 0.001
+        PanMain.Width = PanForm.Width
+        PanMain.Height = Math.Max(0, PanForm.Height - PanTitle.ActualHeight)
+    End Sub
+
+    '最小化
+    Private Sub BtnTitleMin_Click() Handles BtnTitleMin.Click
+        WindowState = WindowState.Minimized
+    End Sub
+
+#End Region
+
+#Region "窗体事件"
+
+    '按键事件
+    Private Sub FormMain_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
+        If e.IsRepeat Then Exit Sub
+        '调用弹窗回车
+        If e.Key = Key.Enter AndAlso PanMsg.Children.Count > 0 Then
+            CType(PanMsg.Children(0), Object).Btn1_Click()
+            Exit Sub
+        End If
+        '更改隐藏版本可见性
+        If e.Key = Key.F11 AndAlso PageCurrent = FormMain.PageType.VersionSelect Then
+            FrmSelectRight.ShowHidden = Not FrmSelectRight.ShowHidden
+            LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+            Exit Sub
+        End If
+        '更改功能隐藏可见性
+        If e.Key = Key.F12 Then
+            PageSetupUI.HiddenForceShow = Not PageSetupUI.HiddenForceShow
+            If PageSetupUI.HiddenForceShow Then
+                Hint("功能隐藏设置已暂时关闭!", HintType.Finish)
+            Else
+                Hint("功能隐藏设置已重新开启!", HintType.Finish)
+            End If
+            PageSetupUI.HiddenRefresh()
+            Exit Sub
+        End If
+        '调用启动游戏
+        If e.Key = Key.Enter AndAlso PageCurrent = FormMain.PageType.Launch Then
+            If IsAprilEnabled AndAlso Not IsAprilGiveup Then
+                Hint("木大!")
+            Else
+                FrmLaunchLeft.LaunchButtonClick()
+            End If
+        End If
+        '修复按下 Alt 后误认为弹出系统菜单导致的冻结
+        If e.SystemKey = Key.LeftAlt OrElse e.SystemKey = Key.RightAlt Then e.Handled = True
+        '按 ESC 返回上一级
+        If e.Key = Key.Escape Then TriggerPageBack()
+    End Sub
+    Private Sub FormMain_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseDown
+        '鼠标侧键返回上一级
+        If e.ChangedButton = MouseButton.XButton1 OrElse e.ChangedButton = MouseButton.XButton2 Then TriggerPageBack()
+    End Sub
+    Private Sub TriggerPageBack()
+        If PageCurrent = PageType.Download AndAlso PageCurrentSub = PageSubType.DownloadInstall Then
+            FrmDownloadInstall.ExitSelectPage()
+        Else
+            PageBack()
+        End If
+    End Sub
+
+    '切回窗口
+    Private Sub FormMain_Activated() Handles Me.Activated
+        Try
+            If PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionMod Then
+                'Mod 管理自动刷新
+                FrmVersionMod.RefreshList()
+            ElseIf PageCurrent = PageType.VersionSelect Then
+                '版本选择自动刷新
+                LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\")
+            End If
+        Catch ex As Exception
+            Log(ex, "切回窗口时出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    '文件拖放
+    Private Sub FrmMain_PreviewDragOver(sender As Object, e As DragEventArgs) Handles Me.PreviewDragOver
+        If e.Data.GetFormats.Contains("FileDrop") Then
+            e.Effects = DragDropEffects.Link
+        Else
+            e.Effects = DragDropEffects.None
+        End If
+    End Sub
+    Private Sub FrmMain_Drop(sender As Object, e As DragEventArgs) Handles Me.PreviewDrop
+        Try
+            If e.Data.GetDataPresent(DataFormats.Text) Then
+                '获取文本
+                Try
+                    Dim Str As String = e.Data.GetData(DataFormats.Text)
+                    Log("[System] 接受文本拖拽:" & Str)
+                    If Str.StartsWith("authlib-injector:yggdrasil-server:") Then
+                        'Authlib 拖拽
+                        e.Handled = True
+                        Dim AuthlibServer As String = Net.WebUtility.UrlDecode(Str.Substring("authlib-injector:yggdrasil-server:".Length))
+                        Log("[System] Authlib 拖拽:" & AuthlibServer)
+                        If Not String.IsNullOrEmpty(New ValidateHttp().Validate(AuthlibServer)) Then
+                            Hint("输入的 Authlib 验证服务器不符合网址格式(" & AuthlibServer & ")!", HintType.Critical)
+                            Exit Sub
+                        End If
+                        If McVersionCurrent Is Nothing Then
+                            Hint("请先下载游戏,再设置第三方登录!", HintType.Critical)
+                            Exit Sub
+                        End If
+                        If AuthlibServer = "https://littleskin.cn/api/yggdrasil" Then
+                            'Little Skin
+                            If MyMsgBox("是否要在版本 " & McVersionCurrent.Name & " 中开启 Little Skin 登录?" & vbCrLf &
+                                        "你可以在 版本设置 → 设置 → 服务器选项 中修改登录方式。", "第三方登录开启确认", "确定", "取消") = 2 Then
+                                Exit Sub
+                            End If
+                            Setup.Set("VersionServerLogin", 4, Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthServer", "https://littleskin.cn/api/yggdrasil", Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthRegister", "https://littleskin.cn/auth/register", Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthName", "Little Skin 登录", Version:=McVersionCurrent)
+                        Else
+                            '第三方 Authlib 服务器
+                            If MyMsgBox("是否要在版本 " & McVersionCurrent.Name & " 中开启第三方登录?" & vbCrLf &
+                                        "登录服务器:" & AuthlibServer & vbCrLf & vbCrLf &
+                                        "你可以在 版本设置 → 设置 → 服务器选项 中修改登录方式。", "第三方登录开启确认", "确定", "取消") = 2 Then
+                                Exit Sub
+                            End If
+                            Setup.Set("VersionServerLogin", 4, Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthServer", AuthlibServer, Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthRegister", AuthlibServer.Replace("api/yggdrasil", "auth/register"), Version:=McVersionCurrent)
+                            Setup.Set("VersionServerAuthName", "", Version:=McVersionCurrent)
+                        End If
+                        If PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionSetup Then
+                            '正在服务器选项页,需要刷新设置项显示
+                            FrmVersionSetup.Reload()
+                        ElseIf PageCurrent = PageType.Launch Then
+                            '正在主页,需要刷新左边栏
+                            FrmLaunchLeft.RefreshPage(True, False)
+                        End If
+                    End If
+                Catch ex As Exception
+                    Log(ex, "无法接取文本拖拽事件", LogLevel.Developer)
+                    Exit Sub
+                End Try
+            ElseIf e.Data.GetDataPresent(DataFormats.FileDrop) Then
+                '获取文件并检查
+                Dim FilePathList As New List(Of String)(CType(e.Data.GetData(DataFormats.FileDrop), Array))
+                e.Handled = True
+                If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
+                    Hint("请拖入一个文件,而非文件夹!", HintType.Critical)
+                    Exit Sub
+                End If
+                '多文件拖拽
+                If FilePathList.Count > 1 Then
+                    '必须要求全部为 jar 文件
+                    For Each File In FilePathList
+                        If File.Split(".").Last.ToLower <> "jar" Then
+                            Hint("一次请只拖入一个文件!", HintType.Critical)
+                            Exit Sub
+                        End If
+                    Next
+                End If
+                '实际执行事件
+                Dim FilePath As String = FilePathList.First
+                Log("[System] 接受文件拖拽:" & FilePath, LogLevel.Developer)
+                RunInNewThread(Sub()
+                                   'Mod 安装
+                                   If FilePath.Split(".").Last.ToLower = "jar" Then
+                                       Log("[System] 文件为 jar 格式,尝试作为 Mod 安装")
+                                       '获取并检查目标版本
+                                       Dim TargetVersion As McVersion = McVersionCurrent
+                                       If PageCurrent = PageType.VersionSetup Then TargetVersion = PageVersionLeft.Version
+                                       If PageCurrent = PageType.VersionSelect OrElse TargetVersion Is Nothing OrElse Not TargetVersion.Version.Modable Then
+                                           '正在选择版本,或当前版本不能安装 Mod
+                                           Hint("若要安装 Mod,请先选择一个可以安装 Mod 的版本!")
+                                       ElseIf Not (PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionMod) Then
+                                           '未处于 Mod 管理页面
+                                           If MyMsgBox("是否要将这些文件作为 Mod 安装到 " & TargetVersion.Name & "?", "Mod 安装确认", "确定", "取消") = 1 Then GoTo Install
+                                       Else
+                                           '处于 Mod 管理页面
+Install:
+                                           Try
+                                               For Each ModFile In FilePathList
+                                                   File.Copy(ModFile, TargetVersion.PathIndie & "mods\" & GetFileNameFromPath(ModFile), True)
+                                               Next
+                                               If FilePathList.Count = 1 Then
+                                                   Hint("已安装 " & GetFileNameFromPath(FilePathList.First) & "!", HintType.Finish)
+                                               Else
+                                                   Hint("已安装 " & FilePathList.Count & " 个 Mod!", HintType.Finish)
+                                               End If
+                                               '刷新列表
+                                               If PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionMod Then
+                                                   LoaderFolderRun(McModLoader, TargetVersion.PathIndie & "mods\", LoaderFolderRunType.ForceRun)
+                                               End If
+                                           Catch ex As Exception
+                                               Log(ex, "复制 Mod 文件失败", LogLevel.Msgbox)
+                                           End Try
+                                       End If
+                                       Exit Sub
+                                   End If
+                                   '安装整合包
+                                   If {"zip", "rar"}.Contains(FilePath.Split(".").Last.ToLower) Then '部分压缩包是 zip 格式但后缀为 rar,总之试一试
+                                       Log("[System] 文件为压缩包,尝试作为整合包安装")
+                                       If ModpackInstall(FilePath, ShowHint:=False) Then Exit Sub
+                                   End If
+                                   'RAR 处理
+                                   If FilePath.Split(".").Last.ToLower = "rar" Then
+                                       Hint("PCL2 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试!")
+                                       Exit Sub
+                                   End If
+                                   '错误报告分析
+                                   Try
+                                       Log("[System] 尝试进行错误报告分析")
+                                       Dim Analyzer As New CrashAnalyzer(GetUuid())
+                                       Analyzer.Import(FilePath)
+                                       If Analyzer.Prepare() = 0 Then Exit Try
+                                       Analyzer.Analyze()
+                                       Analyzer.Output(True, New List(Of String))
+                                       Exit Sub
+                                   Catch ex As Exception
+                                       Log(ex, "自主错误报告分析失败", LogLevel.Feedback)
+                                   End Try
+                                   '未知操作
+                                   Hint("PCL2 无法确定应当执行的文件拖拽操作……")
+                               End Sub, "文件拖拽")
+            End If
+        Catch ex As Exception
+            Log(ex, "接取拖拽事件失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    '接受到 Windows 窗体事件
+    Public IsSystemTimeChanged As Boolean = False
+    Private Function WndProc(hwnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr, ByRef handled As Boolean) As IntPtr
+        If msg = 30 Then
+            Dim NowDate = Date.Now
+            If NowDate.Date = ApplicationOpenTime.Date Then
+                Log("[System] 系统时间微调为:" & NowDate.ToLongDateString & " " & NowDate.ToLongTimeString)
+                IsSystemTimeChanged = False
+            Else
+                Log("[System] 系统时间修改为:" & NowDate.ToLongDateString & " " & NowDate.ToLongTimeString)
+                IsSystemTimeChanged = True
+            End If
+        ElseIf msg = 400 * 16 + 2 Then
+            Log("[System] 收到置顶信息:" & hwnd.ToInt64)
+            If Not IsWindowLoadFinished Then
+                Log("[System] 窗口尚未加载完成,忽略置顶请求")
+                Return IntPtr.Zero
+            End If
+            ShowWindowToTop()
+            handled = True
+        End If
+        Return IntPtr.Zero
+    End Function
+
+    '窗口隐藏与置顶
+    Private _Hidden As Boolean = False
+    Public Property Hidden As Boolean
+        Get
+            Return _Hidden
+        End Get
+        Set(ByVal value As Boolean)
+            If _Hidden = value Then Exit Property
+            _Hidden = value
+            If value Then
+                '隐藏
+                Left -= 10000
+                ShowInTaskbar = False
+                Visibility = Visibility.Hidden
+                'SetWindowLong(Handle, GWL_EXSTYLE, GetWindowLong(Handle, GWL_EXSTYLE) Or WS_EX_TOOLWINDOW)
+                Log("[System] 窗口已隐藏,位置:(" & Left & "," & Top & ")")
+            Else
+                '取消隐藏
+                If Left < -2000 Then Left += 10000
+                ShowWindowToTop()
+            End If
+        End Set
+    End Property
+    ''' <summary>
+    ''' 把当前窗口拖到最前面。
+    ''' </summary>
+    Public Sub ShowWindowToTop()
+        RunInUi(Sub()
+                    '这一坨乱七八糟的,别改,改了指不定就炸了,自己电脑还复现不出来
+                    Visibility = Visibility.Visible
+                    ShowInTaskbar = True
+                    WindowState = WindowState.Normal
+                    Hidden = False
+                    Topmost = True '偶尔 SetForegroundWindow 失效
+                    Topmost = False
+                    SetForegroundWindow(Handle)
+                    Focus()
+                    'ShowWindow(Handle, 1) 'SW_RESTORE
+                    'SetWindowPos(Handle, 0, 0, 0, 0, 0, 3) 'InsertAfter: TOP, NOMOVE & NOSIZE
+                    'SetFocus(Handle)
+                    Log("[System] 窗口已置顶,位置:(" & Left & ", " & Top & "), " & Width & " x " & Height)
+                End Sub)
+    End Sub
+    'Private Declare Function SetFocus Lib "user32" (hWnd As IntPtr) As IntPtr
+    'Private Declare Function SetWindowPos Lib "user32" (hWnd As IntPtr, hWndInsertAfter As IntPtr, x As Integer, y As Integer, cx As Integer, cy As Integer, flags As UInteger) As Boolean
+    'Private Declare Function AttachThreadInput Lib "user32" (attachFrom As IntPtr, attachTo As IntPtr, isAttach As Boolean) As Boolean
+    'Private Declare Function SendMessage Lib "user32" (hWnd As IntPtr, msg As UInteger, wParam As Long, lParam As Long) As IntPtr
+
+#End Region
+
+#Region "切换页面"
+
+    '页面种类与属性
+    ''' <summary>
+    ''' 页面种类。
+    ''' </summary>
+    Public Enum PageType
+        ''' <summary>
+        ''' 启动。
+        ''' </summary>
+        Launch
+        ''' <summary>
+        ''' 下载。
+        ''' </summary>
+        Download
+        ''' <summary>
+        ''' 联机。
+        ''' </summary>
+        Link
+        ''' <summary>
+        ''' 设置。
+        ''' </summary>
+        Setup
+        ''' <summary>
+        ''' 更多。
+        ''' </summary>
+        Other
+        ''' <summary>
+        ''' 版本选择。这是一个副页面。
+        ''' </summary>
+        VersionSelect
+        ''' <summary>
+        ''' 下载管理。这是一个副页面。
+        ''' </summary>
+        DownloadManager
+        ''' <summary>
+        ''' 版本设置。这是一个副页面。
+        ''' </summary>
+        VersionSetup
+        ''' <summary>
+        ''' CurseForge 工程详情。这是一个副页面。
+        ''' </summary>
+        CfDetail
+        ''' <summary>
+        ''' 帮助详情。这是一个副页面。
+        ''' </summary>
+        HelpDetail
+    End Enum
+    ''' <summary>
+    ''' 次要页面种类。其数值必须与 StackPanel 中的下标一致。
+    ''' </summary>
+    Public Enum PageSubType
+        [Default] = 0
+        DownloadInstall = 1
+        DownloadClient = 4
+        DownloadOptiFine = 5
+        DownloadForge = 6
+        DownloadFabric = 7
+        DownloadLiteLoader = 8
+        DownloadMod = 10
+        DownloadPack = 11
+        SetupLaunch = 0
+        SetupUI = 1
+        SetupSystem = 2
+        SetupLink = 3
+        LinkCato = 2
+        LinkIoi = 3
+        LinkSetup = 5
+        LinkHelp = 6
+        LinkFeedback = 7
+        OtherHelp = 0
+        OtherAbout = 1
+        OtherTest = 2
+        OtherFeedback = 3
+        VersionOverall = 0
+        VersionSetup = 1
+        VersionMod = 2
+        VersionModDisabled = 3
+    End Enum
+    ''' <summary>
+    ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
+    ''' </summary>
+    Private Function PageNameGet(Stack As PageStackData) As String
+        Select Case Stack.Page
+            Case PageType.VersionSelect
+                Return "版本选择"
+            Case PageType.DownloadManager
+                Return "下载管理"
+            Case PageType.VersionSetup
+                Return "版本设置 - " & If(PageVersionLeft.Version Is Nothing, "未知版本", PageVersionLeft.Version.Name)
+            Case PageType.CfDetail
+                If Stack.Additional Is Nothing Then
+                    Log("[Control] CurseForge 工程详情页面未提供关键项", LogLevel.Feedback)
+                    Return "未知页面"
+                Else
+                    Dim Project As DlCfProject = Stack.Additional
+                    Return If(Project.IsModPack, "整合包下载 - ", "Mod 下载 - ") & Project.ChineseName
+                End If
+            Case PageType.HelpDetail
+                If Stack.Additional Is Nothing Then
+                    Log("[Control] 帮助详情页面未提供关键项", LogLevel.Msgbox)
+                    Return "未知页面"
+                Else
+                    Dim Entry As HelpEntry = Stack.Additional(0)
+                    Return Entry.Title
+                End If
+            Case Else
+                Return ""
+        End Select
+    End Function
+    ''' <summary>
+    ''' 刷新次级页面的名称。
+    ''' </summary>
+    Public Sub PageNameRefresh(Type As PageStackData)
+        LabTitleInner.Text = PageNameGet(Type)
+    End Sub
+    ''' <summary>
+    ''' 刷新次级页面的名称。
+    ''' </summary>
+    Public Sub PageNameRefresh()
+        PageNameRefresh(PageCurrent)
+    End Sub
+
+    '页面状态存储
+    ''' <summary>
+    ''' 当前的主页面。
+    ''' </summary>
+    Public PageCurrent As PageStackData = PageType.Launch
+    ''' <summary>
+    ''' 当前的子页面。
+    ''' </summary>
+    Public ReadOnly Property PageCurrentSub As PageSubType
+        Get
+            Select Case PageCurrent
+                Case PageType.Download
+                    If FrmDownloadLeft Is Nothing Then FrmDownloadLeft = New PageDownloadLeft
+                    Return FrmDownloadLeft.PageID
+                Case PageType.Link
+                    If FrmLinkLeft Is Nothing Then FrmLinkLeft = New PageLinkLeft
+                    Return FrmLinkLeft.PageID
+                Case PageType.Setup
+                    If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
+                    Return FrmSetupLeft.PageID
+                Case PageType.Other
+                    If FrmOtherLeft Is Nothing Then FrmOtherLeft = New PageOtherLeft
+                    Return FrmOtherLeft.PageID
+                Case PageType.VersionSetup
+                    If FrmVersionLeft Is Nothing Then FrmVersionLeft = New PageVersionLeft
+                    Return FrmVersionLeft.PageID
+                Case Else
+                    Return 0 '没有子页面
+            End Select
+        End Get
+    End Property
+    ''' <summary>
+    ''' 上层页面的编号堆栈,用于返回。
+    ''' </summary>
+    Private ReadOnly PageStack As New List(Of PageStackData)
+    Public Class PageStackData
+
+        Public Page As PageType
+        Public Additional As Object
+
+        Public Overrides Function Equals(other As Object) As Boolean
+            If other Is Nothing Then Return False
+            If TypeOf other Is PageStackData Then
+                Dim PageOther As PageStackData = other
+                If Page <> PageOther.Page Then Return False
+                If Additional Is Nothing Then
+                    Return PageOther.Additional Is Nothing
+                Else
+                    Return PageOther.Additional IsNot Nothing AndAlso Additional.Equals(PageOther.Additional)
+                End If
+            ElseIf TypeOf other Is Integer Then
+                If Page <> other Then Return False
+                Return Additional Is Nothing
+            Else
+                Return False
+            End If
+        End Function
+        Public Shared Operator =(left As PageStackData, right As PageStackData) As Boolean
+            Return EqualityComparer(Of PageStackData).Default.Equals(left, right)
+        End Operator
+        Public Shared Operator <>(left As PageStackData, right As PageStackData) As Boolean
+            Return Not left = right
+        End Operator
+        Public Shared Widening Operator CType(Value As PageType) As PageStackData
+            Return New PageStackData With {.Page = Value}
+        End Operator
+        Public Shared Widening Operator CType(Value As PageStackData) As PageType
+            Return Value.Page
+        End Operator
+    End Class
+    Public PageLeft As MyPageLeft, PageRight As MyPageRight
+
+    '引发实际页面切换的入口
+    Private IsChangingPage As Boolean = False
+    ''' <summary>
+    ''' 切换页面,并引起对应选择 UI 的改变。
+    ''' </summary>
+    Public Sub PageChange(Stack As PageStackData, Optional SubType As PageSubType = PageSubType.Default)
+        If PageNameGet(Stack) = "" Then
+            '切换到主页面
+            PageChangeExit()
+            IsChangingPage = True '防止下面的勾选直接触发了 PageChangeActual
+            CType(PanTitleSelect.Children(Stack), MyRadioButton).SetChecked(True, True, PageNameGet(PageCurrent) = "")
+            IsChangingPage = False
+            Select Case Stack.Page
+                Case PageType.Link
+                    If FrmLinkLeft Is Nothing Then FrmLinkLeft = New PageLinkLeft
+                    CType(FrmLinkLeft.PanItem.Children(SubType), MyListItem).SetChecked(True, True, Stack = PageCurrent)
+                Case PageType.Download
+                    If FrmDownloadLeft Is Nothing Then FrmDownloadLeft = New PageDownloadLeft
+                    CType(FrmDownloadLeft.PanItem.Children(SubType), MyListItem).SetChecked(True, True, Stack = PageCurrent)
+                Case PageType.Setup
+                    If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
+                    CType(FrmSetupLeft.PanItem.Children(SubType), MyListItem).SetChecked(True, True, Stack = PageCurrent)
+                Case PageType.Other
+                    If FrmOtherLeft Is Nothing Then FrmOtherLeft = New PageOtherLeft
+                    CType(FrmOtherLeft.PanItem.Children(SubType), MyListItem).SetChecked(True, True, Stack = PageCurrent)
+            End Select
+            PageChangeActual(Stack, SubType)
+        Else
+            '切换到次页面
+            Select Case Stack.Page
+                Case PageType.VersionSetup
+                    If FrmVersionLeft Is Nothing Then FrmVersionLeft = New PageVersionLeft
+                    CType(FrmVersionLeft.PanItem.Children(SubType), MyListItem).SetChecked(True, True, Stack = PageCurrent)
+            End Select
+            PageChangeActual(Stack, SubType)
+        End If
+    End Sub
+    ''' <summary>
+    ''' 通过点击导航栏改变页面。
+    ''' </summary>
+    Private Sub BtnTitleSelect_Click(sender As MyRadioButton, raiseByMouse As Boolean) Handles BtnTitleSelect0.Check, BtnTitleSelect1.Check, BtnTitleSelect2.Check, BtnTitleSelect3.Check, BtnTitleSelect4.Check
+        If IsChangingPage Then Exit Sub
+        PageChangeActual(Val(sender.Tag))
+    End Sub
+    ''' <summary>
+    ''' 通过点击返回按钮或手动触发返回来改变页面。
+    ''' </summary>
+    Public Sub PageBack() Handles BtnTitleInner.Click
+        If PageStack.Count = 0 Then Exit Sub
+        PageChangeActual(PageStack(0))
+    End Sub
+
+    '实际处理页面切换
+    ''' <summary>
+    ''' 切换现有页面的实际方法。
+    ''' </summary>
+    Private Sub PageChangeActual(Stack As PageStackData, Optional SubType As PageSubType = -1)
+        If PageCurrent = Stack AndAlso (PageCurrentSub = SubType OrElse SubType = -1) Then Exit Sub
+        AniControlEnabled += 1
+        Try
+
+#Region "子页面处理"
+            Dim PageName As String = PageNameGet(Stack)
+            If PageName = "" Then
+                '即将切换到一个顶级页面
+                PageChangeExit()
+            Else
+                '即将切换到一个子页面
+                If PageStack.Count = 0 Then
+                    '主页面 → 子页面,进入
+                    PanTitleInner.Visibility = Visibility.Visible
+                    PanTitleMain.IsHitTestVisible = False
+                    PanTitleInner.IsHitTestVisible = True
+                    PageNameRefresh(Stack)
+                    AniStart({
+                                 AaOpacity(PanTitleMain, -PanTitleMain.Opacity, 150),
+                                 AaX(PanTitleMain, 12 - PanTitleMain.Margin.Left, 150,, New AniEaseInFluent(AniEasePower.Weak)),
+                                 AaOpacity(PanTitleInner, 1 - PanTitleInner.Opacity, 150, 200),
+                                 AaX(PanTitleInner, -PanTitleInner.Margin.Left, 350, 200, New AniEaseOutBack),
+                                 AaCode(Sub() PanTitleMain.Visibility = Visibility.Collapsed,, True)
+                        }, "FrmMain Titlebar FirstLayer")
+                    PageStack.Insert(0, PageCurrent)
+                Else
+                    '子页面 → 另一个子页面,更新
+                    AniStart({
+                                 AaOpacity(LabTitleInner, -LabTitleInner.Opacity, 130),
+                                 AaCode(Sub() LabTitleInner.Text = PageName,, True),
+                                 AaOpacity(LabTitleInner, 1, 150, 30)
+                        }, "FrmMain Titlebar SubLayer")
+                    If PageStack.Contains(Stack) Then
+                        '返回到更上层的子页面
+                        Do While PageStack.Contains(Stack)
+                            PageStack.RemoveAt(0)
+                        Loop
+                    Else
+                        '进入更深层的子页面
+                        PageStack.Insert(0, PageCurrent)
+                    End If
+                End If
+            End If
+#End Region
+
+#Region "实际更改页面框架 UI"
+            PageCurrent = Stack
+            Select Case Stack.Page
+                Case PageType.Launch '启动
+                    PageChangeAnim(FrmLaunchLeft, FrmLaunchRight)
+                Case PageType.Download '下载
+                    If FrmDownloadLeft Is Nothing Then FrmDownloadLeft = New PageDownloadLeft
+                    'PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
+                    PageChangeAnim(FrmDownloadLeft, FrmDownloadLeft.PageGet(SubType))
+                Case PageType.Link '联机
+                    If FrmLinkLeft Is Nothing Then FrmLinkLeft = New PageLinkLeft
+                    If FrmLinkIoi Is Nothing Then FrmLinkIoi = New PageLinkIoi
+                    PageChangeAnim(FrmLinkLeft, FrmLinkLeft.PageGet(SubType))
+                Case PageType.Setup '设置
+                    If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
+                    PageChangeAnim(FrmSetupLeft, FrmSetupLeft.PageGet(SubType))
+                Case PageType.Other '更多
+                    If FrmOtherLeft Is Nothing Then FrmOtherLeft = New PageOtherLeft
+                    PageChangeAnim(FrmOtherLeft, FrmOtherLeft.PageGet(SubType))
+                Case PageType.VersionSelect '版本选择
+                    If FrmSelectLeft Is Nothing Then FrmSelectLeft = New PageSelectLeft
+                    If FrmSelectRight Is Nothing Then FrmSelectRight = New PageSelectRight
+                    PageChangeAnim(FrmSelectLeft, FrmSelectRight)
+                Case PageType.DownloadManager '下载管理
+                    If FrmSpeedLeft Is Nothing Then FrmSpeedLeft = New PageSpeedLeft
+                    If FrmSpeedRight Is Nothing Then FrmSpeedRight = New PageSpeedRight
+                    PageChangeAnim(FrmSpeedLeft, FrmSpeedRight)
+                Case PageType.VersionSetup '版本设置
+                    If FrmVersionLeft Is Nothing Then FrmVersionLeft = New PageVersionLeft
+                    PageChangeAnim(FrmVersionLeft, FrmVersionLeft.PageGet(SubType))
+                Case PageType.CfDetail 'Mod 信息
+                    If FrmDownloadCfDetail Is Nothing Then FrmDownloadCfDetail = New PageDownloadCfDetail
+                    PageChangeAnim(New MyPageLeft, FrmDownloadCfDetail)
+                Case PageType.HelpDetail '帮助详情
+                    PageChangeAnim(New MyPageLeft, Stack.Additional(1))
+            End Select
+#End Region
+
+#Region "设置为最新状态"
+            BtnExtraDownload.ShowRefresh()
+            BtnExtraApril.ShowRefresh()
+#End Region
+
+            Log("[Control] 切换主要页面:" & GetStringFromEnum(Stack) & ", " & SubType)
+        Catch ex As Exception
+            Log(ex, "切换主要页面失败(ID " & PageCurrent.Page & ")", LogLevel.Feedback)
+        Finally
+            AniControlEnabled -= 1
+        End Try
+    End Sub
+    Private Sub PageChangeAnim(TargetLeft As FrameworkElement, TargetRight As FrameworkElement)
+        AniStop("FrmMain LeftChange")
+        AniStop("PageLeft PageChange") '停止左边栏变更导致的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
+        AniControlEnabled += 1
+        '清除新页面关联性
+        If Not IsNothing(TargetLeft.Parent) Then TargetLeft.SetValue(ContentPresenter.ContentProperty, Nothing)
+        If Not IsNothing(TargetRight) AndAlso Not IsNothing(TargetRight.Parent) Then TargetRight.SetValue(ContentPresenter.ContentProperty, Nothing)
+        PageLeft = TargetLeft
+        PageRight = TargetRight
+        '触发页面通用动画
+        CType(PanMainLeft.Child, MyPageLeft).TriggerHideAnimation()
+        CType(PanMainRight.Child, MyPageRight).PageOnExit()
+        AniControlEnabled -= 1
+        '执行动画
+        AniStart({
+            AaCode(Sub()
+                       AniControlEnabled += 1
+                       CType(PanMainRight.Child, MyPageRight).PageOnForceExit()
+                       '把新页面添加进容器
+                       PanMainLeft.Child = PageLeft
+                       PageLeft.Opacity = 0
+                       PanMainLeft.Background = Nothing
+                       AniControlEnabled -= 1
+                       RunInUi(Sub() PanMainLeft_Resize(PanMainLeft.ActualWidth), True)
+                   End Sub, 130),
+            AaCode(Sub()
+                       '延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
+                       PageLeft.Opacity = 1
+                       PageLeft.TriggerShowAnimation()
+                   End Sub, 30, True)
+            }, "FrmMain PageChangeLeft")
+        AniStart({
+            AaCode(Sub()
+                       AniControlEnabled += 1
+                       CType(PanMainRight.Child, MyPageRight).PageOnForceExit()
+                       '把新页面添加进容器
+                       PanMainRight.Child = PageRight
+                       PageRight.Opacity = 0
+                       PanMainRight.Background = Nothing
+                       AniControlEnabled -= 1
+                       RunInUi(Sub() BtnExtraBack.ShowRefresh(), True)
+                   End Sub, 130),
+            AaCode(Sub()
+                       '延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
+                       PageRight.Opacity = 1
+                       PageRight.PageOnEnter()
+                   End Sub, 30, True)
+            }, "FrmMain PageChangeRight")
+    End Sub
+    ''' <summary>
+    ''' 退出子界面。
+    ''' </summary>
+    Private Sub PageChangeExit()
+        If PageStack.Count = 0 Then
+            '主页面 → 主页面,无事发生
+        Else
+            '子页面 → 主页面,退出
+            PanTitleMain.Visibility = Visibility.Visible
+            PanTitleMain.IsHitTestVisible = True
+            PanTitleInner.IsHitTestVisible = False
+            AniStart({
+                         AaOpacity(PanTitleInner, -PanTitleInner.Opacity, 150),
+                         AaX(PanTitleInner, -18 - PanTitleInner.Margin.Left, 150,, New AniEaseInFluent),
+                         AaOpacity(PanTitleMain, 1 - PanTitleMain.Opacity, 150, 200),
+                         AaX(PanTitleMain, -PanTitleMain.Margin.Left, 350, 200, New AniEaseOutBack(AniEasePower.Weak)),
+                         AaCode(Sub() PanTitleInner.Visibility = Visibility.Collapsed,, True)
+                }, "FrmMain Titlebar FirstLayer")
+            PageStack.Clear()
+        End If
+    End Sub
+
+    '左边栏改变
+    Private Sub PanMainLeft_SizeChanged(sender As Object, e As SizeChangedEventArgs) Handles PanMainLeft.SizeChanged
+        If Not e.WidthChanged Then Exit Sub
+        PanMainLeft_Resize(e.NewSize.Width)
+    End Sub
+    Private Sub PanMainLeft_Resize(NewWidth As Double)
+        Dim Delta As Double = NewWidth - RectLeftBackground.Width
+        If Math.Abs(Delta) < 0.1 Then Exit Sub
+        If AniControlEnabled = 0 Then
+            If PanMain.Opacity < 0.1 Then PanMainLeft.IsHitTestVisible = False '避免左边栏指向背景未能完美覆盖左边栏
+            If NewWidth > 0 Then
+                '宽度足够,显示
+                AniStart({
+                              AaWidth(RectLeftBackground, NewWidth - RectLeftBackground.Width, 400,, New AniEaseOutFluent(AniEasePower.ExtraStrong)),
+                              AaOpacity(RectLeftShadow, 1 - RectLeftShadow.Opacity, 200),
+                              AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 250)
+                         }, "FrmMain LeftChange", True)
+            Else
+                '宽度不足,隐藏
+                AniStart({
+                              AaWidth(RectLeftBackground, -RectLeftBackground.Width, 200,, New AniEaseOutFluent),
+                              AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 200),
+                              AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 170)
+                         }, "FrmMain LeftChange", True)
+            End If
+        Else
+            RectLeftBackground.Width = NewWidth
+            PanMainLeft.IsHitTestVisible = True
+            AniStop("FrmMain LeftChange")
+        End If
+    End Sub
+
+#End Region
+
+#Region "控件拖动"
+
+    '在时钟中调用,使得即使鼠标在窗口外松开,也可以释放控件
+    Public Sub DragTick()
+        If DragControl Is Nothing Then Exit Sub
+        If Not Mouse.LeftButton = MouseButtonState.Pressed Then
+            DragStop()
+        End If
+    End Sub
+    '在鼠标移动时调用,以改变 Slider 位置
+    Public Sub DragDoing() Handles PanBack.MouseMove
+        If DragControl Is Nothing Then Exit Sub
+        If Mouse.LeftButton = MouseButtonState.Pressed Then
+            DragControl.DragDoing()
+        Else
+            DragStop()
+        End If
+    End Sub
+    Public Sub DragStop()
+        '存在其他线程调用的可能性,因此需要确保在 UI 线程运行
+        RunInUi(Sub()
+                    If DragControl Is Nothing Then Exit Sub
+                    Dim Control = DragControl
+                    DragControl = Nothing
+                    Control.DragStop() '控件会在该事件中判断 DragControl,所以得放在后面
+                End Sub)
+    End Sub
+
+#End Region
+
+#Region "附加按钮"
+
+    '音乐
+    Private Sub BtnExtraMusic_Click(sender As Object, e As EventArgs) Handles BtnExtraMusic.Click
+        MusicControlPause()
+    End Sub
+    Private Sub BtnExtraMusic_RightClick(sender As Object, e As EventArgs) Handles BtnExtraMusic.RightClick
+        MusicControlNext()
+    End Sub
+
+    '下载管理
+    Private Sub BtnExtraDownload_Click(sender As Object, e As EventArgs) Handles BtnExtraDownload.Click
+        PageChange(PageType.DownloadManager)
+    End Sub
+    Private Function BtnExtraDownload_ShowCheck() As Boolean
+        Return HasDownloadingTask() AndAlso Not PageCurrent = PageType.DownloadManager
+    End Function
+
+    '投降
+    Public Sub AprilGiveup() Handles BtnExtraApril.Click
+        If IsAprilEnabled AndAlso Not IsAprilGiveup Then
+            Hint("=D", HintType.Finish)
+            IsAprilGiveup = True
+            FrmLaunchLeft.AprilScaleTrans.ScaleX = 1
+            FrmLaunchLeft.AprilScaleTrans.ScaleY = 1
+            BtnExtraApril.ShowRefresh()
+        End If
+    End Sub
+    Public Function BtnExtraApril_ShowCheck() As Boolean
+        Return IsAprilEnabled AndAlso Not IsAprilGiveup AndAlso PageCurrent = PageType.Launch
+    End Function
+
+    '关闭 Minecraft
+    Public Sub BtnExtraShutdown_Click() Handles BtnExtraShutdown.Click
+        Try
+            If McLaunchLoaderReal IsNot Nothing Then McLaunchLoaderReal.Abort()
+            For Each Watcher In McWatcherList
+                Watcher.Kill()
+            Next
+            Hint("已关闭运行中的 Minecraft!", HintType.Finish)
+        Catch ex As Exception
+            Log(ex, "强制关闭所有 Minecraft 失败", LogLevel.Feedback)
+        End Try
+    End Sub
+    Public Function BtnExtraShutdown_ShowCheck() As Boolean
+        Return HasRunningMinecraft
+    End Function
+
+    '返回顶部
+    Private Sub BtnExtraBack_Click(sender As Object, e As EventArgs) Handles BtnExtraBack.Click
+        Dim RealScroll As MyScrollViewer = BtnExtraBack_GetRealChild()
+        RealScroll.PerformVerticalOffsetDelta(-RealScroll.VerticalOffset)
+    End Sub
+    Private Function BtnExtraBack_ShowCheck() As Boolean
+        Dim RealScroll As MyScrollViewer = BtnExtraBack_GetRealChild()
+        Return RealScroll IsNot Nothing AndAlso RealScroll.Visibility = Visibility.Visible AndAlso RealScroll.VerticalOffset > Height + If(BtnExtraBack.Show, 0, 1500)
+    End Function
+    Private Function BtnExtraBack_GetRealChild() As MyScrollViewer
+        If PanMainRight.Child Is Nothing Then Return Nothing
+        Dim RightChild = CType(PanMainRight.Child, AdornerDecorator).Child
+        If RightChild Is Nothing Then
+            Return Nothing
+        ElseIf TypeOf RightChild Is MyScrollViewer Then
+            Return RightChild
+        ElseIf TypeOf RightChild Is Grid AndAlso TypeOf CType(RightChild, Grid).Children(0) Is MyScrollViewer Then
+            Return CType(RightChild, Grid).Children(0)
+        Else
+            Return Nothing
+        End If
+    End Function
+
+#End Region
+
+    '愚人节鼠标位置
+    Public lastMouseArg As MouseEventArgs = Nothing
+    Private Sub FormMain_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove
+        lastMouseArg = e
+    End Sub
+
+End Class

+ 12 - 0
Plain Craft Launcher 2/GlobalSuppressions.vb

@@ -0,0 +1,12 @@
+' This file is used by Code Analysis to maintain SuppressMessage
+' attributes that are applied to this project.
+' Project-level suppressions either have no target or are given
+' a specific target and scoped to a namespace, type, member, etc.
+
+Imports System.Diagnostics.CodeAnalysis
+
+<Assembly: SuppressMessage("样式", "IDE0060:删除未使用的参数", Justification:="<挂起>")>
+<Assembly: SuppressMessage("样式", "IDE0058:永远不会使用表达式值", Justification:="<挂起>")>
+<Assembly: SuppressMessage("样式", "IDE0044:添加只读修饰符", Justification:="<挂起>")>
+<Assembly: SuppressMessage("样式", "IDE1006:命名样式", Justification:="<挂起>")>
+<Assembly: SuppressMessage("样式", "IDE0010:添加开关", Justification:="<挂起>")>

BIN
Plain Craft Launcher 2/Images/Blocks/Anvil.png


BIN
Plain Craft Launcher 2/Images/Blocks/CobbleStone.png


BIN
Plain Craft Launcher 2/Images/Blocks/CommandBlock.png


BIN
Plain Craft Launcher 2/Images/Blocks/Egg.png


BIN
Plain Craft Launcher 2/Images/Blocks/Fabric.png


BIN
Plain Craft Launcher 2/Images/Blocks/GoldBlock.png


BIN
Plain Craft Launcher 2/Images/Blocks/Grass.png


BIN
Plain Craft Launcher 2/Images/Blocks/GrassPath.png


BIN
Plain Craft Launcher 2/Images/Blocks/OptiFabric.png


BIN
Plain Craft Launcher 2/Images/Blocks/RedstoneBlock.png


BIN
Plain Craft Launcher 2/Images/Blocks/RedstoneLampOff.png


BIN
Plain Craft Launcher 2/Images/Blocks/RedstoneLampOn.png


BIN
Plain Craft Launcher 2/Images/Heads/00ll00.png


BIN
Plain Craft Launcher 2/Images/Heads/DoodleHuang.png


BIN
Plain Craft Launcher 2/Images/Heads/Hao_Tian.jpg


BIN
Plain Craft Launcher 2/Images/Heads/LTCat.jpg


BIN
Plain Craft Launcher 2/Images/Heads/Logo.png


BIN
Plain Craft Launcher 2/Images/Heads/MCBBS.png


BIN
Plain Craft Launcher 2/Images/Heads/PCL2.png


BIN
Plain Craft Launcher 2/Images/Heads/Patrick.png


BIN
Plain Craft Launcher 2/Images/Heads/bangbang93.png


BIN
Plain Craft Launcher 2/Images/Heads/noin.png


BIN
Plain Craft Launcher 2/Images/Heads/wiki.png


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


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


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


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


BIN
Plain Craft Launcher 2/Images/Skins/Alex.png


BIN
Plain Craft Launcher 2/Images/Skins/Steve.png


BIN
Plain Craft Launcher 2/Images/Themes/10.png


BIN
Plain Craft Launcher 2/Images/Themes/11.png


BIN
Plain Craft Launcher 2/Images/Themes/12.png


BIN
Plain Craft Launcher 2/Images/Themes/13.png


BIN
Plain Craft Launcher 2/Images/Themes/42.png


BIN
Plain Craft Launcher 2/Images/Themes/5.png


BIN
Plain Craft Launcher 2/Images/Themes/6.png


BIN
Plain Craft Launcher 2/Images/Themes/7.png


BIN
Plain Craft Launcher 2/Images/Themes/8.png


BIN
Plain Craft Launcher 2/Images/Themes/9.png


BIN
Plain Craft Launcher 2/Images/icon.ico


+ 1003 - 0
Plain Craft Launcher 2/Modules/Base/ModAnimation.vb

@@ -0,0 +1,1003 @@
+'动画引擎模块
+'使用 Ani 作为方法或属性的开头,使用 Aa 作为单个动画对象的开头(便于自动补全)
+
+Public Module ModAnimation
+
+#Region "声明"
+
+    ''' <summary>
+    ''' 动画速度。最大为 200。
+    ''' </summary>
+    Public AniSpeed As Double = 1
+    ''' <summary>
+    ''' 动画组列表。
+    ''' </summary>
+    Public AniGroups As New Dictionary(Of String, AniGroupEntry)
+    Public Class AniGroupEntry
+        Public Data As List(Of AniData)
+        Public StartTick As Long
+        Public Uuid As Integer = GetUuid()
+    End Class
+    ''' <summary>
+    ''' 上一次记刻的时间。
+    ''' </summary>
+    Private AniLastTick As Long
+    ''' <summary>
+    ''' 动画模块是否正在运行。
+    ''' </summary>
+    Public AniRunning As Boolean = False
+    Private _AniControlEnabled As Integer = 0
+    Private ReadOnly AniControlEnabledLock As New Object
+    ''' <summary>
+    ''' 控件动画执行是否开启。先 +1,再 -1。
+    ''' </summary>
+    Public Property AniControlEnabled() As Integer
+        Get
+            Return _AniControlEnabled
+        End Get
+        Set(value As Integer)
+            SyncLock AniControlEnabledLock
+                _AniControlEnabled = value
+            End SyncLock
+        End Set
+    End Property
+
+#End Region
+
+#Region "类与枚举"
+
+    ''' <summary>
+    ''' 单个动画对象。
+    ''' </summary>
+    ''' <remarks></remarks>
+    Public Structure AniData
+
+        ''' <summary>
+        ''' 动画种类。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public TypeMain As AniType
+        ''' <summary>
+        ''' 动画副种类。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public TypeSub As AniTypeSub
+
+        ''' <summary>
+        ''' 动画总长度。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public TimeTotal As Integer
+        ''' <summary>
+        ''' 已经执行的动画长度。如果为负数则为延迟。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public TimeFinished As Integer
+        ''' <summary>
+        ''' 已经完成的百分比。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public TimePercent As Double
+
+        ''' <summary>
+        ''' 是否为“以后”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public IsAfter As Boolean
+
+        ''' <summary>
+        ''' 插值器类型。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public Ease As AniEase
+        ''' <summary>
+        ''' 动画对象。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public Obj As Object
+        ''' <summary>
+        ''' 动画值。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public Value As Object
+        ''' <summary>
+        ''' 上次执行时的动画值。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Public ValueLast As Object
+
+        Public Overrides Function ToString() As String
+            Return GetStringFromEnum(TypeMain) & " | " & TimeFinished & "/" & TimeTotal & "(" & Math.Round(TimePercent * 100) & "%)" & If(Obj Is Nothing, "", " | " & Obj.ToString & "(" & Obj.GetType.Name & ")")
+        End Function
+
+    End Structure
+
+    ''' <summary>
+    ''' 动画基础种类。
+    ''' </summary>
+    Public Enum AniType
+        ''' <summary>
+        ''' 单个Double的动画,包括位置、长宽、透明度等。这需要附属类型。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Number
+        ''' <summary>
+        ''' 颜色属性的动画。这需要附属类型。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Color
+        ''' <summary>
+        ''' 缩放控件大小。比起4个DoubleAnimation来说效率更高。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Scale
+        ''' <summary>
+        ''' 文字一个个出现。
+        ''' </summary>
+        ''' <remarks></remarks>
+        TextAppear
+        ''' <summary>
+        ''' 执行代码。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Code
+        ''' <summary>
+        ''' 以 WPF 方式缩放控件。
+        ''' </summary>
+        ScaleTransform
+        ''' <summary>
+        ''' 以 WPF 方式旋转控件。
+        ''' </summary>
+        RotateTransform
+    End Enum
+    ''' <summary>
+    ''' 动画扩展种类。
+    ''' </summary>
+    Public Enum AniTypeSub
+        X
+        Y
+        Width
+        Height
+        Opacity
+        Value
+        Radius
+        BorderThickness
+        StrokeThickness
+        TranslateX
+        TranslateY
+        [Double]
+        DoubleParam
+        GridLengthWidth
+    End Enum
+
+#End Region
+
+#Region "种类"
+
+    'DoubleAnimation
+
+    ''' <summary>
+    ''' 移动X轴的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">进行移动的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaX(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.X,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 移动Y轴的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">进行移动的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaY(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Y,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变宽度的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">宽度改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaWidth(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Width,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变高度的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">高度改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaHeight(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Height,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变透明度的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">透明度改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaOpacity(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Opacity,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变对象的Value属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">Value属性改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaValue(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Value,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变对象的Radius属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">Radius属性改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaRadius(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Radius,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变对象的BorderThickness属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">BorderThickness属性改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaBorderThickness(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.BorderThickness,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变对象的StrokeThickness属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">StrokeThickness属性改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    Public Function AaStrokeThickness(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.StrokeThickness,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 改变 Width 的 GridLength 属性的动画。必须为 Star。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">GridLength.Value 改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    Public Function AaGridLengthWidth(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.GridLengthWidth,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'DoubleAnimation(Obj, Prop, [Res])
+
+    ''' <summary>
+    ''' 改变数字属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Prop">动画的依赖属性。</param>
+    ''' <param name="Value">改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaDouble(Obj As Object, Prop As DependencyProperty, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.Double, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = {Obj, Prop, ""}, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 获取数字动画值。
+    ''' </summary>
+    ''' <param name="Value">改变的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaDouble(Lambda As ParameterizedThreadStart, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.DoubleParam, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Lambda, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'ColorAnimation(Obj, Prop, [Res])
+
+    ''' <summary>
+    ''' 改变颜色属性的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Prop">动画的依赖属性。</param>
+    ''' <param name="Value">颜色改变的值。以RGB加减法进行计算。不用担心超额。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaColor(Obj As FrameworkElement, Prop As DependencyProperty, Value As MyColor, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Color, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = {Obj, Prop, ""}, .Value = Value, .IsAfter = After, .TimeFinished = -Delay, .ValueLast = New MyColor(0, 0, 0, 0)}
+    End Function
+    ''' <summary>
+    ''' 改变颜色属性为一个资源的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Prop">动画的依赖属性。</param>
+    ''' <param name="Res">要将颜色改变为该资源值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaColor(Obj As FrameworkElement, Prop As DependencyProperty, Res As String, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Color, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = {Obj, Prop, Res}, .Value = New MyColor(Application.Current.FindResource(Res)) - New MyColor(Obj.GetValue(Prop)), .IsAfter = After, .TimeFinished = -Delay, .ValueLast = New MyColor(0, 0, 0, 0)}
+    End Function
+
+    'Scale
+
+    ''' <summary>
+    ''' 缩放控件的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">大小改变的百分比(如-0.6)或值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <param name="Absolute">大小改变是否为绝对值。若为 True 则为绝对像素,若为 False 则为相对百分比。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaScale(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False, Optional Absolute As Boolean = False) As AniData
+        Dim ChangeRect As MyRect
+        If Absolute Then
+            ChangeRect = New MyRect(-0.5 * Value, -0.5 * Value, Value, Value)
+        Else
+            ChangeRect = New MyRect(-0.5 * Obj.ActualWidth * Value, -0.5 * Obj.ActualHeight * Value, Obj.ActualWidth * Value, Obj.ActualHeight * Value)
+        End If
+        Return New AniData With {.TypeMain = AniType.Scale, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = ChangeRect, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'TextAppear
+
+    ''' <summary>
+    ''' 让一段文字一个个字出现或消失的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。必须是Label或TextBlock。</param>
+    ''' <param name="Hide">是否为一个个字隐藏。默认为False(一个个字出现)。这些字必须已经存在了。</param>
+    ''' <param name="TimePerText">是否采用根据文本长度决定时间的方式。</param>
+    ''' <param name="Time">动画长度(毫秒)。若TimePerText为True,这代表每个字所占据的时间。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaTextAppear(Obj As Object, Optional Hide As Boolean = False, Optional TimePerText As Boolean = True, Optional Time As Integer = 70, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        'Are we cool yet?
+        Return New AniData With {.TypeMain = AniType.TextAppear, .Ease = If(Ease, New AniEaseLinear), .TimeTotal = If(TimePerText, Time * If(TypeOf Obj Is TextBlock, Obj.Text, Obj.Context.ToString).ToString.Length, Time), .Obj = Obj, .Value = {If(TypeOf Obj Is TextBlock, Obj.Text, Obj.Context.ToString), Hide}, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'Code
+
+    ''' <summary>
+    ''' 执行代码。
+    ''' </summary>
+    ''' <param name="Code">一个ThreadStart。这将会在执行时在主线程调用。</param>
+    ''' <param name="Delay">代码延迟执行的时间(毫秒)。</param>
+    ''' <param name="After">是否等到以前的动画完成后才执行。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaCode(Code As ThreadStart, Optional Delay As Integer = 0, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Code,
+                                   .TimeTotal = 1, .Value = Code, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'ScaleTransform
+
+    ''' <summary>
+    ''' 按照 WPF 方式缩放控件的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
+    ''' <param name="Value">大小改变的百分比(如-0.6)。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaScaleTransform(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.ScaleTransform, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'RotateTransform
+
+    ''' <summary>
+    ''' 按照 WPF 方式旋转控件的动画。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
+    ''' <param name="Value">大小改变的百分比(如-0.6)。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    ''' <returns></returns>
+    ''' <remarks></remarks>
+    Public Function AaRotateTransform(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.RotateTransform, .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    'TranslateTransform
+
+    ''' <summary>
+    ''' 利用 TranslateTransform 移动 X 轴的动画,这不会造成布局更新。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">进行移动的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    Public Function AaTranslateX(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.TranslateX,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+    ''' <summary>
+    ''' 利用 TranslateTransform 移动 Y 轴的动画,这不会造成布局更新。
+    ''' </summary>
+    ''' <param name="Obj">动画的对象。</param>
+    ''' <param name="Value">进行移动的值。</param>
+    ''' <param name="Time">动画长度(毫秒)。</param>
+    ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
+    ''' <param name="Ease">插值器类型。</param>
+    ''' <param name="After">是否等到以前的动画完成后才继续本动画。</param>
+    Public Function AaTranslateY(Obj As Object, Value As Double, Optional Time As Integer = 400, Optional Delay As Integer = 0, Optional Ease As AniEase = Nothing, Optional After As Boolean = False) As AniData
+        Return New AniData With {.TypeMain = AniType.Number, .TypeSub = AniTypeSub.TranslateY,
+                                   .TimeTotal = Time, .Ease = If(Ease, New AniEaseLinear), .Obj = Obj, .Value = Value, .IsAfter = After, .TimeFinished = -Delay}
+    End Function
+
+    '特殊
+
+    ''' <summary>
+    ''' 将一个StackPanel中的各个项目依次显示。
+    ''' </summary>
+    ''' <remarks></remarks>
+    Public Function AaStack(Stack As StackPanel, Optional Time As Integer = 100, Optional Delay As Integer = 25) As List(Of AniData)
+        AaStack = New List(Of AniData)
+        Dim AniDelay As Integer = 0
+        For Each Item In Stack.Children
+            Item.Opacity = 0
+            AaStack.Add(AaOpacity(Item, 1, Time, AniDelay))
+            AniDelay += Delay
+        Next
+    End Function
+
+#End Region
+
+#Region "缓动函数"
+
+    '基类
+    Public Enum AniEasePower As Integer
+        Weak = 2
+        Middle = 3
+        Strong = 4
+        ExtraStrong = 5
+    End Enum
+    ''' <summary>
+    ''' 缓动函数基类。
+    ''' </summary>
+    Public MustInherit Class AniEase
+        ''' <summary>
+        ''' 获取函数值。
+        ''' </summary>
+        ''' <param name="t">时间百分比。</param>
+        Public MustOverride Function GetValue(t As Double) As Double
+        ''' <summary>
+        ''' 获取增量值。
+        ''' </summary>
+        ''' <param name="t1">较大的 X。</param>
+        ''' <param name="t0">较小的 X。</param>
+        Public Overridable Function GetDelta(t1 As Double, t0 As Double) As Double
+            Return GetValue(t1) - GetValue(t0)
+        End Function
+    End Class
+    ''' <summary>
+    ''' 渐入渐出组合。
+    ''' </summary>
+    Public Class AniEaseInout
+        Inherits AniEase
+        Private ReadOnly EaseIn As AniEase, EaseOut As AniEase, EaseInPercent As Double
+        Public Sub New(EaseIn As AniEase, EaseOut As AniEase, Optional EaseInPercent As Double = 0.5)
+            Me.EaseIn = EaseIn : Me.EaseOut = EaseOut : Me.EaseInPercent = EaseInPercent
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            If t < EaseInPercent Then
+                Return EaseInPercent * EaseIn.GetValue(t / EaseInPercent)
+            Else
+                Return (1 - EaseInPercent) * EaseOut.GetValue((t - EaseInPercent) / (1 - EaseInPercent)) + EaseInPercent
+            End If
+        End Function
+    End Class
+
+    'Linear / 线性
+    ''' <summary>
+    ''' 线性,无缓动。
+    ''' </summary>
+    Public Class AniEaseLinear
+        Inherits AniEase
+        Public Overrides Function GetValue(t As Double) As Double
+            Return MathRange(t, 0, 1)
+        End Function
+        Public Overrides Function GetDelta(t1 As Double, t0 As Double) As Double
+            Return MathRange(t1, 0, 1) - MathRange(t0, 0, 1)
+        End Function
+    End Class
+
+    'Fluent / 平滑
+    ''' <summary>
+    ''' 平滑开始。
+    ''' </summary>
+    Public Class AniEaseInFluent
+        Inherits AniEase
+        Private ReadOnly p As AniEasePower
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = Power
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            Return MathRange(t, 0, 1) ^ p
+        End Function
+    End Class
+    ''' <summary>
+    ''' 平滑结束。
+    ''' </summary>
+    Public Class AniEaseOutFluent
+        Inherits AniEase
+        Private ReadOnly p As AniEasePower
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = Power
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            Return 1 - MathRange(1 - t, 0, 1) ^ p
+        End Function
+    End Class
+    ''' <summary>
+    ''' 平滑开始与结束。
+    ''' </summary>
+    Public Class AniEaseInoutFluent
+        Inherits AniEase
+        Private Ease As AniEaseInout
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle, Optional Middle As Double = 0.5)
+            Ease = New AniEaseInout(New AniEaseInFluent(Power), New AniEaseOutFluent(Power), Middle)
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            Return Ease.GetValue(t)
+        End Function
+    End Class
+
+    'Back / 回弹
+    ''' <summary>
+    ''' 回弹开始。有效时间为 1/3。
+    ''' </summary>
+    Public Class AniEaseInBack
+        Inherits AniEase
+        Private ReadOnly p As Double
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = 3 - Power * 0.5
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            t = MathRange(t, 0, 1)
+            Return t ^ p * Math.Cos(1.5 * Math.PI * (1 - t))
+        End Function
+    End Class
+    ''' <summary>
+    ''' 回弹结束。有效时间为 1/3。
+    ''' </summary>
+    Public Class AniEaseOutBack
+        Inherits AniEase
+        Private ReadOnly p As Double
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = 3 - Power * 0.5
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            t = MathRange(t, 0, 1)
+            Return 1 - (1 - t) ^ p * Math.Cos(1.5 * Math.PI * t)
+        End Function
+    End Class
+
+    'Car / 平滑-回弹
+    ''' <summary>
+    ''' 回弹开始,短平滑结束。
+    ''' </summary>
+    Public Class AniEaseInCar
+        Inherits AniEase
+        Private Ease As AniEaseInout
+        Public Sub New(Optional Middle As Double = 0.7, Optional Power As AniEasePower = AniEasePower.Middle)
+            Ease = New AniEaseInout(New AniEaseInBack(Power), New AniEaseOutFluent(Power), Middle)
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            Return Ease.GetValue(t)
+        End Function
+    End Class
+    ''' <summary>
+    ''' 短平滑开始,回弹结束。
+    ''' </summary>
+    Public Class AniEaseOutCar
+        Inherits AniEase
+        Private Ease As AniEaseInout
+        Public Sub New(Optional Middle As Double = 0.3, Optional Power As AniEasePower = AniEasePower.Middle)
+            Ease = New AniEaseInout(New AniEaseInFluent(Power), New AniEaseOutBack(Power), Middle)
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            Return Ease.GetValue(t)
+        End Function
+    End Class
+
+    'Elastic / 弹簧
+    ''' <summary>
+    ''' 弹簧开始。约在 60% 到达最小值。
+    ''' </summary>
+    Public Class AniEaseInElastic
+        Inherits AniEase
+        Private ReadOnly p As Integer '6~9
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = Power + 4
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            t = MathRange(t, 0, 1)
+            Return t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
+        End Function
+    End Class
+    ''' <summary>
+    ''' 弹簧结束。约在 40% 到达最大值。
+    ''' </summary>
+    Public Class AniEaseOutElastic
+        Inherits AniEase
+        Private ReadOnly p As Integer
+        Public Sub New(Optional Power As AniEasePower = AniEasePower.Middle)
+            p = Power + 4
+        End Sub
+        Public Overrides Function GetValue(t As Double) As Double
+            t = 1 - MathRange(t, 0, 1)
+            Return 1 - t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
+        End Function
+    End Class
+
+#End Region
+
+#Region "接口(开始、中断、检测)"
+
+    ''' <summary>
+    ''' 开始一个动画组。
+    ''' </summary>
+    ''' <param name="AniGroup">由 Aa 开头的函数初始化的 AniData 对象集合。</param>
+    ''' <param name="Name">动画组的名称。如果重复会直接停止同名动画组。</param>
+    Public Sub AniStart(AniGroup As IList, Optional Name As String = "", Optional RefreshTime As Boolean = False)
+        If RefreshTime Then AniLastTick = GetTimeTick() '避免处理动画时已经造成了极大的延迟,导致动画突然结束
+        '添加到正在执行的动画组
+        Dim NewEntry As New AniGroupEntry With {.Data = GetFullList(Of AniData)(AniGroup), .StartTick = GetTimeTick()}
+        If Name = "" Then
+            Name = NewEntry.Uuid
+        Else
+            AniStop(Name)
+        End If
+        AniGroups.Add(Name, NewEntry)
+    End Sub
+    ''' <summary>
+    ''' 开始一个动画组。
+    ''' </summary>
+    Public Sub AniStart(AniGroup As AniData, Optional Name As String = "", Optional RefreshTime As Boolean = False)
+        AniStart(New List(Of AniData) From {AniGroup}, Name, RefreshTime)
+    End Sub
+    ''' <summary>
+    ''' 直接停止一个动画组。
+    ''' </summary>
+    ''' <param name="name">需要停止的动画组的名称。</param>
+    Public Sub AniStop(Name As String)
+        AniGroups.Remove(Name)
+    End Sub
+    ''' <summary>
+    ''' 获取动画是否正在进行中。
+    ''' </summary>
+    Public Function AniIsRun(Name As String) As Boolean
+        Return AniGroups.ContainsKey(Name)
+    End Function
+
+#End Region
+
+    Private AniCount As Integer = 0
+    Private AniFPSCounter As Integer = 0
+    Private AniFPSTimer As Long = 0
+    ''' <summary>
+    ''' 当前的动画 FPS。
+    ''' </summary>
+    Public AniFPS As Integer = 0
+
+    ''' <summary>
+    ''' 开始动画执行。
+    ''' </summary>
+    Public Sub AniStartRun()
+        '初始化计时器
+        AniLastTick = GetTimeTick()
+        AniFPSTimer = AniLastTick
+        AniRunning = True '标记动画执行开始
+
+        RunInNewThread(Sub()
+                           Try
+                               Log("[Animation] 动画线程开始")
+                               Do While True
+                                   '两帧之间的间隔时间
+                                   Dim DeltaTime As Long = MathRange(GetTimeTick() - AniLastTick, 0, 10000)
+                                   If DeltaTime < 3 Then GoTo Sleeper
+                                   AniLastTick = GetTimeTick()
+                                   '记录 FPS
+                                   If ModeDebug Then
+                                       If MathRange(AniLastTick - AniFPSTimer, 0, 10000) >= 500 Then
+                                           AniFPS = AniFPSCounter
+                                           AniFPSCounter = 0
+                                           AniFPSTimer = AniLastTick
+                                       End If
+                                       AniFPSCounter += 2
+                                   End If
+                                   '执行动画
+                                   RunInUiWait(Sub()
+                                                   AniCount = 0
+                                                   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
+                                                       Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & GetString(NetManager.Speed) & "/s)")
+                                                   End If
+                                               End Sub)
+Sleeper:
+                                   '控制 FPS
+                                   Thread.Sleep(1)
+                               Loop
+                           Catch ex As Exception
+                               Log(ex, "动画帧执行失败", LogLevel.Assert)
+                           End Try
+                       End Sub, "Animation", ThreadPriority.AboveNormal)
+    End Sub
+
+    ''' <summary>
+    ''' 动画定时器事件。
+    ''' </summary>
+    Public Sub AniTimer(DeltaTick As Integer)
+        Try
+
+            If DeltaTick / AniSpeed > 200 Then Log("[Animation] 两个动画帧间隔 " & DeltaTick & " ms", LogLevel.Developer)
+            Dim i As Integer = -1
+            '循环每个动画组
+            Do While i + 1 < AniGroups.Count
+                i += 1
+                '初始化
+                Dim Entry As AniGroupEntry = AniGroups.Values(i)
+                If Entry.StartTick > AniLastTick Then Continue Do '跳过本刻之后开始的动画
+                Dim CanRemoveAfter = True '是否应该去除“之后”标记
+                Dim ii = 0
+
+                '循环每个动画
+                Do While ii < Entry.Data.Count
+                    Dim Anim As AniData = Entry.Data(ii)
+                    '执行种类
+                    If Anim.IsAfter = False Then '之前
+                        CanRemoveAfter = False '取消“之后”标记 
+                        '增加执行时间
+                        Anim.TimeFinished += DeltaTick
+                        '执行动画
+                        If Anim.TimeFinished > 0 Then
+                            Anim = AniRun(Anim)
+                            AniCount += 1
+                        End If
+                        '如果当前动画已执行完毕
+                        If Anim.TimeFinished >= Anim.TimeTotal Then
+                            '如果是去向颜色资源的动画,设置引用
+                            If Anim.TypeMain = AniType.Color AndAlso Not Anim.Obj(2) = "" Then Anim.Obj(0).SetResourceReference(Anim.Obj(1), Anim.Obj(2))
+                            '删除
+                            Entry.Data.RemoveAt(ii)
+                            GoTo NextAni
+                        End If
+                        Entry.Data(ii) = Anim
+                    Else '之后
+                        If CanRemoveAfter Then
+                            '之后改为之前
+                            CanRemoveAfter = False
+                            Anim.IsAfter = False
+                            Entry.Data(ii) = Anim
+                            '重新循环该动画
+                            GoTo NextAni
+                        Else
+                            '不能去除该“之后”标记,结束该动画组
+                            Exit Do
+                        End If
+                    End If
+                    ii += 1
+NextAni:
+                Loop
+
+                '如果当前动画组都执行完毕则删除
+                If Entry.Data.Count = 0 Then
+                    '为了避免新添加的动画影响顺序,不能 RemoveAt(i)
+                    '为了允许动画在执行中添加同名动画组,不能按名字移除
+                    For Current = 0 To AniGroups.Count - 1
+                        If AniGroups.ElementAt(Current).Value.Uuid = Entry.Uuid Then
+                            AniGroups.Remove(AniGroups.ElementAt(Current).Key)
+                            Exit For
+                        End If
+                    Next
+                    i -= 1
+                End If
+            Loop
+
+        Catch ex As Exception
+            Log(ex, "动画刻执行失败", LogLevel.Hint)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 执行一个动画。
+    ''' </summary>
+    ''' <param name="Ani">执行的动画对象。</param>
+    Private Function AniRun(Ani As AniData) As AniData
+        Try
+            Select Case Ani.TypeMain
+
+                Case AniType.Number
+                    Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
+                    If Delta <> 0 Then
+                        Select Case Ani.TypeSub
+                            Case AniTypeSub.X
+                                DeltaLeft(Ani.Obj, Delta)
+                            Case AniTypeSub.Y
+                                DeltaTop(Ani.Obj, Delta)
+                            Case AniTypeSub.Opacity
+                                Ani.Obj.Opacity = MathRange(Ani.Obj.Opacity + Delta, 0, 1)
+                            Case AniTypeSub.Width
+                                Dim Obj As FrameworkElement = Ani.Obj
+                                Obj.Width = Math.Max(If(Double.IsNaN(Obj.Width), Obj.ActualWidth, Obj.Width) + Delta, 0)
+                            Case AniTypeSub.Height
+                                Dim Obj As FrameworkElement = Ani.Obj
+                                Obj.Height = Math.Max(If(Double.IsNaN(Obj.Height), Obj.ActualHeight, Obj.Height) + Delta, 0)
+                            Case AniTypeSub.Value
+                                Ani.Obj.Value += Delta
+                            Case AniTypeSub.Radius
+                                Ani.Obj.Radius += Delta
+                            Case AniTypeSub.StrokeThickness
+                                Ani.Obj.StrokeThickness = Math.Max(Ani.Obj.StrokeThickness + Delta, 0)
+                            Case AniTypeSub.BorderThickness
+                                Ani.Obj.BorderThickness = New Thickness(CType(Ani.Obj.BorderThickness, Thickness).Bottom + Delta)
+                            Case AniTypeSub.TranslateX
+                                If IsNothing(Ani.Obj.RenderTransform) OrElse TypeOf Ani.Obj.RenderTransform IsNot TranslateTransform Then Ani.Obj.RenderTransform = New TranslateTransform(0, 0)
+                                CType(Ani.Obj.RenderTransform, TranslateTransform).X += Delta
+                            Case AniTypeSub.TranslateY
+                                If IsNothing(Ani.Obj.RenderTransform) OrElse TypeOf Ani.Obj.RenderTransform IsNot TranslateTransform Then Ani.Obj.RenderTransform = New TranslateTransform(0, 0)
+                                CType(Ani.Obj.RenderTransform, TranslateTransform).Y += Delta
+                            Case AniTypeSub.Double
+                                Ani.Obj(0).SetValue(Ani.Obj(1), Ani.Obj(0).GetValue(Ani.Obj(1)) + Delta)
+                            Case AniTypeSub.DoubleParam
+                                CType(Ani.Obj, ParameterizedThreadStart)(Delta)
+                            Case AniTypeSub.GridLengthWidth
+                                Ani.Obj.Width = New GridLength(Math.Max(Ani.Obj.Width.Value + Delta, 0), GridUnitType.Star)
+                        End Select
+                    End If
+
+                Case AniType.Color
+                    '利用 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 Obj As FrameworkElement = Ani.Obj(0)
+                    Dim Prop As DependencyProperty = Ani.Obj(1)
+                    Dim NewColor As MyColor = New MyColor(Obj.GetValue(Prop)) + Delta
+                    Obj.SetValue(Prop, If(Prop.PropertyType.Name = "Color", CType(NewColor, Color), CType(NewColor, SolidColorBrush)))
+                    Ani.ValueLast = NewColor - New MyColor(Obj.GetValue(Prop))
+
+                Case AniType.Scale
+                    Dim Obj As FrameworkElement = Ani.Obj
+                    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)
+
+                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)
+                    '添加乱码
+                    If TextCount < Ani.Value(0).ToString.Length Then
+                        Dim NextText As String = Mid(Ani.Value(0), TextCount + 1, 1)
+                        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)})
+                        Else
+                            NewText &= RandomOne("0123456789./*-+\[]{};':/?,!@#$%^&*()_+-=qwwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM".ToCharArray)
+                        End If
+                    End If
+                    '设置文本
+                    If TypeOf Ani.Obj Is TextBlock Then
+                        Ani.Obj.Text = NewText
+                    Else
+                        Ani.Obj.Context = NewText
+                    End If
+
+                Case AniType.Code
+                    CType(Ani.Value, ThreadStart)()
+
+                Case AniType.ScaleTransform
+                    Dim Obj As FrameworkElement = Ani.Obj
+                    If TypeOf Obj.RenderTransform IsNot ScaleTransform Then
+                        Obj.RenderTransformOrigin = New Point(0.5, 0.5)
+                        Obj.RenderTransform = New ScaleTransform(1, 1)
+                    End If
+                    Dim Delta As Double = MathPercent(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).ScaleY = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleY + Delta, 0)
+
+                Case AniType.RotateTransform
+                    Dim Obj As FrameworkElement = Ani.Obj
+                    If TypeOf Obj.RenderTransform IsNot RotateTransform Then
+                        Obj.RenderTransformOrigin = New Point(0.5, 0.5)
+                        Obj.RenderTransform = New RotateTransform(0)
+                    End If
+                    Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
+                    CType(Obj.RenderTransform, RotateTransform).Angle = CType(Obj.RenderTransform, RotateTransform).Angle + Delta
+
+            End Select
+            Ani.TimePercent = Ani.TimeFinished / Ani.TimeTotal '修改执行百分比
+        Catch ex As Exception
+            Log(ex, "执行动画失败:" & Ani.ToString, LogLevel.Hint)
+        End Try
+        Return Ani
+    End Function
+
+End Module

ファイルの差分が大きいため隠しています
+ 109 - 0
Plain Craft Launcher 2/Modules/Base/ModBase.vb


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

@@ -0,0 +1,700 @@
+Public Module ModLoader
+
+    '各类加载器
+    ''' <summary>
+    ''' 加载器的统一基类。
+    ''' </summary>
+    Public MustInherit Class LoaderBase
+        Implements ILoadingTrigger
+
+        Public ReadOnly Property IsLoader As Boolean = True Implements ILoadingTrigger.IsLoader
+
+        '基础属性
+        ''' <summary>
+        ''' 加载器的标识编号。
+        ''' </summary>
+        Public Uuid As Integer = GetUuid()
+        ''' <summary>
+        ''' 加载器的名称。
+        ''' </summary>
+        Public Name As String = "未命名任务 " & Uuid & "#"
+        ''' <summary>
+        ''' 用于状态改变检测的同步锁。
+        ''' </summary>
+        Public ReadOnly LockState As New Object
+        ''' <summary>
+        ''' 父加载器。
+        ''' </summary>
+        Public Parent As LoaderBase = Nothing
+        ''' <summary>
+        ''' 最上级的加载器。
+        ''' </summary>
+        Public ReadOnly Property RealParent As LoaderBase
+            Get
+                Try
+                    RealParent = Parent
+                    While RealParent IsNot Nothing AndAlso RealParent.Parent IsNot Nothing
+                        RealParent = RealParent.Parent
+                    End While
+                Catch ex As Exception
+                    Log(ex, "获取父加载器失败(" & Name & ")", LogLevel.Feedback)
+                    Return Nothing
+                End Try
+            End Get
+        End Property
+
+        Public Overridable Sub InitParent(Parent As LoaderBase)
+            Me.Parent = Parent
+        End Sub
+
+        '事件
+
+        ''' <summary>
+        ''' 当状态改变时,在工作线程触发代码。在添加事件后,必须将 HasOnStateChangedThread 设为 True。
+        ''' </summary>
+        Public Event OnStateChangedThread(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
+        Public HasOnStateChangedThread As Boolean = False
+
+        ''' <summary>
+        ''' 当状态改变时,在 UI 线程触发代码。
+        ''' </summary>
+        Public Event OnStateChangedUi(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
+        ''' <summary>
+        ''' 简易的在 UI 线程添加触发事件的方式。主要用于在新建 Loader 时直接使用 With 绑定事件,以及进行老代码兼容。
+        ''' </summary>
+        Public WriteOnly Property OnStateChanged As Action(Of LoaderBase)
+            Set(value As Action(Of LoaderBase))
+                AddHandler OnStateChangedUi, Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) value(Loader)
+            End Set
+        End Property
+
+        ''' <summary>
+        ''' 在加载器目标事件执行完成,加载器状态即将变为 Finish 时调用。可以视为扩展加载器目标事件。
+        ''' </summary>
+        Public Event PreviewFinish(Loader As LoaderBase)
+        Protected Sub RaisePreviewFinish()
+            RaiseEvent PreviewFinish(Me)
+        End Sub
+
+        '状态监控
+        ''' <summary>
+        ''' 加载器的状态。
+        ''' </summary>
+        Public Property State As LoadState
+            Get
+                Return _State
+            End Get
+            Set(value As LoadState)
+                If _State = value Then Exit Property
+                Dim OldState = _State
+                If value = LoadState.Finished AndAlso Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
+                _State = value
+                Log("[Loader] 加载器 " & Name & " 状态改变:" & GetStringFromEnum(value))
+                '实现 ILoadingTrigger 接口与 OnStateChanged 回调
+                RunInUi(Sub()
+                            Select Case value
+                                Case LoadState.Loading
+                                    LoadingState = MyLoading.MyLoadingState.Run
+                                Case LoadState.Failed
+                                    LoadingState = MyLoading.MyLoadingState.Error
+                                Case Else
+                                    LoadingState = MyLoading.MyLoadingState.Stop
+                            End Select
+                            RaiseEvent OnStateChangedUi(Me, value, OldState)
+                        End Sub)
+                If HasOnStateChangedThread Then RunInThread(Sub() RaiseEvent OnStateChangedThread(Me, value, OldState))
+            End Set
+        End Property
+        Private _State As LoadState = LoadState.Waiting
+        Public Property LoadingState As MyLoading.MyLoadingState Implements ILoadingTrigger.LoadingState
+            Get
+                Return _LoadingState
+            End Get
+            Set(value As MyLoading.MyLoadingState)
+                If _LoadingState = value Then Exit Property
+                Dim OldState = _LoadingState
+                _LoadingState = value
+                RaiseEvent LoadingStateChanged(value, OldState)
+            End Set
+        End Property
+        Private _LoadingState As MyLoading.MyLoadingState = MyLoading.MyLoadingState.Stop
+        Public Event LoadingStateChanged(NewState As MyLoading.MyLoadingState, OldState As MyLoading.MyLoadingState) Implements ILoadingTrigger.LoadingStateChanged
+        ''' <summary>
+        ''' 若加载器出错,可提供给外部参考的异常。
+        ''' </summary>
+        Public Property [Error] As Exception = Nothing
+        ''' <summary>
+        ''' 使用 LoaderCombo 加载时,该任务是否会阻碍后续任务的进行。
+        ''' </summary>
+        Public Block As Boolean = True
+        ''' <summary>
+        ''' 该加载器是否显示在列表中。
+        ''' </summary>
+        Public Show As Boolean = True
+        ''' <summary>
+        ''' 当前加载器是否由 IsForceRestart 强制调起。
+        ''' 这个属性自身不会干任何事,而是提供给加载器执行的函数,使得加载器调用另一个加载器时,可以继承强制重启属性。
+        ''' </summary>
+        Public IsForceRestarting As Boolean = False
+
+        '进度监控
+        ''' <summary>
+        ''' 加载器的执行进度,为 0 至 1 的小数。
+        ''' </summary>
+        Public Overridable Property Progress As Double
+            Get
+                Select Case State
+                    Case LoadState.Waiting
+                        Return 0
+                    Case LoadState.Loading
+                        Return If(_Progress = -1, 0.02, _Progress)
+                    Case Else
+                        Return 1
+                End Select
+            End Get
+            Set(value As Double)
+                _Progress = value
+            End Set
+        End Property
+        Private _Progress As Double = -1
+        ''' <summary>
+        ''' 计算总进度时的权重。它应该为预计时间(秒)。
+        ''' </summary>
+        Public Property ProgressWeight As Double = 1
+
+        '状态变化
+        Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
+        Public MustOverride Sub Abort()
+
+        '等待结束
+        Public Const WaitForExitTimeoutMessage As String = "等待加载器执行超时。"
+
+        ''' <summary>
+        ''' 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。
+        ''' </summary>
+        Public Sub WaitForExit(Optional Input As Object = Nothing, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional IsForceRestart As Boolean = False)
+            Start(Input, IsForceRestart)
+            Do While State = LoadState.Loading
+                If LoaderToSyncProgress IsNot Nothing Then LoaderToSyncProgress.Progress = Progress
+                Thread.Sleep(10)
+            Loop
+            If State = LoadState.Finished Then
+                Exit Sub
+            ElseIf State = LoadState.Aborted Then
+                Throw New ThreadInterruptedException("加载器执行已中断。")
+            ElseIf IsNothing([Error]) Then
+                Throw New Exception("未知错误!")
+            Else
+                Throw New Exception([Error].Message, [Error]) '保留调用堆栈,同时不影响信息输出与单元测试
+            End If
+        End Sub
+        ''' <summary>
+        ''' 等待加载器完成,直到结束、抛出异常或超时。若加载器尚未开始,则会开始执行。
+        ''' </summary>
+        ''' <param name="Timeout">等待的超时时间,以毫秒为单位。</param>
+        ''' <param name="TimeoutMessage">若执行超时,将会抛出的异常信息。</param>
+        Public Sub WaitForExitTime(Timeout As Integer, Optional Input As Object = Nothing, Optional TimeoutMessage As String = WaitForExitTimeoutMessage, Optional LoaderToSyncProgress As Object = Nothing, Optional IsForceRestart As Boolean = False)
+            Start(Input, IsForceRestart)
+            Do While State = LoadState.Loading
+                If LoaderToSyncProgress IsNot Nothing Then LoaderToSyncProgress.Progress = Progress
+                Thread.Sleep(10)
+                Timeout -= 10
+                If Timeout < 0 Then Throw New TimeoutException(TimeoutMessage)
+            Loop
+            If State = LoadState.Finished Then
+                Exit Sub
+            ElseIf State = LoadState.Aborted Then
+                Throw New ThreadInterruptedException("加载器执行已中断。")
+            ElseIf IsNothing([Error]) Then
+                Throw New Exception("未知错误!")
+            Else
+                Throw [Error]
+            End If
+        End Sub
+
+        '相同重载
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim base = TryCast(obj, LoaderBase)
+            Return base IsNot Nothing AndAlso Uuid = base.Uuid
+        End Function
+
+    End Class
+    ''' <summary>
+    ''' 用于异步执行并监控单一函数的加载器。
+    ''' </summary>
+    Public Class LoaderTask(Of InputType, OutputType)
+        Inherits LoaderBase
+
+        '线程设定
+        Protected Friend ThreadPriority As ThreadPriority
+        '执行事件
+        Protected Friend LoadDelegate As Action(Of LoaderTask(Of InputType, OutputType))
+        Protected Friend InputDelegate As Func(Of Object)
+        '状态指示
+        ''' <summary>
+        ''' 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。
+        ''' </summary>
+        Public ReadOnly Property IsAborted As Boolean
+            Get
+                Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread.CurrentThread, LastRunningThread)
+            End Get
+        End Property
+        ''' <summary>
+        ''' 在输入相同时使用原有结果的超时,单位为毫秒。
+        ''' </summary>
+        Public ReloadTimeout As Integer = -1
+        ''' <summary>
+        ''' 上次完成加载时的时间。
+        ''' </summary>
+        Public LastFinishedTime As Long = 0
+        ''' <summary>
+        ''' 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。
+        ''' </summary>
+        Public LastRunningThread As Thread = Nothing
+        '输入输出
+        Public Input As InputType = Nothing
+        Public Output As OutputType = Nothing
+
+        '获取输入
+        Public Function StartGetInput(Optional Input As InputType = Nothing, Optional InputDelegate As Func(Of Object) = Nothing) As InputType
+            If InputDelegate Is Nothing Then InputDelegate = Me.InputDelegate
+            Dim NewInput As InputType = Nothing '若 InputType 不能为 Nothing,则会导致 Input Is Nothing 永远失败,因此需要额外判断
+            If (Input Is Nothing OrElse (NewInput IsNot Nothing AndAlso Input.Equals(NewInput))) AndAlso InputDelegate IsNot Nothing Then
+                RunInUiWait(Sub() Input = InputDelegate())
+            End If
+            Return Input
+        End Function
+        '代码执行
+        Public Function ShouldStart(ByRef Input As Object, Optional IsForceRestart As Boolean = False, Optional IgnoreReloadTimeout As Boolean = False) As Boolean
+            '获取输入
+            Try
+                Input = StartGetInput(Input)
+            Catch ex As Exception
+                Log(ex, "加载输入获取失败(" & Name & ")", LogLevel.Hint)
+                [Error] = ex
+                SyncLock LockState
+                    State = LoadState.Failed
+                End SyncLock
+            End Try
+            '检验输入以确定情况
+            If (Not IsForceRestart) AndAlso '不要求强行重启
+               ((Input IsNot Nothing AndAlso Input.Equals(Me.Input)) OrElse (Input Is Nothing AndAlso Me.Input Is Nothing)) AndAlso '输入相同
+               (State = LoadState.Loading OrElse State = LoadState.Finished) AndAlso '正在加载或已加载成功
+               (IgnoreReloadTimeout OrElse ReloadTimeout = -1 OrElse LastFinishedTime = 0 OrElse GetTimeTick() - LastFinishedTime < ReloadTimeout) Then '没有超时
+                '如果输入相同且未出错则不重试
+                Return False
+            Else
+                '需要开始
+                Return True
+            End If
+        End Function
+        Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
+            '确认是否开始加载
+            If ShouldStart(Input, IsForceRestart) Then
+                '输入不同或失败,开始加载
+                If State = LoadState.Loading Then TriggerThreadAbort()
+                Me.Input = Input
+                SyncLock LockState
+                    State = LoadState.Loading
+                End SyncLock
+            Else
+                Exit Sub
+            End If
+
+            LastRunningThread = New Thread(
+                Sub()
+                    Try
+                        IsForceRestarting = IsForceRestart
+                        If ModeDebug Then Log("[Loader] 加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已启动")
+                        LoadDelegate(Me)
+                        If IsAborted Then
+                            Log("[Loader] 加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已中断但线程正常运行至结束,输出被弃用(最新线程:" & If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId) & ")", LogLevel.Developer)
+                        Else
+                            RaisePreviewFinish()
+                            State = LoadState.Finished
+                            LastFinishedTime = GetTimeTick() '未中断,本次输出有效
+                        End If
+                    Catch ex As ThreadInterruptedException
+                        If ModeDebug Then Log(ex, "加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已触发线程中断")
+                        '如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted
+                        '如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
+                        If Not IsAborted Then State = LoadState.Aborted
+                    Catch ex As Exception
+                        If IsAborted Then Exit Sub
+                        Log(ex, "加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 发生运行时错误", LogLevel.Developer)
+                        [Error] = ex
+                        State = LoadState.Failed
+                    End Try
+                End Sub) With {.Name = Name, .Priority = ThreadPriority}
+            LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
+        End Sub
+        Public Overrides Sub Abort()
+            If State <> LoadState.Loading Then Exit Sub
+            SyncLock LockState
+                State = LoadState.Aborted
+            End SyncLock
+            TriggerThreadAbort()
+        End Sub
+        Private Sub TriggerThreadAbort()
+            If LastRunningThread Is Nothing Then Exit Sub
+            If ModeDebug Then Log("[Loader] 加载线程 " & Name & " (" & LastRunningThread.ManagedThreadId & ") 已中断")
+            If LastRunningThread.IsAlive Then LastRunningThread.Interrupt()
+            LastRunningThread = Nothing
+        End Sub
+
+        Public Sub New()
+            '仅仅是为了避免一些智障报错(继承类必须重写 New 的情况)
+        End Sub
+        Public Sub New(Name As String, LoadDelegate As Action(Of LoaderTask(Of InputType, OutputType)), Optional InputDelegate As Func(Of InputType) = Nothing, Optional Priority As ThreadPriority = ThreadPriority.Normal)
+            Me.Name = Name
+            Me.LoadDelegate = LoadDelegate
+            Me.InputDelegate = InputDelegate
+            ThreadPriority = Priority
+        End Sub
+
+    End Class
+    ''' <summary>
+    ''' 支持多个加载器连续运作的复合加载器。
+    ''' </summary>
+    Public Class LoaderCombo(Of InputType)
+        Inherits LoaderBase
+
+        Public Overrides Property Progress() As Double
+            Get
+                Select Case State
+                    Case LoadState.Waiting
+                        Return 0
+                    Case LoadState.Loading
+                        Dim Total As Double = 0, Finished As Double = 0
+                        For Each Loader In Loaders
+                            Total += Loader.ProgressWeight
+                            Finished += Loader.ProgressWeight * Loader.Progress
+                        Next
+                        If Total = 0 Then Return 0
+                        Return Finished / Total
+                    Case Else
+                        Return 1
+                End Select
+            End Get
+            Set(value As Double)
+                Throw New Exception("多重加载器不支持设置进度")
+            End Set
+        End Property
+
+        Public Loaders As New List(Of LoaderBase)
+        Public Input As InputType
+        Public Sub New(Name As String, Loaders As IEnumerable(Of LoaderBase))
+            Me.Loaders.Clear()
+            For Each Loader As LoaderBase In Loaders
+                If Loader IsNot Nothing Then
+                    Me.Loaders.Add(Loader)
+                    AddHandler Loader.OnStateChangedThread, AddressOf SubTaskStateChanged
+                    Loader.HasOnStateChangedThread = True
+                End If
+            Next
+            InitParent(Nothing)
+            Me.Name = Name
+        End Sub
+        Public Overrides Sub InitParent(Parent As LoaderBase)
+            Me.Parent = Parent
+            For Each Loader In Loaders
+                Loader.InitParent(Me)
+            Next
+        End Sub
+        Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
+            IsForceRestarting = IsForceRestart
+            '改变状态
+            SyncLock LockState
+                If State = LoadState.Loading Then
+                    Exit Sub
+                Else
+                    State = LoadState.Loading
+                End If
+            End SyncLock
+            '启动加载
+            Me.Input = Input
+            If IsForceRestart Then
+                For Each Loader In Loaders
+                    Loader.State = LoadState.Waiting
+                Next
+            End If
+            Update()
+        End Sub
+        Public Overrides Sub Abort()
+            '改变状态
+            SyncLock LockState
+                If State = LoadState.Loading OrElse State = LoadState.Waiting Then
+                    State = LoadState.Aborted
+                Else
+                    Exit Sub
+                End If
+            End SyncLock
+            RunInThread(Sub()
+                            '中断加载器
+                            For Each Loader In Loaders
+                                Loader.Abort()
+                            Next
+                            '从列表删除
+                            SyncLock LoaderTaskbarLock
+                                If LoaderTaskbar.Contains(Me) Then LoaderTaskbar.Remove(Me)
+                            End SyncLock
+                        End Sub)
+        End Sub
+
+        ''' <summary>
+        ''' 子任务状态变更。
+        ''' </summary>
+        Private Sub SubTaskStateChanged(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
+            Select Case NewState
+                Case LoadState.Loading
+                    '开始,啥都不干
+                Case LoadState.Waiting
+                    '子加载器可能由于外部输入改变而暂时变为 Waiting,之后会立即重新启动
+                    '所以啥都不干就行
+                Case LoadState.Finished
+                    '正常结束,触发刷新
+                    Update()
+                Case LoadState.Aborted
+                    '被中断,这个任务也中断
+                    Abort()
+                Case Else
+                    '完蛋,出错了
+                    SyncLock LockState
+                        If State >= LoadState.Finished Then Exit Sub
+                        [Error] = New Exception(Loader.Name & "失败", Loader.Error)
+                        State = Loader.State
+                    End SyncLock
+                    For Each Loader In Loaders
+                        Loader.Abort()
+                    Next
+                    FrmMain.BtnExtraDownload.ShowRefresh()
+                    Exit Sub
+            End Select
+        End Sub
+        ''' <summary>
+        ''' 触发一次更新,以启动新加载器或完成。
+        ''' </summary>
+        Private Sub Update()
+            If State = LoadState.Finished OrElse State = LoadState.Failed OrElse State = LoadState.Aborted Then Exit Sub
+            Dim IsFinished As Boolean = True
+            Dim Ignore As Boolean = False
+            Dim Input As Object = Me.Input
+            For Each Loader In Loaders
+                Select Case Loader.State
+                    Case LoadState.Finished
+                        '检查是否需要重启
+                        If Loader.GetType.Name.StartsWith("LoaderTask") Then '类型名后面带有泛型,必须用 StartsWith
+                            If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
+                                If ModeDebug Then Log("[Loader] 由于输入条件变更,重启已完成的加载器 " & Loader.Name)
+                                GoTo Restart
+                            End If
+                            '更新下一个加载器的输入
+                            Input = CType(Loader, Object).Output
+                        End If
+                    Case LoadState.Loading
+                        '检查是否需要重启
+                        If Loader.GetType.Name.StartsWith("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 ModeDebug Then Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, LogLevel.Developer)
+                                GoTo Restart
+                            End If
+                        End If
+                        '已经有正在加载中的了,不需要再启动了
+                        IsFinished = False
+                        Ignore = True
+                    Case Else
+Restart:
+                        '未启动,则启动加载器
+                        IsFinished = False
+                        If Ignore Then Continue For
+                        If Input IsNot Nothing Then
+                            '若输入类型与下一个加载器相同才继续
+                            Dim LoaderType As String = Loader.GetType.Name
+                            If LoaderType.StartsWith("LoaderTask") OrElse LoaderType.StartsWith("LoaderCombo") Then
+                                Loader.Start(If(Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IsForceRestarting)
+                            ElseIf LoaderType.StartsWith("LoaderDownload") Then
+                                Loader.Start(If(TypeOf Input Is List(Of NetFile), Input, Nothing), IsForceRestarting)
+                            Else
+                                Throw New Exception("未知的加载器类型(" & LoaderType & ")")
+                            End If
+                        Else
+                            Loader.Start(IsForceRestart:=IsForceRestarting)
+                        End If
+                        '如果不让继续启动,那就不启动呗
+                        If Loader.Block Then Ignore = True
+                End Select
+            Next
+            If IsFinished Then
+                '顺利完成,贼棒
+                RaisePreviewFinish()
+                State = LoadState.Finished
+                FrmMain.BtnExtraDownload.ShowRefresh()
+            End If
+        End Sub
+
+        ''' <summary>
+        ''' 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
+        ''' </summary>
+        Public Shared Sub GetLoaderList(Loader As Object, ByRef List As List(Of LoaderBase), Optional RequireShow As Boolean = True)
+            For Each SubLoader In Loader.Loaders
+                If SubLoader.Show OrElse Not RequireShow Then List.Add(SubLoader)
+                If SubLoader.GetType.Name.StartsWith("LoaderCombo") Then GetLoaderList(SubLoader, List)
+            Next
+        End Sub
+        ''' <summary>
+        ''' 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
+        ''' </summary>
+        Public Sub GetLoaderList(ByRef List As List(Of LoaderBase), Optional RequireShow As Boolean = True)
+            GetLoaderList(Me, List, RequireShow)
+        End Sub
+        ''' <summary>
+        ''' 获得最底层的,应被显示给用户的加载器列表。
+        ''' </summary>
+        Public Function GetLoaderList(Optional RequireShow As Boolean = True) As List(Of LoaderBase)
+            Dim List As New List(Of LoaderBase)
+            GetLoaderList(List, RequireShow)
+            Return List
+        End Function
+
+    End Class
+
+    '任务栏进度条
+    Public LoaderTaskbarLock As New Object
+    Public LoaderTaskbar As New List(Of LoaderBase)
+    Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
+    Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
+
+    Public Sub LoaderTaskbarAdd(Loader As LoaderBase)
+        If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRemove(Loader)
+        SyncLock LoaderTaskbarLock
+            LoaderTaskbar.Add(Loader)
+        End SyncLock
+    End Sub
+    Public Sub LoaderTaskbarProgressRefresh()
+        Try
+            Dim NewState As Shell.TaskbarItemProgressState
+            Dim NewProgress As Double = LoaderTaskbarProgressGet()
+            '检查任务是否完成,若完成则移除
+            SyncLock LoaderTaskbarLock
+                '外显任务是否已经全部完成
+                Dim IsAllDownloadTaskCompleted As Boolean = True
+                For Each Loader In LoaderTaskbar
+                    If Loader.Show AndAlso Loader.State = LoadState.Loading Then IsAllDownloadTaskCompleted = False
+                Next
+                '若单个任务已中止或全部任务已完成,则刷新并移除
+                Dim RemovedTask As New List(Of LoaderBase)
+                For i = 0 To LoaderTaskbar.Count - 1
+                    If (IsAllDownloadTaskCompleted OrElse LoaderTaskbar(i).State = LoadState.Aborted) AndAlso LoaderTaskbar(i).Show Then
+                        If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRefresh(LoaderTaskbar(i))
+                        RemovedTask.Add(LoaderTaskbar(i))
+                    End If
+                Next
+                For Each Task In RemovedTask
+                    LoaderTaskbar.Remove(Task)
+                Next
+            End SyncLock
+            '更新平滑后的进度
+            If NewProgress <= 0 OrElse NewProgress >= 1 OrElse LoaderTaskbarProgress > NewProgress Then
+                LoaderTaskbarProgress = NewProgress
+            Else
+                LoaderTaskbarProgress = LoaderTaskbarProgress * 0.9 + NewProgress * 0.1
+            End If
+            '更新任务栏信息
+            If LoaderTaskbar.Count = 0 OrElse LoaderTaskbarProgress = 1 Then
+                NewState = Shell.TaskbarItemProgressState.None
+            ElseIf LoaderTaskbarProgress < 0.015 Then
+                NewState = Shell.TaskbarItemProgressState.Indeterminate
+            Else
+                NewState = Shell.TaskbarItemProgressState.Normal
+                FrmMain.TaskbarItemInfo.ProgressValue = LoaderTaskbarProgress
+            End If
+            If LoaderTaskbarProgressLast <> NewState Then
+                LoaderTaskbarProgressLast = NewState
+                FrmMain.TaskbarItemInfo.ProgressState = NewState
+                FrmMain.BtnExtraDownload.ShowRefresh()
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新任务栏进度显示失败", LogLevel.Feedback)
+        End Try
+    End Sub
+    Public Function LoaderTaskbarProgressGet() As Double
+        Try
+            Dim Total As Double = 0, Count As Integer = 0
+            SyncLock LoaderTaskbarLock
+                For i = 0 To LoaderTaskbar.Count - 1
+                    Total += LoaderTaskbar(i).Progress
+                    Count += 1 '避免多线程影响导致计数出错
+                Next
+            End SyncLock
+            If Count = 0 Then Return 1
+            Return MathRange(Total / Count, 0, 1)
+        Catch ex As Exception
+            Log(ex, "获取任务栏进度出错", LogLevel.Feedback)
+            Return 0.5
+        End Try
+    End Function
+
+    '文件夹刷新类委托
+    Private LoaderFolderDictionary As New Dictionary(Of LoaderBase, LoaderFolderDictionaryEntry)
+    Private Structure LoaderFolderDictionaryEntry
+        Public LastCheckTime As Date?
+        Public FolderPath As String
+        Public Overrides Function Equals(obj As Object) As Boolean
+            If TypeOf obj IsNot LoaderFolderDictionaryEntry Then Return False
+            Dim entry = DirectCast(obj, LoaderFolderDictionaryEntry)
+            Return EqualityComparer(Of Date?).Default.Equals(LastCheckTime, entry.LastCheckTime) AndAlso
+                   FolderPath = entry.FolderPath
+        End Function
+    End Structure
+    Public Enum LoaderFolderRunType
+        RunOnUpdated
+        ForceRun
+        UpdateOnly
+    End Enum
+    ''' <summary>
+    ''' 执行以文件夹检测作为输入的加载器。加载器需以文件夹路径为输入值。
+    ''' 返回是否执行了加载器。
+    ''' </summary>
+    ''' <param name="ExtraPath">用于检查文件夹修改的额外路径。该路径不会传入加载器。</param>
+    Public Function LoaderFolderRun(Loader As LoaderBase, FolderPath As String, Type As LoaderFolderRunType, Optional MaxDepth As Integer = 0, Optional ExtraPath As String = "", Optional WaitForExit As Boolean = False) As Boolean
+        Dim FolderInfo As DirectoryInfo
+        Dim Value As New LoaderFolderDictionaryEntry With {.FolderPath = FolderPath & ExtraPath, .LastCheckTime = Nothing}
+        Try
+            '获取数据
+            FolderInfo = New DirectoryInfo(FolderPath & ExtraPath)
+            Value.LastCheckTime = If(FolderInfo.Exists, GetActualLastWriteTimeUtc(FolderInfo, MaxDepth), DirectCast(Nothing, Date?))
+            '如果已经检查过,则跳过
+            If Type = LoaderFolderRunType.RunOnUpdated AndAlso LoaderFolderDictionary.ContainsKey(Loader) Then
+                If FolderInfo.Exists Then
+                    If LoaderFolderDictionary(Loader).LastCheckTime IsNot Nothing AndAlso
+                       Value.Equals(LoaderFolderDictionary(Loader)) Then Return False
+                Else
+                    If LoaderFolderDictionary(Loader).LastCheckTime Is Nothing Then Return False
+                End If
+            End If
+        Catch ex As Exception
+            Log(ex, "文件夹加载器启动检测出错")
+        End Try
+        '写入检查数据
+        DictionaryAdd(LoaderFolderDictionary, Loader, Value)
+        '开始检查
+        If Type = LoaderFolderRunType.UpdateOnly Then Return False
+        If WaitForExit Then
+            Loader.WaitForExit(FolderPath, IsForceRestart:=True)
+        Else
+            Loader.Start(FolderPath, IsForceRestart:=True)
+        End If
+        Return True
+    End Function
+    Private Function GetActualLastWriteTimeUtc(FolderInfo As DirectoryInfo, MaxDepth As Integer) As Date
+        Dim Time As Date = FolderInfo.LastWriteTimeUtc
+        If MaxDepth > 0 Then
+            For Each Folder In FolderInfo.EnumerateDirectories
+                Dim FolderTime As Date = GetActualLastWriteTimeUtc(Folder, MaxDepth - 1)
+                If FolderTime > Time Then Time = FolderTime
+            Next
+        End If
+        Return Time
+    End Function
+
+End Module

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

@@ -0,0 +1,2054 @@
+Imports System.Net
+
+Public Module ModNet
+
+    Public Const NetDownloadEnd As String = ".PCLDownloading"
+
+    ''' <summary>
+    ''' 以 WebClient 获取网页源代码。会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。
+    ''' </summary>
+    ''' <param name="Url">网页的 Url。</param>
+    ''' <param name="Encoding">网页的编码,通常为 UTF-8。</param>
+    Public Function NetGetCodeByClient(Url As String, Encoding As Encoding, Optional Accept As String = "application/json, text/javascript, */*; q=0.01") As String
+        Dim RetryCount As Integer = 0
+        Dim RetryException As Exception = Nothing
+        Dim StartTime As Long = GetTimeTick()
+        Try
+Retry:
+            Select Case RetryCount
+                Case 0 '正常尝试
+                    Return NetGetCodeByClient(Url, Encoding, 10000, Accept)
+                Case 1 '慢速重试
+                    Thread.Sleep(500)
+                    Return NetGetCodeByClient(Url, Encoding, 30000, Accept)
+                Case Else '快速重试
+                    If GetTimeTick() - StartTime > 5500 Then
+                        '若前两次加载耗费 5 秒以上,才进行重试
+                        Thread.Sleep(500)
+                        Return NetGetCodeByClient(Url, Encoding, 4000, Accept)
+                    Else
+                        Throw RetryException
+                    End If
+            End Select
+        Catch ex As Exception
+            Select Case RetryCount
+                Case 0
+                    RetryException = ex
+                    RetryCount += 1
+                    GoTo Retry
+                Case 1
+                    RetryCount += 1
+                    GoTo Retry
+                Case Else
+                    Throw
+            End Select
+        End Try
+    End Function
+    Public Function NetGetCodeByClient(Url As String, Encoding As Encoding, Timeout As Integer, Accept As String) As String
+        Url = SecretCdnSign(Url)
+        Log("[Net] 获取客户端网络结果:" & Url & ",最大超时 " & Timeout)
+        Dim Request As CookieWebClient
+        Dim res As HttpWebResponse = Nothing
+        Dim HttpStream As Stream = Nothing
+        Try
+            Request = New CookieWebClient With {
+                .Encoding = Encoding,
+                .Timeout = Timeout
+            }
+            Request.Headers("Accept") = Accept
+            Request.Headers("Accept-Language") = "en-US,en;q=0.5"
+            Request.Headers("X-Requested-With") = "XMLHttpRequest"
+            SecretHeadersSign(Url, Request)
+            Return Request.DownloadString(Url)
+        Catch ex As Exception
+            If ex.GetType.Equals(GetType(WebException)) AndAlso CType(ex, WebException).Status = WebExceptionStatus.Timeout Then
+                Throw New TimeoutException("连接服务器超时(" & Url & ")", ex)
+            Else
+                Throw New WebException("获取结果失败," & ex.Message & "(" & Url & ")", ex)
+            End If
+        Finally
+            If Not IsNothing(HttpStream) Then HttpStream.Dispose()
+            If Not IsNothing(res) Then res.Dispose()
+        End Try
+    End Function
+
+    ''' <summary>
+    ''' 以 WebRequest 获取网页源代码或 Json。会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。
+    ''' </summary>
+    ''' <param name="Url">网页的 Url。</param>
+    ''' <param name="Encode">网页的编码,通常为 UTF-8。</param>
+    Public Function NetGetCodeByRequestRetry(Url As String, Optional Encode As Encoding = Nothing, Optional Accept As String = "", Optional IsJson As Boolean = False)
+        Dim RetryCount As Integer = 0
+        Dim RetryException As Exception = Nothing
+        Dim StartTime As Long = GetTimeTick()
+        Try
+Retry:
+            Select Case RetryCount
+                Case 0 '正常尝试
+                    Return NetGetCodeRequest(Url, Encode, 10000, IsJson, Accept)
+                Case 1 '慢速重试
+                    Thread.Sleep(500)
+                    Return NetGetCodeRequest(Url, Encode, 30000, IsJson, Accept)
+                Case Else '快速重试
+                    If GetTimeTick() - StartTime > 5500 Then
+                        '若前两次加载耗费 5 秒以上,才进行重试
+                        Thread.Sleep(500)
+                        Return NetGetCodeRequest(Url, Encode, 4000, IsJson, Accept)
+                    Else
+                        Throw RetryException
+                    End If
+            End Select
+        Catch ex As ThreadInterruptedException
+            Throw
+        Catch ex As Exception
+            Select Case RetryCount
+                Case 0
+                    RetryException = ex
+                    RetryCount += 1
+                    GoTo Retry
+                Case 1
+                    RetryCount += 1
+                    GoTo Retry
+                Case Else
+                    Throw
+            End Select
+        End Try
+    End Function
+    ''' <summary>
+    ''' 以 WebRequest 获取网页源代码或 Json。会逐渐生成 4 个尝试线程,并在 60s 后超时。
+    ''' </summary>
+    ''' <param name="Url">网页的 Url。</param>
+    ''' <param name="Encode">网页的编码,通常为 UTF-8。</param>
+    Public Function NetGetCodeByRequestMuity(Url As String, Optional Encode As Encoding = Nothing, Optional Accept As String = "", Optional IsJson As Boolean = False)
+        Dim Threads As New List(Of Thread)
+        Dim RequestResult = Nothing
+        Dim RequestEx As Exception = Nothing
+        Dim FailCount As Integer = 0
+        For i = 1 To 4
+            Dim th As New Thread(Sub()
+                                     Try
+                                         RequestResult = NetGetCodeRequest(Url, Encode, 30000, IsJson, Accept)
+                                     Catch ex As Exception
+                                         FailCount += 1
+                                         RequestEx = ex
+                                     End Try
+                                 End Sub)
+            th.Start()
+            Threads.Add(th)
+            Thread.Sleep(i * 250)
+            If RequestResult IsNot Nothing Then GoTo RequestFinished
+        Next
+        Do While True
+            If RequestResult IsNot Nothing Then
+RequestFinished:
+                Try
+                    For Each th In Threads
+                        If th.IsAlive Then th.Interrupt()
+                    Next
+                Catch
+                End Try
+                Return RequestResult
+            ElseIf FailCount = 4 Then
+                Try
+                    For Each th In Threads
+                        If th.IsAlive Then th.Interrupt()
+                    Next
+                Catch
+                End Try
+                Throw RequestEx
+            End If
+            Thread.Sleep(20)
+        Loop
+        Throw New Exception("未知错误")
+    End Function
+    Private Function NetGetCodeRequest(Url As String, Encode As Encoding, Timeout As Integer, IsJson As Boolean, Accept As String)
+        If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求")
+        Url = SecretCdnSign(Url)
+        Log("[Net] 获取网络结果:" & Url & ",最大超时 " & Timeout)
+        Dim Request As HttpWebRequest = WebRequest.Create(Url)
+        Dim Result As New List(Of Byte)
+        Try
+            If Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then Request.ProtocolVersion = HttpVersion.Version11
+            Request.Timeout = Timeout
+            Request.Accept = Accept
+            Request.KeepAlive = False
+            SecretHeadersSign(Url, Request)
+            Using res As HttpWebResponse = Request.GetResponse()
+                Using HttpStream As Stream = res.GetResponseStream()
+                    HttpStream.ReadTimeout = Timeout
+                    Dim HttpData As Byte() = New Byte(16384) {}
+                    Using Reader As New StreamReader(HttpStream, If(Encode, Encoding.UTF8))
+                        Dim ResultString As String = Reader.ReadToEnd
+                        Return If(IsJson, GetJson(ResultString), ResultString)
+                    End Using
+                End Using
+            End Using
+        Catch ex As ThreadInterruptedException
+            Throw
+        Catch ex As Exception
+            If ex.GetType.Equals(GetType(WebException)) AndAlso CType(ex, WebException).Status = WebExceptionStatus.Timeout Then
+                Throw New TimeoutException("连接服务器超时,已接收 " & Result.Count & " B(" & Url & ")")
+            Else
+                Throw New WebException("获取结果失败," & ex.Message & "(" & Url & ")", ex)
+            End If
+        Finally
+            Request.Abort()
+        End Try
+    End Function
+
+    ''' <summary>
+    ''' 以多线程下载网页文件的方式获取网页源代码。
+    ''' </summary>
+    ''' <param name="Url">网页的 Url。</param>
+    Public Function NetGetCodeByDownload(Url As String, Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False) As String
+        Dim Temp As String = PathTemp & "Cache\Code\" & Url.GetHashCode() & "_" & GetUuid()
+        Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile({Url}, Temp, New FileChecker With {.IsJson = IsJson})})
+        Try
+            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(" & Url & ")")
+            NetGetCodeByDownload = ReadFile(Temp)
+            File.Delete(Temp)
+        Finally
+            NewTask.Abort()
+        End Try
+    End Function
+    ''' <summary>
+    ''' 以多线程下载网页文件的方式获取网页源代码。
+    ''' </summary>
+    ''' <param name="Urls">网页的 Url 列表。</param>
+    Public Function NetGetCodeByDownload(Urls As String(), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False) As String
+        Dim Temp As String = PathTemp & "Cache\Code\" & Urls(0).GetHashCode() & "_" & GetUuid()
+        Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, Temp, New FileChecker With {.IsJson = IsJson})})
+        Try
+            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls(0) & ")")
+            NetGetCodeByDownload = ReadFile(Temp)
+            File.Delete(Temp)
+        Finally
+            NewTask.Abort()
+        End Try
+    End Function
+
+    ''' <summary>
+    ''' 从网络中直接下载文件。这不能下载 CDN 中的文件。
+    ''' </summary>
+    ''' <param name="Url">网络 Url。</param>
+    ''' <param name="LocalFile">下载的本地地址。</param>
+    Public Sub NetDownload(Url As String, LocalFile As String)
+        Log("[Net] 直接下载文件:" & Url)
+
+        '初始化
+        Try
+            '建立目录
+            Directory.CreateDirectory(GetPathFromFullPath(LocalFile))
+            '尝试删除原文件
+            File.Delete(LocalFile)
+        Catch ex As Exception
+            Throw New WebException("预处理下载文件路径失败(" & LocalFile & ")。", ex)
+        End Try
+
+        '下载
+        Using Client As New WebClient
+            Try
+                Client.DownloadFile(Url, LocalFile)
+            Catch ex As Exception
+                Throw New WebException("直接下载文件失败(" & Url & ")。", ex)
+            End Try
+        End Using
+
+    End Sub
+
+    ''' <summary>
+    ''' 发送一个网络请求并获取返回内容,会重试三次并在最长 45s 后超时。
+    ''' </summary>
+    ''' <param name="Url">请求的服务器地址。</param>
+    ''' <param name="Method">请求方式(POST 或 GET)。</param>
+    ''' <param name="Data">请求的内容。</param>
+    ''' <param name="ContentType">请求的套接字类型。</param>
+    ''' <param name="DontRetryOnRefused">当返回 40x 时不重试。</param>
+    Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing) As String
+        Dim RetryCount As Integer = 0
+        Dim RetryException As Exception = Nothing
+        Dim StartTime As Long = GetTimeTick()
+        Try
+Retry:
+            Select Case RetryCount
+                Case 0 '正常尝试
+                    Return NetRequestOnce(Url, Method, Data, ContentType, 15000, Headers)
+                Case 1 '慢速重试
+                    Thread.Sleep(500)
+                    Return NetRequestOnce(Url, Method, Data, ContentType, 25000, Headers)
+                Case Else '快速重试
+                    If GetTimeTick() - StartTime > 5500 Then
+                        '若前两次加载耗费 5 秒以上,才进行重试
+                        Thread.Sleep(500)
+                        Return NetRequestOnce(Url, Method, Data, ContentType, 4000, Headers)
+                    Else
+                        Throw RetryException
+                    End If
+            End Select
+        Catch ex As ThreadInterruptedException
+            Throw
+        Catch ex As Exception
+            If ex.InnerException IsNot Nothing AndAlso ex.InnerException.Message.Contains("(40") AndAlso DontRetryOnRefused Then RetryCount = 999
+            Select Case RetryCount
+                Case 0
+                    If ModeDebug Then Log(ex, "[Net] 网络请求第一次失败(" & Url & ")")
+                    RetryException = ex
+                    RetryCount += 1
+                    GoTo Retry
+                Case 1
+                    If ModeDebug Then Log(ex, "[Net] 网络请求第二次失败(" & Url & ")")
+                    RetryCount += 1
+                    GoTo Retry
+                Case Else
+                    Throw
+            End Select
+        End Try
+    End Function
+    ''' <summary>
+    ''' 同时发送多个网络请求并要求返回内容。
+    ''' </summary>
+    Public Function NetRequestMuity(Url As String, Method As String, Data As Object, ContentType As String, Optional RequestCount As Integer = 4, Optional Headers As Dictionary(Of String, String) = Nothing)
+        Dim Threads As New List(Of Thread)
+        Dim RequestResult = Nothing
+        Dim RequestEx As Exception = Nothing
+        Dim FailCount As Integer = 0
+        For i = 1 To RequestCount
+            Dim th As New Thread(Sub()
+                                     Try
+                                         RequestResult = NetRequestOnce(Url, Method, Data, ContentType, 30000, Headers)
+                                     Catch ex As Exception
+                                         FailCount += 1
+                                         RequestEx = ex
+                                     End Try
+                                 End Sub)
+            th.Start()
+            Threads.Add(th)
+            Thread.Sleep(i * 250)
+            If RequestResult IsNot Nothing Then GoTo RequestFinished
+        Next
+        Do While True
+            If RequestResult IsNot Nothing Then
+RequestFinished:
+                For Each th In Threads
+                    If th.IsAlive Then th.Interrupt()
+                Next
+                Return RequestResult
+            ElseIf FailCount = RequestCount Then
+                For Each th In Threads
+                    If th.IsAlive Then th.Interrupt()
+                Next
+                Throw RequestEx
+            End If
+            Thread.Sleep(20)
+        Loop
+        Throw New Exception("未知错误")
+    End Function
+    ''' <summary>
+    ''' 发送一次网络请求并获取返回内容。
+    ''' </summary>
+    Public Function NetRequestOnce(Url As String, Method As String, Data As Object, ContentType As String, Optional Timeout As Integer = 25000, Optional Headers As Dictionary(Of String, String) = Nothing, Optional MakeLog As Boolean = True) As String
+        If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求")
+        Url = SecretCdnSign(Url)
+        If MakeLog Then Log("[Net] 发起网络请求(" & Method & "," & Url & "),最大超时 " & Timeout)
+        Dim DataStream As Stream = Nothing
+        Dim Resp As WebResponse = Nothing
+        Dim Req As HttpWebRequest
+        Try
+            Req = WebRequest.Create(New Uri(Url))
+            Req.Method = Method
+            Dim SendData As Byte()
+            If TypeOf Data Is Byte() Then
+                SendData = Data
+            Else
+                SendData = New UTF8Encoding(False).GetBytes(Data.ToString)
+            End If
+            If Headers IsNot Nothing Then
+                For Each Pair In Headers
+                    Req.Headers.Add(Pair.Key, Pair.Value)
+                Next
+            End If
+            Req.ContentType = ContentType
+            Req.Timeout = Timeout
+            Req.KeepAlive = False
+            SecretHeadersSign(Url, Req)
+            If Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then Req.ProtocolVersion = HttpVersion.Version11
+            If Method = "POST" OrElse Method = "PUT" Then
+                Req.ContentLength = SendData.Length
+                DataStream = Req.GetRequestStream()
+                DataStream.WriteTimeout = Timeout
+                DataStream.ReadTimeout = Timeout
+                DataStream.Write(SendData, 0, SendData.Length)
+                DataStream.Close()
+            End If
+            Resp = Req.GetResponse()
+            DataStream = Resp.GetResponseStream()
+            DataStream.WriteTimeout = Timeout
+            DataStream.ReadTimeout = Timeout
+            Using Reader As New StreamReader(DataStream)
+                Return Reader.ReadToEnd()
+            End Using
+        Catch ex As ThreadInterruptedException
+            Throw
+        Catch ex As WebException
+            If ex.Status = WebExceptionStatus.Timeout Then
+                Throw New TimeoutException("连接服务器超时,请检查你的网络环境是否良好(" & Url & ")", ex)
+            Else
+                '获取请求失败的返回
+                Dim Res As String = ""
+                Try
+                    If ex.Response Is Nothing Then Exit Try
+                    DataStream = ex.Response.GetResponseStream()
+                    DataStream.WriteTimeout = Timeout
+                    DataStream.ReadTimeout = Timeout
+                    Using Reader As New StreamReader(DataStream)
+                        Res = Reader.ReadToEnd()
+                    End Using
+                Catch
+                End Try
+                Throw New WebException("网络请求失败(" & Url & "," & ex.Message & ")" & If(String.IsNullOrEmpty(Res), "", vbCrLf & Res), ex)
+            End If
+        Catch ex As Exception
+            Throw New WebException("网络请求失败(" & Url & ")", ex)
+        Finally
+            If DataStream IsNot Nothing Then DataStream.Dispose()
+            If Resp IsNot Nothing Then Resp.Dispose()
+        End Try
+    End Function
+
+    ''' <summary>
+    ''' 最大线程数。
+    ''' </summary>
+    Public NetTaskThreadLimit As Integer
+    ''' <summary>
+    ''' 速度下限。
+    ''' </summary>
+    Public NetTaskSpeedLimitLow As Long = 1024 * 1024L
+    ''' <summary>
+    ''' 速度上限。若无限制则为 -1。
+    ''' </summary>
+    Public NetTaskSpeedLimitHigh As Long = -1
+    ''' <summary>
+    ''' 基于限速,当前可以下载的剩余量。
+    ''' </summary>
+    Public NetTaskSpeedLimitLeft As Long = -1
+    Private ReadOnly NetTaskSpeedLimitLeftLock As New Object
+    Private NetTaskSpeedLimitLeftLast As Long
+    ''' <summary>
+    ''' 正在运行中的线程数。
+    ''' </summary>
+    Public NetTaskThreadCount As Integer = 0
+    Private ReadOnly NetTaskThreadCountLock As New Object
+
+    ''' <summary>
+    ''' 下载源。
+    ''' </summary>
+    Public Class NetSource
+        Public Id As Integer
+        Public Url As String
+        Public FailCount As Integer
+        Public Ex As Exception
+        Public Thread As NetThread
+        Public IsFailed As Boolean
+        Public Overrides Function ToString() As String
+            Return Url
+        End Function
+    End Class
+    ''' <summary>
+    ''' 下载进度标示。
+    ''' </summary>
+    Public Enum NetState
+        ''' <summary>
+        ''' 尚未进行已存在检查。
+        ''' </summary>
+        WaitForCheck = -1
+        ''' <summary>
+        ''' 尚未开始。
+        ''' </summary>
+        WaitForDownload = 0
+        ''' <summary>
+        ''' 正在连接,尚未获取文件大小。
+        ''' </summary>
+        Connect = 1
+        ''' <summary>
+        ''' 已获取文件大小,尚未有有效下载。
+        ''' </summary>
+        [Get] = 2
+        ''' <summary>
+        ''' 正在下载。
+        ''' </summary>
+        Download = 3
+        ''' <summary>
+        ''' 正在合并文件。
+        ''' </summary>
+        Merge = 4
+        ''' <summary>
+        ''' 不进行下载,因为已发现现存的文件。
+        ''' </summary>
+        WaitForCopy = 5
+        ''' <summary>
+        ''' 已完成。
+        ''' </summary>
+        Finish = 6
+        ''' <summary>
+        ''' 已失败。
+        ''' </summary>
+        [Error] = 7
+    End Enum
+    ''' <summary>
+    ''' 预下载检查行为。
+    ''' </summary>
+    Public Enum NetPreDownloadBehaviour
+        ''' <summary>
+        ''' 当文件已存在时,显示提示以提醒用户是否继续下载。
+        ''' </summary>
+        HintWhileExists
+        ''' <summary>
+        ''' 当文件已存在或正在下载时,直接退出下载函数执行,不对用户进行提示。
+        ''' </summary>
+        ExitWhileExistsOrDownloading
+        ''' <summary>
+        ''' 不进行已存在检查。
+        ''' </summary>
+        IgnoreCheck
+    End Enum
+
+    ''' <summary>
+    ''' 下载线程。
+    ''' </summary>
+    Public Class NetThread
+        Implements IEnumerable(Of NetThread)
+
+        ''' <summary>
+        ''' 对应的下载任务。
+        ''' </summary>
+        Public Task As NetFile
+        ''' <summary>
+        ''' 对应的线程。
+        ''' </summary>
+        Public Thread As Thread
+        ''' <summary>
+        ''' 链表中的下一个线程。
+        ''' </summary>
+        Public NextThread As NetThread
+        Private ReadOnly Iterator Property [Next]() As IEnumerable(Of NetThread)
+            Get
+                Dim CurrentChain As NetThread = Me
+                While CurrentChain IsNot Nothing
+                    Yield CurrentChain
+                    CurrentChain = CurrentChain.NextThread
+                End While
+            End Get
+        End Property
+        Public Function GetEnumerator() As IEnumerator(Of NetThread) Implements IEnumerable(Of NetThread).GetEnumerator
+            Return [Next].GetEnumerator()
+        End Function
+        Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
+            Return [Next].GetEnumerator()
+        End Function
+
+        ''' <summary>
+        ''' 分配给任务中每个线程(无论其是否失败)的编号。
+        ''' </summary>
+        Public Uuid As Integer
+        ''' <summary>
+        ''' 是否为第一个线程。
+        ''' </summary>
+        Public ReadOnly Property IsFirstThread As Boolean
+            Get
+                Return DownloadStart = 0 AndAlso Task.FileSize = -2
+            End Get
+        End Property
+        ''' <summary>
+        ''' 该线程的缓存文件。
+        ''' </summary>
+        Public Temp As String
+
+        ''' <summary>
+        ''' 线程下载起始位置。
+        ''' </summary>
+        Public DownloadStart As Long
+        ''' <summary>
+        ''' 线程下载结束位置。
+        ''' </summary>
+        Public ReadOnly Property DownloadEnd As Long
+            Get
+                SyncLock Task.LockChain
+                    If NextThread Is Nothing Then
+                        If Task.IsUnknownSize Then
+                            Return 1024 * 1024 * 1024 '1G
+                        Else
+                            Return Task.FileSize - 1
+                        End If
+                    Else
+                        Return NextThread.DownloadStart - 1
+                    End If
+                End SyncLock
+            End Get
+        End Property
+        ''' <summary>
+        ''' 线程未下载的文件大小。
+        ''' </summary>
+        Public ReadOnly Property DownloadUndone As Long
+            Get
+                Return DownloadEnd + 1 - (DownloadStart + DownloadDone)
+            End Get
+        End Property
+        ''' <summary>
+        ''' 线程已下载的文件大小。
+        ''' </summary>
+        Public DownloadDone As Long = 0
+
+        ''' <summary>
+        ''' 上次记速时的时间。
+        ''' </summary>
+        Private SpeedLastTime As Long = GetTimeTick()
+        ''' <summary>
+        ''' 上次记速时的已下载大小。
+        ''' </summary>
+        Private SpeedLastDone As Long = 0
+        ''' <summary>
+        ''' 当前的下载速度,单位为 Byte / 秒。
+        ''' </summary>
+        Public ReadOnly Property Speed As Long
+            Get
+                If GetTimeTick() - SpeedLastTime > 200 Then
+                    Dim DeltaTime As Long = GetTimeTick() - SpeedLastTime
+                    _Speed = (DownloadDone - SpeedLastDone) / (DeltaTime / 1000)
+                    SpeedLastDone = DownloadDone
+                    SpeedLastTime += DeltaTime
+                End If
+                Return _Speed
+            End Get
+        End Property
+        Private _Speed As Long = 0
+
+        ''' <summary>
+        ''' 线程初始化时的时间。
+        ''' </summary>
+        Public InitTime As Long = GetTimeTick()
+        ''' <summary>
+        ''' 上次接受到有效数据的时间,-1 表示尚未有有效数据。
+        ''' </summary>
+        Public LastReceiveTime As Long = -1
+
+        ''' <summary>
+        ''' 当前线程的状态。
+        ''' </summary>
+        Public State As NetState = NetState.WaitForDownload
+        ''' <summary>
+        ''' 是否已经结束。
+        ''' </summary>
+        Public ReadOnly Property IsEnded As Boolean
+            Get
+                Return State = NetState.Finish OrElse State = NetState.Error
+            End Get
+        End Property
+
+        ''' <summary>
+        ''' 当前选取的是哪一个 Url。
+        ''' </summary>
+        Public Source As NetSource
+
+    End Class
+    ''' <summary>
+    ''' 下载单个文件。
+    ''' </summary>
+    Public Class NetFile
+
+#Region "属性"
+
+        ''' <summary>
+        ''' 所属的文件列表任务。
+        ''' </summary>
+        Public Tasks As New List(Of LoaderDownload)
+
+        ''' <summary>
+        ''' 所有下载源。
+        ''' </summary>
+        Public Sources As NetSource()
+        ''' <summary>
+        ''' 用于在第一个线程出错时切换下载源。
+        ''' </summary>
+        Private FirstThreadSource As Integer = 0
+        ''' <summary>
+        ''' 所有已经被标记为失败的,但未完整尝试过的,不允许断点续传的下载源。
+        ''' </summary>
+        Public SourcesOnce As New List(Of NetSource)
+        ''' <summary>
+        ''' 获取从某个源开始,第一个可用的源。
+        ''' </summary>
+        Private Function GetSource(Optional Id As Integer = 0) As NetSource
+            If Id >= Sources.Count OrElse Id < 0 Then Id = 0
+            SyncLock LockSource
+                If Not IsSourceFailed(False) Then
+                    '存在多线程可用源
+                    Dim CurrentSource As NetSource = Sources(Id)
+                    While CurrentSource.IsFailed
+                        Id += 1
+                        If Id >= Sources.Count Then Id = 0
+                        CurrentSource = Sources(Id)
+                    End While
+                    Return CurrentSource
+                ElseIf SourcesOnce.Count > 0 Then
+                    '仅存在单线程可用源
+                    Return SourcesOnce(0)
+                Else
+                    '没有可用源
+                    Return Nothing
+                End If
+            End SyncLock
+        End Function
+        ''' <summary>
+        ''' 是否已经没有可用源了。
+        ''' </summary>
+        Public Function IsSourceFailed(Optional AllowOnceSource As Boolean = True) As Boolean
+            If AllowOnceSource AndAlso SourcesOnce.Count > 0 Then Return False
+            SyncLock LockSource
+                For Each Source As NetSource In Sources
+                    If Not Source.IsFailed Then Return False
+                Next
+            End SyncLock
+            Return True
+        End Function
+
+        ''' <summary>
+        ''' 存储在本地的带文件名的地址。
+        ''' </summary>
+        Public LocalPath As String = Nothing
+        ''' <summary>
+        ''' 存储在本地的文件名。
+        ''' </summary>
+        Public LocalName As String = Nothing
+
+        ''' <summary>
+        ''' 当前的下载状态。
+        ''' </summary>
+        Public State As NetState = NetState.WaitForCheck
+        ''' <summary>
+        ''' 导致下载失败的原因。
+        ''' </summary>
+        Public Ex As New List(Of Exception)
+        '''' <summary>
+        '''' 文件进行整体重试的计数。
+        '''' </summary>
+        'Private RetryCount As Integer = 0
+
+        ''' <summary>
+        ''' 作为文件组成部分的线程链表。
+        ''' </summary>
+        Private Threads As NetThread
+
+        ''' <summary>
+        ''' 文件的总大小。若为 -2 则为未获取,若为 -1 则为无法获取准确大小。
+        ''' </summary>
+        Public FileSize As Long = -2
+        ''' <summary>
+        ''' 该文件是否无法获取准确大小。
+        ''' </summary>
+        Public IsUnknownSize As Boolean = False
+        ''' <summary>
+        ''' 该文件是否不需要分割。
+        ''' </summary>
+        Public ReadOnly Property IsNoSplit As Boolean
+            Get
+                Return IsUnknownSize OrElse FileSize < FilePieceLimit
+            End Get
+        End Property
+        ''' <summary>
+        ''' 为不需要分割的小文件进行临时存储。
+        ''' </summary>
+        Private SmailFileCache As List(Of Byte)
+
+        ''' <summary>
+        ''' 文件的已下载大小。
+        ''' </summary>
+        Public DownloadDone As Long = 0
+        Private ReadOnly LockDone As New Object
+        ''' <summary>
+        ''' 文件的校验规则。
+        ''' </summary>
+        Public Check As FileChecker
+
+        ''' <summary>
+        ''' 上次记速时的时间。
+        ''' </summary>
+        Private SpeedLastTime As Long = GetTimeTick()
+        ''' <summary>
+        ''' 上次记速时的已下载大小。
+        ''' </summary>
+        Private SpeedLastDone As Long = 0
+        ''' <summary>
+        ''' 当前的下载速度,单位为 Byte / 秒。
+        ''' </summary>
+        Public ReadOnly Property Speed As Long
+            Get
+                If GetTimeTick() - SpeedLastTime > 200 Then
+                    Dim DeltaTime As Long = GetTimeTick() - SpeedLastTime
+                    _Speed = (DownloadDone - SpeedLastDone) / (DeltaTime / 1000)
+                    SpeedLastDone = DownloadDone
+                    SpeedLastTime += DeltaTime
+                End If
+                Return _Speed
+            End Get
+        End Property
+        Private _Speed As Long = 0
+
+        ''' <summary>
+        ''' 该文件是否由本地文件直接拷贝完成。
+        ''' </summary>
+        Public IsCopy As Boolean = False
+        ''' <summary>
+        ''' 本文件的显示进度。
+        ''' </summary>
+        Public ReadOnly Property Progress As Double
+            Get
+                Select Case State
+                    Case NetState.WaitForCheck
+                        Return 0
+                    Case NetState.WaitForCopy
+                        Return 0.2
+                    Case NetState.WaitForDownload
+                        Return 0.01
+                    Case NetState.Connect
+                        Return 0.02
+                    Case NetState.Get
+                        Return 0.04
+                    Case NetState.Download
+                        '正在下载中,对应 5% ~ 99%
+                        Dim OriginalProgress As Double = If(IsUnknownSize, 0.5, DownloadDone / Math.Max(FileSize, 1))
+                        OriginalProgress = 1 - (1 - OriginalProgress) ^ 0.9
+                        Return OriginalProgress * 0.94 + 0.05
+                    Case NetState.Merge
+                        Return 0.99
+                    Case NetState.Finish, NetState.Error
+                        Return 1
+                    Case Else
+                        Throw New ArgumentOutOfRangeException("文件状态未知:" & State)
+                End Select
+            End Get
+        End Property
+
+        ''' <summary>
+        ''' 各个线程建立连接成功的总次数。
+        ''' </summary>
+        Private ConnectCount As Integer = 0
+        ''' <summary>
+        ''' 各个线程建立连接成功的总时间。
+        ''' </summary>
+        Private ConnectTime As Long = 0
+        ''' <summary>
+        ''' 各个线程建立连接成功的平均时间,单位为毫秒,-1 代表尚未有成功连接。
+        ''' </summary>
+        Private ReadOnly Property ConnectAverage As Integer
+            Get
+                SyncLock LockCount
+                    Return If(ConnectCount = 0, -1, ConnectTime / ConnectCount)
+                End SyncLock
+            End Get
+        End Property
+
+        Private Const FilePieceLimit As Long = 256 * 1024
+        Public ReadOnly RandomCode As Integer = RandomInteger(0, 999999)
+        Public ReadOnly LockCount As New Object
+        Public ReadOnly LockState As New Object
+        Public ReadOnly LockChain As New Object
+        Public ReadOnly LockSource As New Object
+        Public ReadOnly LockTasks As New Object
+
+        Private ReadOnly Uuid As Integer = GetUuid()
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim file = TryCast(obj, NetFile)
+            Return file IsNot Nothing AndAlso Uuid = file.Uuid
+        End Function
+
+#End Region
+
+        ''' <summary>
+        ''' 新建一个需要下载的文件。
+        ''' </summary>
+        ''' <param name="LocalPath">包含文件名的本地地址。</param>
+        Public Sub New(Urls As String(), LocalPath As String, Optional Check As FileChecker = Nothing)
+            Dim Sources As New List(Of NetSource)
+            Dim Count As Integer = 0
+            Urls = ArrayNoDouble(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})
+                Count += 1
+            Next
+            Me.Sources = Sources.ToArray
+            Me.LocalPath = LocalPath
+            Me.Check = Check
+            Me.LocalName = GetFileNameFromPath(LocalPath)
+        End Sub
+
+        ''' <summary>
+        ''' 尝试开始一个新的下载线程。
+        ''' </summary>
+        Public Function TryBeginThread() As Boolean
+            Try
+
+                '条件检测
+                If NetTaskThreadCount >= NetTaskThreadLimit OrElse IsSourceFailed() OrElse
+               (IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Error) Then Return False
+                If State >= NetState.Merge OrElse State = NetState.WaitForCheck Then Return False
+                SyncLock LockState
+                    If State < NetState.Connect Then State = NetState.Connect
+                End SyncLock
+                '初始化参数
+                Dim StartPosition As Long, StartSource As NetSource = Nothing
+                Dim Th As Thread, ThreadInfo As NetThread
+                SyncLock LockChain
+
+                    '获取线程起点与下载源
+                    '不分割
+                    If IsNoSplit Then GoTo Capture
+                    '单线程
+                    If IsSourceFailed(False) Then
+                        '确认没有其他线程正使用此点
+                        If SourcesOnce(0).Thread IsNot Nothing AndAlso SourcesOnce(0).Thread.State <> NetState.Error Then Return False
+                        '占用此点
+Capture:
+                        SmailFileCache = Nothing
+                        Threads = Nothing
+                        NetManager.DownloadDone -= DownloadDone
+                        SyncLock LockDone
+                            DownloadDone = 0
+                        End SyncLock
+                        SpeedLastDone = 0
+                        State = NetState.Get
+                    End If
+                    '首个开始点
+                    If Threads Is Nothing Then
+                        StartPosition = 0
+                        StartSource = GetSource(FirstThreadSource)
+                        If StartSource Is Nothing Then
+                            '没有可用源了
+                            Throw New Exception("无可用源,请反馈此问题。")
+                        Else
+                            '有可用源
+                            FirstThreadSource = StartSource.Id + 1
+                            GoTo StartThread
+                        End If
+                    End If
+                    '寻找失败点
+                    For Each Thread As NetThread In Threads
+                        If Thread.State = NetState.Error AndAlso Thread.DownloadUndone > 0 Then
+                            StartPosition = Thread.DownloadStart + Thread.DownloadDone
+                            StartSource = GetSource(Thread.Source.Id + 1)
+                            GoTo StartThread
+                        End If
+                    Next
+                    '是否禁用多线程,以及规定碎片大小
+                    Dim TargetUrl As String = GetSource().Url
+                    If TargetUrl.Contains("pcl2-server") Then Return False
+                    Dim RealFilePieceLimit = If(TargetUrl.Contains("download.mcbbs.net"), FilePieceLimit * 5, FilePieceLimit)
+                    '寻找最大碎片
+                    Dim FilePieceMax As NetThread = Threads
+                    For Each Thread As NetThread In Threads
+                        If Thread.DownloadUndone > FilePieceMax.DownloadUndone Then FilePieceMax = Thread
+                    Next
+                    If FilePieceMax Is Nothing OrElse FilePieceMax.DownloadUndone < RealFilePieceLimit Then Return False
+                    StartPosition = FilePieceMax.DownloadEnd - FilePieceMax.DownloadUndone * 0.4
+                    StartSource = GetSource()
+
+                    '开始线程
+StartThread:
+                    If (StartPosition > FileSize AndAlso FileSize >= 0 AndAlso Not IsUnknownSize) OrElse StartPosition < 0 OrElse IsNothing(StartSource) Then Return False
+                    '构建线程
+                    Dim ThreadUuid As Integer = GetUuid()
+                    SyncLock LockTasks
+                        If Tasks.Count = 0 Then Return False '由于中断,已没有可用任务
+                        Th = New Thread(AddressOf Thread) With {.Name = "NetTask " & Tasks(0).Uuid & "/" & Uuid & " Download " & ThreadUuid & "#", .Priority = ThreadPriority.BelowNormal}
+                    End SyncLock
+                    ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload}
+                    '链表处理
+                    If ThreadInfo.IsFirstThread OrElse Threads Is Nothing Then
+                        Threads = ThreadInfo
+                    Else
+                        Dim CurrentChain As NetThread = Threads
+                        While CurrentChain.DownloadEnd <= StartPosition
+                            CurrentChain = CurrentChain.NextThread
+                        End While
+                        ThreadInfo.NextThread = CurrentChain.NextThread
+                        CurrentChain.NextThread = ThreadInfo
+                    End If
+
+                End SyncLock
+                '开始线程
+                SyncLock NetTaskThreadCountLock
+                    NetTaskThreadCount += 1
+                End SyncLock
+                SyncLock LockSource
+                    If IsSourceFailed(False) Then SourcesOnce(0).Thread = ThreadInfo
+                End SyncLock
+                Th.Start(ThreadInfo)
+                Return True
+
+            Catch ex As Exception
+                Log(ex, "尝试开始下载线程失败(" & If(LocalName, "Nothing") & ")", LogLevel.Hint)
+                Return False
+            End Try
+        End Function
+        ''' <summary>
+        ''' 每个下载线程执行的代码。
+        ''' </summary>
+        Private Sub Thread(Info As NetThread)
+            If ModeDebug OrElse Info.DownloadStart = 0 Then Log("[Download] " & LocalName & " " & Info.Uuid & "#:开始,起始点 " & Info.DownloadStart & "," & Info.Source.Url)
+            Dim HttpRequest As HttpWebRequest
+            Dim ResultStream As Stream = Nothing
+            '部分下载源真的特别慢,并且只需要一个请求,例如 Ping 为 20s,如果增长太慢,就会造成类似 2.5s 5s 7.5s 10s 12.5s... 的极大延迟
+            '延迟过长会导致某些特别慢的链接迟迟不被掐死,即使 MCBBS 源在 10s 下也会出现类似情况
+            Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + Info.Source.FailCount), 30000)
+            Info.State = NetState.Connect
+            Try
+                Dim HttpDataCount As Integer = 0
+                If SourcesOnce.Contains(Info.Source) AndAlso Not Info.Equals(Info.Source.Thread) Then GoTo SourceBreak
+                '请求头
+                HttpRequest = WebRequest.Create(Info.Source.Url)
+                If Info.Source.Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then HttpRequest.ProtocolVersion = HttpVersion.Version11
+                'HttpRequest.Proxy = Nothing 'new WebProxy(Ip, Port)
+                HttpRequest.Timeout = Timeout
+                HttpRequest.AddRange(Info.DownloadStart)
+                SecretHeadersSign(Info.Source.Url, HttpRequest)
+                Using HttpResponse As HttpWebResponse = HttpRequest.GetResponse()
+                    '文件大小校验
+                    Dim ThreadFileSize = HttpResponse.ContentLength
+                    If ThreadFileSize = -1 Then
+                        If FileSize > 1 Then
+                            Log("[Download] " & LocalName & " " & Info.Uuid & "#:文件大小未知,但已从其他下载源获取,不作处理")
+                        Else
+                            FileSize = -1 : IsUnknownSize = True
+                            Log("[Download] " & LocalName & " " & Info.Uuid & "#:文件大小未知")
+                        End If
+                    ElseIf ThreadFileSize < 0 Then
+                        Throw New Exception("获取片大小失败,结果为 " & ThreadFileSize & "。")
+                    ElseIf Info.IsFirstThread Then
+                        If Check IsNot Nothing Then
+                            If ThreadFileSize < Check.MinSize AndAlso Check.MinSize > 0 Then
+                                Throw New Exception("文件大小不足,获取结果为 " & ThreadFileSize & ",要求至少为 " & Check.MinSize & "。")
+                            End If
+                            If ThreadFileSize <> Check.ActualSize AndAlso Check.ActualSize > 0 Then
+                                Throw New Exception("文件大小不一致,获取结果为 " & ThreadFileSize & ",要求必须为 " & Check.ActualSize & "。")
+                            End If
+                        End If
+                        FileSize = ThreadFileSize : IsUnknownSize = False
+                        Log("[Download] " & LocalName & ":文件大小 " & ThreadFileSize & " B" & "(" & GetString(ThreadFileSize) & ")")
+                        '若文件大小大于 50 M,进行剩余磁盘空间校验
+                        If ThreadFileSize > 50 * 1024 * 1024 Then
+                            For Each Drive As DriveInfo In DriveInfo.GetDrives
+                                Dim DriveName As String = Drive.Name.First.ToString
+                                Dim RequiredSpace = If(PathTemp.StartsWith(DriveName), ThreadFileSize * 1.1, 0) +
+                                                If(LocalPath.StartsWith(DriveName), ThreadFileSize + 5 * 1024 * 1024, 0)
+                                If Drive.TotalFreeSpace < RequiredSpace Then
+                                    Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
+                                                    If(PathTemp.StartsWith(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
+                                End If
+                            Next
+                        End If
+                    ElseIf FileSize < 0 Then
+                        Throw New Exception("非首线程运行时,尚未获取文件大小。")
+                    ElseIf Info.DownloadStart > 0 AndAlso ThreadFileSize = FileSize Then
+                        SyncLock LockSource
+                            If SourcesOnce.Contains(Info.Source) Then
+                                GoTo SourceBreak
+                            Else
+                                SourcesOnce.Add(Info.Source)
+                            End If
+                        End SyncLock
+                        Throw New WebException("该下载源不支持断点续传。")
+                    ElseIf Not FileSize - Info.DownloadStart = ThreadFileSize Then
+                        Throw New WebException("获取到的片大小不一致:线程结果为 " & ThreadFileSize & " B,任务结果为 " & FileSize & "B,起点为 " & Info.DownloadStart & "B。")
+                    End If
+                    Info.State = NetState.Get
+                    SyncLock LockState
+                        If State < NetState.Get Then State = NetState.Get
+                    End SyncLock
+                    '创建缓存文件
+                    If IsNoSplit Then
+                        Info.Temp = Nothing
+                        SmailFileCache = New List(Of Byte)
+                    Else
+                        Info.Temp = PathTemp & "Download\" & Uuid & "_" & Info.Uuid & "_" & RandomInteger(0, 999999) & ".tmp"
+                        ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
+                    End If
+                    '开始下载
+                    Using HttpStream = HttpResponse.GetResponseStream()
+                        HttpStream.ReadTimeout = Timeout
+                        If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
+                        Dim HttpData As Byte() = New Byte(16384) {}
+                        HttpDataCount = HttpStream.Read(HttpData, 0, 16384)
+                        While (IsUnknownSize OrElse Info.DownloadUndone > 0) AndAlso '判断是否下载完成
+                      HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Info.Source.IsFailed OrElse Info.Equals(Info.Source.Thread))
+                            '限速
+                            While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0
+                                Threading.Thread.Sleep(16)
+                            End While
+                            Dim RealDataCount As Integer = If(IsUnknownSize, HttpDataCount, Math.Min(HttpDataCount, Info.DownloadUndone))
+                            SyncLock NetTaskSpeedLimitLeftLock
+                                If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft -= RealDataCount
+                            End SyncLock
+                            Dim DeltaTime = GetTimeTick() - Info.LastReceiveTime
+                            If DeltaTime > 1000000 Then DeltaTime = 0 '时间刻反转导致出现极大值
+                            If RealDataCount > 0 Then
+                                '有数据
+                                If Info.DownloadDone = 0 Then
+                                    '第一次接受到数据
+                                    Info.State = NetState.Download
+                                    SyncLock LockState
+                                        If State < NetState.Download Then State = NetState.Download
+                                    End SyncLock
+                                    SyncLock LockCount
+                                        ConnectCount += 1
+                                        ConnectTime += GetTimeTick() - Info.InitTime
+                                    End SyncLock
+                                End If
+                                SyncLock LockCount
+                                    Info.Source.FailCount = 0
+                                    SyncLock LockTasks
+                                        For Each Task In Tasks
+                                            Task.FailCount = 0
+                                        Next
+                                    End SyncLock
+                                End SyncLock
+                                NetManager.DownloadDone += RealDataCount
+                                SyncLock LockDone
+                                    DownloadDone += RealDataCount
+                                End SyncLock
+                                Info.DownloadDone += RealDataCount
+                                If IsNoSplit Then
+                                    If HttpData.Count = RealDataCount Then
+                                        SmailFileCache.AddRange(HttpData)
+                                    Else
+                                        SmailFileCache.AddRange(HttpData.ToList.GetRange(0, RealDataCount))
+                                    End If
+                                Else
+                                    ResultStream.Write(HttpData, 0, RealDataCount)
+                                End If
+                                '检查速度是否过慢
+                                If DeltaTime > 1000 AndAlso DeltaTime > RealDataCount Then '数据包间隔大于 1s,且速度小于 1K/s
+                                    Throw New TimeoutException("由于速度过慢断开链接,下载 " & RealDataCount & " B,消耗 " & DeltaTime & " ms。")
+                                End If
+                                Info.LastReceiveTime = GetTimeTick()
+                                '已完成
+                                If Info.DownloadUndone = 0 AndAlso Not IsUnknownSize Then Exit While
+                            ElseIf Info.LastReceiveTime > 0 AndAlso DeltaTime > Timeout Then
+                                '无数据,且已超时
+                                Throw New TimeoutException("操作超时,无数据。")
+                            End If
+                            HttpDataCount = HttpStream.Read(HttpData, 0, 16384)
+                        End While
+                    End Using
+                End Using
+SourceBreak:
+                If HttpDataCount = 0 AndAlso Info.DownloadUndone > 0 AndAlso Not IsUnknownSize Then
+                    '服务器无返回数据
+                    Throw New Exception("服务器无返回数据,但下载尚未完成")
+                ElseIf State = NetState.Error OrElse Info.Source.IsFailed OrElse (Info.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
+                    '被外部中断
+                    Info.State = NetState.Error
+                    Log("[Download] " & LocalName & " " & Info.Uuid & "#:中断")
+                Else
+                    '本线程完成
+                    Info.State = NetState.Finish
+                    If ModeDebug Then Log("[Download] " & LocalName & " " & Info.Uuid & "#:完成,已下载 " & GetString(Info.DownloadDone))
+                End If
+            Catch ex As Exception
+                '状态变更
+                SyncLock LockCount
+                    Info.Source.FailCount += 1
+                    SyncLock LockTasks
+                        For Each Task In Tasks
+                            Task.FailCount += 1
+                        Next
+                    End SyncLock
+                End SyncLock
+                Dim IsTimeoutString As String = GetString(ex, False).ToLower.Replace(" ", "")
+                Dim IsTimeout As Boolean = IsTimeoutString.Contains("由于连接方在一段时间后没有正确答复或连接的主机没有反应") OrElse
+                                           IsTimeoutString.Contains("超时") OrElse IsTimeoutString.Contains("timeout") OrElse IsTimeoutString.Contains("timedout")
+                Log("[Download] " & LocalName & " " & Info.Uuid & If(IsTimeout, "#:超时(" & (Timeout * 0.001) & "s)", "#:出错," & GetString(ex, False)))
+                Info.State = NetState.Error
+                ''使用该下载源的线程是否没有速度
+                ''下载超时也会导致没有速度,容易误判下载失败,所以已弃用此方法
+                'Dim IsNoSpeed As Boolean = True
+                'SyncLock LockChain
+                '    If Threads IsNot Nothing Then
+                '        For Each Thread As NetThread In Threads
+                '            If Thread.Source.Id = Info.Source.Id AndAlso Thread.Speed > 0 Then
+                '                IsNoSpeed = False
+                '                Exit For
+                '            End If
+                '        Next
+                '    End If
+                'End SyncLock
+                Info.Source.Ex = ex
+                '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
+                If ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(403)") OrElse ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
+                    (Info.Source.FailCount >= MathRange(NetTaskThreadLimit, 5, 40) AndAlso DownloadDone < 1) OrElse
+                    Info.Source.FailCount > NetTaskThreadLimit Then
+                    Dim IsThisFail As Boolean = False
+                    SyncLock LockSource
+                        If Info.Source.Thread IsNot Nothing AndAlso Info.Source.Thread.Equals(Info) Then
+                            '单线程下,本线程出错
+                            SourcesOnce.RemoveAt(0)
+                            GoTo Wrong
+                        ElseIf Not Info.Source.IsFailed Then
+                            '多线程下,本线程出错
+Wrong:
+                            Info.Source.IsFailed = True
+                            IsThisFail = True
+                        End If
+                    End SyncLock
+                    '本线程引发下载源被禁用
+                    If IsThisFail Then
+                        Log("[Download] " & LocalName & " " & Uuid & "#:下载源被禁用(" & Info.Source.Id & "):" & Info.Source.Url)
+                        Log(ex, "下载源 " & Info.Source.Id & " 已被禁用", If(ex.Message.Contains("不支持断点续传") OrElse ex.Message.Contains("404") OrElse ex.Message.Contains("416"), LogLevel.Developer, LogLevel.Debug))
+                        If IsSourceFailed() Then
+                            '没有可用源
+                            Log("[Download] 文件 " & LocalName & " 已无可用下载源")
+                            Dim ExampleEx As Exception = Nothing
+                            SyncLock LockSource
+                                For Each Source As NetSource In Sources
+                                    Log("[Download] 已禁用的下载源:" & Source.Url)
+                                    If Source.Ex IsNot Nothing Then
+                                        ExampleEx = Source.Ex
+                                        Log(Source.Ex, "下载源禁用原因", LogLevel.Developer)
+                                    End If
+                                Next
+                            End SyncLock
+                            Fail(ExampleEx)
+                        ElseIf ex.Message.Contains("空间不足") Then
+                            '没有空间
+                            Fail(ex)
+                        End If
+                    End If
+                End If
+                '首线程错误
+                If FileSize = -2 Then
+                    SyncLock LockChain
+                        Threads = Nothing
+                    End SyncLock
+                End If
+            Finally
+                If ResultStream IsNot Nothing Then ResultStream.Dispose()
+                SyncLock NetTaskThreadCountLock
+                    NetTaskThreadCount -= 1
+                End SyncLock
+                If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge()
+            End Try
+        End Sub
+
+        '下载文件的最终收束事件
+        ''' <summary>
+        ''' 下载完成。合并文件。
+        ''' </summary>
+        Private Sub Merge()
+            '状态判断
+            SyncLock LockState
+                If State < NetState.Merge Then
+                    State = NetState.Merge
+                Else
+                    Exit Sub
+                End If
+            End SyncLock
+            Dim RetryCount As Integer = 0
+            Dim MergeFile As Stream = Nothing, AddWriter As BinaryWriter = Nothing
+            Try
+Retry:
+                SyncLock LockChain
+                    ''大小检测
+                    'If DownloadDone <> FileSize AndAlso Not IsUnknownSize Then
+                    '    For Each Th As NetThread In Threads
+                    '        Log("[Download] " & "File size detail: ")
+                    '        Log("[Download] " & Th.Uuid & "#, State " & GetStringFromEnum(CType(Th.State, [Enum])) & ", Range " & Th.DownloadStart & "~" & (Th.DownloadStart + Th.DownloadDone) & ", Left " & Th.DownloadUndone)
+                    '    Next
+                    '    Throw New Exception("文件大小应为 " & FileSize & " B,实际下载为 " & DownloadDone & " B。")
+                    'End If
+                    If ModeDebug Then Log("[Download] " & LocalName & ":正在合并文件")
+                    '创建文件夹
+                    If File.Exists(LocalPath) Then File.Delete(LocalPath)
+                    Dim Info As New FileInfo(LocalPath)
+                    Info.Directory.Create()
+                    '合并文件
+                    If IsNoSplit Then
+                        '仅有一个线程,从缓存中输出
+                        MergeFile = New FileStream(LocalPath, FileMode.Create)
+                        AddWriter = New BinaryWriter(MergeFile)
+                        AddWriter.Write(SmailFileCache.ToArray)
+                        AddWriter.Dispose() : AddWriter = Nothing
+                        MergeFile.Dispose() : MergeFile = Nothing
+                    ElseIf Threads.DownloadDone = DownloadDone Then
+                        '仅有一个文件,直接复制
+                        File.Copy(Threads.Temp, LocalPath, True)
+                    Else
+                        '有多个线程,合并
+                        MergeFile = New FileStream(LocalPath, FileMode.Create)
+                        AddWriter = New BinaryWriter(MergeFile)
+                        For Each Thread As NetThread In Threads
+                            If Thread.DownloadDone = 0 Then Continue For
+                            Using fs As New FileStream(Thread.Temp, FileMode.Open, FileAccess.Read, FileShare.Read)
+                                Dim TempReader As New BinaryReader(fs)
+                                AddWriter.Write(TempReader.ReadBytes(fs.Length))
+                                TempReader.Close()
+                            End Using
+                        Next
+                        AddWriter.Dispose() : AddWriter = Nothing
+                        MergeFile.Dispose() : MergeFile = Nothing
+                    End If
+                    '写入大小要求
+                    If Not IsUnknownSize AndAlso Check IsNot Nothing Then
+                        If Check.ActualSize = -1 Then
+                            Check.ActualSize = FileSize
+                        ElseIf Check.ActualSize <> FileSize Then
+                            Throw New Exception("文件大小不一致:任务要求为 " & Check.ActualSize & " B,网络获取结果为 " & FileSize & "B")
+                        End If
+                    End If
+                    '检查文件
+                    Dim CheckResult As String = Check?.Check(LocalPath)
+                    If CheckResult IsNot Nothing Then
+                        Log("[Download] " & "File size detail of " & Uuid & "# :")
+                        For Each Th As NetThread In Threads
+                            Log("[Download] " & Th.Uuid & "#, State " & GetStringFromEnum(CType(Th.State, [Enum])) & ", Range " & Th.DownloadStart & "~" & (Th.DownloadStart + Th.DownloadDone) & ", Left " & Th.DownloadUndone)
+                        Next
+                        Throw New Exception(CheckResult)
+                    End If
+                    '后处理
+                    If IsNoSplit Then
+                        SmailFileCache = Nothing
+                    Else
+                        For Each Thread As NetThread In Threads
+                            If Thread.Temp IsNot Nothing Then File.Delete(Thread.Temp)
+                        Next
+                    End If
+                    Finish()
+                End SyncLock
+            Catch ex As Exception
+                Log(ex, "合并文件出错(" & LocalName & ")")
+                If MergeFile IsNot Nothing Then
+                    MergeFile.Dispose() : MergeFile = Nothing
+                End If
+                If AddWriter IsNot Nothing Then
+                    AddWriter.Dispose() : AddWriter = Nothing
+                End If
+                '重试
+                If RetryCount <= 3 Then
+                    Threading.Thread.Sleep(RandomInteger(500, 1000))
+                    RetryCount += 1
+                    GoTo Retry
+                End If
+                Fail(ex)
+            End Try
+        End Sub
+        ''' <summary>
+        ''' 下载失败。
+        ''' </summary>
+        Private Sub Fail(Optional RaiseEx As Exception = Nothing)
+            SyncLock LockState
+                If State >= NetState.Finish Then Exit Sub
+                If RaiseEx IsNot Nothing Then Ex.Add(RaiseEx)
+                '凉凉
+                State = NetState.Error
+            End SyncLock
+            Try
+                If File.Exists(LocalPath) Then File.Delete(LocalPath)
+            Catch
+            End Try
+            SyncLock NetManager.LockRemain
+                NetManager.FileRemain -= 1
+                Log("[Download] " & LocalName & ":已失败,剩余文件 " & NetManager.FileRemain)
+                'If NetManage.FileRemainList.Contains(LocalPath) Then
+                '    NetManage.FileRemainList.Remove(LocalPath)
+                'Else
+                '    Log("ERROR " & LocalPath)
+                'End If
+            End SyncLock
+            For Each Task In Tasks
+                Task.OnFileFail(Me)
+            Next
+        End Sub
+        ''' <summary>
+        ''' 下载中断。
+        ''' </summary>
+        Public Sub Abort(CausedByTask As LoaderDownload)
+            '确认任务移除
+            SyncLock LockTasks
+                Tasks.Remove(CausedByTask)
+                If Tasks.Count > 0 Then Exit Sub
+            End SyncLock
+            '确认中断
+            SyncLock LockState
+                If State >= NetState.Finish Then Exit Sub
+                State = NetState.Error
+            End SyncLock
+            SyncLock NetManager.LockRemain
+                NetManager.FileRemain -= 1
+                If ModeDebug Then Log("[Download] " & LocalName & ":已取消,剩余文件 " & NetManager.FileRemain)
+                'If NetManage.FileRemainList.Contains(LocalPath) Then
+                '    NetManage.FileRemainList.Remove(LocalPath)
+                'Else
+                '    Log("ERROR " & LocalPath)
+                'End If
+            End SyncLock
+        End Sub
+
+        '状态改变接口
+        ''' <summary>
+        ''' 将该文件设置为已下载完成。
+        ''' </summary>
+        Public Sub Finish(Optional PrintLog As Boolean = True)
+            SyncLock LockState
+                If State >= NetState.Finish Then Exit Sub
+                State = NetState.Finish
+            End SyncLock
+            SyncLock NetManager.LockRemain
+                NetManager.FileRemain -= 1
+                If PrintLog Then Log("[Download] " & LocalName & ":已完成,剩余文件 " & NetManager.FileRemain)
+                'If NetManage.FileRemainList.Contains(LocalPath) Then
+                '    NetManage.FileRemainList.Remove(LocalPath)
+                'Else
+                '    Log("ERROR " & LocalPath)
+                'End If
+            End SyncLock
+            SyncLock LockTasks
+                For Each Task In Tasks
+                    Task.OnFileFinish(Me)
+                Next
+            End SyncLock
+        End Sub
+
+    End Class
+    ''' <summary>
+    ''' 下载一系列文件的加载器。
+    ''' </summary>
+    Public Class LoaderDownload
+        Inherits LoaderBase
+
+#Region "属性"
+
+        ''' <summary>
+        ''' 需要下载的文件。
+        ''' </summary>
+        Public Files As List(Of NetFile)
+        Private ReadOnly FilesLock As New Object
+        ''' <summary>
+        ''' 剩余未完成的文件数。(用于减轻 FilesLock 的占用)
+        ''' </summary>
+        Private FileRemain As Integer
+        Private ReadOnly FileRemainLock As New Object
+
+        ''' <summary>
+        ''' 用于显示的百分比进度。
+        ''' </summary>
+        Public Overrides Property Progress As Double
+            Get
+                If State >= LoadState.Finished Then Return 1
+                If Files.Count = 0 Then Return 0 '必须返回 0,否则在获取列表的时候会错觉已经下载完了
+                Return _Progress
+            End Get
+            Set(value As Double)
+                Throw New Exception("文件下载不允许指定进度")
+            End Set
+        End Property
+        Private _Progress As Double = 0
+
+        ''' <summary>
+        ''' 任务中的文件的连续失败计数。
+        ''' </summary>
+        Public Property FailCount As Integer
+            Get
+                Return _FailCount
+            End Get
+            Set(value As Integer)
+                _FailCount = value
+                If State = LoadState.Loading AndAlso value >= Math.Min(2000, Math.Max(FileRemain * 5.5, NetTaskThreadLimit * 5.5 + 3)) Then
+                    Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " & value & " 次", LogLevel.Debug)
+                    On Error Resume Next
+                    Dim ExList As New List(Of Exception)
+                    SyncLock FilesLock
+                        For Each File In Files
+                            For Each Source In File.Sources
+                                If Source.Ex IsNot Nothing Then
+                                    ExList.Add(Source.Ex)
+                                    If ExList.Count > 10 Then GoTo FinishExCatch
+                                End If
+                            Next
+                        Next
+                    End SyncLock
+FinishExCatch:
+                    OnFail(ExList)
+                End If
+            End Set
+        End Property
+        Private _FailCount As Integer = 0
+
+#End Region
+
+        ''' <summary>
+        ''' 刷新公开属性。由 NetManager 每 0.2 秒调用一次。
+        ''' </summary>
+        Public Sub RefreshStat()
+            '计算进度
+            Dim NewProgress As Double = 0
+            Dim TotalProgress As Double = 0
+            SyncLock FilesLock
+                For Each File In Files
+                    If File.IsCopy Then
+                        NewProgress += File.Progress * 0.2
+                        TotalProgress += 0.2
+                    Else
+                        NewProgress += File.Progress '* If(File.Progress < 1, 0.9, 1)
+                        TotalProgress += 1
+                    End If
+                Next
+            End SyncLock
+            If TotalProgress > 0 Then NewProgress /= TotalProgress
+            '刷新进度
+            If NewProgress < 1 AndAlso NewProgress > 0 Then NewProgress = 1 - (1 - NewProgress) ^ 0.9 'MathBezier(NewProgress, 0, 0.05, 1, 0.8, 0.0001)
+            _Progress = NewProgress * 0.99 + 0.01
+        End Sub
+
+        Public Sub New(Name As String, FileTasks As List(Of NetFile))
+            Me.Name = Name
+            Files = FileTasks
+        End Sub
+        Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
+            SyncLock FilesLock
+                If Input IsNot Nothing Then Files = Input
+                '去重
+                Dim ResultArray As New List(Of NetFile)
+                For i = 0 To Files.Count - 1
+                    For ii = i + 1 To Files.Count - 1
+                        If Files(i).LocalPath = Files(ii).LocalPath Then GoTo NextElement
+                    Next
+                    ResultArray.Add(Files(i))
+NextElement:
+                Next i
+                Files = ResultArray
+                '设置剩余文件数
+                SyncLock FileRemainLock
+                    For Each File In Files
+                        If File.State <> NetState.Finish Then FileRemain += 1
+                    Next
+                End SyncLock
+            End SyncLock
+            State = LoadState.Loading
+            '开始执行
+            RunInNewThread(Sub()
+                               Try
+                                   '输入检测
+                                   If Files.Count = 0 Then
+                                       OnFinish()
+                                       Exit Sub
+                                   End If
+                                   For Each File As NetFile In Files
+                                       If File Is Nothing Then Throw New ArgumentException("存在空文件请求!")
+                                       For Each Source As NetSource In File.Sources
+                                           If Not (Source.Url.ToLower.StartsWith("https://") OrElse Source.Url.ToLower.StartsWith("http://")) Then
+                                               Source.Ex = New ArgumentException("输入的下载链接不正确!")
+                                               Source.IsFailed = True
+                                           End If
+                                       Next
+                                       If File.IsSourceFailed() Then Throw New ArgumentException("输入的下载链接不正确!")
+                                       If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确!")
+                                       If File.LocalPath.EndsWith("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!")
+                                       '文件夹检测
+                                       Dim DirPath As String = New FileInfo(File.LocalPath).Directory.FullName
+                                       If Not Directory.Exists(DirPath) Then Directory.CreateDirectory(DirPath)
+                                   Next
+                                   '接入下载管理器
+                                   NetManager.Start(Me)
+                                   '将文件分配给多个线程以进行已存在查找
+                                   Dim Folders As New List(Of String) '可能会用于已存在查找的文件夹列表
+                                   Dim FoldersFinal As New List(Of String) '最终用于查找的列表
+                                   If Not Setup.Get("SystemDebugSkipCopy") Then '在设置中禁用复制
+                                       Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
+                                       For Each Folder In McFolderList
+                                           Folders.Add(Folder.Path)
+                                       Next
+                                       Folders = ArrayNoDouble(Folders)
+                                       For Each Folder In Folders
+                                           If Folder <> PathMcFolder AndAlso Directory.Exists(Folder) Then FoldersFinal.Add(Folder)
+                                       Next
+                                   End If
+                                   '最多 5 个线程,最少每个线程分配 10 个文件
+                                   SyncLock FilesLock
+                                       Dim FilesPerThread As Integer = Math.Max(5, Files.Count / 10 + 1)
+                                       Dim FilesInThread As New List(Of NetFile)
+                                       For Each File In Files
+                                           FilesInThread.Add(File)
+                                           If FilesInThread.Count = FilesPerThread Then
+                                               Dim FilesToRun As New List(Of NetFile)
+                                               FilesToRun.AddRange(FilesInThread)
+                                               RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
+                                               FilesInThread.Clear()
+                                           End If
+                                       Next
+                                       If FilesInThread.Count > 0 Then
+                                           Dim FilesToRun As New List(Of NetFile)
+                                           FilesToRun.AddRange(FilesInThread)
+                                           RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
+                                           FilesInThread.Clear()
+                                       End If
+                                   End SyncLock
+                               Catch ex As Exception
+                                   OnFail(New List(Of Exception) From {ex})
+                               End Try
+                           End Sub, "NetTask " & Uuid & " Main")
+        End Sub
+        Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String))
+            Try
+                If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count)
+                '试图从已存在的 Minecraft 文件夹中寻找目标文件
+                Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String}
+                For Each File As NetFile In Files
+                    Dim ExistFilePath As String = Nothing
+                    '判断是否有已存在的文件
+                    If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWith(PathMcFolder) Then
+                        Dim Relative = File.LocalPath.Replace(PathMcFolder, "")
+                        For Each Folder In FolderList
+                            Dim Target = Folder & Relative
+                            If File.Check.Check(Target) Is Nothing Then
+                                ExistFilePath = Target
+                                Exit For
+                            End If
+                        Next
+                    End If
+                    '若存在,则改变状态
+                    SyncLock LockState
+                        If ExistFilePath IsNot Nothing Then
+                            File.State = NetState.WaitForCopy
+                            File.IsCopy = True
+                            ExistFiles.Add(New KeyValuePair(Of NetFile, String)(File, ExistFilePath))
+                        Else
+                            File.State = NetState.WaitForDownload
+                            File.IsCopy = False
+                        End If
+                    End SyncLock
+                Next
+                '复制已存在的文件
+                For Each FileToken In ExistFiles
+                    Dim File As NetFile = FileToken.Key
+                    Dim LocalPath As String = FileToken.Value
+                    Dim RetryCount As Integer = 0
+Retry:
+                    Try
+                        Log("[Download] 复制已存在的文件(" & LocalPath & ")")
+                        FileIO.FileSystem.CopyFile(LocalPath, File.LocalPath, True)
+                        File.Finish(False)
+                    Catch ex As Exception
+                        RetryCount += 1
+                        Log(ex, String.Format("复制已存在的文件失败,重试第 {2} 次({0} -> {1})", LocalPath, File.LocalPath, RetryCount))
+                        If RetryCount < 3 Then
+                            Thread.Sleep(200)
+                            GoTo Retry
+                        End If
+                        File.State = NetState.WaitForDownload
+                        File.IsCopy = False
+                    End Try
+                Next
+            Catch ex As Exception
+                Log(ex, "下载已存在文件查找失败", LogLevel.Feedback)
+            End Try
+        End Sub
+
+        Public Sub OnFileFinish(File As NetFile)
+            '要求全部文件完成
+            SyncLock FileRemainLock
+                FileRemain -= 1
+                If FileRemain > 0 Then Exit Sub
+            End SyncLock
+            OnFinish()
+        End Sub
+        Public Sub OnFinish()
+            RaisePreviewFinish()
+            SyncLock LockState
+                If State > LoadState.Loading Then Exit Sub
+                State = LoadState.Finished
+            End SyncLock
+        End Sub
+        Public Sub OnFileFail(File As NetFile)
+            '将下载源的错误加入主错误列表
+            For Each Source In File.Sources
+                If Not IsNothing(Source.Ex) Then File.Ex.Add(Source.Ex)
+            Next
+            OnFail(File.Ex)
+        End Sub
+        Public Sub OnFail(ExList As List(Of Exception))
+            SyncLock LockState
+                If State > LoadState.Loading Then Exit Sub
+                If ExList Is Nothing OrElse ExList.Count = 0 Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
+                [Error] = ExList(0)
+                '获取实际失败的文件
+                SyncLock FilesLock
+                    For Each File In Files
+                        If File.State = NetState.Error Then
+                            [Error] = New Exception("文件下载失败:" & File.LocalPath & "(第一下载源:" & File.Sources(0).Url & ")", ExList(0))
+                            Exit For
+                        End If
+                    Next
+                End SyncLock
+                '在设置 Error 对象后再更改为失败,避免 WaitForExit 无法捕获错误
+                State = LoadState.Failed
+            End SyncLock
+            '中断所有文件
+            SyncLock FilesLock
+                For Each TaskFile In Files
+                    If TaskFile.State < NetState.Merge Then TaskFile.State = NetState.Error
+                Next
+            End SyncLock
+            '在退出同步锁后再进行日志输出
+            Dim ErrOutput As New List(Of String)
+            For Each Ex As Exception In ExList
+                ErrOutput.Add(GetString(Ex, False))
+            Next
+            Log("[Download] " & Join(ArrayNoDouble(ErrOutput.ToArray), vbCrLf))
+        End Sub
+        Public Overrides Sub Abort()
+            SyncLock LockState
+                If State >= LoadState.Finished Then Exit Sub
+                State = LoadState.Aborted
+            End SyncLock
+            Log("[Download] " & Name & " 已取消!")
+            '中断所有文件
+            SyncLock FilesLock
+                For Each TaskFile In Files
+                    TaskFile.Abort(Me)
+                Next
+            End SyncLock
+        End Sub
+
+    End Class
+
+    Public NetManager As New NetManagerClass
+    ''' <summary>
+    ''' 下载文件管理。
+    ''' </summary>
+    Public Class NetManagerClass
+
+#Region "属性"
+
+        ''' <summary>
+        ''' 需要下载的文件。为“本地地址 - 文件对象”键值对。
+        ''' </summary>
+        Public Files As New Dictionary(Of String, NetFile)
+        Public ReadOnly LockFiles As New Object
+
+        ''' <summary>
+        ''' 当前的所有下载任务。
+        ''' </summary>
+        Public Tasks As New List(Of LoaderDownload)
+        Private ReadOnly LockTasks As New Object
+
+        ''' <summary>
+        ''' 已下载完成的大小。
+        ''' </summary>
+        Public Property DownloadDone As Long
+            Get
+                Return _DownloadDone
+            End Get
+            Set(value As Long)
+                SyncLock LockDone
+                    _DownloadDone = value
+                End SyncLock
+            End Set
+        End Property
+        Private _DownloadDone As Long = 0
+        Private ReadOnly LockDone As New Object
+
+
+        ''' <summary>
+        ''' 尚未完成下载的文件数。
+        ''' </summary>
+        Public FileRemain As Integer = 0
+        Public ReadOnly LockRemain As New Object
+
+        ''' <summary>
+        ''' 上次记速时的已下载大小。
+        ''' </summary>
+        Private SpeedLastDone As Long = 0
+        ''' <summary>
+        ''' 最近 10 次下载速度的记录,较新的在后面。间隔为 0.2 秒,故记录了 2 秒的速度。
+        ''' </summary>
+        Public SpeedLast As New List(Of Long) From {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
+        '这些属性由 RefreshStat 刷新
+        ''' <summary>
+        ''' 当前的全局下载速度,单位为 Byte / 秒。
+        ''' </summary>
+        Public Speed As Long = 0
+
+        Public ReadOnly Uuid As Integer = GetUuid()
+
+#End Region
+
+        ''' <summary>
+        ''' 进度与下载速度由下载管理线程每隔约 0.2 秒刷新一次。
+        ''' </summary>
+        Private Sub RefreshStat()
+            Try
+                Dim DeltaTime As Long = GetTimeTick() - RefreshStatLast
+                RefreshStatLast += DeltaTime
+#Region "刷新整体速度"
+                '计算即时速度
+                Dim ActualSpeed As Double = Math.Max(0, (DownloadDone - SpeedLastDone) / (DeltaTime / 1000))
+                SpeedLast.Add(ActualSpeed)
+                SpeedLast.RemoveAt(0)
+                SpeedLastDone = DownloadDone
+                '计算用于显示的速度
+                'If SpeedLast(9) = 0 Then
+                '    Speed = 0
+                'Else
+                Speed = (SpeedLast(9) + SpeedLast(8) * 0.9 + SpeedLast(7) * 0.8 + SpeedLast(6) * 0.7 + SpeedLast(5) * 0.6 + SpeedLast(4) * 0.5 + SpeedLast(3) * 0.4 + SpeedLast(2) * 0.3 + SpeedLast(1) * 0.2 + SpeedLast(0) * 0.1) / 5.5
+                'End If
+                '计算新的速度下限
+                SpeedLast.Add(ActualSpeed)
+                SpeedLast.RemoveAt(0)
+                Dim Limit As Long = Math.Min(Math.Min(Math.Min(Math.Min(SpeedLast(5), SpeedLast(6)), SpeedLast(7)), SpeedLast(8)), SpeedLast(9)) * 0.9
+                If Limit > NetTaskSpeedLimitLow Then
+                    NetTaskSpeedLimitLow = Limit
+                    Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
+                End If
+#End Region
+#Region "刷新下载任务属性"
+                SyncLock LockTasks
+                    For Each Task In Tasks
+                        Task.RefreshStat()
+                    Next
+                End SyncLock
+#End Region
+            Catch ex As Exception
+                Log(ex, "刷新下载公开属性失败")
+            End Try
+        End Sub
+        Private RefreshStatLast As Long
+
+        ''' <summary>
+        ''' 启动监控线程,用于新增下载线程。
+        ''' </summary>
+        Private Sub StartManager()
+            If IsManagerStarted Then Exit Sub
+            IsManagerStarted = True
+            RunInNewThread(Sub()
+                               Try
+                                   Dim LastLoopTime As Long
+                                   While True
+                                       LastLoopTime = GetTimeTick()
+                                       '若已完成,则清空
+                                       If FileRemain = 0 AndAlso Files.Count > 0 Then
+                                           SyncLock LockFiles
+                                               Files.Clear()
+                                           End SyncLock
+                                       End If
+                                       '开启新线程
+                                       If Speed < NetTaskSpeedLimitLow OrElse FileRemain > NetTaskThreadLimit Then
+                                           '速度小于下限或剩余文件还贼多,尝试开启线程
+                                           Dim IsSuccess As Boolean = False
+                                           Dim NewThreadCount As Integer
+                                           '确定最大线程追加数
+                                           NewThreadCount = Math.Max(FileRemain, MathRange(NetTaskThreadCount / 2, 1, 4))
+                                           NewThreadCount = Math.Floor(NewThreadCount / 2) '双线程启用减半
+                                           NewThreadCount = MathRange(NewThreadCount, 1, NetTaskThreadLimit)
+                                           '循环追加
+                                           Do
+                                               IsSuccess = False
+                                               '启动 Wait 的文件后立即会变成 Connect,导致第二次循环再次调用,所以需要先暂时存储进去……
+                                               '此外为了减少 LockFiles 的占用时间,所以先遍历列表再开始
+                                               Dim FilesWaiting As New List(Of NetFile)
+                                               Dim FilesLoading As New List(Of NetFile)
+                                               SyncLock LockFiles
+                                                   For Each File As NetFile In Files.Values
+                                                       If File.RandomCode Mod 2 = 0 Then Continue For
+                                                       If File.State = NetState.WaitForDownload Then
+                                                           FilesWaiting.Add(File)
+                                                       ElseIf File.State < NetState.Merge Then
+                                                           FilesLoading.Add(File)
+                                                       End If
+                                                   Next
+                                               End SyncLock
+                                               '为文件列表中的文件开始线程
+                                               For Each File As NetFile In FilesWaiting
+                                                   If NewThreadCount = 0 Then Exit For
+                                                   If File.TryBeginThread() Then
+                                                       IsSuccess = True
+                                                       NewThreadCount -= 1
+                                                   End If
+                                               Next
+                                               For Each File As NetFile In FilesLoading
+                                                   If NewThreadCount = 0 Then Exit For
+                                                   If File.TryBeginThread() Then
+                                                       IsSuccess = True
+                                                       NewThreadCount -= 1
+                                                   End If
+                                               Next
+                                           Loop While NewThreadCount > 0 AndAlso IsSuccess
+                                       End If
+                                       '等待直至 120 ms
+                                       Do While GetTimeTick() - LastLoopTime < 120
+                                           Thread.Sleep(10)
+                                       Loop
+                                   End While
+                               Catch ex As Exception
+                                   Log(ex, "下载管理启动线程 1 出错", LogLevel.Assert)
+                               End Try
+                           End Sub, "NetManager ThreadStarter Single")
+            RunInNewThread(Sub()
+                               Try
+                                   Dim LastLoopTime As Long
+                                   While True
+                                       LastLoopTime = GetTimeTick()
+                                       '开启新线程
+                                       If Speed < NetTaskSpeedLimitLow OrElse FileRemain > NetTaskThreadLimit Then
+                                           '速度小于下限或剩余文件还贼多,尝试开启线程
+                                           Dim IsSuccess As Boolean = False
+                                           Dim NewThreadCount As Integer
+                                           '确定最大线程追加数
+                                           NewThreadCount = Math.Max(FileRemain, MathRange(NetTaskThreadCount / 2, 1, 4))
+                                           NewThreadCount = Math.Floor(NewThreadCount / 2) '双线程启用减半
+                                           NewThreadCount = MathRange(NewThreadCount, 1, NetTaskThreadLimit)
+                                           '循环追加
+                                           Do
+                                               IsSuccess = False
+                                               '启动 Wait 的文件后立即会变成 Connect,导致第二次循环再次调用,所以需要先暂时存储进去……
+                                               '此外为了减少 LockFiles 的占用时间,所以先遍历列表再开始
+                                               Dim FilesWaiting As New List(Of NetFile)
+                                               Dim FilesLoading As New List(Of NetFile)
+                                               SyncLock LockFiles
+                                                   For Each File As NetFile In Files.Values
+                                                       If File.RandomCode Mod 2 = 1 Then Continue For
+                                                       If File.State = NetState.WaitForDownload Then
+                                                           FilesWaiting.Add(File)
+                                                       ElseIf File.State < NetState.Merge Then
+                                                           FilesLoading.Add(File)
+                                                       End If
+                                                   Next
+                                               End SyncLock
+                                               '为文件列表中的文件开始线程
+                                               For Each File As NetFile In FilesWaiting
+                                                   If NewThreadCount = 0 Then Exit For
+                                                   If File.TryBeginThread() Then
+                                                       IsSuccess = True
+                                                       NewThreadCount -= 1
+                                                   End If
+                                               Next
+                                               For Each File As NetFile In FilesLoading
+                                                   If NewThreadCount = 0 Then Exit For
+                                                   If File.TryBeginThread() Then
+                                                       IsSuccess = True
+                                                       NewThreadCount -= 1
+                                                   End If
+                                               Next
+                                           Loop While NewThreadCount > 0 AndAlso IsSuccess
+                                       End If
+                                       '等待直至 120 ms
+                                       Do While GetTimeTick() - LastLoopTime < 120
+                                           Thread.Sleep(10)
+                                       Loop
+                                   End While
+                               Catch ex As Exception
+                                   Log(ex, "下载管理启动线程 2 出错", LogLevel.Assert)
+                               End Try
+                           End Sub, "NetManager ThreadStarter Odd")
+            RunInNewThread(Sub()
+                               Try
+                                   Dim LastLoopTime As Long
+                                   NetTaskSpeedLimitLeftLast = GetTimeTick()
+                                   While True
+                                       Dim TimeNow = GetTimeTick()
+                                       LastLoopTime = TimeNow
+                                       '增加限速余量
+                                       If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft = NetTaskSpeedLimitHigh / 1000 * (TimeNow - NetTaskSpeedLimitLeftLast)
+                                       NetTaskSpeedLimitLeftLast = TimeNow
+                                       '刷新公开属性
+                                       RefreshStat()
+                                       '等待直至 170 ms
+                                       Do While GetTimeTick() - LastLoopTime < 170
+                                           Thread.Sleep(10)
+                                       Loop
+                                   End While
+                               Catch ex As Exception
+                                   Log(ex, "下载管理刷新线程出错", LogLevel.Assert)
+                               End Try
+                           End Sub, "NetManager StatRefresher")
+        End Sub
+        Private IsManagerStarted As Boolean = False
+
+        'Public FileRemainList As New List(Of String)
+        Private IsDownloadCacheCleared As Boolean = False
+        ''' <summary>
+        ''' 开始一个下载任务。
+        ''' </summary>
+        Public Sub Start(Task As LoaderDownload)
+            StartManager()
+            '清理缓存
+            If Not IsDownloadCacheCleared Then
+                Try
+                    DeleteDirectory(PathTemp & "Download")
+                Catch ex As Exception
+                    Log(ex, "清理下载缓存失败")
+                End Try
+                IsDownloadCacheCleared = True
+            End If
+            Directory.CreateDirectory(PathTemp & "Download")
+            '文件处理
+            SyncLock LockFiles
+                '添加每个文件
+                For i = 0 To Task.Files.Count - 1
+                    Dim File = Task.Files(i)
+                    If Files.ContainsKey(File.LocalPath) Then
+                        '已有该文件
+                        If Files(File.LocalPath).State >= NetState.Finish Then
+                            '该文件已经下载过一次,且下载完成
+                            '将已下载的文件替换成当前文件,重新下载
+                            SyncLock File.LockTasks
+                                File.Tasks.Add(Task)
+                            End SyncLock
+                            Files(File.LocalPath) = File
+                            SyncLock LockRemain
+                                FileRemain += 1
+                                If ModeDebug Then Log("[Download] " & File.LocalName & ":已替换列表,剩余文件 " & FileRemain)
+                                'FileRemainList.Add(File.LocalPath)
+                            End SyncLock
+                        Else
+                            '该文件正在下载中
+                            '将当前文件替换成下载中的文件,即两个任务指向同一个文件
+                            File = Files(File.LocalPath)
+                            SyncLock File.LockTasks
+                                File.Tasks.Add(Task)
+                            End SyncLock
+                        End If
+                    Else
+                        '没有该文件
+                        SyncLock File.LockTasks
+                            File.Tasks.Add(Task)
+                        End SyncLock
+                        Files.Add(File.LocalPath, File)
+                        SyncLock LockRemain
+                            FileRemain += 1
+                            If ModeDebug Then Log("[Download] " & File.LocalName & ":已加入列表,剩余文件 " & FileRemain)
+                            'FileRemainList.Add(File.LocalPath)
+                        End SyncLock
+                    End If
+                    Task.Files(i) = File '回设
+                Next
+            End SyncLock
+            SyncLock LockTasks
+                Tasks.Add(Task)
+            End SyncLock
+        End Sub
+
+    End Class
+
+    ''' <summary>
+    ''' 是否有正在进行中、需要在下载管理页面显示的下载任务?
+    ''' </summary>
+    Public Function HasDownloadingTask(Optional IgnoreCustomDownload As Boolean = False) As Boolean
+        SyncLock LoaderTaskbarLock
+            For Each Task In LoaderTaskbar
+                If (Task.Show AndAlso Task.State = LoadState.Loading) AndAlso
+                   (Not IgnoreCustomDownload OrElse Not Task.Name.ToString.Contains("自定义下载")) Then
+                    Return True
+                End If
+            Next
+        End SyncLock
+        Return False
+    End Function
+
+End Module

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

@@ -0,0 +1,315 @@
+'提供不同的输入验证方法,名称以 Validate 开头
+
+''' <summary>
+''' 输入验证规则基类。要查看所有的输入验证规则,可在输入 Validate 后查看自动补全。
+''' </summary>
+Public MustInherit Class Validate
+    ''' <summary>
+    ''' 验证某字符串是否符合验证要求。若符合,返回空字符串;若不符合,返回错误原因。
+    ''' </summary>
+    Public MustOverride Function Validate(Str As String) As String
+End Class
+
+''' <summary>
+''' 若为空则直接通过检查。
+''' </summary>
+Public Class ValidateNullable
+    Inherits Validate
+    Public Overrides Function Validate(Str As String) As String
+        If IsNothing(Str) OrElse String.IsNullOrEmpty(Str) Then Return Nothing
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 不能为 Nothing 或空字符串(不包括全空格检查)。
+''' </summary>
+Public Class ValidateNullOrEmpty
+    Inherits Validate
+    Public Overrides Function Validate(Str As String) As String
+        If IsNothing(Str) OrElse String.IsNullOrEmpty(Str) Then Return "输入内容不能为空!"
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 不能为 Nothing 或空字符串(包括全空格检查)。
+''' </summary>
+Public Class ValidateNullOrWhiteSpace
+    Inherits Validate
+    Public Overrides Function Validate(Str As String) As String
+        If IsNothing(Str) OrElse String.IsNullOrWhiteSpace(Str) Then Return "输入内容不能为空!"
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 必须满足正则表达式。
+''' </summary>
+Public Class ValidateRegex
+    Inherits Validate
+    Public Property Regex As String
+    Public Property ErrorDescription As String = ""
+    Public Sub New()
+    End Sub '用于 XAML 初始化
+    Public Sub New(Regex As String, Optional ErrorDescription As String = "")
+        Me.Regex = Regex
+        Me.ErrorDescription = ErrorDescription
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        If Not RegexCheck(Str, Regex) Then Return ErrorDescription
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 必须是一个完整网址。
+''' </summary>
+Public Class ValidateHttp
+    Inherits Validate
+    Public Sub New()
+    End Sub '用于 XAML 初始化
+    Public Overrides Function Validate(Str As String) As String
+        If Str.EndsWith("/") Then Str = Str.Substring(0, Str.Length - 1)
+        If Not RegexCheck(Str, "^(http[s]?)\://") Then Return "输入的网址无效!"
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 必须为整数。
+''' </summary>
+Public Class ValidateInteger
+    Inherits Validate
+    Public Property Min As Integer
+    Public Property Max As Integer = Integer.MaxValue
+    Public Sub New()
+    End Sub '用于 XAML 初始化
+    Public Sub New(Min As Integer, Max As Integer)
+        Me.Min = Min
+        Me.Max = Max
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        If Str.Length > 9 Then Return "请输入一个大小合理的数字!"
+        Dim Valed As Integer = Val(Str)
+        If Valed.ToString <> Str Then Return "请输入一个整数!"
+        If Val(Str) > Max Then Return "不可超过 " & Max & "!"
+        If Val(Str) < Min Then Return "不可低于 " & Min & "!"
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 长度限制。
+''' </summary>
+Public Class ValidateLength
+    Inherits Validate
+    Public Property Min As Integer = 0
+    Public Property Max As Integer = Integer.MaxValue
+    Public Sub New()
+    End Sub '用于 XAML 初始化
+    Public Sub New(Min As Integer, Optional Max As Integer = Integer.MaxValue)
+        Me.Min = Min
+        Me.Max = Max
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        If Len(Str) <> Max AndAlso Max = Min Then Return "长度必须为 " & Max & " 个字符!"
+        If Len(Str) > Max Then Return "长度最长为 " & Max & " 个字符!"
+        If Len(Str) < Min Then Return "长度至少需 " & Min & " 个字符!"
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 不能包含某些特定字符串。忽略大小写。
+''' </summary>
+Public Class ValidateExcept
+    Inherits Validate
+    Public Property Excepts As ObjectModel.Collection(Of String) = New ObjectModel.Collection(Of String)
+    Public Property ErrorMessage As String
+    Public Sub New()
+        ErrorMessage = "输入内容不能包含 %!"
+    End Sub '用于 XAML 初始化
+    Public Sub New(Excepts As ObjectModel.Collection(Of String), Optional ErrorMessage As String = "输入内容不能包含 %!")
+        Me.Excepts = Excepts
+        Me.ErrorMessage = ErrorMessage
+    End Sub
+    Public Sub New(Excepts As IEnumerable, Optional ErrorMessage As String = "输入内容不能包含 %!")
+        Me.Excepts = New ObjectModel.Collection(Of String)
+        Me.ErrorMessage = ErrorMessage
+        For Each Data As String In Excepts
+            Me.Excepts.Add(Data)
+        Next
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        For Each Ch As String In Excepts
+            If Str.IndexOf(Ch, StringComparison.OrdinalIgnoreCase) >= 0 Then
+                If IsNothing(ErrorMessage) Then ErrorMessage = ""
+                Return ErrorMessage.Replace("%", Ch)
+            End If
+        Next
+        Return ""
+    End Function
+
+End Class
+
+''' <summary>
+''' 不能与某些特定字符串相同。
+''' </summary>
+Public Class ValidateExceptSame
+    Inherits Validate
+    Public Property Excepts As ObjectModel.Collection(Of String) = New ObjectModel.Collection(Of String)
+    Public Property ErrorMessage As String
+    Public Property IgnoreCase As Boolean = False
+    Public Sub New(Excepts As ObjectModel.Collection(Of String), Optional ErrorMessage As String = "输入内容不能为 %!", Optional IgnoreCase As Boolean = False)
+        Me.Excepts = Excepts
+        Me.ErrorMessage = ErrorMessage
+        Me.IgnoreCase = IgnoreCase
+    End Sub
+    Public Sub New(Excepts As IEnumerable, Optional ErrorMessage As String = "输入内容不能为 %!", Optional IgnoreCase As Boolean = False)
+        Me.Excepts = New ObjectModel.Collection(Of String)
+        For Each Data As String In Excepts
+            Me.Excepts.Add(Data)
+        Next
+        Me.ErrorMessage = ErrorMessage
+        Me.IgnoreCase = IgnoreCase
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        If Str Is Nothing Then Return ErrorMessage.Replace("%", "null")
+        For Each Ch As String In Excepts
+            If IgnoreCase Then
+                If Str.ToLower = Ch.ToLower Then Return ErrorMessage.Replace("%", Ch)
+            Else
+                If Str.Equals(Ch) Then Return ErrorMessage.Replace("%", Ch) '使用 = 不确定是否会忽略大小写
+            End If
+        Next
+        Return ""
+    End Function
+
+End Class
+
+''' <summary>
+''' 对文件夹名的粗略的特化检测。
+''' </summary>
+Public Class ValidateFolderName
+    Inherits Validate
+    Public Property Path As String
+    Public Property UseMinecraftCharCheck As Boolean = True
+    Public Property IgnoreCase As Boolean = True
+    Private ReadOnly PathIgnore As IEnumerable(Of DirectoryInfo)
+    Public Sub New(Path As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
+        Me.Path = Path
+        Me.IgnoreCase = IgnoreCase
+        Me.UseMinecraftCharCheck = UseMinecraftCharCheck
+        On Error Resume Next
+        PathIgnore = New DirectoryInfo(Path).EnumerateDirectories
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        Try
+            '检查是否为空
+            Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
+            If Not LengthCheck = "" Then Return LengthCheck
+            '检查空格
+            If Str.StartsWith(" ") Then Return "文件夹名不能以空格开头!"
+            If Str.EndsWith(" ") Then Return "文件夹名不能以空格结尾!"
+            '检查长度
+            LengthCheck = New ValidateLength(1, 100).Validate(Str)
+            If Not LengthCheck = "" Then Return LengthCheck
+            '检查尾部小数点
+            If Str.EndsWith(".") Then Return "文件夹名不能以小数点结尾!"
+            '检查特殊字符
+            Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
+            If Not 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"}, "文件夹名不可为 %!").Validate(Str)
+            If Not InvalidStrCheck = "" Then Return InvalidStrCheck
+            '检查文件夹重名
+            Dim Arr As New List(Of String)
+            If PathIgnore IsNot Nothing Then
+                For Each Folder As DirectoryInfo In PathIgnore
+                    Arr.Add(Folder.Name)
+                Next
+            End If
+            Dim SameNameCheck = New ValidateExceptSame(Arr, "不可与现有文件夹重名!", IgnoreCase).Validate(Str)
+            If Not SameNameCheck = "" Then Return SameNameCheck
+            Return ""
+        Catch ex As Exception
+            Log(ex, "检查文件夹名出错")
+            Return "错误:" & ex.Message
+        End Try
+    End Function
+End Class
+
+''' <summary>
+''' 要求输入一个可用的文件夹路径。
+''' </summary>
+Public Class ValidateFolderPath
+    Inherits Validate
+    Public Property UseMinecraftCharCheck As Boolean = True
+    Public Sub New()
+    End Sub
+    Public Sub New(UseMinecraftCharCheck As Boolean)
+        Me.UseMinecraftCharCheck = UseMinecraftCharCheck
+    End Sub
+    Public Overrides Function Validate(Str As String) As String
+        '去除尾部斜线,统一为 \
+        Str = Str.Replace("/", "\")
+        If Not Str.TrimEnd("\").EndsWith(":") Then Str = Str.TrimEnd("\")
+        '检查是否为空
+        Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
+        If Not LengthCheck = "" Then Return LengthCheck
+        '检查长度
+        LengthCheck = New ValidateLength(1, 254).Validate(Str)
+        If Not LengthCheck = "" Then Return LengthCheck
+        '检查开头
+        If Str.StartsWith("\\Mac\") Then GoTo Fin
+        For Each Drive As DriveInfo In My.Computer.FileSystem.Drives
+            If Str.ToUpper = Drive.Name Then Return ""
+            If Str.ToUpper.StartsWith(Drive.Name) Then GoTo Fin
+        Next
+        Return "文件夹路径头存在错误!"
+Fin:
+        '对首层以外的路径检查
+        For i = If(Str.StartsWith("\\Mac\"), 2, 1) To Str.Split("\").Count - 1
+            Dim SubStr As String = Str.Split("\")(i)
+            '检查是否为空
+            Dim SubLengthCheck As String = New ValidateNullOrWhiteSpace().Validate(SubStr)
+            If Not SubLengthCheck = "" Then Return "文件夹路径存在错误!"
+            '检查特殊字符
+            Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
+            If Not CharactCheck = "" Then Return CharactCheck
+            '检查头部空格
+            If SubStr.StartsWith(" ") Then Return "文件夹名不能以空格开头!"
+            If SubStr.EndsWith(" ") Then Return "文件夹名不能以空格结尾!"
+            '检查尾部小数点
+            If SubStr.EndsWith(".") Then Return "文件夹名不能以小数点结尾!"
+            '检查特殊字符串
+            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"}, "文件夹名不可为 %!").Validate(SubStr)
+            If Not InvalidStrCheck = "" Then Return InvalidStrCheck
+        Next
+        Return ""
+    End Function
+End Class
+
+''' <summary>
+''' 敏感词过滤。
+''' </summary>
+Public Class ValidateFilter
+    Inherits Validate
+    Private Shared Excepts As String() = SecretDecrypt(SecretSensitiveWordsRaw).Split("|")
+    Public Sub New()
+    End Sub '用于 XAML 初始化
+    Public Overrides Function Validate(Str As String) As String
+        '提取无间隔符的字符串
+        Dim RealStr As String = ""
+        For Each St In If(Str, "").ToLower
+            If String.IsNullOrWhiteSpace(St.ToString) OrElse ("!@#$%^&*()_+-={}[]|\;':"",./<>? ~ ·!¥…()—,。、《》?‘;:【】{}" & vbLQ & vbRQ).ToCharArray.Contains(St) Then Continue For
+            RealStr += St
+        Next
+        '检查
+        For Each Except In Excepts
+            If RealStr.Contains(Except) Then Return "存在敏感内容!"
+        Next
+        Return ""
+    End Function
+End Class

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

@@ -0,0 +1,158 @@
+'一个万能的自动图片类型转换工具类
+
+Public Class MyBitmap
+
+    ''' <summary>
+    ''' 位图缓存。
+    ''' </summary>
+    Public Shared BitmapCache As New Dictionary(Of String, MyBitmap)
+
+    ''' <summary>
+    ''' 存储的图片
+    ''' </summary>
+    Public Pic As System.Drawing.Bitmap
+
+    '自动类型转换
+    '支持的类:Image,ImageSource,Bitmap,ImageBrush,BitmapSource
+    Public Shared Widening Operator CType(Image As System.Drawing.Image) As MyBitmap
+        Return New MyBitmap(Image)
+    End Operator
+    Public Shared Widening Operator CType(Image As MyBitmap) As System.Drawing.Image
+        Return Image.Pic
+    End Operator
+    Public Shared Widening Operator CType(Image As ImageSource) As MyBitmap
+        Return New MyBitmap(Image)
+    End Operator
+    Public Shared Widening Operator CType(Image As MyBitmap) As ImageSource
+        Dim Bitmap = Image.Pic
+        Dim rect = New System.Drawing.Rectangle(0, 0, Bitmap.Width, Bitmap.Height)
+        Dim bitmapData = Bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
+        Try
+            Dim size = rect.Width * rect.Height * 4
+            Return BitmapSource.Create(Bitmap.Width, Bitmap.Height, DPI, DPI, PixelFormats.Bgra32, Nothing, bitmapData.Scan0, size, bitmapData.Stride)
+        Finally
+            Bitmap.UnlockBits(bitmapData)
+        End Try
+    End Operator
+    Public Shared Widening Operator CType(Image As System.Drawing.Bitmap) As MyBitmap
+        Return New MyBitmap(Image)
+    End Operator
+    Public Shared Widening Operator CType(Image As MyBitmap) As System.Drawing.Bitmap
+        Return Image.Pic
+    End Operator
+    Public Shared Widening Operator CType(Image As ImageBrush) As MyBitmap
+        Return New MyBitmap(Image)
+    End Operator
+    Public Shared Widening Operator CType(Image As MyBitmap) As ImageBrush
+        Return New ImageBrush(New MyBitmap(Image.Pic))
+    End Operator
+
+    '构造函数
+    Public Sub New()
+    End Sub
+    Public Sub New(FilePathOrResourceName As String)
+        Try
+            If FilePathOrResourceName.StartsWith(PathImage) Then
+                '使用缓存
+                If BitmapCache.ContainsKey(FilePathOrResourceName) Then
+                    Pic = BitmapCache(FilePathOrResourceName).Pic
+                Else
+                    Pic = New MyBitmap(CType((New ImageSourceConverter).ConvertFromString(FilePathOrResourceName), ImageSource))
+                    BitmapCache.Add(FilePathOrResourceName, Pic)
+                End If
+            Else
+                '使用这种自己接管 FileStream 的方法加载才能解除文件占用
+                Using InputStream As New FileStream(FilePathOrResourceName, FileMode.Open)
+                    Pic = New System.Drawing.Bitmap(InputStream)
+                    InputStream.Dispose()
+                End Using
+            End If
+        Catch ex As Exception
+            Pic = My.Application.TryFindResource(FilePathOrResourceName)
+            If Pic Is Nothing Then
+                Pic = New System.Drawing.Bitmap(1, 1)
+                Log(ex, "加载位图失败(" & FilePathOrResourceName & ")")
+                Throw
+            Else
+                Log(ex, "指定类型有误的位图加载(" & FilePathOrResourceName & ")", LogLevel.Developer)
+                Exit Try
+            End If
+        End Try
+    End Sub
+    Public Sub New(Image As ImageSource)
+        Using MS = New MemoryStream()
+            Dim Encoder = New PngBitmapEncoder()
+            Encoder.Frames.Add(BitmapFrame.Create(Image))
+            Encoder.Save(MS)
+            Pic = New System.Drawing.Bitmap(MS)
+        End Using
+    End Sub
+    Public Sub New(Image As System.Drawing.Image)
+        Pic = Image
+    End Sub
+    Public Sub New(Image As System.Drawing.Bitmap)
+        Pic = Image
+    End Sub
+    Public Sub New(Image As ImageBrush)
+        Using MS = New MemoryStream()
+            Dim Encoder = New BmpBitmapEncoder()
+            Encoder.Frames.Add(BitmapFrame.Create(Image.ImageSource))
+            Encoder.Save(MS)
+            Pic = New System.Drawing.Bitmap(MS)
+        End Using
+    End Sub
+
+    ''' <summary>
+    ''' 获取旋转的图片,这个方法不会导致原对象改变且会返回一个新的对象。图片长宽必须相等。
+    ''' </summary>
+    ''' <param name="angle">旋转角度(单位为角度)。</param>
+    Public Function Rotation(angle As Double) As System.Drawing.Bitmap
+        With Me
+            Dim img As System.Drawing.Image = Me.Pic
+            Dim bitSize As Single = img.Width
+            Dim bmp As New System.Drawing.Bitmap(CInt(bitSize), CInt(bitSize))
+            Using g As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(bmp)
+                g.TranslateTransform(bitSize / 2, bitSize / 2)
+                g.RotateTransform(angle)
+                g.TranslateTransform(-bitSize / 2, -bitSize / 2)
+                g.DrawImage(img, New System.Drawing.Rectangle(0, 0, img.Width, img.Width))
+            End Using
+            Return bmp
+        End With
+    End Function
+
+    ''' <summary>
+    ''' 获取裁切的图片,这个方法不会导致原对象改变且会返回一个新的对象。
+    ''' </summary>
+    Public Function Clip(rect As System.Drawing.Rectangle) As System.Drawing.Bitmap
+        With Me
+            Dim img As System.Drawing.Image = Pic
+            Dim bmp As New System.Drawing.Bitmap(rect.Width, rect.Height)
+            Using g As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(bmp)
+                g.DrawImageUnscaled(img, rect)
+            End Using
+            Return bmp
+        End With
+    End Function
+
+    ''' <summary>
+    ''' 获取左右翻转的图片,这个方法不会导致原对象改变。
+    ''' </summary>
+    Public Function LeftRightFilp() As System.Drawing.Bitmap
+        Dim bmp As New System.Drawing.Bitmap(Pic)
+        bmp.RotateFlip(System.Drawing.RotateFlipType.RotateNoneFlipX)
+        Return bmp
+    End Function
+
+    ''' <summary>
+    ''' 将图像保存到文件。
+    ''' </summary>
+    Public Sub Save(FilePath As String)
+        Dim encoder As BitmapEncoder = New PngBitmapEncoder()
+        encoder.Frames.Add(BitmapFrame.Create(Me))
+        Using fileStream = New System.IO.FileStream(FilePath, FileMode.Create)
+            encoder.Save(fileStream)
+        End Using
+    End Sub
+
+End Class

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

@@ -0,0 +1,887 @@
+Public Class CrashAnalyzer
+    Private Shared IsAnalyzeCacheCleared As Boolean = False
+    Private Shared IsAnalyzeCacheClearedLock As New Object
+
+    '构造函数
+    Private TempFolder As String
+    Public Sub New(UUID As Integer)
+        '清理缓存
+        SyncLock IsAnalyzeCacheClearedLock
+            If Not IsAnalyzeCacheCleared Then
+                Try
+                    DeleteDirectory(PathTemp & "CrashAnalyzer")
+                Catch ex As Exception
+                    Log(ex, "清理崩溃分析缓存失败")
+                End Try
+                IsAnalyzeCacheCleared = True
+            End If
+        End SyncLock
+        '构建文件结构
+        TempFolder = PathTemp & "CrashAnalyzer\" & UUID & RandomInteger(0, 99999999) & "\"
+        DeleteDirectory(TempFolder)
+        Directory.CreateDirectory(TempFolder & "Temp\")
+        Directory.CreateDirectory(TempFolder & "Report\")
+        Log("[Crash] 崩溃分析暂存文件夹:" & TempFolder)
+    End Sub
+
+    '1:准备用于分析的 Log 文件
+    Private AnalyzeRawFiles As New List(Of KeyValuePair(Of String, String())) '暂存的日志文件:文件完整路径 -> 文件内容
+    ''' <summary>
+    ''' 将可用于分析的日志存储到 AnalyzeRawFiles。
+    ''' </summary>
+    ''' <param name="LatestLog">从 PCL2 捕获到的最后 200 行程序输出。</param>
+    Public Sub Collect(VersionPathIndie As String, Optional LatestLog As IList(Of String) = Nothing)
+        Log("[Crash] 步骤 1:收集日志文件")
+
+        '简单收集可能的日志文件路径
+        Dim PossibleLogs As New List(Of String)
+        Try
+            Dim DirInfo As New DirectoryInfo(VersionPathIndie & "crash-reports\")
+            If DirInfo.Exists Then
+                For Each File In DirInfo.EnumerateFiles
+                    PossibleLogs.Add(File.FullName)
+                Next
+            End If
+        Catch ex As Exception
+            Log(ex, "收集 Minecraft 崩溃日志文件夹下的日志失败")
+        End Try
+        Try
+            For Each File In New DirectoryInfo(VersionPathIndie).Parent.Parent.EnumerateFiles
+                If If(File.Extension, "") <> ".log" Then Continue For
+                PossibleLogs.Add(File.FullName)
+            Next
+        Catch ex As Exception
+            Log(ex, "收集 Minecraft 主文件夹下的日志失败")
+        End Try
+        Try
+            For Each File In New DirectoryInfo(VersionPathIndie).EnumerateFiles
+                If If(File.Extension, "") <> ".log" Then Continue For
+                PossibleLogs.Add(File.FullName)
+            Next
+        Catch ex As Exception
+            Log(ex, "收集 Minecraft 隔离文件夹下的日志失败")
+        End Try
+        PossibleLogs.Add(VersionPathIndie & "logs\latest.log") 'Minecraft 日志
+        PossibleLogs.Add(VersionPathIndie & "logs\debug.log") 'Minecraft Debug 日志
+        PossibleLogs = ArrayNoDouble(PossibleLogs)
+
+        '确定最新的日志文件
+        Dim RightLogs As New List(Of String)
+        For Each LogFile In PossibleLogs
+            Try
+                Dim Info As New FileInfo(LogFile)
+                If Not Info.Exists Then Continue For
+                Dim Time = Math.Abs((Info.LastWriteTime - Date.Now).TotalMinutes)
+                If Time < 3 AndAlso Info.Length > 0 Then
+                    RightLogs.Add(LogFile)
+                    Log("[Crash] 可能可用的日志文件:" & LogFile & "(" & Math.Round(Time, 1) & " 分钟)")
+                End If
+            Catch ex As Exception
+                Log(ex, "确认崩溃日志时间失败(" & LogFile & ")")
+            End Try
+        Next
+        If RightLogs.Count = 0 Then Log("[Crash] 未发现可能可用的日志文件")
+
+        '将可能可用的日志文件导出
+        For Each FilePath In RightLogs
+            Try
+                If FilePath.Contains("crash-") Then
+                    AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(FilePath, ReadFile(FilePath).Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Split(vbCr)))
+                Else
+                    AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(FilePath, File.ReadAllLines(FilePath, Encoding.UTF8)))
+                End If
+            Catch ex As Exception
+                Log(ex, "读取可能的崩溃日志文件失败(" & FilePath & ")")
+            End Try
+        Next
+        If LatestLog IsNot Nothing AndAlso LatestLog.Count > 0 Then
+            Dim RawOutput As String = Join(LatestLog, vbCrLf)
+            Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
+            WriteFile(TempFolder & "RawOutput.log", RawOutput)
+            AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TempFolder & "RawOutput.log", LatestLog.ToArray))
+            LatestLog.Clear()
+        End If
+
+        Log("[Crash] 步骤 1:收集日志文件完成,收集到 " & AnalyzeRawFiles.Count & " 个文件")
+    End Sub
+    ''' <summary>
+    ''' 从文件路径直接导入日志文件或崩溃报告压缩包。
+    ''' </summary>
+    Public Sub Import(FilePath As String)
+        Log("[Crash] 步骤 1:自主导入日志文件")
+
+        '解压压缩包
+        Try
+            Dim Info As New FileInfo(FilePath)
+            If Not Info.Exists OrElse Info.Length = 0 Then Exit Try
+            If Not FilePath.ToLower.EndsWith(".jar") AndAlso ExtractFile(FilePath, TempFolder & "Temp\") Then
+                '解压成功
+                Log("[Crash] 已解压导入的日志文件:" & FilePath)
+            Else
+                '解压失败
+                File.Copy(FilePath, TempFolder & "Temp\" & GetFileNameFromPath(FilePath))
+                Log("[Crash] 已复制导入的日志文件:" & FilePath)
+            End If
+        Catch ex As Exception
+            Log(ex, "解压导入文件中的压缩包失败")
+        End Try
+
+        '导入其中的日志文件
+        For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles
+            Try
+                If Not TargetFile.Exists OrElse TargetFile.Length = 0 Then Continue For
+                Dim Ext As String = TargetFile.Extension.ToLower
+                If Ext = ".log" OrElse Ext = ".txt" Then
+                    If TargetFile.Name.StartsWith("crash-") Then
+                        AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TargetFile.FullName, ReadFile(TargetFile.FullName).Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Split(vbCr)))
+                    Else
+                        AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TargetFile.FullName, File.ReadAllLines(TargetFile.FullName, Encoding.UTF8)))
+                    End If
+                End If
+            Catch ex As Exception
+                Log(ex, "导入单个日志文件失败")
+            End Try
+        Next
+
+        Log("[Crash] 步骤 1:自主导入日志文件,收集到 " & AnalyzeRawFiles.Count & " 个文件")
+    End Sub
+
+    '2:确认实际用于分析的 Log 文本
+    Private Enum AnalyzeFileType
+        HsErr
+        MinecraftLog
+        ExtraLog
+        CrashReport
+    End Enum
+    ''' <summary>
+    ''' 从 AnalyzeRawFiles 中提取实际有用的文本片段存储到 AnalyzeFiles,并整理可用于生成报告的文件。
+    ''' 返回找到的用于分析的项目数。
+    ''' </summary>
+    Public Function Prepare() As Integer
+        Log("[Crash] 步骤 2:准备日志文本")
+
+        '对日志文件进行分类
+        Dim TotalFiles As New List(Of KeyValuePair(Of AnalyzeFileType, KeyValuePair(Of String, String())))
+        For Each LogFile In AnalyzeRawFiles
+            Dim FileName As String = GetFileNameFromPath(LogFile.Key)
+            Dim TargetType As AnalyzeFileType
+            If FileName.StartsWith("hs_err") Then
+                TargetType = AnalyzeFileType.HsErr
+            ElseIf FileName.StartsWith("crash-") Then
+                TargetType = AnalyzeFileType.CrashReport
+            ElseIf FileName = "latest.log" OrElse FileName = "latest log.txt" OrElse
+                   FileName = "debug.log" OrElse FileName = "debug log.txt" OrElse FileName = "游戏崩溃前的输出.txt" OrElse
+                   FileName = "rawoutput.log" OrElse FileName = "启动器日志.txt" OrElse FileName = "PCL2 启动器日志.txt" OrElse FileName = "log1.txt" Then
+                TargetType = AnalyzeFileType.MinecraftLog
+            ElseIf FileName.EndsWith(".log") OrElse FileName.EndsWith(".txt") Then
+                TargetType = AnalyzeFileType.ExtraLog
+            Else
+                Log("[Crash] " & FileName & " 分类为 Ignore")
+                Continue For
+            End If
+            If LogFile.Value.Count = 0 Then
+                Log("[Crash] " & FileName & " 由于内容为空跳过")
+            Else
+                TotalFiles.Add(New KeyValuePair(Of AnalyzeFileType, KeyValuePair(Of String, String()))(TargetType, LogFile))
+                Log("[Crash] " & FileName & " 分类为 " & GetStringFromEnum(TargetType))
+            End If
+        Next
+
+        '将分类后的文件分别写入
+        For Each SelectType In {AnalyzeFileType.MinecraftLog, AnalyzeFileType.HsErr, AnalyzeFileType.ExtraLog, AnalyzeFileType.CrashReport}
+            '获取该种类的所有文件 {文件路径 -> 文件内容行}
+            Dim SelectedFiles As New List(Of KeyValuePair(Of String, String()))
+            For Each File In TotalFiles
+                If SelectType = File.Key Then SelectedFiles.Add(File.Value)
+            Next
+            If SelectedFiles.Count = 0 Then Continue For
+            Try
+                '根据文件类别判断
+                Select Case SelectType
+                    Case AnalyzeFileType.HsErr, AnalyzeFileType.CrashReport
+                        '获取文件的修改日期
+                        Dim DatedFiles As New SortedList(Of Date, KeyValuePair(Of String, String()))()
+                        For Each File In SelectedFiles
+                            Try
+                                DatedFiles.Add(New FileInfo(File.Key).LastWriteTime, File)
+                            Catch ex As Exception
+                                Log(ex, "获取日志文件修改时间失败")
+                                DatedFiles.Add(New Date(1900, 1, 1), File)
+                            End Try
+                        Next
+                        '输出最新的文件
+                        Dim NewestFile As KeyValuePair(Of String, String()) = DatedFiles.Last.Value
+                        OutputFiles.Add(NewestFile.Key)
+                        If SelectType = AnalyzeFileType.HsErr Then
+                            LogHs = GetHeadTailLines(NewestFile.Value, 200, 100)
+                            Log("[Crash] 输出报告:" & NewestFile.Key & ",作为虚拟机错误信息")
+                            Log("[Crash] 导入分析:" & NewestFile.Key & ",作为虚拟机错误信息")
+                        Else
+                            LogCrash = GetHeadTailLines(NewestFile.Value, 300, 700)
+                            Log("[Crash] 输出报告:" & NewestFile.Key & ",作为 Minecraft 崩溃报告")
+                            Log("[Crash] 导入分析:" & NewestFile.Key & ",作为 Minecraft 崩溃报告")
+                        End If
+                    Case AnalyzeFileType.MinecraftLog
+                        LogMc = ""
+                        '创建文件名词典
+                        Dim FileNameDict As New Dictionary(Of String, KeyValuePair(Of String, String()))
+                        For Each SelectedFile In SelectedFiles
+                            DictionaryAdd(FileNameDict, GetFileNameFromPath(SelectedFile.Key).ToLower, SelectedFile)
+                            OutputFiles.Add(SelectedFile.Key)
+                            Log("[Crash] 输出报告:" & SelectedFile.Key & ",作为 Minecraft 或启动器日志")
+                        Next
+                        '选择一份最佳的来自启动器的游戏日志
+                        For Each FileName As String In {"rawoutput.log", "启动器日志.txt", "log1.txt", "游戏崩溃前的输出.txt", "PCL2 启动器日志.txt"}
+                            If FileNameDict.ContainsKey(FileName) Then
+                                Dim CurrentLog = FileNameDict(FileName)
+                                '截取 “以下为游戏输出的最后一段内容” 后的内容
+                                Dim HasLauncherMark As Boolean = False
+                                For Each Line In CurrentLog.Value
+                                    If HasLauncherMark Then
+                                        LogMc += Line & vbLf
+                                    ElseIf Line.Contains("以下为游戏输出的最后一段内容") Then
+                                        HasLauncherMark = True
+                                        Log("[Crash] 找到 PCL2 输出的游戏实时日志头")
+                                    End If
+                                Next
+                                '导入后 500 行
+                                If Not HasLauncherMark Then LogMc += GetHeadTailLines(CurrentLog.Value, 0, 500)
+                                LogMc = LogMc.TrimEnd(vbCrLf.ToCharArray) & vbLf & "[" '路径包含中文且存在编码问题导致找不到或无法加载主类的末端检测
+                                Log("[Crash] 导入分析:" & CurrentLog.Key & ",作为启动器日志")
+                                Exit For
+                            End If
+                        Next
+                        '选择一份最佳的 Minecraft Log
+                        For Each FileName As String In {"latest.log", "latest log.txt", "debug.log", "debug log.txt"}
+                            If FileNameDict.ContainsKey(FileName) Then
+                                Dim CurrentLog = FileNameDict(FileName)
+                                LogMc += GetHeadTailLines(CurrentLog.Value, 250, 500) & vbLf & "["
+                                Log("[Crash] 导入分析:" & CurrentLog.Key & ",作为 Minecraft 日志")
+                                Exit For
+                            End If
+                        Next
+                        '检查错误
+                        If LogMc = "" Then
+                            LogMc = Nothing
+                            Throw New Exception("无法找到匹配的 Minecraft Log")
+                        End If
+                    Case AnalyzeFileType.ExtraLog
+                        '全部丢过去
+                        For Each SelectedFile In SelectedFiles
+                            OutputFiles.Add(SelectedFile.Key)
+                            Log("[Crash] 输出报告:" & SelectedFile.Key & ",作为额外日志")
+                        Next
+                End Select
+            Catch ex As Exception
+                Log(ex, "分类处理日志文件时出错")
+            End Try
+        Next
+
+        '获取种类数
+        Dim ResultCount As Integer = If(LogMc Is Nothing, 0, 1) + If(LogHs Is Nothing, 0, 1) + If(LogCrash Is Nothing, 0, 1)
+        If ResultCount = 0 Then
+            Log("[Crash] 步骤 2:准备日志文本完成,没有任何可供分析的日志")
+        Else
+            Log(("[Crash] 步骤 2:准备日志文本完成,找到" & If(LogMc Is Nothing, "", "游戏日志、") & If(LogHs Is Nothing, "", "虚拟机日志、") & If(LogCrash Is Nothing, "", "崩溃日志、")).TrimEnd("、") & "用作分析")
+        End If
+        Return ResultCount
+
+    End Function
+    ''' <summary>
+    ''' 输出字符串的前后某些行,并统一行尾为 vbLf (\r)、删除空行。
+    ''' </summary>
+    Private Function GetHeadTailLines(Raw As String(), HeadLines As Integer, TailLines As Integer) As String
+        If Raw.Length <= HeadLines + TailLines Then Return Join(Raw, vbLf)
+        Dim Result As New StringBuilder
+        For i = 0 To Raw.Count - 1
+            If i < HeadLines OrElse Raw.Count - i < TailLines Then
+                If Raw(i) = "" Then Continue For
+                Result.Append(Raw(i) & vbLf)
+            End If
+        Next
+        Return Result.ToString
+    End Function
+
+    '3:根据文本分析崩溃原因
+    Private LogMc As String = Nothing, LogHs As String = Nothing, LogCrash As String = Nothing
+    Private LogAll As String
+    '可能导致崩溃的原因与附加信息
+    Private CrashReasons As New Dictionary(Of CrashReason, List(Of String))
+    ''' <summary>
+    ''' 导致崩溃的原因枚举。
+    ''' </summary>
+    Private Enum CrashReason
+        Mod文件被解压
+        内存不足
+        使用JDK
+        显卡不支持OpenGL
+        使用OpenJ9
+        Java版本过高
+        显卡驱动不支持导致无法设置像素格式
+        路径包含中文且存在编码问题导致找不到或无法加载主类
+        Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION 'https://bugs.mojang.com/browse/MC-32606
+        AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION 'https://bugs.mojang.com/browse/MC-31618
+        Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION
+        玩家手动触发调试崩溃
+        光影或资源包导致OpenGL1282错误
+        文件或内容校验失败
+        确定Mod导致游戏崩溃
+        Mod配置文件导致游戏崩溃
+        ModMixin失败
+        Mod加载器报错
+        Mod初始化失败
+        崩溃日志堆栈分析发现关键字
+        崩溃日志堆栈分析发现Mod名称
+        MC日志堆栈分析发现关键字
+        OptiFine导致无法加载世界 'https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world
+        特定方块导致崩溃
+        特定实体导致崩溃
+        材质过大或显卡配置不足
+        没有可用的分析文件
+        使用32位Java导致JVM无法分配足够多的内存
+        Mod重复安装
+        Fabric报错
+        Fabric报错并给出解决方案
+        Forge报错
+        低版本Forge与高版本Java不兼容
+        版本Json中存在多个Forge
+        Mod过多导致超出ID限制
+    End Enum
+    ''' <summary>
+    ''' 根据 AnalyzeLogs 与可能的版本信息分析崩溃原因。
+    ''' </summary>
+    Public Sub Analyze(Optional Version As McVersion = Nothing)
+        Log("[Crash] 步骤 3:分析崩溃原因")
+        LogAll = (If(LogMc, "") & If(LogHs, "") & If(LogCrash, "")).ToLower
+
+        '1. 精准日志匹配 1
+        AnalyzeCrit1()
+        If CrashReasons.Count > 0 Then GoTo Done
+
+        '2. 堆栈分析
+        If LogAll.Contains("forge") OrElse LogAll.Contains("fabric") OrElse LogAll.Contains("liteloader") Then
+            '来源崩溃日志的分析
+            If LogCrash IsNot Nothing Then
+                Log("[Crash] 开始进行崩溃日志堆栈分析")
+                Dim Keywords = AnalyzeStackKeyword(LogCrash.Replace("A detailed walkthrough of the error", "¨").Split("¨").First)
+                If Keywords.Count > 0 Then
+                    Dim Names = AnalyzeModName(Keywords)
+                    If Names Is Nothing Then
+                        AppendReason(CrashReason.崩溃日志堆栈分析发现关键字, Keywords)
+                    Else
+                        AppendReason(CrashReason.崩溃日志堆栈分析发现Mod名称, Names)
+                    End If
+                    GoTo Done
+                End If
+            End If
+            '来自 Minecraft 日志的分析
+            If LogMc IsNot Nothing Then
+                Dim Fatals = RegexSearch(LogMc, "/FATAL] [\w\W]+?(?=[\n]+\[)")
+                Log("[Crash] 开始进行 Minecraft 日志堆栈分析,发现 " & Fatals.Count & " 个报错项")
+                If Fatals.Count > 0 Then
+                    Dim Keywords As New List(Of String)
+                    For Each Fatal In Fatals
+                        Keywords.AddRange(AnalyzeStackKeyword(Fatal))
+                    Next
+                    If Keywords.Count > 0 Then
+                        AppendReason(CrashReason.MC日志堆栈分析发现关键字, ArrayNoDouble(Keywords))
+                        GoTo Done
+                    End If
+                End If
+            End If
+        Else
+            Log("[Crash] 可能并未安装 Mod,不进行堆栈分析")
+        End If
+
+        '3. 精准日志匹配 2
+        AnalyzeCrit2()
+
+        '输出到日志
+Done:
+        If CrashReasons.Count = 0 Then
+            Log("[Crash] 步骤 3:分析崩溃原因完成,未找到可能的原因")
+        Else
+            Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " & CrashReasons.Count & " 条可能的原因")
+            For Each Reason In CrashReasons
+                Log("[Crash]  - " & GetStringFromEnum(Reason.Key) & If(Reason.Value.Count > 0, "(" & Join(Reason.Value, ";") & ")", ""))
+            Next
+        End If
+    End Sub
+    ''' <summary>
+    ''' 增加一个可能的崩溃原因。
+    ''' </summary>
+    Private Sub AppendReason(Reason As CrashReason, Optional Additional As ICollection(Of String) = Nothing)
+        If CrashReasons.ContainsKey(Reason) Then
+            If Additional IsNot Nothing Then
+                CrashReasons(Reason).AddRange(Additional)
+                CrashReasons(Reason) = ArrayNoDouble(CrashReasons(Reason))
+            End If
+        Else
+            CrashReasons.Add(Reason, New List(Of String)(If(Additional, {})))
+        End If
+        Log("[Crash] 可能的崩溃原因:" & GetStringFromEnum(Reason) & If(Additional IsNot Nothing AndAlso Additional.Count > 0, "(" & Join(Additional, ";") & ")", ""))
+    End Sub
+    Private Sub AppendReason(Reason As CrashReason, Additional As String)
+        AppendReason(Reason, If(String.IsNullOrEmpty(Additional), Nothing, New List(Of String) From {Additional}))
+    End Sub
+
+    '具体的分析代码
+    ''' <summary>
+    ''' 进行精准日志匹配。匹配优先级高于堆栈分析的崩溃。
+    ''' </summary>
+    Private Sub AnalyzeCrit1()
+
+        '空白分析
+        If LogMc Is Nothing AndAlso LogHs Is Nothing AndAlso LogCrash Is Nothing Then
+            AppendReason(CrashReason.没有可用的分析文件)
+            Exit Sub
+        End If
+
+        '崩溃报告分析,高优先级
+        If LogCrash IsNot Nothing Then
+            If LogCrash.Contains("Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass") Then AppendReason(CrashReason.Java版本过高)
+        End If
+
+        '游戏日志分析
+        If LogMc IsNot Nothing Then
+            If LogMc.Contains("Found multiple arguments for option fml.forgeVersion, but you asked for only one") Then AppendReason(CrashReason.版本Json中存在多个Forge)
+            If LogMc.Contains("The driver does not appear to support OpenGL") Then AppendReason(CrashReason.显卡不支持OpenGL)
+            If LogMc.Contains("java.lang.ClassCastException: java.base/jdk") Then AppendReason(CrashReason.使用JDK)
+            If LogMc.Contains("java.lang.ClassCastException: class jdk.") Then AppendReason(CrashReason.使用JDK)
+            If LogMc.Contains("Open J9 is not supported") OrElse LogMc.Contains("OpenJ9 is incompatible") OrElse LogMc.Contains(".J9VMInternals.") Then AppendReason(CrashReason.使用OpenJ9)
+            If LogMc.Contains("because module java.base does not export") Then AppendReason(CrashReason.Java版本过高)
+            If LogMc.Contains("java.lang.ClassNotFoundException: java.lang.invoke.LambdaMetafactory") Then AppendReason(CrashReason.Java版本过高)
+            If LogMc.Contains("The directories below appear to be extracted jar files. Fix this before you continue.") Then AppendReason(CrashReason.Mod文件被解压)
+            If LogMc.Contains("Extracted mod jars found, loading will NOT continue") Then AppendReason(CrashReason.Mod文件被解压)
+            If LogMc.Contains("Couldn't set pixel format") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
+            If LogMc.Contains("java.lang.OutOfMemoryError") Then AppendReason(CrashReason.内存不足)
+            If LogMc.Contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier") Then AppendReason(CrashReason.低版本Forge与高版本Java不兼容)
+            If LogMc.Contains("1282: Invalid operation") Then AppendReason(CrashReason.光影或资源包导致OpenGL1282错误)
+            If LogMc.Contains("signer information does not match signer information of other classes in the same package") Then AppendReason(CrashReason.文件或内容校验失败, If(RegexSeek(LogMc, "(?<=class "")[^']+(?=""'s signer information)"), "").TrimEnd(vbCrLf))
+            If LogMc.Contains("An exception was thrown, the game will display an error screen and halt.") Then AppendReason(CrashReason.Forge报错, If(RegexSeek(LogMc, "(?<=the game will display an error screen and halt[\s\S]+?Exception: )[\s\S]+?(?=\n\tat)"), "").TrimEnd(vbCrLf))
+            If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)(\t - [^\n]+\n)+"), ""), "(?<=\t)[^\n]+"), vbLf))
+            If LogMc.Contains("Maybe try a lower resolution resourcepack?") Then AppendReason(CrashReason.材质过大或显卡配置不足)
+            If LogMc.Contains("java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z") AndAlso LogMc.Contains("OptiFine") Then AppendReason(CrashReason.OptiFine导致无法加载世界)
+            If LogMc.Contains("Could not reserve enough space") Then
+                If LogMc.Contains("for 1048576KB object heap") Then
+                    AppendReason(CrashReason.使用32位Java导致JVM无法分配足够多的内存)
+                Else
+                    AppendReason(CrashReason.内存不足)
+                End If
+            End If
+            'Mod 重复安装
+            If LogMc.Contains("DuplicateModsFoundException") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(LogMc, "(?<=\n\t[\w]+ : [A-Z]{1}:[^\n]+(/|\\))[^/\\\n]+?.jar", RegularExpressions.RegexOptions.IgnoreCase))
+            If LogMc.Contains("Found a duplicate mod") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "Found a duplicate mod[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
+            If LogMc.Contains("ModResolutionException: Duplicate") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "ModResolutionException: Duplicate[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
+            '找不到或无法加载主类
+            If (RegexCheck(LogMc, "^[^\n.]+.\w+.[^\n]+\n\[$") OrElse RegexCheck(LogMc, "^\[[^\]]+\] [^\n.]+.\w+.[^\n]+\n\[")) AndAlso
+               Not (LogMc.Contains("at net.") OrElse LogMc.Contains("/INFO]")) AndAlso
+               LogHs Is Nothing AndAlso LogCrash Is Nothing Then
+                AppendReason(CrashReason.路径包含中文且存在编码问题导致找不到或无法加载主类)
+            End If
+            'Mod 导致的崩溃
+            If LogMc.Contains("Mixin prepare failed ") OrElse LogMc.Contains("mixin.injection.throwables.InjectionError") OrElse LogMc.Contains(".mixins.json] FAILED during )") Then
+                Dim ModId As String = RegexSeek(LogMc, "(?<=in )[^./ ]+(?=.mixins.json.+failed injection check)")
+                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)")
+                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[)[^./ ]+(?=.mixins.json\] FAILED during )")
+                AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            End If
+            If LogMc.Contains("Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogMc, "[^\n]+?(?)"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            If LogMc.Contains("Failed to create mod instance.") Then AppendReason(CrashReason.Mod初始化失败, TryAnalyzeModName(If(RegexSeek(LogMc, "(?<=Failed to create mod instance. ModID: )[^,]+"), If(RegexSeek(LogMc, "(?<=Failed to create mod instance. ModId )[^\n]+(?= for )"), "")).TrimEnd(vbCrLf)))
+        End If
+
+        '虚拟机日志分析
+        If LogHs IsNot Nothing Then
+            If LogHs.Contains("The system is out of physical RAM or swap space") Then AppendReason(CrashReason.内存不足)
+            If LogHs.Contains("Out of Memory Error") Then AppendReason(CrashReason.内存不足)
+            If LogHs.Contains("EXCEPTION_ACCESS_VIOLATION") Then
+                If LogHs.Contains("# C  [ig") Then AppendReason(CrashReason.Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION)
+                If LogHs.Contains("# C  [atio") Then AppendReason(CrashReason.AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION)
+                If LogHs.Contains("# C  [nvoglv") Then AppendReason(CrashReason.Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION)
+            End If
+        End If
+
+        '崩溃报告分析
+        If LogCrash IsNot Nothing Then
+            If LogCrash.Contains("maximum id range exceeded") Then AppendReason(CrashReason.Mod过多导致超出ID限制)
+            If LogCrash.Contains("Entity being rendered") AndAlso LogCrash.Contains(vbTab & "Entity's Exact location: ") Then AppendReason(CrashReason.特定实体导致崩溃, If(RegexSeek(LogCrash, "(?<=\tEntity Type: )[^\n]+(?= \()"), "") & " (" & If(RegexSeek(LogCrash, "(?<=\tEntity's Exact location: )[^\n]+"), "").TrimEnd(vbCrLf.ToCharArray) & ")")
+            If LogCrash.Contains("java.lang.OutOfMemoryError") Then AppendReason(CrashReason.内存不足)
+            If LogCrash.Contains("Pixel format not accelerated") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
+            If LogCrash.Contains("Manually triggered debug crash") Then AppendReason(CrashReason.玩家手动触发调试崩溃)
+            'Mod 导致的崩溃
+            If LogCrash.Contains("-- MOD ") Then
+                Dim LogLeft = LogCrash.Split("-- MOD").Last
+                If LogLeft.Contains("Failure message: MISSING") Then
+                    AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrash, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
+                Else
+                    AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
+                End If
+            End If
+            If LogCrash.Contains("Multiple entries with same key: ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Multiple entries with same key: )[^=]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            If LogCrash.Contains("LoaderExceptionModCrash: Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            If LogCrash.Contains("Failed loading config file ") Then AppendReason(CrashReason.Mod配置文件导致游戏崩溃, {TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Failed loading config file .+ for modid )[^\n]+"), "").TrimEnd(vbCrLf)).First, If(RegexSeek(LogCrash, "(?<=Failed loading config file ).+(?= of type)"), "").TrimEnd(vbCrLf)})
+        End If
+
+    End Sub
+    ''' <summary>
+    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
+    ''' </summary>
+    Private Sub AnalyzeCrit2()
+
+        '游戏日志分析
+        If LogMc IsNot Nothing Then
+            If LogMc.Contains("]: Warnings were found!") Then AppendReason(CrashReason.Fabric报错, If(RegexSeek(LogMc, "(?<=\]: Warnings were found! ?[\n]+)[\w\W]+?(?=[\n]+\[)"), "").Trim(vbCrLf.ToCharArray))
+        End If
+
+        '崩溃报告分析
+        If LogCrash IsNot Nothing Then
+            If LogCrash.Contains(vbTab & "Block location: World: ") Then AppendReason(CrashReason.特定方块导致崩溃, If(RegexSeek(LogCrash, "(?<=\tBlock: Block\{)[^\}]+"), "") & " " & If(RegexSeek(LogCrash, "(?<=\tBlock location: World: )\([^\)]+\)"), ""))
+            If LogCrash.Contains(vbTab & "Entity's Exact location: ") Then AppendReason(CrashReason.特定实体导致崩溃, If(RegexSeek(LogCrash, "(?<=\tEntity Type: )[^\n]+(?= \()"), "") & " (" & If(RegexSeek(LogCrash, "(?<=\tEntity's Exact location: )[^\n]+"), "").TrimEnd(vbCrLf.ToCharArray) & ")")
+        End If
+
+    End Sub
+    ''' <summary>
+    ''' 从堆栈中提取 Mod ID 关键字。若失败则返回空列表。
+    ''' </summary>
+    Private Function AnalyzeStackKeyword(ErrorStack As String) As List(Of String)
+
+        '进行正则匹配
+        Dim StackSearchResults As List(Of String) = RegexSearch(If(ErrorStack, "") & vbCrLf, "(?<=\n[^{]+)[a-zA-Z]+\w+\.[a-zA-Z]+[\w\.]+(?=\.[\w\.$]+\.)")
+
+        '检查堆栈开头
+        Dim PossibleStacks As New List(Of String)
+        For Each Stack As String In StackSearchResults
+            'If Not Stack.Contains(".") Then Continue For
+            For Each IgnoreStack In {
+                "java", "sun", "javax", "jdk",
+                "org.lwjgl", "com.sun", "net.minecraftforge", "com.mojang", "net.minecraft", "cpw.mods", "com.google", "org.apache", "org.spongepowered", "net.fabricmc", "com.mumfrey",
+                "com.electronwill.nightconfig",
+                "MojangTricksIntelDriversForPerformance_javaw"}
+                If Stack.StartsWith(IgnoreStack) Then GoTo NextStack
+            Next
+            PossibleStacks.Add(Stack.Trim) '.Split("$").First)
+NextStack:
+        Next
+        PossibleStacks = ArrayNoDouble(PossibleStacks)
+        Log("[Crash] 找到 " & PossibleStacks.Count & " 条可能的堆栈信息")
+        If PossibleStacks.Count = 0 Then Return New List(Of String)
+        For Each Stack As String In PossibleStacks
+            Log("[Crash]  - " & Stack)
+        Next
+
+        '检查堆栈关键词
+        Dim PossibleWords As New List(Of String)
+        For Each Stack As String In PossibleStacks
+            Dim Splited = Stack.Split(".")
+            For i = 0 To Math.Min(3, Splited.Count - 1) '最多取前 4 节
+                Dim Word As String = Splited(i)
+                If Word.Length <= 2 OrElse Word.StartsWith("func_") Then Continue For
+                If {"com", "org", "net", "asm", "fml", "mod", "jar", "sun", "lib", "map", "gui", "dev", "nio", "api",
+                    "core", "init", "mods", "main", "file", "game", "load", "read", "done", "util", "tile", "item", "base",
+                    "forge", "setup", "block", "model", "mixin", "event",
+                    "common", "server", "config", "loader", "launch", "entity", "assist", "client", "modapi", "mojang", "shader", "events", "github",
+                    "preinit", "preload", "machine", "reflect", "channel", "general", "handler",
+                    "optifine", "minecraft", "transformers", "universal", "internal", "multipart", "minecraftforge", "override"
+                   }.Contains(Word.ToLower) Then Continue For
+                PossibleWords.Add(Word.Trim)
+            Next
+        Next
+        PossibleWords = ArrayNoDouble(PossibleWords)
+        Log("[Crash] 从堆栈信息中找到 " & PossibleWords.Count & " 个可能的 Mod ID 关键词")
+        If PossibleWords.Count > 0 Then Log("[Crash]  - " & Join(PossibleWords, ", "))
+        If PossibleWords.Count > 10 Then
+            Log("[Crash] 关键词过多,考虑匹配出错,不纳入考虑")
+            Return New List(Of String)
+        Else
+            Return PossibleWords
+        End If
+
+    End Function
+    ''' <summary>
+    ''' 根据 Mod 关键词与详细信息(崩溃报告第二部分)与,获取实际的 Mod 名称。
+    ''' 若失败则返回 Nothing。
+    ''' </summary>
+    Private Function AnalyzeModName(Keywords As List(Of String)) As List(Of String)
+        Dim ModFileNames As New List(Of String)
+
+        '预处理关键词(分割括号)
+        Dim RealKeywords As New List(Of String)
+        For Each Keyword In Keywords
+            For Each SubKeyword In Keyword.Split("(")
+                RealKeywords.Add(SubKeyword.Trim(" )".ToCharArray))
+            Next
+        Next
+        Keywords = RealKeywords
+
+        '获取崩溃报告对应部分
+        If LogCrash Is Nothing Then Return Nothing
+        If Not LogCrash.Contains("A detailed walkthrough of the error") Then Return Nothing
+        Dim Details As String = LogCrash.Replace("A detailed walkthrough of the error", "¨")
+        Dim IsFabricDetail As Boolean = Details.Contains("Fabric Mods") '是否为 Fabric 信息格式
+        If IsFabricDetail Then
+            Details = Details.Replace("Fabric Mods", "¨")
+            Log("[Crash] 检测到 Fabric Mod 信息格式")
+        End If
+        Details = Details.Split("¨").Last
+
+        '[Forge] 获取所有包含 .jar 的行
+        '[Fabric] 获取所有包含 Mod 信息的行
+        Dim ModNameLines As New List(Of String)
+        For Each Line In Details.Split(vbLf)
+            If Line.ToLower.Contains(".jar") OrElse
+               (IsFabricDetail AndAlso Line.StartsWith(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
+        Next
+        Log("[Crash] 找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
+
+        '获取 Mod ID 与关键词的匹配行
+        Dim HintLines As New List(Of String)
+        For Each KeyWord As String In Keywords
+            For Each ModString As String In ModNameLines
+                Dim RealModString As String = ModString.ToLower.Replace("_", "")
+                If Not RealModString.Contains(KeyWord.ToLower.Replace("_", "")) Then Continue For
+                If RealModString.Contains("minecraft.jar") OrElse RealModString.Contains(" forge-") Then Continue For
+                HintLines.Add(ModString.Trim(vbCrLf.ToCharArray))
+                Exit For
+            Next
+        Next
+        HintLines = ArrayNoDouble(HintLines)
+        Log("[Crash] 找到 " & HintLines.Count & " 个可能的崩溃 Mod 匹配行")
+        For Each ModName As String In HintLines
+            Log("[Crash]  - " & ModName)
+        Next
+
+        '从 Mod 匹配行中提取 .jar 文件的名称
+        For Each Line As String In HintLines
+            Dim Name As String
+            If IsFabricDetail Then
+                Name = RegexSeek(Line, "(?<=: )[^\n]+(?= [^\n]+)")
+            Else
+                Name = RegexSeek(Line, "(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar", RegularExpressions.RegexOptions.IgnoreCase)
+            End If
+            If Name IsNot Nothing Then ModFileNames.Add(Name)
+        Next
+        ModFileNames = ArrayNoDouble(ModFileNames)
+        Log("[Crash] 找到 " & ModFileNames.Count & " 个可能的崩溃 Mod 文件名")
+        For Each ModFileName As String In ModFileNames
+            Log("[Crash]  - " & ModFileName)
+        Next
+
+        Return If(ModFileNames.Count = 0, Nothing, ModFileNames)
+    End Function
+    ''' <summary>
+    ''' 尝试获取 Mod 名称,若失败则返回原关键字。
+    ''' </summary>
+    Private Function TryAnalyzeModName(Keywords As String) As List(Of String)
+        Dim RawList As New List(Of String) From {Keywords}
+        If String.IsNullOrEmpty(Keywords) Then Return RawList
+        Return If(AnalyzeModName(RawList), RawList)
+    End Function
+
+    '4:根据原因输出信息
+    Private OutputFiles As New List(Of String)
+    ''' <summary>
+    ''' 弹出崩溃弹窗,并指导导出崩溃报告。
+    ''' </summary>
+    Public Sub Output(IsHandAnalyze As Boolean, Optional ExtraFiles As List(Of String) = Nothing)
+        '弹窗提示
+        FrmMain.ShowWindowToTop()
+        Select Case MyMsgBox(GetAnalyzeResult(IsHandAnalyze), If(IsHandAnalyze, "错误报告分析结果", "Minecraft 出现错误"), "确定", If(IsHandAnalyze, "", "导出错误报告"))
+            Case 2
+                Dim FileAddress As String = Nothing
+                Try
+                    '获取文件路径
+                    RunInUiWait(Sub() FileAddress = SelectAs("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
+                    If String.IsNullOrEmpty(FileAddress) Then Exit Sub
+                    Directory.CreateDirectory(GetPathFromFullPath(FileAddress))
+                    If File.Exists(FileAddress) Then File.Delete(FileAddress)
+                    '输出诊断信息
+                    FeedbackInfo()
+                    LogFlush()
+                    '复制文件
+                    If ExtraFiles IsNot Nothing Then OutputFiles.AddRange(ExtraFiles)
+                    For Each OutputFile In OutputFiles
+                        Try
+                            Dim FileName As String = GetFileNameFromPath(OutputFile)
+                            Select Case FileName
+                                Case "LatestLaunch.bat"
+                                    FileName = "启动脚本.bat"
+                                Case "Log1.txt"
+                                    FileName = "PCL2 启动器日志.txt"
+                                Case "RawOutput.log"
+                                    FileName = "游戏崩溃前的输出.txt"
+                            End Select
+                            File.Copy(OutputFile, TempFolder & "Report\" & FileName, True)
+                        Catch ex As Exception
+                            Log(ex, "复制错误报告文件失败(" & OutputFile & ")")
+                        End Try
+                    Next
+                    '导出报告
+                    Compression.ZipFile.CreateFromDirectory(TempFolder & "Report\", FileAddress)
+                    DeleteDirectory(TempFolder & "Report\")
+                    Hint("错误报告已导出!", HintType.Finish)
+                Catch ex As Exception
+                    Log(ex, "导出错误报告失败", LogLevel.Feedback)
+                    Exit Sub
+                End Try
+                Try
+                    Process.Start("explorer", "/select," & FileAddress)
+                Catch ex As Exception
+                    Log(ex, "打开错误报告的存放文件夹失败")
+                End Try
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 获取崩溃分析的结果描述。
+    ''' </summary>
+    Private Function GetAnalyzeResult(IsHandAnalyze As Boolean) As String
+
+        '非一个结果的处理
+        Dim ResultString As String
+        If CrashReasons.Count = 0 Then
+            If IsHandAnalyze Then
+                Return "很抱歉,PCL2 无法确定错误原因。"
+            Else
+                Return "很抱歉,你的游戏出现了一些问题……" & vbCrLf &
+                       "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。" & vbCrLf &
+                       "你也可以查看错误报告,其中可能会有出错的原因。"
+            End If
+        ElseIf CrashReasons.Count >= 2 Then
+            Hint("错误分析时发现数个可能的原因,窗口中仅显示了第一条,你可以在启动器日志中检查更多可能的原因!", HintType.Finish)
+        End If
+
+        '根据不同原因判断
+        Dim Additional As List(Of String) = CrashReasons.First.Value
+        Select Case CrashReasons.First.Key
+            Case CrashReason.Mod文件被解压
+                ResultString = "由于 Mod 文件被解压了,导致游戏无法继续运行。\n直接把整个 Mod 文件放进 Mod 文件夹中即可,若解压就会导致游戏出错。\n\n请删除 Mod 文件夹中已被解压的 Mod,然后再启动游戏。"
+            Case CrashReason.内存不足
+                ResultString = "Minecraft 内存不足,导致其无法继续运行。\n这很可能是由于你为游戏分配的内存不足,或是游戏的配置要求过高。\n\n你可以在启动设置中增加为游戏分配的内存,删除配置要求较高的材质、Mod、光影。\n如果这依然不奏效,请在开始游戏前尽量关闭其他软件,或者……换台电脑?\h"
+            Case CrashReason.使用OpenJ9
+                ResultString = "游戏因为使用 Open J9 而崩溃了。\n请在启动设置的 Java 选择一项中改用非 OpenJ9 的 Java 8,然后再启动游戏。\n如果你没有安装 JRE 8,你可以从网络中下载、安装一个。"
+            Case CrashReason.使用JDK
+                ResultString = "游戏似乎因为使用 JDK,或 Java 版本过高而崩溃了。\n请在启动设置的 Java 选择一项中改用 JRE 8(Java 8),然后再启动游戏。\n如果你没有安装 JRE 8,你可以从网络中下载、安装一个。"
+            Case CrashReason.Java版本过高
+                ResultString = "游戏似乎因为你所使用的 Java 版本过高而崩溃了。\n请在启动设置的 Java 选择一项中改用 JRE 8(Java 8),然后再启动游戏。\n如果你没有安装 JRE 8,你可以从网络中下载、安装一个。"
+            Case CrashReason.使用32位Java导致JVM无法分配足够多的内存
+                If Environment.Is64BitOperatingSystem Then
+                    ResultString = "你似乎正在使用 32 位 Java,这会导致 Minecraft 无法使用 1GB 以上的内存,进而造成崩溃。\n\n请在启动设置的 Java 选择一项中改用 64 位的 Java 再启动游戏,然后再启动游戏。\n如果你没有安装 64 位的 Java,你可以从网络中下载、安装一个。"
+                Else
+                    ResultString = "你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用 1GB 以上的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h"
+                End If
+            Case CrashReason.崩溃日志堆栈分析发现关键字, CrashReason.MC日志堆栈分析发现关键字
+                If Additional.Count = 1 Then
+                    ResultString = "你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL2 找到了一个可疑的关键词:" & Additional.First & "。\n\n如果你知道它对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h"
+                Else
+                    ResultString = "你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL2 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h"
+                End If
+            Case CrashReason.崩溃日志堆栈分析发现Mod名称
+                If Additional.Count = 1 Then
+                    ResultString = "名为 " & Additional.First & " 的 Mod 可能导致了游戏出错。\n\e\h"
+                Else
+                    ResultString = "可能是以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n\e\h"
+                End If
+            Case CrashReason.确定Mod导致游戏崩溃
+                If Additional.Count = 1 Then
+                    ResultString = "名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h"
+                Else
+                    ResultString = "以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n\e\h"
+                End If
+            Case CrashReason.ModMixin失败
+                If Additional.Count = 1 Then
+                    ResultString = "名称或 ID 为 " & Additional.First & " 的 Mod 注入失败,导致游戏出错。\n这一般代表着该 Mod 存在 Bug,或与当前环境不兼容。\n\e\h"
+                Else
+                    ResultString = "以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n这一般代表着这些 Mod 存在 Bug,或与当前环境不兼容。\n\e\h"
+                End If
+            Case CrashReason.Mod配置文件导致游戏崩溃
+                If Additional(1) Is Nothing Then
+                    ResultString = "名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h"
+                Else
+                    ResultString = "名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错:\n其配置文件 " & Additional(1) & " 存在异常,无法读取。"
+                End If
+            Case CrashReason.Mod初始化失败
+                If Additional.Count = 1 Then
+                    ResultString = "名为 " & Additional.First & " 的 Mod 初始化失败,导致游戏无法继续加载。\n\e\h"
+                Else
+                    ResultString = "以下 Mod 初始化失败,导致游戏无法继续加载:\n - " & Join(Additional, "\n - ") & "\n\e\h"
+                End If
+            Case CrashReason.特定方块导致崩溃
+                If Additional.Count = 1 Then
+                    ResultString = "游戏似乎因为方块 " & Additional.First & " 出现了问题。\n\n你可以创建一个新世界,并观察游戏的运行情况:\n - 若正常运行,则是该方块导致出错,你或许需要使用一些方式删除此方块。\n - 若仍然出错,问题就可能来自其他原因……\h"
+                Else
+                    ResultString = "游戏似乎因为世界中的某些方块出现了问题。\n\n你可以创建一个新世界,并观察游戏的运行情况:\n - 若正常运行,则是某些方块导致出错,你或许需要删除该世界。\n - 若仍然出错,问题就可能来自其他原因……\h"
+                End If
+            Case CrashReason.Mod重复安装
+                If Additional.Count >= 2 Then
+                    ResultString = "你重复安装了多个相同的 Mod:\n - " & Join(Additional, "\n - ") & "\n\n每个 Mod 只能出现一次,请删除重复的 Mod,然后再启动游戏。"
+                Else
+                    ResultString = "你可能重复安装了多个相同的 Mod,导致游戏无法继续加载。\n\n每个 Mod 只能出现一次,请删除重复的 Mod,然后再启动游戏。\e\h"
+                End If
+            Case CrashReason.特定实体导致崩溃
+                If Additional.Count = 1 Then
+                    ResultString = "游戏似乎因为实体 " & Additional.First & " 出现了问题。\n\n你可以创建一个新世界,并生成一个该实体,然后观察游戏的运行情况:\n - 若正常运行,则是该实体导致出错,你或许需要使用一些方式删除此实体。\n - 若仍然出错,问题就可能来自其他原因……\h"
+                Else
+                    ResultString = "游戏似乎因为世界中的某些实体出现了问题。\n\n你可以创建一个新世界,并生成各种实体,观察游戏的运行情况:\n - 若正常运行,则是某些实体导致出错,你或许需要删除该世界。\n - 若仍然出错,问题就可能来自其他原因……\h"
+                End If
+            Case CrashReason.低版本Forge与高版本Java不兼容
+                ResultString = "由于低版本 Forge 与当前 Java 不兼容,导致了游戏崩溃。\n\n请尝试以下解决方案:\n - 更新 Forge 到 36.2.26 或更高版本\n - 换用版本低于 8.0.320 的 Java"
+            Case CrashReason.版本Json中存在多个Forge
+                ResultString = "可能由于使用其他启动器修改了 Forge 版本,当前版本的文件存在异常,导致了游戏崩溃。\n请尝试重新全新安装 Forge,而非使用其他启动器修改 Forge 版本。"
+            Case CrashReason.玩家手动触发调试崩溃
+                ResultString = "* 事实上,你的游戏没有任何问题,这是你自己触发的崩溃。\n* 你难道没有更重要的事要做吗?"
+            Case CrashReason.路径包含中文且存在编码问题导致找不到或无法加载主类
+                ResultString = "由于游戏路径含有中文字符,且 Java 或系统编码存在错误,导致游戏无法运行。\n\n解决这一问题的最简单方法是检查游戏的完整路径,并删除各个文件夹名中的中文字符。\n这包括了游戏的版本名、它所处的 .minecraft 文件夹及其之前的文件夹名。\h"
+            Case CrashReason.OptiFine导致无法加载世界 'https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world
+                ResultString = "你所使用的 OptiFine 可能导致了你的游戏出现问题。\n\n该问题只在特定 OptiFine 版本中出现,你可以尝试更换 OptiFine 的版本。\h"
+            Case CrashReason.显卡驱动不支持导致无法设置像素格式, CrashReason.Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, CrashReason.AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, CrashReason.Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, CrashReason.显卡不支持OpenGL
+                If LogAll.Contains("hd graphics ") Then
+                    ResultString = "你的显卡驱动存在问题,或未使用独立显卡,导致游戏无法正常运行。\n\n如果你的电脑存在独立显卡,请使用独立显卡而非 Intel 核显启动 PCL2 与 Minecraft。\n如果问题依然存在,请尝试升级你的显卡驱动到最新版本,或回退到出厂版本。\n如果还是不行,还可以尝试使用 8.0.51 或更低版本的 Java。\h"
+                Else
+                    ResultString = "你的显卡驱动存在问题,导致游戏无法正常运行。\n\n请尝试升级你的显卡驱动到最新版本,或回退到出厂版本,然后再启动游戏。\n如果还是不行,可以尝试使用 8.0.51 或更低版本的 Java。\n如果问题依然存在,那么你可能需要换个更好的显卡……\h"
+                End If
+            Case CrashReason.材质过大或显卡配置不足
+                ResultString = "你所使用的材质分辨率过高,或显卡配置不足,导致游戏无法继续运行。\n\n如果你正在使用高清材质,请将它移除。\n如果你没有使用材质,那么你可能需要更新显卡驱动,或者换个更好的显卡……\h"
+            Case CrashReason.光影或资源包导致OpenGL1282错误
+                ResultString = "你所使用的光影或材质导致游戏出现了一些问题……\n\n请尝试删除你所添加的这些额外资源。\h"
+            Case CrashReason.Mod过多导致超出ID限制
+                ResultString = "你所安装的 Mod 过多,超出了游戏的 ID 限制,导致了游戏崩溃。\n请尝试安装 JEID 等修复 Mod,或删除部分大型 Mod。"
+            Case CrashReason.文件或内容校验失败
+                ResultString = "部分文件或内容校验失败,导致游戏出现了问题。\n\n请尝试删除游戏(包括 Mod)并重新下载,或尝试在重新下载时使用 VPN。\h"
+            Case CrashReason.Fabric报错
+                If Additional.Count = 1 Then
+                    ResultString = "Fabric 提供了以下错误信息:\n" & Additional.First & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"
+                Else
+                    ResultString = "Fabric 可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h"
+                End If
+            Case CrashReason.Mod加载器报错
+                If Additional.Count = 1 Then
+                    ResultString = "Mod 加载器提供了以下错误信息:\n" & Additional.First & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"
+                Else
+                    ResultString = "Mod 加载器可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h"
+                End If
+            Case CrashReason.Fabric报错并给出解决方案
+                If Additional.Count = 1 Then
+                    ResultString = "Fabric 提供了以下解决方案:\n" & Additional.First & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"
+                Else
+                    ResultString = "Fabric 可能已经提供了解决方案,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h"
+                End If
+            Case CrashReason.Forge报错
+                If Additional.Count = 1 Then
+                    ResultString = "Forge 提供了以下错误信息:\n" & Additional.First & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"
+                Else
+                    ResultString = "Forge 可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h"
+                End If
+            Case CrashReason.没有可用的分析文件
+                ResultString = "你的游戏出现了一些问题,但 PCL2 未能找到相关记录文件,因此无法进行分析。\h"
+            Case Else
+                ResultString = "PCL2 获取到了没有详细信息的错误原因(" & CrashReasons.First.Key & "),请向 PCL2 作者提交反馈以获取详情。\h"
+        End Select
+
+        ResultString = ResultString.
+            Replace("\n", vbCrLf).
+            Replace("\h", If(IsHandAnalyze, "", vbCrLf & "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。")).
+            Replace("\e", If(IsHandAnalyze, "", vbCrLf & "你可以查看错误报告了解错误具体是如何发生的。")).
+            Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf)
+        Return ResultString.Trim(vbCrLf.ToCharArray)
+    End Function
+
+End Class

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

@@ -0,0 +1,1670 @@
+Public Module ModDownload
+
+#Region "DlClient* | Minecraft 客户端"
+
+    ''' <summary>
+    ''' 返回某 Minecraft 版本对应的原版主 Jar 文件的下载信息,要求对应依赖版本已存在。
+    ''' 失败则抛出异常,不需要下载则返回 Nothing。
+    ''' </summary>
+    Public Function DlClientJarGet(Version As McVersion, ReturnNothingOnFileUseable As Boolean) As NetFile
+        '获取底层继承版本
+        Do While Not String.IsNullOrEmpty(Version.InheritVersion)
+            Version = New McVersion(Version.InheritVersion)
+        Loop
+        '检查 Json 是否标准
+        If Version.JsonObject("downloads") Is Nothing OrElse Version.JsonObject("downloads")("client") Is Nothing OrElse Version.JsonObject("downloads")("client")("url") Is Nothing Then
+            Throw New Exception("底层版本 " & Version.Name & " 中无 Jar 文件下载信息")
+        End If
+        '检查文件
+        Dim Checker As New FileChecker(MinSize:=1024, ActualSize:=If(Version.JsonObject("downloads")("client")("size"), -1), Hash:=Version.JsonObject("downloads")("client")("sha1"))
+        If ReturnNothingOnFileUseable Then
+            '是否跳过
+            Dim IsSetupSkip As Boolean = Setup.Get("LaunchAdvanceAssets")
+            Select Case Setup.Get("VersionAdvanceAssets", Version:=Version)
+                Case 0 '使用全局设置
+                Case 1 '开启
+                    IsSetupSkip = False
+                Case 2 '关闭
+                    IsSetupSkip = True
+            End Select
+            If IsSetupSkip AndAlso File.Exists(Version.Path & Version.Name & ".jar") Then Return Nothing '跳过校验
+            If Checker.Check(Version.Path & Version.Name & ".jar") Is Nothing Then Return Nothing '通过校验
+        End If
+        '返回下载信息
+        Dim JarUrl As String = Version.JsonObject("downloads")("client")("url")
+        Return New NetFile(DlSourceLauncherOrMetaGet(JarUrl), Version.Path & Version.Name & ".jar", Checker)
+    End Function
+
+    ''' <summary>
+    ''' 返回某 Minecraft 版本对应的原版主 AssetIndex 文件的下载信息,要求对应依赖版本已存在。
+    ''' 若未找到,则会返回 Legacy 资源文件或 Nothing。
+    ''' </summary>
+    Public Function DlClientAssetIndexGet(Version As McVersion) As NetFile
+        '获取底层继承版本
+        Do While Not String.IsNullOrEmpty(Version.InheritVersion)
+            Version = New McVersion(Version.InheritVersion)
+        Loop
+        '获取信息
+        Dim IndexInfo = McAssetsGetIndex(Version, True, True)
+        Dim IndexAddress As String = PathMcFolder & "assets\indexes\" & IndexInfo("id").ToString & ".json"
+        Log("[Download] 版本 " & Version.Name & " 对应的资源文件索引为 " & IndexInfo("id").ToString)
+        Dim IndexUrl As String = If(IndexInfo("url"), "")
+        If IndexUrl = "" Then
+            Return Nothing
+        Else
+            Return New NetFile(DlSourceLauncherOrMetaGet(IndexUrl, False), IndexAddress, New FileChecker(CanUseExistsFile:=False, IsJson:=True))
+        End If
+    End Function
+
+    ''' <summary>
+    ''' 构造补全某 Minecraft 版本的所有文件的加载器列表。失败会抛出异常。
+    ''' </summary>
+    Public Function DlClientFix(Version As McVersion, CheckAssetsHash As Boolean, AssetsIndexBehaviour As AssetsIndexExistsBehaviour, SkipAssetsDownloadWhileSetupRequired As Boolean) As List(Of LoaderBase)
+        Dim Loaders As New List(Of LoaderBase)
+
+#Region "下载支持库文件"
+        Dim LoadersLib As New List(Of LoaderBase) From {
+                New LoaderTask(Of String, List(Of NetFile))("分析缺失支持库文件", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(Version)) With {.ProgressWeight = 1},
+                New LoaderDownload("下载支持库文件", New List(Of NetFile)) With {.ProgressWeight = 15}
+            }
+        '构造加载器
+        Loaders.Add(New LoaderCombo(Of String)("下载支持库文件(主加载器)", LoadersLib) With {.Block = False, .Show = False, .ProgressWeight = 16})
+#End Region
+
+#Region "下载资源文件"
+        Dim IsSetupSkip As Boolean = Setup.Get("LaunchAdvanceAssets")
+        Select Case Setup.Get("VersionAdvanceAssets", Version:=Version)
+            Case 0
+                '使用全局设置
+            Case 1
+                '开启
+                IsSetupSkip = False
+            Case 2
+                '关闭
+                IsSetupSkip = True
+        End Select
+        If IsSetupSkip Then Log("[Download] 已跳过 Assets 下载")
+        If (Not SkipAssetsDownloadWhileSetupRequired) OrElse Not IsSetupSkip Then
+            Dim LoadersAssets As New List(Of LoaderBase)
+            '获取资源文件索引地址
+            LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析资源文件索引地址",
+                                                                Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+                                                                    Try
+                                                                        Dim IndexFile = DlClientAssetIndexGet(Version)
+                                                                        Dim IndexFileInfo As New FileInfo(IndexFile.LocalPath)
+                                                                        If AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
+                                                                            Task.Output = New List(Of NetFile)
+                                                                        Else
+                                                                            Task.Output = New List(Of NetFile) From {IndexFile}
+                                                                        End If
+                                                                    Catch ex As Exception
+                                                                        Throw New Exception("分析资源文件索引地址失败", ex)
+                                                                    End Try
+                                                                End Sub) With {.ProgressWeight = 0.5, .Show = False})
+            '下载资源文件索引
+            LoadersAssets.Add(New LoaderDownload("下载资源文件索引", New List(Of NetFile)) With {.ProgressWeight = 2})
+            '要求独立更新索引
+            If AssetsIndexBehaviour = AssetsIndexExistsBehaviour.DownloadInBackground Then
+                Dim LoadersAssetsUpdate As New List(Of LoaderBase)
+                Dim TempAddress As String = Nothing
+                Dim RealAddress As String = Nothing
+                LoadersAssetsUpdate.Add(New LoaderTask(Of String, List(Of NetFile))("后台分析资源文件索引地址", Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+                                                                                                        Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Version)
+                                                                                                        RealAddress = BackAssetsFile.LocalPath
+                                                                                                        TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
+                                                                                                        BackAssetsFile.LocalPath = TempAddress
+                                                                                                        Task.Output = New List(Of NetFile) From {BackAssetsFile}
+                                                                                                    End Sub))
+                LoadersAssetsUpdate.Add(New LoaderDownload("后台下载资源文件索引", New List(Of NetFile)))
+                LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引", Sub(Task As LoaderTask(Of List(Of NetFile), String))
+                                                                                                      Try
+                                                                                                          File.Copy(TempAddress, RealAddress, True)
+                                                                                                          McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
+                                                                                                      Catch ex As Exception
+                                                                                                          Log(ex, "后台更新资源文件索引失败", LogLevel.Debug)
+                                                                                                          McLaunchLog("后台更新资源文件索引失败:" & TempAddress)
+                                                                                                      End Try
+                                                                                                  End Sub))
+                Dim Updater As New LoaderCombo(Of String)("后台更新资源文件索引", LoadersAssetsUpdate)
+                Log("[Download] 开始后台更新资源文件索引")
+                Updater.Start()
+            End If
+            '获取资源文件地址
+            LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析缺失资源文件",
+                                                                Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+                                                                    Task.Output = McAssetsFixList(McAssetsGetIndexName(Version), CheckAssetsHash, Task)
+                                                                End Sub) With {.ProgressWeight = 3})
+            '下载资源文件
+            LoadersAssets.Add(New LoaderDownload("下载资源文件", New List(Of NetFile)) With {.ProgressWeight = 25})
+            '构造加载器
+            Loaders.Add(New LoaderCombo(Of String)("下载资源文件(主加载器)", LoadersAssets) With {.Block = False, .Show = False, .ProgressWeight = 30.5})
+        End If
+#End Region
+
+        Return Loaders
+    End Function
+    Public Enum AssetsIndexExistsBehaviour
+        ''' <summary>
+        ''' 如果文件存在,则不进行下载。
+        ''' </summary>
+        DontDownload
+        ''' <summary>
+        ''' 如果文件存在,则启动新的下载加载器进行独立的更新。
+        ''' </summary>
+        DownloadInBackground
+        ''' <summary>
+        ''' 如果文件存在,也同样进行下载。
+        ''' </summary>
+        AlwaysDownload
+    End Enum
+
+#End Region
+
+#Region "DlClientList | Minecraft 客户端 版本列表"
+
+    '主加载器
+    Public Structure DlClientListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Mojang”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的 Json 数据。
+        ''' </summary>
+        Public Value As JObject
+        '''' <summary>
+        '''' 官方源的失败原因。若没有则为 Nothing。
+        '''' </summary>
+        'Public OfficialError As Exception
+    End Structure
+    ''' <summary>
+    ''' Minecraft 客户端 版本列表,主加载器。
+    ''' </summary>
+    Public DlClientListLoader As New LoaderTask(Of Integer, DlClientListResult)("DlClientList Main", AddressOf DlClientListMain)
+    Private Sub DlClientListMain(Loader As LoaderTask(Of Integer, DlClientListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListMojangLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListMojangLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListMojangLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlClientListResult), Integer)(DlClientListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    '各个下载源的分加载器
+    ''' <summary>
+    ''' Minecraft 客户端 版本列表,Mojang 官方源加载器。
+    ''' </summary>
+    Public DlClientListMojangLoader As New LoaderTask(Of Integer, DlClientListResult)("DlClientList Mojang", AddressOf DlClientListMojangMain)
+    Private IsNewClientVersionHinted As Boolean = False
+    Private Sub DlClientListMojangMain(Loader As LoaderTask(Of Integer, DlClientListResult))
+        Dim Json As JObject = NetGetCodeByRequestRetry("https://launchermeta.mojang.com/mc/game/version_manifest.json", IsJson:=True)
+        Try
+            Dim Versions As JArray = Json("versions")
+            If Versions.Count < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Json.ToString & ")")
+            '添加 PCL2 特供项
+            If File.Exists(PathTemp & "Cache\download.json") Then Versions.Merge(GetJson(ReadFile(PathTemp & "Cache\download.json")))
+            '返回
+            Loader.Output = New DlClientListResult With {.IsOfficial = True, .SourceName = "Mojang 官方源", .Value = Json}
+            '解析更新提示(Release)
+            Dim Version As String = Json("latest")("release")
+            If Setup.Get("ToolUpdateRelease") AndAlso Not Setup.Get("ToolUpdateReleaseLast") = "" AndAlso Version IsNot Nothing AndAlso Not Setup.Get("ToolUpdateReleaseLast") = Version Then
+                McDownloadClientUpdateHint(Version, Json)
+                IsNewClientVersionHinted = True
+            End If
+            Setup.Set("ToolUpdateReleaseLast", Version)
+            '解析更新提示(Snapshot)
+            Version = Json("latest")("snapshot")
+            If Setup.Get("ToolUpdateSnapshot") AndAlso Not Setup.Get("ToolUpdateSnapshotLast") = "" AndAlso Version IsNot Nothing AndAlso Not Setup.Get("ToolUpdateSnapshotLast") = Version AndAlso Not IsNewClientVersionHinted Then
+                McDownloadClientUpdateHint(Version, Json)
+            End If
+            Setup.Set("ToolUpdateSnapshotLast", If(Version, "Nothing"))
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败", ex)
+        End Try
+    End Sub
+    ''' <summary>
+    ''' Minecraft 客户端 版本列表,BMCLAPI 源加载器。
+    ''' </summary>
+    Public DlClientListBmclapiLoader As New LoaderTask(Of Integer, DlClientListResult)("DlClientList Bmclapi", AddressOf DlClientListBmclapiMain)
+    Private Sub DlClientListBmclapiMain(Loader As LoaderTask(Of Integer, DlClientListResult))
+        Dim Json As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json", IsJson:=True)
+        Try
+            Dim Versions As JArray = Json("versions")
+            If Versions.Count < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Json.ToString & ")")
+            '添加 PCL2 特供项
+            If File.Exists(PathTemp & "Cache\download.json") Then Versions.Merge(GetJson(ReadFile(PathTemp & "Cache\download.json")))
+            '返回
+            Loader.Output = New DlClientListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Json}
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Json.ToString & ")", ex)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 获取某个版本的 Json 下载地址,若失败则返回 Nothing。必须在工作线程执行。
+    ''' </summary>
+    Public Function DlClientListGet(Id As String)
+        Try
+            '确认 Minecraft 版本列表已完成获取
+            Select Case DlClientListLoader.State
+                Case LoadState.Loading
+                    DlClientListLoader.WaitForExit()
+                Case LoadState.Failed, LoadState.Aborted, LoadState.Waiting
+                    DlClientListLoader.WaitForExit(IsForceRestart:=True)
+            End Select
+            '确认版本格式标准
+            Id = Id.Replace("_", "-") '1.7.10_pre4 在版本列表中显示为 1.7.10-pre4
+            If Id <> "1.0" AndAlso Id.EndsWith(".0") Then Id = Left(Id, Id.Length - 2) 'OptiFine 1.8 的下载会触发此问题,显示版本为 1.8.0
+            '查找版本并开始
+            For Each Version As JObject In DlClientListLoader.Output.Value("versions")
+                If Version("id") = Id Then
+                    Return Version("url").ToString
+                End If
+            Next
+            Log("未发现版本 " & Id & " 的 Json 下载地址,版本列表返回为:" & vbCrLf & DlClientListLoader.Output.Value.ToString, LogLevel.Debug)
+            Return Nothing
+        Catch ex As Exception
+            Log(ex, "获取版本 " & Id & " 的 Json 下载地址失败")
+            Return Nothing
+        End Try
+    End Function
+
+#End Region
+
+#Region "DlOptiFineList | OptiFine 版本列表"
+
+    Public Structure DlOptiFineListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Official”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的数据。
+        ''' </summary>
+        Public Value As List(Of DlOptiFineListEntry)
+    End Structure
+    Public Class DlOptiFineListEntry
+        ''' <summary>
+        ''' 显示名称,已去除 HD_U 字样,如“1.12.2 C8”。
+        ''' </summary>
+        Public NameDisplay As String
+        ''' <summary>
+        ''' 原始文件名称,如“preview_OptiFine_1.11_HD_U_E1_pre.jar”。
+        ''' </summary>
+        Public NameFile As String
+        ''' <summary>
+        ''' 对应的版本名称,如“1.13.2-OptiFine_HD_U_E6”。
+        ''' </summary>
+        Public NameVersion As String
+        ''' <summary>
+        ''' 是否为测试版。
+        ''' </summary>
+        Public IsPreview As Boolean
+        ''' <summary>
+        ''' 对应的 Minecraft 版本,如“1.12.2”。
+        ''' </summary>
+        Public Property Inherit As String
+            Get
+                Return _inherit
+            End Get
+            Set(value As String)
+                If value.EndsWith(".0") Then value = Left(value, value.Length - 2)
+                _inherit = value
+            End Set
+        End Property
+        Private _inherit As String
+        ''' <summary>
+        ''' 发布时间,格式为“yyyy/mm/dd”。OptiFine 源无此数据。
+        ''' </summary>
+        Public ReleaseTime As String
+    End Class
+
+    ''' <summary>
+    ''' OptiFine 版本列表,主加载器。
+    ''' </summary>
+    Public DlOptiFineListLoader As New LoaderTask(Of Integer, DlOptiFineListResult)("DlOptiFineList Main", AddressOf DlOptiFineListMain)
+    Private Sub DlOptiFineListMain(Loader As LoaderTask(Of Integer, DlOptiFineListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlOptiFineListResult), Integer)(DlOptiFineListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' OptiFine 版本列表,官方源。
+    ''' </summary>
+    Public DlOptiFineListOfficialLoader As New LoaderTask(Of Integer, DlOptiFineListResult)("DlOptiFineList Official", AddressOf DlOptiFineListOfficialMain)
+    Private Sub DlOptiFineListOfficialMain(Loader As LoaderTask(Of Integer, DlOptiFineListResult))
+        Dim Result As String = NetGetCodeByClient("https://optifine.net/downloads", Encoding.Default)
+        If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")")
+        '获取所有版本信息
+        Dim ReleaseTime As List(Of String) = RegexSearch(Result, "(?<=Date'>)[^<]+")
+        Dim Name As List(Of String) = RegexSearch(Result, "(?<=OptiFine_)[0-9A-Za-z_.]+(?=.jar"")")
+        If Not ReleaseTime.Count = Name.Count Then Throw New Exception("版本与发布时间数据无法对应")
+        If ReleaseTime.Count < 10 Then Throw New Exception("获取到的版本数量不足(" & Result & ")")
+        '转化为列表输出
+        Dim Versions As New List(Of DlOptiFineListEntry)
+        For i = 0 To ReleaseTime.Count - 1
+            Name(i) = Name(i).Replace("_", " ")
+            Dim Entry As New DlOptiFineListEntry With {
+                         .NameDisplay = Name(i).ToString.Replace("HD U ", "").Replace(".0 ", " "),
+                         .ReleaseTime = Join({ReleaseTime(i).Split(".")(2), ReleaseTime(i).Split(".")(1), ReleaseTime(i).Split(".")(0)}, "/"),
+                         .IsPreview = Name(i).ToString.ToLower.Contains("pre"),
+                         .Inherit = Name(i).ToString.Split(" ")(0),
+                         .NameFile = If(Name(i).ToString.ToLower.Contains("pre"), "preview_", "") &
+                                                        "OptiFine_" & Name(i).ToString.Replace(" ", "_") & ".jar"}
+            Entry.NameVersion = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
+            Versions.Add(Entry)
+        Next
+        Loader.Output = New DlOptiFineListResult With {.IsOfficial = True, .SourceName = "OptiFine 官方源", .Value = Versions}
+    End Sub
+
+    ''' <summary>
+    ''' OptiFine 版本列表,BMCLAPI。
+    ''' </summary>
+    Public DlOptiFineListBmclapiLoader As New LoaderTask(Of Integer, DlOptiFineListResult)("DlOptiFineList Bmclapi", AddressOf DlOptiFineListBmclapiMain)
+    Private Sub DlOptiFineListBmclapiMain(Loader As LoaderTask(Of Integer, DlOptiFineListResult))
+        Dim Json As JArray = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/optifine/versionList", IsJson:=True)
+        Try
+            Dim Versions As New List(Of DlOptiFineListEntry)
+            For Each Token As JObject In Json
+                Dim Entry As New DlOptiFineListEntry With {
+                             .NameDisplay = (Token("mcversion").ToString & Token("type").ToString.Replace("HD_U", "").Replace("_", " ") & " " & Token("patch").ToString).Replace(".0 ", " "),
+                             .ReleaseTime = "",
+                             .IsPreview = Token("patch").ToString.ToLower.Contains("pre"),
+                             .Inherit = Token("mcversion").ToString,
+                             .NameFile = Token("filename").ToString
+                         }
+                Entry.NameVersion = Entry.Inherit & "-OptiFine_" & (Token("type").ToString & " " & Token("patch").ToString).Replace(".0 ", " ").Replace(" ", "_").Replace(Entry.Inherit & "_", "")
+                Versions.Add(Entry)
+            Next
+            Loader.Output = New DlOptiFineListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Versions}
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Json.ToString & ")", ex)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "DlForgeList | Forge Minecraft 版本列表"
+
+    Public Structure DlForgeListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Official”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的数据。
+        ''' </summary>
+        Public Value As List(Of String)
+    End Structure
+
+    ''' <summary>
+    ''' Forge 版本列表,主加载器。
+    ''' </summary>
+    Public DlForgeListLoader As New LoaderTask(Of Integer, DlForgeListResult)("DlForgeList Main", AddressOf DlForgeListMain)
+    Private Sub DlForgeListMain(Loader As LoaderTask(Of Integer, DlForgeListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlForgeListResult), Integer)(DlForgeListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' Forge 版本列表,官方源。
+    ''' </summary>
+    Public DlForgeListOfficialLoader As New LoaderTask(Of Integer, DlForgeListResult)("DlForgeList Official", AddressOf DlForgeListOfficialMain)
+    Private Sub DlForgeListOfficialMain(Loader As LoaderTask(Of Integer, DlForgeListResult))
+        Dim Result As String = NetGetCodeByRequestRetry("http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.2.4.html", Encoding.Default, "text/html")
+        If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")")
+        '获取所有版本信息
+        Dim Names As List(Of String) = RegexSearch(Result, "(?<=a href=""index_)[0-9.]+(_pre[0-9]?)?(?=.html)")
+        Names.Add("1.2.4") '1.2.4 不会被匹配上
+        If Names.Count < 10 Then Throw New Exception("获取到的版本数量不足(" & Result & ")")
+        Loader.Output = New DlForgeListResult With {.IsOfficial = True, .SourceName = "Forge 官方源", .Value = Names}
+    End Sub
+
+    ''' <summary>
+    ''' Forge 版本列表,BMCLAPI。
+    ''' </summary>
+    Public DlForgeListBmclapiLoader As New LoaderTask(Of Integer, DlForgeListResult)("DlForgeList Bmclapi", AddressOf DlForgeListBmclapiMain)
+    Private Sub DlForgeListBmclapiMain(Loader As LoaderTask(Of Integer, DlForgeListResult))
+        Dim Result As String = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/forge/minecraft", Encoding.Default)
+        If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")")
+        '获取所有版本信息
+        Dim Names As List(Of String) = RegexSearch(Result, "[0-9.]+(_pre[0-9]?)?")
+        If Names.Count < 10 Then Throw New Exception("获取到的版本数量不足(" & Result & ")")
+        Loader.Output = New DlForgeListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Names}
+    End Sub
+
+#End Region
+
+#Region "DlForgeVersion | Forge 版本列表"
+
+    Public Class DlForgeVersionEntry
+        ''' <summary>
+        ''' 完整的版本名,如 “14.22.1.2478”。
+        ''' </summary>
+        Public Version As String
+        ''' <summary>
+        ''' 对应的 Minecraft 版本,如“1.12.2”。
+        ''' </summary>
+        Public Inherit As String
+        ''' <summary>
+        ''' 发布时间,格式为“yyyy/MM/dd HH:mm”。
+        ''' </summary>
+        Public ReleaseTime As String
+        ''' <summary>
+        ''' 文件的 MD5 或 SHA1(BMCLAPI 的老版本是 MD5,新版本是 SHA1;官方源总是 MD5)。
+        ''' </summary>
+        Public Hash As String = Nothing
+        ''' <summary>
+        ''' 是否为推荐版本。
+        ''' </summary>
+        Public IsRecommended As Boolean
+        ''' <summary>
+        ''' 安装类型。有 installer、client、universal 三种。
+        ''' </summary>
+        Public Category As String
+        ''' <summary>
+        ''' 版本分支。若无分支则为 Nothing。
+        ''' </summary>
+        Public Branch As String = Nothing
+        ''' <summary>
+        ''' 是否为新版 Forge。(即为 Minecraft 1.13+)
+        ''' </summary>
+        Public ReadOnly Property IsNewType As Boolean
+            Get
+                Return Version.Split(".")(0) >= 20
+            End Get
+        End Property
+        ''' <summary>
+        ''' 构建数。
+        ''' </summary>
+        Public ReadOnly Property Build As Integer
+            Get
+                Dim Version = Me.Version.Split(".")
+                If Version(0) < 15 Then
+                    Return Version(Version.Count - 1)
+                Else
+                    Return Version(Version.Count - 1) + 10000
+                End If
+            End Get
+        End Property
+        ''' <summary>
+        ''' 用于下载的文件版本名。可能在 Version 的基础上添加了分支。
+        ''' </summary>
+        Public ReadOnly Property FileVersion As String
+            Get
+                Return Version & If(Branch Is Nothing, "", "-" & Branch)
+            End Get
+        End Property
+        ''' <summary>
+        ''' 即将下载的文件全名。
+        ''' </summary>
+        Public ReadOnly Property FileName As String
+            Get
+                Return "forge-" & Inherit & "-" & FileVersion & "-" & Category & "." & FileSuffix
+            End Get
+        End Property
+        ''' <summary>
+        ''' 文件扩展名。
+        ''' </summary>
+        Public ReadOnly Property FileSuffix As String
+            Get
+                If Category = "installer" Then
+                    Return "jar"
+                Else
+                    Return "zip"
+                End If
+            End Get
+        End Property
+    End Class
+
+    ''' <summary>
+    ''' Forge 版本列表,主加载器。
+    ''' </summary>
+    Public Sub DlForgeVersionMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)))
+        Dim DlForgeVersionOfficialLoader As New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion Official", AddressOf DlForgeVersionOfficialMain)
+        Dim DlForgeVersionBmclapiLoader As New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion Bmclapi", AddressOf DlForgeVersionBmclapiMain)
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlForgeVersionEntry)), Integer)(DlForgeVersionBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' Forge 版本列表,官方源。
+    ''' </summary>
+    Public Sub DlForgeVersionOfficialMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)))
+        Dim Result As String
+        Try
+            Result = NetGetCodeByDownload("http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" & Loader.Input & ".html")
+        Catch ex As Exception
+            If GetString(ex).Contains("(404)") Then
+                Throw New Exception("没有可用版本")
+            Else
+                Throw
+            End If
+        End Try
+        If Result.Length < 1000 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")")
+        Dim Versions As New List(Of DlForgeVersionEntry)
+        Try
+            '分割版本信息
+            Dim VersionCodes = Mid(Result, 1, Result.LastIndexOf("</table>")).Replace("<td class=""download-version", "¨").Split("¨")
+            '获取所有版本信息
+            For i = 1 To VersionCodes.Count - 1
+                Dim VersionCode = VersionCodes(i)
+                Try
+                    '基础信息获取
+                    Dim Name As String = RegexSeek(VersionCode, "(?<=[^(0-9)]+)[0-9\.]+")
+                    Dim IsRecommended As Boolean = VersionCode.Contains("fa promo-recommended")
+                    Dim Inherit As String = Loader.Input
+                    '分支获取
+                    Dim Branch As String = RegexSeek(VersionCode, "(?<=Branch:</strong>[\s]*)[^<]+")
+                    Branch = RegexSeek(If(Branch, ""), "[^\s]+")
+                    If String.IsNullOrWhiteSpace(Branch) Then Branch = Nothing
+                    If Name = "11.15.1.2318" OrElse Name = "11.15.1.1902" OrElse Name = "11.15.1.1890" Then Branch = "1.8.9"
+                    If Branch Is Nothing AndAlso Inherit = "1.7.10" AndAlso Name.Split(".")(3) >= 1300 Then Branch = "1.7.10"
+                    '发布时间获取
+                    Dim ReleaseTimeOriginal = RegexSeek(VersionCode, "(?<=""download-time"" title="")[^""]+")
+                    Dim ReleaseTimeSplit = ReleaseTimeOriginal.Split(" -:".ToCharArray) '原格式:"2021-02-15 03:24:02"
+                    Dim ReleaseDate As New Date(ReleaseTimeSplit(0), ReleaseTimeSplit(1), ReleaseTimeSplit(2), '年月日
+                                                ReleaseTimeSplit(3), ReleaseTimeSplit(4), ReleaseTimeSplit(5), '时分秒
+                                                0, DateTimeKind.Utc) '以 UTC 时间作为标准
+                    Dim ReleaseTime As String = ReleaseDate.ToLocalTime.ToString("yyyy/MM/dd HH:mm") '时区与格式转换
+                    '分类与 MD5 获取
+                    Dim MD5 As String, Category As String
+                    If VersionCode.Contains("classifier-installer""") Then
+                        '类型为 installer.jar,支持范围 ~753 (~ 1.6.1 部分), 738~684 (1.5.2 全部)
+                        VersionCode = VersionCode.Substring(VersionCode.IndexOf("installer.jar"))
+                        MD5 = RegexSeek(VersionCode, "(?<=MD5:</strong> )[^<]+")
+                        Category = "installer"
+                    ElseIf VersionCode.Contains("classifier-universal""") Then
+                        '类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分)
+                        VersionCode = VersionCode.Substring(VersionCode.IndexOf("universal.zip"))
+                        MD5 = RegexSeek(VersionCode, "(?<=MD5:</strong> )[^<]+")
+                        Category = "universal"
+                    Else
+                        '类型为 client.zip,支持范围 182~ (1.3.2 部分 ~)
+                        VersionCode = VersionCode.Substring(VersionCode.IndexOf("client.zip"))
+                        MD5 = RegexSeek(VersionCode, "(?<=MD5:</strong> )[^<]+")
+                        Category = "client"
+                    End If
+                    '添加进列表
+                    Versions.Add(New DlForgeVersionEntry With {.Category = Category, .Version = Name, .IsRecommended = IsRecommended, .Hash = MD5.Trim(vbCr, vbLf), .Inherit = Inherit, .ReleaseTime = ReleaseTime, .Branch = Branch})
+                Catch ex As Exception
+                    Throw New Exception("版本信息提取失败(" & VersionCode & ")", ex)
+                End Try
+            Next
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Result & ")", ex)
+        End Try
+        If Versions.Count = 0 Then Throw New Exception("没有可用版本")
+        Loader.Output = Versions
+    End Sub
+
+    ''' <summary>
+    ''' Forge 版本列表,BMCLAPI。
+    ''' </summary>
+    Public Sub DlForgeVersionBmclapiMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)))
+        Dim Json As JArray = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/forge/minecraft/" & Loader.Input, IsJson:=True)
+        Dim Versions As New List(Of DlForgeVersionEntry)
+        Try
+            Dim Recommended As String = McDownloadForgeRecommendedGet(Loader.Input)
+            For Each Token As JObject In Json
+                '分类与 Hash 获取
+                Dim Hash As String = Nothing, Category As String = "unknown", Proi As Integer = -1
+                For Each File As JObject In Token("files")
+                    Select Case File("category").ToString
+                        Case "installer"
+                            If File("format").ToString = "jar" Then
+                                '类型为 installer.jar,支持范围 ~753 (~ 1.6.1 部分), 738~684 (1.5.2 全部)
+                                Hash = File("hash")
+                                Category = "installer"
+                                Proi = 2
+                            End If
+                        Case "universal"
+                            If Proi <= 1 AndAlso File("format").ToString = "zip" Then
+                                '类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分)
+                                Hash = File("hash")
+                                Category = "universal"
+                                Proi = 1
+                            End If
+                        Case "client"
+                            If Proi <= 0 AndAlso File("format").ToString = "zip" Then
+                                '类型为 client.zip,支持范围 182~ (1.3.2 部分 ~)
+                                Hash = File("hash")
+                                Category = "client"
+                                Proi = 0
+                            End If
+                    End Select
+                Next
+                '获取 Entry
+                Dim Inherit As String = Loader.Input
+                Dim Branch As String = Token("branch")
+                Dim Name As String = Token("version")
+                If Name = "11.15.1.2318" OrElse Name = "11.15.1.1902" OrElse Name = "11.15.1.1890" Then Branch = "1.8.9"
+                If Branch Is Nothing AndAlso Inherit = "1.7.10" AndAlso Name.Split(".")(3) >= 1300 Then Branch = "1.7.10"
+                '基础信息获取
+                Dim Entry = New DlForgeVersionEntry With {.Hash = Hash, .Category = Category, .Version = Name, .Branch = Branch, .Inherit = Inherit, .IsRecommended = Recommended = Name}
+                Dim TimeSplit = Token("modified").ToString.Split("-"c, "T"c, ":"c, "."c, " "c, "/"c)
+                Entry.ReleaseTime = Token("modified").ToObject(Of Date).ToLocalTime.ToString("yyyy/MM/dd HH:mm")
+                '添加项
+                Versions.Add(Entry)
+            Next
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Json.ToString & ")", ex)
+        End Try
+        If Versions.Count = 0 Then Throw New Exception("没有可用版本")
+        Loader.Output = Versions
+    End Sub
+
+#End Region
+
+#Region "DlLiteLoaderList | LiteLoader 版本列表"
+
+    Public Structure DlLiteLoaderListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Official”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的数据。
+        ''' </summary>
+        Public Value As List(Of DlLiteLoaderListEntry)
+        ''' <summary>
+        ''' 官方源的失败原因。若没有则为 Nothing。
+        ''' </summary>
+        Public OfficialError As Exception
+    End Structure
+    Public Class DlLiteLoaderListEntry
+        ''' <summary>
+        ''' 实际的文件名,如“liteloader-installer-1.12-00-SNAPSHOT.jar”。
+        ''' </summary>
+        Public FileName As String
+        ''' <summary>
+        ''' 是否为测试版。
+        ''' </summary>
+        Public IsPreview As Boolean
+        ''' <summary>
+        ''' 对应的 Minecraft 版本,如“1.12.2”。
+        ''' </summary>
+        Public Inherit As String
+        ''' <summary>
+        ''' 是否为 1.7 及更早的远古版。
+        ''' </summary>
+        Public IsLegacy As Boolean
+        ''' <summary>
+        ''' 发布时间,格式为“yyyy/mm/dd HH:mm:ss”。
+        ''' </summary>
+        Public ReleaseTime As String
+        ''' <summary>
+        ''' 文件的 MD5。
+        ''' </summary>
+        Public MD5 As String
+        ''' <summary>
+        ''' 对应的 Json 项。
+        ''' </summary>
+        Public JsonToken As JToken
+    End Class
+
+    ''' <summary>
+    ''' LiteLoader 版本列表,主加载器。
+    ''' </summary>
+    Public DlLiteLoaderListLoader As New LoaderTask(Of Integer, DlLiteLoaderListResult)("DlLiteLoaderList Main", AddressOf DlLiteLoaderListMain)
+    Private Sub DlLiteLoaderListMain(Loader As LoaderTask(Of Integer, DlLiteLoaderListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlLiteLoaderListResult), Integer)(DlLiteLoaderListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' LiteLoader 版本列表,官方源。
+    ''' </summary>
+    Public DlLiteLoaderListOfficialLoader As New LoaderTask(Of Integer, DlLiteLoaderListResult)("DlLiteLoaderList Official", AddressOf DlLiteLoaderListOfficialMain)
+    Private Sub DlLiteLoaderListOfficialMain(Loader As LoaderTask(Of Integer, DlLiteLoaderListResult))
+        Dim Result As JObject = NetGetCodeByRequestRetry("http://dl.liteloader.com/versions/versions.json", IsJson:=True)
+        Try
+            Dim Json As JObject = Result("versions")
+            Dim Versions As New List(Of DlLiteLoaderListEntry)
+            For Each Pair As KeyValuePair(Of String, JToken) In Json
+                If Pair.Key.StartsWith("1.6") OrElse Pair.Key.StartsWith("1.5") Then Continue For
+                Dim RealEntry As JToken = If(Pair.Value("artefacts"), Pair.Value("snapshots"))("com.mumfrey:liteloader")("latest")
+                Versions.Add(New DlLiteLoaderListEntry With {
+                             .Inherit = Pair.Key,
+                             .IsLegacy = Pair.Key.Split(".")(1) < 8,
+                             .IsPreview = RealEntry("stream").ToString.ToLower = "snapshot",
+                             .FileName = "liteloader-installer-" & Pair.Key & If(Pair.Key = "1.8" OrElse Pair.Key = "1.9", ".0", "") & "-00-SNAPSHOT.jar",
+                             .MD5 = RealEntry("md5"),
+                             .ReleaseTime = GetLocalTime(GetDate(RealEntry("timestamp"))).ToString("yyyy/MM/dd HH:mm:ss"),
+                             .JsonToken = RealEntry
+                         })
+            Next
+            Loader.Output = New DlLiteLoaderListResult With {.IsOfficial = True, .SourceName = "LiteLoader 官方源", .Value = Versions}
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Result.ToString & ")", ex)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' LiteLoader 版本列表,BMCLAPI。
+    ''' </summary>
+    Public DlLiteLoaderListBmclapiLoader As New LoaderTask(Of Integer, DlLiteLoaderListResult)("DlLiteLoaderList Bmclapi", AddressOf DlLiteLoaderListBmclapiMain)
+    Private Sub DlLiteLoaderListBmclapiMain(Loader As LoaderTask(Of Integer, DlLiteLoaderListResult))
+        Dim Result As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json", IsJson:=True)
+        Try
+            Dim Json As JObject = Result("versions")
+            Dim Versions As New List(Of DlLiteLoaderListEntry)
+            For Each Pair As KeyValuePair(Of String, JToken) In Json
+                If Pair.Key.StartsWith("1.6") OrElse Pair.Key.StartsWith("1.5") Then Continue For
+                Dim RealEntry As JToken = If(Pair.Value("artefacts"), Pair.Value("snapshots"))("com.mumfrey:liteloader")("latest")
+                Versions.Add(New DlLiteLoaderListEntry With {
+                             .Inherit = Pair.Key,
+                             .IsLegacy = Pair.Key.Split(".")(1) < 8,
+                             .IsPreview = RealEntry("stream").ToString.ToLower = "snapshot",
+                             .FileName = "liteloader-installer-" & Pair.Key & If(Pair.Key = "1.8" OrElse Pair.Key = "1.9", ".0", "") & "-00-SNAPSHOT.jar",
+                             .MD5 = RealEntry("md5"),
+                             .ReleaseTime = GetLocalTime(GetDate(RealEntry("timestamp"))).ToString("yyyy/MM/dd HH:mm:ss"),
+                             .JsonToken = RealEntry
+                         })
+            Next
+            Loader.Output = New DlLiteLoaderListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Versions}
+        Catch ex As Exception
+            Throw New Exception("版本列表解析失败(" & Result.ToString & ")", ex)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "DlFabricList | Fabric 列表"
+
+    Public Structure DlFabricListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Official”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的数据。
+        ''' </summary>
+        Public Value As JObject
+    End Structure
+
+    ''' <summary>
+    ''' Fabric 列表,主加载器。
+    ''' </summary>
+    Public DlFabricListLoader As New LoaderTask(Of Integer, DlFabricListResult)("DlFabricList Main", AddressOf DlFabricListMain)
+    Private Sub DlFabricListMain(Loader As LoaderTask(Of Integer, DlFabricListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlFabricListResult), Integer)(DlFabricListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' Fabric 列表,官方源。
+    ''' </summary>
+    Public DlFabricListOfficialLoader As New LoaderTask(Of Integer, DlFabricListResult)("DlFabricList Official", AddressOf DlFabricListOfficialMain)
+    Private Sub DlFabricListOfficialMain(Loader As LoaderTask(Of Integer, DlFabricListResult))
+        Dim Result As JObject = NetGetCodeByRequestRetry("https://meta.fabricmc.net/v2/versions", IsJson:=True)
+        Try
+            Dim Output = New DlFabricListResult With {.IsOfficial = True, .SourceName = "Fabric 官方源", .Value = Result}
+            If Output.Value("game") Is Nothing OrElse Output.Value("loader") Is Nothing OrElse Output.Value("installer") Is Nothing Then Throw New Exception("获取到的列表缺乏必要项")
+            Loader.Output = Output
+        Catch ex As Exception
+            Throw New Exception("列表解析失败(" & Result.ToString & ")", ex)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' Fabric 列表,BMCLAPI。
+    ''' </summary>
+    Public DlFabricListBmclapiLoader As New LoaderTask(Of Integer, DlFabricListResult)("DlFabricList Bmclapi", AddressOf DlFabricListBmclapiMain)
+    Private Sub DlFabricListBmclapiMain(Loader As LoaderTask(Of Integer, DlFabricListResult))
+        Dim Result As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/fabric-meta/v2/versions", IsJson:=True)
+        Try
+            Dim Output = New DlFabricListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Result}
+            If Output.Value("game") Is Nothing OrElse Output.Value("loader") Is Nothing OrElse Output.Value("installer") Is Nothing Then Throw New Exception("获取到的列表缺乏必要项")
+            Loader.Output = Output
+        Catch ex As Exception
+            Throw New Exception("列表解析失败(" & Result.ToString & ")", ex)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' Fabric API 列表,官方源。
+    ''' </summary>
+    Public DlFabricApiLoader As New LoaderTask(Of Integer, List(Of DlCfFile))("Fabric API List Loader",
+        Sub(Task As LoaderTask(Of Integer, List(Of DlCfFile))) Task.Output = DlCfGetFiles(306612, False))
+
+    ''' <summary>
+    ''' OptiFabric 列表,官方源。
+    ''' </summary>
+    Public DlOptiFabricLoader As New LoaderTask(Of Integer, List(Of DlCfFile))("OptiFabric List Loader",
+        Sub(Task As LoaderTask(Of Integer, List(Of DlCfFile))) Task.Output = DlCfGetFiles(322385, False))
+
+#End Region
+
+#Region "DlCfProject | CurseForge 工程"
+
+    Private DlCfProjectDb As JObject = Nothing
+    Private DlCfProjectCache As New Dictionary(Of Integer, DlCfProject)
+
+    ''' <summary>
+    ''' CurseForge 工程列表获取、搜索请求。
+    ''' </summary>
+    Public Class DlCfProjectRequest
+        Public CategoryId As Integer = 0 '421: Mod(?), 4780: Fabric
+        Public IsModPack As Boolean = False
+        Public GameVersion As String = Nothing
+        Public PageSize As String = 50
+        Public SearchFilter As String = Nothing
+        Public Index As Integer? = Nothing
+        Public ModLoader As Integer? = Nothing
+        Public Function GetAddress() As String
+            GetAddress = "https://api.curseforge.com/v1/mods/search?gameId=432&sortField=Featured&sortOrder=desc" &
+                         "&pageSize=" & PageSize &
+                         "&categoryId=" & CategoryId &
+                         If(IsModPack, "&classId=4471", "&classId=6") &
+                         If(String.IsNullOrEmpty(GameVersion), "", "&gameVersion=" & GameVersion) &
+                         If(String.IsNullOrEmpty(SearchFilter), "", "&searchFilter=" & Net.WebUtility.UrlEncode(SearchFilter)) &
+                         If(Index IsNot Nothing, "&index=" & Index, "") &
+                         If(ModLoader IsNot Nothing AndAlso ModLoader > 0, "&modLoaderType=" & ModLoader, "")
+        End Function
+
+#Region "相同判断"
+
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim request = TryCast(obj, DlCfProjectRequest)
+            Return request IsNot Nothing AndAlso request.GetAddress = GetAddress()
+        End Function
+        Public Shared Operator =(left As DlCfProjectRequest, right As DlCfProjectRequest) As Boolean
+            Return EqualityComparer(Of DlCfProjectRequest).Default.Equals(left, right)
+        End Operator
+        Public Shared Operator <>(left As DlCfProjectRequest, right As DlCfProjectRequest) As Boolean
+            Return Not left = right
+        End Operator
+
+#End Region
+
+    End Class
+
+    ''' <summary>
+    ''' CurseForge 工程信息。
+    ''' </summary>
+    Public Class DlCfProject
+
+        Public Id As Integer
+        Public Name As String
+        Public McWikiId As Integer = 0
+        Public Description As String
+        Public Website As String
+        Public DateUpdate As Date
+        Public DownloadCount As Integer
+        Public ModLoaders As New List(Of String)
+        Public GameVersionDesc As String
+        Private CategoryDesc As List(Of String)
+        Private Thumb As String
+        Public IsModPack As Boolean
+        Public FileIndexes As New List(Of Integer)
+        Public Files As List(Of DlCfFile)
+        Private _MCBBS As String
+        Public Property MCBBS As String
+            Get
+                If DlCfProjectDb Is Nothing Then
+                    DlCfProjectDb = GetJson(DecodeBytes(GetResources("ModData")))
+                End If
+                If _MCBBS Is Nothing Then
+                    If Website IsNot Nothing Then
+                        Dim Keyword As String = Website.TrimEnd("/").Split("/").Last
+                        If DlCfProjectDb.ContainsKey(Keyword) Then
+                            Dim Result As String() = DlCfProjectDb(Keyword).ToString.Split("|")
+                            If Result.Length = 3 Then _MCBBS = Result.Last
+                        End If
+                    End If
+                End If
+                Return _MCBBS
+            End Get
+            Set(value As String)
+                _MCBBS = value
+            End Set
+        End Property
+        Private _ChineseName As String
+        Public Property ChineseName As String
+            Get
+                If DlCfProjectDb Is Nothing Then
+                    DlCfProjectDb = GetJson(DecodeBytes(GetResources("ModData")))
+                End If
+                If _ChineseName Is Nothing Then
+                    _ChineseName = Name
+                    If Website IsNot Nothing Then
+                        Dim Keyword As String = Website.TrimEnd("/").Split("/").Last
+                        If DlCfProjectDb.ContainsKey(Keyword) Then
+                            Dim Result As String = DlCfProjectDb(Keyword)
+                            McWikiId = Result.Split("|")(0)
+                            If Not Result.Split("|")(1) = "~" Then '使用原名
+                                _ChineseName = Result.Split("|")(1) '& " (" & Name & ")"
+                            End If
+                        End If
+                    End If
+                End If
+                Return _ChineseName
+            End Get
+            Set(ByVal value As String)
+                _ChineseName = value
+            End Set
+        End Property
+
+        ''' <summary>
+        ''' 从 CurseForge 工程 Json 项中初始化实例。若出错会抛出异常。
+        ''' </summary>
+        Public Sub New(Data As JObject)
+            'https://api.curseforge.com/v1/mods/search?gameId=432&pageSize=5&categoryID=421
+            Id = Data("id")
+            Name = Data("name")
+            Description = Data("summary")
+            Website = Data("links")("websiteUrl")
+            DateUpdate = Data("dateModified")
+            DownloadCount = Data("downloadCount")
+            IsModPack = Not Website.Contains("/mc-mods/")
+            '获取常见文件
+            For Each File In If(Data("latestFiles"), {})
+                Dim NewFile As New DlCfFile(File, IsModPack)
+                If Not NewFile.IsAvailable Then Continue For
+            Next
+            '获取游戏版本、Mod Loader
+            Dim GameVersions As New List(Of Integer)
+            ModLoaders = New List(Of String)
+            For Each File In If(Data("latestFilesIndexes"), {})
+                Dim Version As String = File("gameVersion")
+                If Not Version.Contains("1.") Then Continue For
+                GameVersions.Add(Version.Split(".")(1).Split("-").First)
+                Select Case If(File("modLoader"), "0").ToString
+                    Case 1
+                        ModLoaders.Add("Forge")
+                    Case 2
+                        ModLoaders.Add("Cauldron")
+                    Case 3
+                        ModLoaders.Add("LiteLoader")
+                    Case 4
+                        ModLoaders.Add("Fabric")
+                End Select
+                FileIndexes.Add(File("fileId"))
+            Next
+            GameVersions = Sort(GameVersions.Distinct.ToList, AddressOf VersionSortBoolean)
+            ModLoaders = ModLoaders.Distinct.ToList
+            ModLoaders.Sort()
+            If GameVersions.Count = 0 Then
+                GameVersionDesc = ""
+            Else
+                Dim SpaVersions As New List(Of String)
+                For i = 0 To GameVersions.Count - 1
+                    Dim StartVersion As Integer = GameVersions(i), EndVersion As Integer = GameVersions(i)
+                    For ii = i + 1 To GameVersions.Count - 1
+                        If GameVersions(ii) = EndVersion - 1 Then
+                            EndVersion = GameVersions(ii)
+                            i = ii
+                        Else
+                            Exit For
+                        End If
+                    Next
+                    If StartVersion = EndVersion Then
+                        SpaVersions.Add("1." & StartVersion)
+                    Else
+                        SpaVersions.Add("1." & StartVersion & "-1." & EndVersion)
+                    End If
+                Next
+                GameVersionDesc = "[" & Join(SpaVersions, ", ") & "] "
+            End If
+            '获取分类
+            Dim Categories = Data("categories")
+            Dim CategoryId As New List(Of Integer)
+            For Each CategoryToken In Categories
+                CategoryId.Add(CategoryToken("id"))
+            Next
+            CategoryId = ArrayNoDouble(CategoryId)
+            CategoryId.Sort()
+            CategoryDesc = New List(Of String)
+            For Each Category In CategoryId
+                Dim Desc As String
+                Select Case Category
+
+                    Case 406 : Desc = "世界生成"
+                    Case 407 : Desc = "生物群系"
+                    Case 409 : Desc = "天然结构"
+                    Case 410 : Desc = "维度"
+                    Case 411 : Desc = "生物"
+                    Case 412 : Desc = "科技"
+                    Case 414 : Desc = "交通与移动"
+                    Case 415 : Desc = "管道与物流"
+                    Case 417 : Desc = "能源"
+                    Case 4558 : Desc = "红石"
+                    Case 420 : Desc = "仓储"
+                    Case 416 : Desc = "农业"
+                    Case 436 : Desc = "食物"
+                    Case 419 : Desc = "魔法"
+                    Case 434 : Desc = "装备与工具"
+                    Case 422 : Desc = "冒险与探索"
+                    Case 424 : Desc = "建筑与装饰"
+                    Case 423 : Desc = "信息显示"
+                    Case 425 : Desc = "杂项"
+                    Case 421 : Desc = "支持库"
+                    Case 435 : Desc = "服务器"
+
+                    Case 4471 : Desc = "科幻"
+                    Case 4483 : Desc = "战斗"
+                    Case 4477 : Desc = "小游戏"
+                    Case 4478 : Desc = "任务"
+                    Case 4484 : Desc = "多人"
+                    Case 4476 : Desc = "探索"
+                    Case 4736 : Desc = "空岛"
+                    Case 4475 : Desc = "冒险"
+                    Case 4487 : Desc = "FTB"
+                    Case 4480 : Desc = "基于地图"
+                    Case 4479 : Desc = "硬核"
+                    Case 4472 : Desc = "科技"
+                    Case 4473 : Desc = "魔法"
+                    Case 4481 : Desc = "小型整合"
+                    Case 4482 : Desc = "大型整合"
+
+                    Case Else : Continue For
+                End Select
+                CategoryDesc.Add(Desc)
+            Next
+            If CategoryDesc.Count = 0 Then CategoryDesc.Add("杂类")
+            '获取图片
+            If Data("logo").Count > 0 Then
+                If GetPixelSize(1) > 1.25 Then
+                    Thumb = Data("logo")("thumbnailUrl").ToString '使用 256x256 图标
+                Else
+                    Thumb = Data("logo")("thumbnailUrl").ToString.Replace("/256/256/", "/64/64/") '使用 64x64 图标
+                End If
+            End If
+            '保存缓存
+            If Not DlCfProjectCache.ContainsKey(Id) OrElse DlCfProjectCache(Id).Files Is Nothing Then DictionaryAdd(DlCfProjectCache, Id, Me)
+        End Sub
+
+        Public Function ToCfItem(ShowVersionDesc As Boolean, ShowLoaderDesc As Boolean, Optional OnClick As MyCfItem.ClickEventHandler = Nothing) As MyCfItem
+            Dim NewItem As New MyCfItem With {.Tag = Me}
+            NewItem.LabTitle.Text = ChineseName
+            NewItem.LabInfo.Text = Description.Replace(vbCr, "").Replace(vbLf, "")
+            NewItem.LabLeft.Text = If(ModLoaders.Count > 0 AndAlso ShowLoaderDesc, "[" & Join(ModLoaders, " & ") & "] ", "") &
+                                   If(ShowVersionDesc, GameVersionDesc, "") &
+                                   Join(CategoryDesc, ",") & " (" &
+                                   GetTimeSpanString(DateUpdate - Date.Now) & "更新" &
+                                   If(DownloadCount > 0, "," & If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万次下载)", DownloadCount & " 次下载)"), "")
+            If Thumb Is Nothing Then
+                NewItem.Logo = "pack://application:,,,/images/Icons/NoIcon.png"
+            Else
+                NewItem.Logo = Thumb
+            End If
+            If OnClick IsNot Nothing Then
+                AddHandler NewItem.Click, OnClick
+                NewItem.HasAnimation = True
+            Else
+                NewItem.HasAnimation = False
+            End If
+            '结束
+            Return NewItem
+        End Function
+
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim project = TryCast(obj, DlCfProject)
+            Return project IsNot Nothing AndAlso Id = project.Id
+        End Function
+        Public Shared Operator =(left As DlCfProject, right As DlCfProject) As Boolean
+            Return EqualityComparer(Of DlCfProject).Default.Equals(left, right)
+        End Operator
+        Public Shared Operator <>(left As DlCfProject, right As DlCfProject) As Boolean
+            Return Not left = right
+        End Operator
+    End Class
+
+    Public Class DlCfProjectResult
+        Public Projects As List(Of DlCfProject)
+        Public Index As Integer
+        Public RealCount As Long
+    End Class
+    ''' <summary>
+    ''' CurseForge 工程列表获取事件。
+    ''' </summary>
+    Public Sub DlCfProjectSub(Task As LoaderTask(Of DlCfProjectRequest, DlCfProjectResult))
+        Dim RawFilter As String = If(Task.Input.SearchFilter, "")
+        Task.Input.SearchFilter = RawFilter
+        Log("[Download] CurseForge 工程列表搜索原始文本:" & RawFilter)
+        '中文请求关键字处理
+        Dim IsChineseSearch As Boolean = RegexCheck(RawFilter, "[\u4e00-\u9fbb]")
+        If IsChineseSearch AndAlso Not String.IsNullOrEmpty(RawFilter) Then
+            If Task.Input.IsModPack Then Throw New Exception("整合包搜索仅支持英文")
+            '释放资料库文件
+            If DlCfProjectDb Is Nothing Then
+                DlCfProjectDb = GetJson(DecodeBytes(GetResources("ModData")))
+            End If
+            '构造搜索请求
+            Dim SearchEntries As New List(Of SearchEntry(Of String))
+            For Each Entry In DlCfProjectDb
+                If Entry.Value.ToString.Contains("动态的树") Then Continue For '傻逼 Mod 的附属太多了
+                SearchEntries.Add(New SearchEntry(Of String) With {
+                    .Item = Entry.Value.ToString.Split("|")(1),
+                    .SearchSource = New List(Of KeyValuePair(Of String, Double)) From {
+                        New KeyValuePair(Of String, Double)(Entry.Value.ToString.Replace(" (", "|").Split("|")(1), 1)
+                    }
+                })
+            Next
+            '获取搜索结果
+            Dim SearchResults = Search(SearchEntries, Task.Input.SearchFilter, 3)
+            If SearchResults.Count = 0 Then Throw New Exception("无搜索结果,请尝试搜索英文名称")
+            Dim SearchResult As String = ""
+            For i = 0 To SearchResults.Count - 1
+                If Not SearchResults(i).AbsoluteRight AndAlso i >= Math.Min(2, SearchResults.Count - 1) Then Exit For '把 3 个结果拼合以提高准确度
+                SearchResult += SearchResults(i).Item.Replace(" (", "|").Split("|").Last.TrimEnd(")") & " "
+            Next
+            Log("[Download] CurseForge 工程列表中文搜索原始关键词:" & SearchResult, LogLevel.Developer)
+            '去除常见连接词
+            Dim RealFilter As String = ""
+            For Each Word In SearchResult.Split(" ")
+                If {"the", "of", "a", "mod"}.Contains(Word.ToLower) Then Continue For
+                If SearchResult.Split(" ").Count > 3 AndAlso {"ftb"}.Contains(Word.ToLower) Then Continue For
+                RealFilter += Word & " "
+            Next
+            Task.Input.SearchFilter = RealFilter
+            Log("[Download] CurseForge 工程列表中文搜索最终关键词:" & RealFilter, LogLevel.Developer)
+        End If
+        '驼峰英文请求关键字处理
+        Dim SpacedKeywords = RegexReplace(Task.Input.SearchFilter, "$& ", "([A-Z]+|[a-z]+?)(?=[A-Z]+[a-z]+[a-z ]*)")
+        Dim ConnectedKeywords = Task.Input.SearchFilter.Replace(" ", "")
+        Dim AllPossibleKeywords = SpacedKeywords & " " & If(IsChineseSearch, Task.Input.SearchFilter, ConnectedKeywords & " " & RawFilter)
+        '最终处理关键字:分割、去重
+        Dim RightKeywords As New List(Of String)
+        For Each Keyword In AllPossibleKeywords.Split(" ")
+            If Keyword.Trim = "" Then Continue For
+            RightKeywords.Add(Keyword)
+        Next
+        Task.Input.SearchFilter = Join(ArrayNoDouble(RightKeywords), " ").ToLower
+        Log("[Download] CurseForge 工程列表搜索最终文本:" & Task.Input.SearchFilter, LogLevel.Developer)
+        '正式获取
+        Dim Url = Task.Input.GetAddress()
+        Log("[Download] 开始获取 CurseForge 工程列表:" & Url)
+        Dim RequestResult As JObject = NetGetCodeByRequestRetry(Url, IsJson:=True, Encode:=Encoding.UTF8)
+        Dim FileList = GetCfProjectListFromJson(RequestResult("data"), Task.Input.IsModPack)
+        If FileList.Count = 0 Then Throw New Exception("无搜索结果")
+        '重新排序
+        If Not String.IsNullOrEmpty(Task.Input.SearchFilter) Then
+            '排序分 = 搜索相对相似度 + 默认排序相对活跃度
+            Dim Scores As New Dictionary(Of DlCfProject, Double)
+            Dim Entry As New List(Of SearchEntry(Of DlCfProject))
+            For i = 1 To FileList.Count
+                Dim File = FileList(i - 1)
+                Scores.Add(File, (1 - (i / FileList.Count)))
+                Entry.Add(New SearchEntry(Of DlCfProject) With {.Item = File, .SearchSource = New List(Of KeyValuePair(Of String, Double)) From {
+                          New KeyValuePair(Of String, Double)(If(IsChineseSearch, File.ChineseName, File.Name), 1),
+                          New KeyValuePair(Of String, Double)(File.Description, 0.05)}})
+            Next
+            Dim SearchResult = Search(Entry, RawFilter, 101, -1)
+            For Each OneResult In SearchResult
+                Scores(OneResult.Item) += OneResult.Similarity / SearchResult(0).Similarity
+            Next
+            '根据排序分得出结果
+            Dim ResultList = Sort(Scores.ToList, Function(Left As KeyValuePair(Of DlCfProject, Double), Right As KeyValuePair(Of DlCfProject, Double))
+                                                     Return Left.Value > Right.Value
+                                                 End Function)
+            FileList = New List(Of DlCfProject)
+            For Each Result In ResultList
+                FileList.Add(Result.Key)
+            Next
+        End If
+        Task.Output = New DlCfProjectResult With {.Projects = FileList, .Index = RequestResult("pagination")("index"), .RealCount = RequestResult("pagination")("totalCount")}
+    End Sub
+    ''' <summary>
+    ''' 从 API 返回的 Json 数组中获取工程信息数组。
+    ''' </summary>
+    Private Function GetCfProjectListFromJson(Json As JArray, IsModPack As Boolean) As List(Of DlCfProject)
+        Dim FileList As New List(Of DlCfProject)
+        For Each JsonEntry As JObject In Json
+            Dim Result = GetCfProjectFromJson(JsonEntry, IsModPack)
+            If Result IsNot Nothing Then FileList.Add(Result)
+        Next
+        Return FileList
+    End Function
+    ''' <summary>
+    ''' 从 API 返回的 Json 中获取工程信息。
+    ''' </summary>
+    Private Function GetCfProjectFromJson(Json As JObject, IsModPack As Boolean) As DlCfProject
+        If Json("links")("websiteUrl") Is Nothing Then
+            Log("[Download] 发现关键项为 Nothing 的工程:" & If(Json, "").ToString, LogLevel.Debug)
+            Return Nothing
+        End If
+        If IsModPack = Json("links")("websiteUrl").ToString.Contains("curseforge.com/minecraft/mc-mods") Then
+            Log("[Download] 返回的工程与要求的类别不一致:" & Json("links")("websiteUrl").ToString, LogLevel.Debug)
+            Return Nothing
+        End If
+        Return New DlCfProject(Json)
+    End Function
+
+#End Region
+
+#Region "DlCfFile | CurseForge Files"
+
+    Public DlCfFileLoader As New LoaderTask(Of KeyValuePair(Of Integer, Boolean), List(Of DlCfFile))("DlCfFile Main", Sub(Task As LoaderTask(Of KeyValuePair(Of Integer, Boolean), List(Of DlCfFile))) Task.Output = DlCfGetFiles(Task.Input.Key, Task.Input.Value))
+
+    ''' <summary>
+    ''' CurseForge 工程下的单一版本文件。
+    ''' </summary>
+    Public Class DlCfFile
+
+        Public IsModPack As Boolean
+        Public FileId As Integer
+        Public DisplayName As String
+        Public [Date] As Date
+        Public GameVersion As String()
+        Public ReleaseType As Integer
+        Public FileName As String
+        Public DownloadCount As Integer
+        Public Dependencies As New List(Of Integer)
+        Private DownloadAddress As String()
+
+        Public ReadOnly Property ReleaseTypeString As String
+            Get
+                Select Case ReleaseType
+                    Case 1
+                        Return "正式版"
+                    Case 2
+                        Return If(ModeDebug, "Beta 测试版", "测试版")
+                    Case Else
+                        Return If(ModeDebug, "Alpha 测试版", "测试版")
+                End Select
+            End Get
+        End Property
+        Public ReadOnly Property IsAvailable As Boolean
+            Get
+                Return DisplayName IsNot Nothing AndAlso GameVersion IsNot Nothing AndAlso FileName IsNot Nothing AndAlso DownloadAddress(0).StartsWith("http")
+            End Get
+        End Property
+
+        ''' <summary>
+        ''' 从 CurseForge 文件 Json 项中初始化实例。若出错会抛出异常。
+        ''' </summary>
+        Public Sub New(Data As JObject, IsModPack As Boolean)
+            FileId = Data("id")
+            DisplayName = Data("displayName").ToString.Replace("	", "").Trim(" ")
+            [Date] = Data("fileDate")
+            ReleaseType = Data("releaseType")
+            DownloadCount = Data("downloadCount")
+            FileName = Data("fileName")
+            Me.IsModPack = IsModPack
+            '获取下载地址
+            Dim Url = Data("downloadUrl").ToString
+            If Url = "" Then Url = "https://media.forgecdn.net/files/" & FileId.ToString.Substring(0, 4) & "/" & FileId.ToString.Substring(4).TrimStart("0") & "/" & FileName
+            Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码
+            DownloadAddress = ArrayNoDouble({Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"), Url.Replace("-service.overwolf.wtf", ".forgecdn.net"), Url.Replace("://edge", "://media"), Url})
+            '获取前置 Mod 的 Addon ID
+            If Not IsModPack Then
+                For Each Dep In Data("dependencies")
+                    If Dep("relationType").ToObject(Of Integer) = 3 AndAlso '是一个依赖
+                        Dep("modId").ToObject(Of Integer) <> 306612 Then '排除 Fabric API
+                        Dependencies.Add(Dep("modId"))
+                    End If
+                Next
+            End If
+            '获取游戏版本
+            Dim Versions As New List(Of String)
+            For Each Version In Data("gameVersions")
+                If Version.ToString.StartsWith("1.") OrElse Version.ToString.Contains("w") Then
+                    Versions.Add(Version.ToString.Trim.ToLower)
+                End If
+            Next
+            If Versions.Count > 1 Then
+                GameVersion = Sort(Versions, AddressOf VersionSortBoolean).ToArray
+                If IsModPack Then GameVersion = {GameVersion(0)}
+            ElseIf Versions.Count = 1 Then
+                GameVersion = Versions.ToArray
+                'ElseIf Data("gameVersion").Count = 1 AndAlso Not Data("gameVersion")(0).ToString.Contains("1.") Then
+                '    GameVersion = {"1.16.4"}
+            Else
+                GameVersion = {"未知版本"}
+            End If
+        End Sub
+
+        ''' <summary>
+        ''' 获取对应的下载文件信息。
+        ''' </summary>
+        ''' <param name="LocalAddress">本地文件夹或文件地址。</param>
+        Public Function GetDownloadFile(LocalAddress As String, IsFullPath As Boolean) As NetFile
+            Return New NetFile(DownloadAddress, LocalAddress & If(IsFullPath, "", FileName))
+        End Function
+
+        Public Function ToListItem(OnClick As MyListItem.ClickEventHandler, Optional OnSaveClick As MyIconButton.ClickEventHandler = Nothing) As MyListItem
+
+            '获取描述信息
+            Dim Info As String = ""
+            If Not IsModPack Then
+                Info += "适用于 " & Join(GameVersion, "、").Replace("-snapshot", " 快照") &
+                          If(ModeDebug AndAlso Dependencies.Count > 0, "," & Dependencies.Count & " 个前置,", ",")
+            End If
+            Info += If(DownloadCount > 0, If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万次下载,", DownloadCount & " 次下载,"), "")
+            Info += GetTimeSpanString([Date] - Date.Now) & "更新"
+            Info += If(ReleaseType <> 1, "," & ReleaseTypeString, "")
+
+            '建立控件
+            Dim NewItem As New MyListItem With {
+                .Title = DisplayName, .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Me,
+                .Info = Info
+            }
+            Select Case ReleaseType
+                Case 1 'R
+                    NewItem.Logo = "pack://application:,,,/images/Icons/R.png"
+                Case 2 'B
+                    NewItem.Logo = "pack://application:,,,/images/Icons/B.png"
+                Case Else 'A
+                    NewItem.Logo = "pack://application:,,,/images/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}
+                NewItem.PaddingRight = 35
+            End If
+
+            '结束
+            Return NewItem
+        End Function
+        Public Overrides Function ToString() As String
+            Return DisplayName
+        End Function
+
+    End Class
+
+    ''' <summary>
+    ''' 获取某个 CurseForge 工程下的全部文件列表。
+    ''' 必须在工作线程执行,失败会抛出异常。
+    ''' </summary>
+    Public Function DlCfGetFiles(ProjectId As Integer, IsModPack As Boolean) As List(Of DlCfFile)
+        '获取工程对象
+        Dim TargetProject As DlCfProject
+        If DlCfProjectCache.ContainsKey(ProjectId) Then
+            TargetProject = DlCfProjectCache(ProjectId)
+        Else
+            TargetProject = GetCfProjectFromJson(NetGetCodeByRequestRetry("https://api.curseforge.com/v1/mods/" & ProjectId, IsJson:=True, Encode:=Encoding.UTF8)("data"), IsModPack)
+        End If
+        '获取工程对象的文件列表
+        If TargetProject.Files Is Nothing Then
+            Log("[Download] 开始获取 CurseForge 工程 ID 为 " & ProjectId & " 的文件列表")
+            Dim Json As JArray
+            If TargetProject.IsModPack Then
+                '整合包使用全部文件
+                Json = NetGetCodeByRequestRetry("https://api.curseforge.com/v1/mods/" & ProjectId & "/files?pageSize=999", Accept:="application/json", IsJson:=True)("data")
+            Else
+                'Mod 使用每个版本最新的文件
+                'TODO: 完整 Mod 文件列表支持
+                Json = GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(TargetProject.FileIndexes, ",") & "]}", "application/json"))("data")
+            End If
+            Dim Files As New Dictionary(Of Integer, DlCfFile)
+            For Each JsonEntry As JObject In Json
+                Dim NewFile As New DlCfFile(JsonEntry, IsModPack)
+                If NewFile.IsAvailable AndAlso Not Files.ContainsKey(NewFile.FileId) Then Files.Add(NewFile.FileId, NewFile)
+            Next
+            TargetProject.Files = Files.Values.ToList
+        End If
+        '获取前置 Mod 列表
+        If IsModPack Then GoTo ExitSub
+        Dim Deps As New List(Of Integer)
+        For Each File In TargetProject.Files
+            For Each AddonId In File.Dependencies
+                If Not Deps.Contains(AddonId) AndAlso Not DlCfProjectCache.ContainsKey(AddonId) Then Deps.Add(AddonId)
+            Next
+        Next
+        If Deps.Count > 0 Then
+            '获取前置 Mod 工程信息
+            Log("[Download] 文件列表中需要获取的前置 Mod:" & Join(Deps, ","))
+            GetCfProjectListFromJson(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods", "POST", "{""modIds"": [" & Join(Deps, ",") & "]}", "application/json"))("data"), False)
+        End If
+        '返回
+ExitSub:
+        Return TargetProject.Files
+    End Function
+
+    Public Sub DlCfFilesPreload(Stack As StackPanel, Entrys As List(Of DlCfFile), OnClick As MyListItem.ClickEventHandler)
+        '获取卡片对应的前置 ID
+        '如果为整合包就不会有 Dependencies 信息,所以不用管
+        Dim Deps As New List(Of Integer)
+        For Each File In Entrys
+            For Each AddonId In If(File.Dependencies, New List(Of Integer))
+                If Not Deps.Contains(AddonId) Then
+                    If Not DlCfProjectCache.ContainsKey(AddonId) Then
+                        Log("[Download] 未找到 ID 为 " & AddonId & " 的前置 Mod 信息", LogLevel.Developer)
+                        Continue For
+                    End If
+                    Deps.Add(AddonId)
+                End If
+            Next
+        Next
+        Deps.Remove(306612) '移除 Fabric API
+        If Deps.Count = 0 Then Exit Sub
+        Deps.Sort()
+        '添加开头间隔
+        Stack.Children.Add(New TextBlock With {.Text = "前置 Mod", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
+        '添加前置 Mod 列表
+        For Each Dep In Deps
+            Dim Item = DlCfProjectCache(Dep).ToCfItem(False, True, AddressOf FrmDownloadMod.ProjectClick)
+            'Item.Margin = New Thickness(5, 0, 0, 0)
+            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
+
+#Region "DlSource | 镜像下载源"
+
+    Public Function DlSourceResourceGet(MojangBase As String) As String()
+        Return {MojangBase.Replace("http://resources.download.minecraft.net", "https://download.mcbbs.net/assets"),
+                MojangBase,
+                MojangBase.Replace("http://resources.download.minecraft.net", "https://bmclapi2.bangbang93.com/assets")
+               }
+    End Function
+
+    Public Function DlSourceLibraryGet(MojangBase As String) As String()
+        Return {MojangBase.Replace("https://libraries.minecraft.net", "https://download.mcbbs.net/maven"),
+                MojangBase.Replace("https://libraries.minecraft.net", "https://download.mcbbs.net/libraries"),
+                MojangBase,
+                MojangBase.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/maven"),
+                MojangBase.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
+               }
+    End Function
+
+    Public Function DlSourceLauncherOrMetaGet(MojangBase As String, Optional IsStatic As Boolean = True) As String()
+        If MojangBase Is Nothing Then Throw New Exception("无对应的 Json 下载地址")
+        If IsStatic Then
+            Return {MojangBase.Replace("https://launcher.mojang.com", "https://download.mcbbs.net").Replace("https://launchermeta.mojang.com", "https://download.mcbbs.net"),
+                    MojangBase,
+                    MojangBase.Replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com").Replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
+                   }
+        Else
+            Return {MojangBase.Replace("https://launcher.mojang.com", "https://download.mcbbs.net").Replace("https://launchermeta.mojang.com", "https://download.mcbbs.net"),
+                    MojangBase,
+                    MojangBase.Replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com").Replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
+                   }
+        End If
+    End Function
+
+    Private Sub DlSourceLoader(Of InputType, OutputType)(MainLoader As LoaderTask(Of InputType, OutputType),
+                                                         LoaderList As List(Of KeyValuePair(Of LoaderTask(Of InputType, OutputType), Integer)),
+                                                         Optional IsForceRestart As Boolean = False)
+        Dim WaitCycle As Integer = 0
+        Do While True
+            '检查加载结束
+            Dim IsAllFailed As Boolean = True
+            For Each SubLoader In LoaderList
+                If WaitCycle = 0 Then
+                    '如果要强制刷新,就不使用已经加载好的值
+                    If IsForceRestart Then Exit For
+                    '如果输入不一样,就不使用已经加载好的值
+                    If (SubLoader.Key.Input Is Nothing Xor MainLoader.Input Is Nothing) OrElse
+                       (SubLoader.Key.Input IsNot Nothing AndAlso Not SubLoader.Key.Input.Equals(MainLoader.Input)) Then Continue For
+                End If
+                If SubLoader.Key.State <> LoadState.Failed Then IsAllFailed = False
+                If SubLoader.Key.State = LoadState.Finished Then
+                    MainLoader.Output = SubLoader.Key.Output
+                    DlSourceLoaderAbort(LoaderList)
+                    Exit Sub
+                ElseIf IsAllFailed Then
+                    If WaitCycle < SubLoader.Value * 100 Then WaitCycle = SubLoader.Value * 100
+                End If
+                '由于 Forge BMCLAPI 没有可用版本导致强制失败
+                '在没有可用版本时,官方源会一直卡住,直接使用 BMCLAPI 判定失败即可
+                If SubLoader.Key.Error IsNot Nothing AndAlso SubLoader.Key.Error.Message.Contains("没有可用版本") Then
+                    For Each SubLoader2 In LoaderList
+                        If WaitCycle < SubLoader2.Value * 100 Then WaitCycle = SubLoader2.Value * 100
+                    Next
+                End If
+            Next
+            '启动加载源
+            If WaitCycle = 0 Then
+                '启动第一个源
+                LoaderList(0).Key.Start(MainLoader.Input, IsForceRestart)
+            End If
+            For i = 0 To LoaderList.Count - 1
+                If WaitCycle = LoaderList(i).Value * 100 Then
+                    If i < LoaderList.Count - 1 Then
+                        '启动下一个源
+                        LoaderList(i + 1).Key.Start(MainLoader.Input, IsForceRestart)
+                    Else
+                        '失败
+                        Dim ErrorInfo As Exception = Nothing
+                        For ii = 0 To LoaderList.Count - 1
+                            LoaderList(ii).Key.Input = Nothing '重置输入,以免以同样的输入“重试加载”时直接失败
+                            If LoaderList(ii).Key.Error IsNot Nothing Then
+                                If ErrorInfo Is Nothing OrElse LoaderList(ii).Key.Error.Message.Contains("没有可用版本") Then
+                                    ErrorInfo = LoaderList(ii).Key.Error
+                                End If
+                            End If
+                        Next
+                        If ErrorInfo Is Nothing Then ErrorInfo = New TimeoutException("下载源连接超时")
+                        DlSourceLoaderAbort(LoaderList)
+                        Throw ErrorInfo
+                    End If
+                    Exit For
+                End If
+            Next
+            '计时
+            If MainLoader.IsAborted Then
+                DlSourceLoaderAbort(LoaderList)
+                Exit Sub
+            End If
+            Thread.Sleep(10)
+            WaitCycle += 1
+        Loop
+    End Sub
+    Private Sub DlSourceLoaderAbort(Of InputType, OutputType)(LoaderList As List(Of KeyValuePair(Of LoaderTask(Of InputType, OutputType), Integer)))
+        For Each Loader In LoaderList
+            If Loader.Key.State = LoadState.Loading Then Loader.Key.Abort()
+        Next
+    End Sub
+
+#End Region
+
+End Module

+ 1826 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -0,0 +1,1826 @@
+Imports System.IO.Compression
+
+Public Module ModLaunch
+
+#Region "开始"
+
+    ''' <summary>
+    ''' 记录启动日志。
+    ''' </summary>
+    Public Sub McLaunchLog(Text As String)
+        RunInUi(Sub()
+                    FrmLaunchRight.LabLog.Text += vbCrLf & "[" & GetTimeNow() & "] " & Text
+                End Sub)
+        Log("[Launch] " & Text)
+    End Sub
+
+    '启动状态切换
+    Public McLaunchLoader As New LoaderTask(Of String, Object)("Loader Launch", AddressOf McLaunchStart) With {.OnStateChanged = AddressOf McLaunchState, .ReloadTimeout = 1}
+    Public McLaunchLoaderReal As LoaderCombo(Of Object)
+    Public McLaunchProcess As Process
+    Public McLaunchWatcher As Watcher
+    Private Sub McLaunchState(Loader As LoaderTask(Of String, Object))
+        Select Case McLaunchLoader.State
+            Case LoadState.Finished, LoadState.Failed, LoadState.Waiting, LoadState.Aborted
+                FrmLaunchLeft.PageChangeToLogin()
+            Case LoadState.Loading
+                '在预检测结束后再触发动画
+                FrmLaunchRight.LabLog.Text = ""
+        End Select
+    End Sub
+    Private Sub McLaunchStart(Loader As LoaderTask(Of String, Object))
+        '开始动画
+        RunInUiWait(AddressOf FrmLaunchLeft.PageChangeToLaunching)
+        '预检测(预检测的错误将直接抛出)
+        Try
+            McLaunchPrecheck()
+            McLaunchLog("预检测已通过")
+        Catch ex As Exception
+            Hint(ex.Message, HintType.Critical)
+            Throw
+        End Try
+        '正式加载
+        Try
+            '构造主加载器
+            Dim LaunchLoader As New LoaderCombo(Of Object)("Minecraft 启动", {
+                New LoaderCombo(Of Integer)("Java 处理", {
+                    New LoaderTask(Of Integer, List(Of NetFile))("Java 验证", AddressOf McLaunchJavaValidate) With {.ProgressWeight = 2}
+                }) With {.ProgressWeight = 2, .Show = False, .Block = False},
+                McLoginLoader,
+                New LoaderCombo(Of String)("补全文件", DlClientFix(McVersionCurrent, False, AssetsIndexExistsBehaviour.DownloadInBackground, True)) With {.ProgressWeight = 15, .Show = False, .Block = True},
+                New LoaderTask(Of Integer, String)("提供参数中的服务器 IP", Sub(InnerLoader As LoaderTask(Of Integer, String)) InnerLoader.Output = Loader.Input) With {.ProgressWeight = 0.01, .Show = False},
+                New LoaderTask(Of String, List(Of McLibToken))("获取启动参数", AddressOf McLaunchArgumentMain) With {.ProgressWeight = 2},
+                New LoaderTask(Of List(Of McLibToken), Integer)("解压文件", AddressOf McLaunchNatives) With {.ProgressWeight = 2},
+                New LoaderTask(Of Integer, Integer)("预启动处理", AddressOf McLaunchPrerun) With {.ProgressWeight = 1},
+                New LoaderTask(Of Integer, Process)("启动进程", AddressOf McLaunchRun) With {.ProgressWeight = 2},
+                New LoaderTask(Of Process, Integer)("等待游戏窗口出现", AddressOf McLaunchWait) With {.ProgressWeight = 1},
+                New LoaderTask(Of Integer, Integer)("结束处理", AddressOf McLaunchEnd) With {.ProgressWeight = 1}
+            }) With {.Show = False}
+            '等待加载器执行并更新 UI
+            McLaunchLoaderReal = LaunchLoader
+            LaunchLoader.Start()
+            '任务栏进度条
+            LoaderTaskbarAdd(LaunchLoader)
+            Do While LaunchLoader.State = LoadState.Loading
+                FrmLaunchLeft.Dispatcher.Invoke(AddressOf FrmLaunchLeft.LaunchingRefresh)
+                Thread.Sleep(200)
+            Loop
+            FrmLaunchLeft.Dispatcher.Invoke(AddressOf FrmLaunchLeft.LaunchingRefresh)
+            '成功与失败处理
+            Select Case LaunchLoader.State
+                Case LoadState.Finished
+                    Hint(McVersionCurrent.Name & " 启动成功!", HintType.Finish)
+                Case LoadState.Aborted
+                    Hint("已取消启动!", HintType.Info)
+                Case LoadState.Failed
+                    Throw LaunchLoader.Error
+                Case Else
+                    Throw New Exception("错误的状态改变:" & GetStringFromEnum(CType(LaunchLoader.State, [Enum])))
+            End Select
+        Catch ex As Exception
+            Dim CurrentEx = ex
+NextInner:
+            If CurrentEx.Message.StartsWith("$") Then
+                '若有以 $ 开头的错误信息,则以此为准显示提示
+                '若错误信息为 $$,则不提示
+                If Not CurrentEx.Message = "$$" Then MyMsgBox(CurrentEx.Message.TrimStart("$"), "启动失败")
+                Throw
+            ElseIf CurrentEx.InnerException IsNot Nothing Then
+                '检查下一级错误
+                CurrentEx = CurrentEx.InnerException
+                GoTo NextInner
+            Else
+                '没有特殊处理过的错误信息
+                McLaunchLog("错误:" & GetString(ex, False))
+                Log(ex, "Minecraft 启动失败", LogLevel.Msgbox, "启动失败")
+                Throw
+            End If
+        End Try
+    End Sub
+
+#End Region
+
+#Region "预检测"
+
+    Private Sub McLaunchPrecheck()
+        '检查路径
+        If McVersionCurrent.PathIndie.Contains("!") OrElse McVersionCurrent.PathIndie.Contains(";") Then Throw New Exception("游戏路径中不可包含 ! 或 ;(" & McVersionCurrent.PathIndie & ")")
+        If McVersionCurrent.Path.Contains("!") OrElse McVersionCurrent.Path.Contains(";") Then Throw New Exception("游戏路径中不可包含 ! 或 ;(" & McVersionCurrent.Path & ")")
+        '检查输入信息
+        Dim CheckResult As String = Nothing
+        RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput))
+        If CheckResult <> "" Then Throw New ArgumentException(CheckResult)
+        '检查版本
+        If McVersionCurrent Is Nothing Then Throw New Exception("未选择 Minecraft 版本!")
+        McVersionCurrent.Load()
+        If McVersionCurrent.State = McVersionState.Error Then Throw New Exception("Minecraft 存在问题:" & McVersionCurrent.Info)
+        '求赞助
+        If ThemeCheckGold() Then Exit Sub
+        RunInNewThread(Sub()
+                           Select Case Setup.Get("SystemLaunchCount")
+                               Case 20, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000
+                                   If MyMsgBox("PCL2 已经为你启动了 " & Setup.Get("SystemLaunchCount") & " 次游戏啦!" & vbCrLf &
+                                               "如果觉得 PCL2 还算好用的话,也可以考虑小小地赞助一下作者呢 qwq……" & vbCrLf &
+                                               "毕竟一个人开发也不容易(小声)……",
+                                               "求赞助啦……", "这就赞助!", "但是我拒绝") = 1 Then
+                                       OpenWebsite("https://afdian.net/@LTCat/plan")
+                                   End If
+                           End Select
+                       End Sub, "Donate")
+    End Sub
+
+#End Region
+
+#Region "主登录模块"
+
+    '登录方式
+    Public Enum McLoginType
+        Legacy = 0
+        Mojang = 1
+        Nide = 2
+        Auth = 3
+        Ms = 5
+    End Enum
+
+    '各个登录方式的对应数据
+    Public MustInherit Class McLoginData
+        ''' <summary>
+        ''' 登录方式。
+        ''' </summary>
+        Public Type As McLoginType
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Return obj IsNot Nothing AndAlso obj.GetHashCode() = GetHashCode()
+        End Function
+    End Class
+    Public Class McLoginServer
+        Inherits McLoginData
+
+        ''' <summary>
+        ''' 登录用户名。
+        ''' </summary>
+        Public UserName As String
+        ''' <summary>
+        ''' 登录密码。
+        ''' </summary>
+        Public Password As String
+        ''' <summary>
+        ''' 登录服务器基础地址。
+        ''' </summary>
+        Public BaseUrl As String
+        ''' <summary>
+        ''' 登录所使用的标识符,如 “Mojang”、“Nide”,用于存储缓存等。
+        ''' </summary>
+        Public Token As String
+        ''' <summary>
+        ''' 登录方式的描述字符串,如 “正版”、“统一通行证”。
+        ''' </summary>
+        Public Description As String
+        ''' <summary>
+        ''' 是否在本次登录中强制要求玩家重新选择角色,目前仅对 Authlib-Injector 生效。
+        ''' </summary>
+        Public ForceReselectProfile As Boolean = False
+
+        Public Sub New(Type As McLoginType)
+            Me.Type = Type
+        End Sub
+        Public Overrides Function GetHashCode() As Integer
+            Return GetHash(UserName & Password & BaseUrl & Token & Type) Mod Integer.MaxValue
+        End Function
+
+    End Class
+    Public Class McLoginMs
+        Inherits McLoginData
+
+        ''' <summary>
+        ''' 缓存的 OAuth Refresh Token。若没有则为空字符串。
+        ''' </summary>
+        Public OAuthRefreshToken As String = ""
+        Public AccessToken As String = ""
+        Public Uuid As String = ""
+        Public UserName As String = ""
+
+        Public Sub New()
+            Type = McLoginType.Ms
+        End Sub
+        Public Overrides Function GetHashCode() As Integer
+            Return GetHash(OAuthRefreshToken & AccessToken & Uuid & UserName) Mod Integer.MaxValue
+        End Function
+    End Class
+    Public Class McLoginLegacy
+        Inherits McLoginData
+        ''' <summary>
+        ''' 登录用户名。
+        ''' </summary>
+        Public UserName As String
+        ''' <summary>
+        ''' 皮肤种类。
+        ''' </summary>
+        Public SkinType As Integer
+        ''' <summary>
+        ''' 若采用正版皮肤,则为该皮肤名。
+        ''' </summary>
+        Public SkinName As String
+
+        Public Sub New()
+            Type = McLoginType.Legacy
+        End Sub
+        Public Overrides Function GetHashCode() As Integer
+            Return GetHash(UserName & SkinType & SkinName & Type) Mod Integer.MaxValue
+        End Function
+    End Class
+
+    '登录返回结果
+    Public Structure McLoginResult
+        Public Name As String
+        Public Uuid As String
+        Public AccessToken As String
+        Public Type As String
+        ''' <summary>
+        ''' 仅用于登录方式为 Mojang 正版时的 Refresh Login,其余时间为一个无意义的 ID(一般为玩家的 UUID)。
+        ''' </summary>
+        Public ClientToken As String
+        ''' <summary>
+        ''' 用于登录的邮箱。仅用于登录方式为 Mojang 正版时的 launcher_profile 更新。
+        ''' </summary>
+        Public Email As String
+        ''' <summary>
+        ''' 进行微软登录时返回的 profile 信息。
+        ''' </summary>
+        Public ProfileJson As String
+    End Structure
+
+    ''' <summary>
+    ''' 根据登录信息获取玩家的 MC 用户名。如果无法获取则返回 Nothing。
+    ''' </summary>
+    Public Function McLoginName() As String
+        '根据当前登录方式优先返回
+        Select Case Setup.Get("LoginType")
+            Case McLoginType.Mojang
+                If Setup.Get("CacheMojangName") <> "" Then Return Setup.Get("CacheMojangName")
+            Case McLoginType.Ms
+                If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
+            Case McLoginType.Legacy
+                If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Split("¨")(0)
+            Case McLoginType.Nide
+                If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
+            Case McLoginType.Auth
+                If Setup.Get("CacheAuthName") <> "" Then Return Setup.Get("CacheAuthName")
+        End Select
+        '查找所有可能的项
+        If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
+        If Setup.Get("CacheMojangName") <> "" Then Return Setup.Get("CacheMojangName")
+        If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
+        If Setup.Get("CacheAuthName") <> "" Then Return Setup.Get("CacheAuthName")
+        If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Split("¨")(0)
+        Return Nothing
+    End Function
+    ''' <summary>
+    ''' 当前是否可以进行登录。若不可以则会返回错误原因。
+    ''' </summary>
+    Public Function McLoginAble() As String
+        Select Case Setup.Get("LoginType")
+            Case McLoginType.Mojang
+                If Setup.Get("CacheMojangAccess") = "" Then
+                    Return FrmLoginMojang.IsVaild()
+                Else
+                    Return ""
+                End If
+            Case McLoginType.Ms
+                If Setup.Get("CacheMsOAuthRefresh") = "" Then
+                    Return FrmLoginMs.IsVaild()
+                Else
+                    Return ""
+                End If
+            Case McLoginType.Legacy
+                Return FrmLoginLegacy.IsVaild()
+            Case McLoginType.Nide
+                If Setup.Get("CacheNideAccess") = "" Then
+                    Return FrmLoginNide.IsVaild()
+                Else
+                    Return ""
+                End If
+            Case McLoginType.Auth
+                If Setup.Get("CacheAuthAccess") = "" Then
+                    Return FrmLoginAuth.IsVaild()
+                Else
+                    Return ""
+                End If
+            Case Else
+                Return "未知的登录方式"
+        End Select
+    End Function
+    ''' <summary>
+    ''' 登录输入是否可以进行登录。若不可以则会返回错误原因。
+    ''' </summary>
+    Public Function McLoginAble(LoginData As McLoginData) As String
+        Select Case LoginData.Type
+            Case McLoginType.Mojang
+                Return PageLoginMojang.IsVaild(LoginData)
+            Case McLoginType.Ms
+                Return PageLoginMs.IsVaild(LoginData)
+            Case McLoginType.Legacy
+                Return PageLoginLegacy.IsVaild(LoginData)
+            Case McLoginType.Nide
+                Return PageLoginNide.IsVaild(LoginData)
+            Case McLoginType.Auth
+                Return PageLoginAuth.IsVaild(LoginData)
+            Case Else
+                Return "未知的登录方式"
+        End Select
+    End Function
+
+    '登录主模块加载器
+    Public McLoginLoader As New LoaderTask(Of McLoginData, McLoginResult)("登录", AddressOf McLoginStart, AddressOf McLoginInput, ThreadPriority.BelowNormal) With {.ReloadTimeout = 1, .ProgressWeight = 15, .Block = False}
+    Public Function McLoginInput() As McLoginData
+        Dim LoginData As McLoginData = Nothing
+        Dim LoginType As McLoginType = Setup.Get("LoginType")
+        Try
+            Select Case LoginType
+                Case McLoginType.Legacy
+                    LoginData = PageLoginLegacy.GetLoginData()
+                Case McLoginType.Mojang
+                    If Setup.Get("CacheMojangAccess") = "" Then
+                        LoginData = PageLoginMojang.GetLoginData()
+                    Else
+                        LoginData = PageLoginMojangSkin.GetLoginData()
+                    End If
+                Case McLoginType.Ms
+                    If Setup.Get("CacheMsOAuthRefresh") = "" Then
+                        LoginData = PageLoginMs.GetLoginData()
+                    Else
+                        LoginData = PageLoginMsSkin.GetLoginData()
+                    End If
+                Case McLoginType.Nide
+                    If Setup.Get("CacheNideAccess") = "" Then
+                        LoginData = PageLoginNide.GetLoginData()
+                    Else
+                        LoginData = PageLoginNideSkin.GetLoginData()
+                    End If
+                Case McLoginType.Auth
+                    If Setup.Get("CacheAuthAccess") = "" Then
+                        LoginData = PageLoginAuth.GetLoginData()
+                    Else
+                        LoginData = PageLoginAuthSkin.GetLoginData()
+                    End If
+            End Select
+        Catch ex As Exception
+            Log(ex, "获取登录输入信息失败(" & GetStringFromEnum(LoginType) & ")", LogLevel.Feedback)
+        End Try
+        Return LoginData
+    End Function
+    Private Sub McLoginStart(Data As LoaderTask(Of McLoginData, McLoginResult))
+        McLaunchLog("登录线程已启动")
+        '校验登录信息
+        Dim CheckResult As String = McLoginAble(Data.Input)
+        If Not CheckResult = "" Then Throw New ArgumentException(CheckResult)
+        '获取对应加载器
+        Dim Loader As LoaderBase = Nothing
+        Select Case Data.Input.Type
+            Case McLoginType.Mojang
+                Loader = McLoginMojangLoader
+            Case McLoginType.Ms
+                Loader = McLoginMsLoader
+            Case McLoginType.Legacy
+                Loader = McLoginLegacyLoader
+            Case McLoginType.Nide
+                Loader = McLoginNideLoader
+            Case McLoginType.Auth
+                Loader = McLoginAuthLoader
+        End Select
+        '尝试加载
+        Loader.WaitForExit(Data.Input, McLoginLoader, Data.IsForceRestarting)
+        Data.Output = CType(Loader, Object).Output
+        RunInUi(Sub() FrmLaunchLeft.RefreshPage(True, False)) '刷新自动填充列表
+    End Sub
+
+#End Region
+#Region "分方式登录模块"
+
+    '各个登录方式的主对象与输入构造
+    Public McLoginMojangLoader As New LoaderTask(Of McLoginServer, McLoginResult)("Loader Login Mojang", AddressOf McLoginServerStart) With {.ReloadTimeout = 60000}
+    Public McLoginMsLoader As New LoaderTask(Of McLoginMs, McLoginResult)("Loader Login Ms", AddressOf McLoginMsStart) With {.ReloadTimeout = 300000}
+    Public McLoginLegacyLoader As New LoaderTask(Of McLoginLegacy, McLoginResult)("Loader Login Legacy", AddressOf McLoginLegacyStart)
+    Public McLoginNideLoader As New LoaderTask(Of McLoginServer, McLoginResult)("Loader Login Nide", AddressOf McLoginServerStart) With {.ReloadTimeout = 60000}
+    Public McLoginAuthLoader As New LoaderTask(Of McLoginServer, McLoginResult)("Loader Login Auth", AddressOf McLoginServerStart) With {.ReloadTimeout = 60000}
+
+    '主加载函数,返回所有需要的登录信息
+    Private Sub McLoginMsStart(Data As LoaderTask(Of McLoginMs, McLoginResult))
+        Dim Input As McLoginMs = Data.Input
+        Dim LogUsername As String = Input.UserName
+        McLaunchLog("登录方式:微软正版(" & If(LogUsername = "", "尚未登录", LogUsername) & ")")
+        Data.Progress = 0.05
+        '检查是否已经登录完成
+        If Input.AccessToken <> "" AndAlso Not Data.IsForceRestarting Then
+            Data.Output = New McLoginResult With {.AccessToken = Input.AccessToken, .Name = Input.UserName, .Uuid = Input.Uuid, .Type = "Microsoft", .ClientToken = Input.Uuid}
+            GoTo SkipLogin
+        End If
+        '尝试登录
+        Dim OAuthTokens As String()
+        If Setup.Get("CacheMsOAuthRefresh") = "" Then
+            '无 RefreshToken
+Relogin:
+            Dim OAuthCode As String = MsLoginStep1(Data)
+            If Data.IsAborted Then Throw New ThreadInterruptedException
+            Data.Progress = 0.2
+            OAuthTokens = MsLoginStep2(OAuthCode, False)
+        Else
+            '有 RefreshToken
+            OAuthTokens = MsLoginStep2(Setup.Get("CacheMsOAuthRefresh"), True)
+        End If
+        '要求重新打开登录网页认证
+        If OAuthTokens(0) = "Relogin" Then GoTo Relogin
+        Data.Progress = 0.35
+        If Data.IsAborted Then Throw New ThreadInterruptedException
+        Dim OAuthAccessToken As String = OAuthTokens(0)
+        Dim OAuthRefreshToken As String = OAuthTokens(1)
+        Dim XBLToken As String = MsLoginStep3(OAuthAccessToken)
+        Data.Progress = 0.5
+        If Data.IsAborted Then Throw New ThreadInterruptedException
+        Dim Tokens = MsLoginStep4(XBLToken)
+        Data.Progress = 0.65
+        If Data.IsAborted Then Throw New ThreadInterruptedException
+        Dim AccessToken As String = MsLoginStep5(Tokens)
+        Data.Progress = 0.8
+        If Data.IsAborted Then Throw New ThreadInterruptedException
+        Dim Result = MsLoginStep6(AccessToken)
+        '输出登录结果
+        Setup.Set("CacheMsOAuthRefresh", OAuthRefreshToken)
+        Setup.Set("CacheMsAccess", AccessToken)
+        Setup.Set("CacheMsUuid", Result(0))
+        Setup.Set("CacheMsName", Result(1))
+        Data.Output = New McLoginResult With {.AccessToken = AccessToken, .Name = Result(1), .Uuid = Result(0), .Type = "Microsoft", .ClientToken = Result(0), .ProfileJson = Result(2)}
+        '解锁主题
+SkipLogin:
+        McLaunchLog("微软登录完成")
+        Data.Progress = 0.95
+        If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
+    End Sub
+    Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
+        Dim Input As McLoginServer = Data.Input
+        Dim NeedRefresh As Boolean = False
+        Dim LogUsername As String = Input.UserName
+        If LogUsername.Contains("@") AndAlso Setup.Get("UiLauncherEmail") Then
+            LogUsername = AccountFilter(LogUsername)
+        End If
+        McLaunchLog("登录方式:" & Input.Description & "(" & LogUsername & ")")
+        Data.Progress = 0.05
+        '尝试登录
+        If (Not Data.Input.ForceReselectProfile) AndAlso Setup.Get("Cache" & Input.Token & "Username") = Data.Input.UserName AndAlso Setup.Get("Cache" & Input.Token & "Pass") = Data.Input.Password AndAlso Not Setup.Get("Cache" & Input.Token & "Access") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Client") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Uuid") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Name") = "" Then
+            '尝试验证登录
+            Try
+                If Data.IsAborted Then Throw New ThreadInterruptedException
+                McLoginRequestValidate(Data)
+                GoTo LoginFinish
+            Catch ex As Exception
+                Dim AllMessage = GetString(ex)
+                McLaunchLog("验证登录失败:" & AllMessage)
+                If (AllMessage.Contains("超时") OrElse AllMessage.Contains("imeout")) AndAlso Not AllMessage.Contains("403") Then
+                    McLaunchLog("已触发超时登录失败")
+                    Throw New Exception("$登录失败:连接登录服务器超时。" & vbCrLf & "请检查你的网络状况是否良好,或尝试使用 VPN!")
+                End If
+            End Try
+            Data.Progress = 0.25
+            '尝试刷新登录
+Refresh:
+            Try
+                If Data.IsAborted Then Throw New ThreadInterruptedException
+                McLoginRequestRefresh(Data, NeedRefresh)
+                GoTo LoginFinish
+            Catch ex As Exception
+                McLaunchLog("刷新登录失败:" & GetString(ex))
+            End Try
+            Data.Progress = If(NeedRefresh, 0.85, 0.45)
+        End If
+        '尝试普通登录
+        Try
+            If Data.IsAborted Then Throw New ThreadInterruptedException
+            NeedRefresh = McLoginRequestLogin(Data)
+        Catch ex As Exception
+            McLaunchLog("登录失败:" & GetString(ex))
+            Throw
+        End Try
+        If NeedRefresh Then
+            Data.Progress = 0.65
+            GoTo Refresh
+        End If
+LoginFinish:
+        Data.Progress = 0.95
+        '保存启动记录
+        Dim Dict As New Dictionary(Of String, String)
+        Dim Emails As New List(Of String)
+        Dim Passwords As New List(Of String)
+        Try
+            If Not Setup.Get("Login" & Input.Token & "Email") = "" Then Emails.AddRange(Setup.Get("Login" & Input.Token & "Email").ToString.Split("¨"))
+            If Not Setup.Get("Login" & Input.Token & "Pass") = "" Then Passwords.AddRange(Setup.Get("Login" & Input.Token & "Pass").ToString.Split("¨"))
+            For i = 0 To Emails.Count - 1
+                Dict.Add(Emails(i), Passwords(i))
+            Next
+            Dict.Remove(Input.UserName)
+            Emails = New List(Of String)(Dict.Keys)
+            Emails.Insert(0, Input.UserName)
+            Passwords = New List(Of String)(Dict.Values)
+            Passwords.Insert(0, Input.Password)
+            Setup.Set("Login" & Input.Token & "Email", Join(Emails, "¨"))
+            Setup.Set("Login" & Input.Token & "Pass", Join(Passwords, "¨"))
+        Catch ex As Exception
+            Log(ex, "保存启动记录失败", LogLevel.Hint)
+            Setup.Set("Login" & Input.Token & "Email", "")
+            Setup.Set("Login" & Input.Token & "Pass", "")
+        End Try
+    End Sub
+    Private Sub McLoginLegacyStart(Data As LoaderTask(Of McLoginLegacy, McLoginResult))
+        Dim Input As McLoginLegacy = Data.Input
+        McLaunchLog("登录方式:离线(" & Input.UserName & ")")
+        Data.Progress = 0.1
+        With Data.Output
+            .Name = Input.UserName
+            .Uuid = McLoginLegacyUuid(Input.UserName)
+            .Type = "Legacy"
+        End With
+        '根据离线皮肤获取实际使用的 Uuid
+        Select Case Input.SkinType
+            Case 0
+                '默认,不需要处理
+            Case 1
+                'Steve
+                Do Until McSkinSex(Data.Output.Uuid) = "Steve"
+                    If Data.Output.Uuid.EndsWith("FFFFF") Then Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & "00000"
+                    Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & (Long.Parse(Right(Data.Output.Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
+                Loop
+            Case 2
+                'Alex
+                Do Until McSkinSex(Data.Output.Uuid) = "Alex"
+                    If Data.Output.Uuid.EndsWith("FFFFF") Then Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & "00000"
+                    Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & (Long.Parse(Right(Data.Output.Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
+                Loop
+            Case 3
+                '使用正版用户名
+                Try
+                    If Not Input.SkinName = "" Then
+                        Log("[Skin] 由于离线皮肤设置,使用正版 UUID:" & Input.SkinName)
+                        Data.Output.Uuid = McLoginMojangUuid(Input.SkinName, False)
+                    End If
+                Catch ex As Exception
+                    Log(ex, "离线启动时使用的正版皮肤获取失败")
+                    MyMsgBox("由于设置的离线启动时使用的正版皮肤获取失败,游戏将以无皮肤的方式启动。" & vbCrLf & "请检查你的网络是否通畅,或尝试使用 VPN!" & vbCrLf & vbCrLf & "详细的错误信息:" & ex.Message, "皮肤获取失败")
+                End Try
+            Case 4
+                '自定义
+                Do Until McSkinSex(Data.Output.Uuid) = If(Setup.Get("LaunchSkinSlim"), "Alex", "Steve")
+                    If Data.Output.Uuid.EndsWith("FFFFF") Then Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & "00000"
+                    Data.Output.Uuid = Mid(Data.Output.Uuid, 1, 32 - 5) & (Long.Parse(Right(Data.Output.Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
+                Loop
+        End Select
+        '将结果扩展到所有项目中
+        'Data.Output.AccessToken = Setup.Get("CacheMojangAccess") '不能优先使用缓存的 AccessToken,这会导致离线用户名设置失效
+        'If Data.Output.AccessToken.Length < 300 Then
+        Data.Output.AccessToken = Data.Output.Uuid
+        'Data.Output.ClientToken = Setup.Get("CacheMojangClient")
+        'If Data.Output.AccessToken.Length <> 32 Then
+        Data.Output.ClientToken = Data.Output.Uuid
+        '保存启动记录
+        Dim Names As New List(Of String)
+        If Not Setup.Get("LoginLegacyName") = "" Then Names.AddRange(Setup.Get("LoginLegacyName").ToString.Split("¨"))
+        Names.Remove(Input.UserName)
+        Names.Insert(0, Input.UserName)
+        Setup.Set("LoginLegacyName", Join(Names.ToArray, "¨"))
+    End Sub
+
+    'Server 登录:三种验证方式的请求
+    Private Sub McLoginRequestValidate(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult))
+        McLaunchLog("验证登录开始(Validate, " & Data.Input.Token & ")")
+        '提前缓存信息,否则如果在登录请求过程中退出登录,设置项目会被清空,导致输出存在空值
+        Dim AccessToken As String = Setup.Get("Cache" & Data.Input.Token & "Access")
+        Dim ClientToken As String = Setup.Get("Cache" & Data.Input.Token & "Client")
+        Dim Uuid As String = Setup.Get("Cache" & Data.Input.Token & "Uuid")
+        Dim Name As String = Setup.Get("Cache" & Data.Input.Token & "Name")
+        '发送登录请求
+        NetRequestRetry(
+               Url:=Data.Input.BaseUrl & "/validate",
+               Method:="POST",
+               Data:="{""accessToken"":""" & AccessToken & """,""clientToken"":""" & ClientToken & """,""requestUser"":true}",
+               ContentType:="application/json; charset=utf-8") '没有返回值的
+        '将登录结果输出
+        Data.Output.AccessToken = AccessToken
+        Data.Output.ClientToken = ClientToken
+        Data.Output.Uuid = Uuid
+        Data.Output.Name = Name
+        Data.Output.Type = Data.Input.Token
+        Data.Output.Email = Data.Input.UserName
+        '不更改缓存,直接结束
+        McLaunchLog("验证登录成功(Validate, " & Data.Input.Token & ")")
+    End Sub
+    Private Sub McLoginRequestRefresh(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult), RequestUser As Boolean)
+        McLaunchLog("刷新登录开始(Refresh, " & Data.Input.Token & ")")
+        Dim LoginJson As JObject = GetJson(NetRequestRetry(
+               Url:=Data.Input.BaseUrl & "/refresh",
+               Method:="POST",
+               Data:="{" &
+               If(RequestUser, "
+               ""requestUser"": true,
+               ""selectedProfile"": {
+                   ""id"":""" & Setup.Get("Cache" & Data.Input.Token & "Uuid") & """,
+                   ""name"":""" & Setup.Get("Cache" & Data.Input.Token & "Name") & """},", "") & "
+               ""accessToken"":""" & Setup.Get("Cache" & Data.Input.Token & "Access") & """,
+               ""clientToken"":""" & Setup.Get("Cache" & Data.Input.Token & "Client") & """}",
+               ContentType:="application/json; charset=utf-8"))
+        '将登录结果输出
+        If LoginJson("selectedProfile") Is Nothing Then Throw New Exception("选择的角色 " & Setup.Get("Cache" & Data.Input.Token & "Name") & " 无效!")
+        Data.Output.AccessToken = LoginJson("accessToken").ToString
+        Data.Output.ClientToken = LoginJson("clientToken").ToString
+        Data.Output.Uuid = LoginJson("selectedProfile")("id").ToString
+        Data.Output.Name = LoginJson("selectedProfile")("name").ToString
+        Data.Output.Type = Data.Input.Token
+        Data.Output.Email = Data.Input.UserName
+        '保存缓存
+        Setup.Set("Cache" & Data.Input.Token & "Access", Data.Output.AccessToken)
+        Setup.Set("Cache" & Data.Input.Token & "Client", Data.Output.ClientToken)
+        Setup.Set("Cache" & Data.Input.Token & "Uuid", Data.Output.Uuid)
+        Setup.Set("Cache" & Data.Input.Token & "Name", Data.Output.Name)
+        Setup.Set("Cache" & Data.Input.Token & "Username", Data.Input.UserName)
+        Setup.Set("Cache" & Data.Input.Token & "Pass", Data.Input.Password)
+        McLaunchLog("刷新登录成功(Refresh, " & Data.Input.Token & ")")
+    End Sub
+    Private Function McLoginRequestLogin(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult)) As Boolean
+        Try
+            Dim NeedRefresh As Boolean = False
+            McLaunchLog("登录开始(Login, " & Data.Input.Token & ")")
+            Dim LoginJson As JObject = GetJson(NetRequestRetry(
+                   Url:=Data.Input.BaseUrl & "/authenticate",
+                   Method:="POST",
+                   Data:="{""agent"": {""name"": ""Minecraft"",""version"": 1},""username"":""" & Data.Input.UserName & """,""password"":""" & Data.Input.Password & """,""requestUser"":true}",
+                   ContentType:="application/json; charset=utf-8"))
+            '检查登录结果
+            If LoginJson("availableProfiles").Count = 0 Then
+                If Data.Input.Type = McLoginType.Auth Then
+                    If Data.Input.ForceReselectProfile Then Hint("你还没有创建角色,无法更换!", HintType.Critical)
+                    Throw New Exception("$你还没有创建角色,请在创建角色后再试!")
+                Else
+                    Throw New Exception("$你还没有购买 Minecraft 正版,请在购买后再试!")
+                End If
+            ElseIf Data.Input.ForceReselectProfile AndAlso LoginJson("availableProfiles").Count = 1 Then
+                Hint("你的账户中只有一个角色,无法更换!", HintType.Critical)
+            End If
+            Dim SelectedName As String = Nothing
+            Dim SelectedId As String = Nothing
+            If (LoginJson("selectedProfile") Is Nothing OrElse Data.Input.ForceReselectProfile) AndAlso LoginJson("availableProfiles").Count > 1 Then
+                '要求选择档案;优先从缓存读取
+                NeedRefresh = True
+                Dim CacheId As String = Setup.Get("Cache" & Data.Input.Token & "Uuid")
+                For Each Profile In LoginJson("availableProfiles")
+                    If Profile("id").ToString = CacheId Then
+                        SelectedName = Profile("name").ToString
+                        SelectedId = Profile("id").ToString
+                        McLaunchLog("根据缓存选择的角色:" & SelectedName)
+                    End If
+                Next
+                '缓存无效,要求玩家选择
+                If SelectedName Is Nothing Then
+                    McLaunchLog("要求玩家选择角色")
+                    RunInUiWait(Sub()
+                                    Dim SelectionControl As New List(Of IMyRadio)
+                                    Dim SelectionJson As New List(Of JToken)
+                                    For Each Profile In LoginJson("availableProfiles")
+                                        SelectionControl.Add(New MyRadioBox With {.Text = Profile("name").ToString})
+                                        SelectionJson.Add(Profile)
+                                    Next
+                                    Dim SelectedIndex As Integer = MyMsgBoxSelect(SelectionControl, "选择使用的角色")
+                                    SelectedName = SelectionJson(SelectedIndex)("name").ToString
+                                    SelectedId = SelectionJson(SelectedIndex)("id").ToString
+                                End Sub)
+                    McLaunchLog("玩家选择的角色:" & SelectedName)
+                End If
+            Else
+                SelectedName = LoginJson("selectedProfile")("name").ToString
+                SelectedId = LoginJson("selectedProfile")("id").ToString
+            End If
+            '将登录结果输出
+            Data.Output.AccessToken = LoginJson("accessToken").ToString
+            Data.Output.ClientToken = LoginJson("clientToken").ToString
+            Data.Output.Name = SelectedName
+            Data.Output.Uuid = SelectedId
+            Data.Output.Type = Data.Input.Token
+            Data.Output.Email = Data.Input.UserName
+            '保存缓存
+            Setup.Set("Cache" & Data.Input.Token & "Access", Data.Output.AccessToken)
+            Setup.Set("Cache" & Data.Input.Token & "Client", Data.Output.ClientToken)
+            Setup.Set("Cache" & Data.Input.Token & "Uuid", Data.Output.Uuid)
+            Setup.Set("Cache" & Data.Input.Token & "Name", Data.Output.Name)
+            Setup.Set("Cache" & Data.Input.Token & "Username", Data.Input.UserName)
+            Setup.Set("Cache" & Data.Input.Token & "Pass", Data.Input.Password)
+            McLaunchLog("登录成功(Login, " & Data.Input.Token & ")")
+            Return NeedRefresh
+        Catch ex As Exception
+            Dim AllMessage As String = GetString(ex)
+            Log(ex, "登录失败原始错误信息", LogLevel.Normal)
+            If AllMessage.Contains("410") AndAlso AllMessage.Contains("Migrated") Then
+                Throw New Exception("$登录失败:该 Mojang 账号已迁移至微软账号,请在上方的登录方式中选择 微软 并再次尝试登录!")
+            ElseIf AllMessage.Contains("403") Then
+                Select Case Data.Input.Type
+                    Case McLoginType.Auth
+                        Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
+                                            " - 输入的账号或密码错误。" & vbCrLf &
+                                            " - 登录尝试过于频繁,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
+                                            " - 只注册了账号,但没有在皮肤站新建角色。")
+                    Case McLoginType.Legacy
+                        Throw
+                    Case McLoginType.Mojang
+                        If AllMessage.Contains("Invalid username or password") Then
+                            Throw New Exception("$登录失败:输入的账号或密码有误。")
+                        Else
+                            Throw New Exception("$登录尝试过于频繁,导致被 Mojang 暂时屏蔽。请不要操作,等待 10 分钟后再试。")
+                        End If
+                    Case McLoginType.Ms
+                        Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
+                                            " - 登录尝试过于频繁,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
+                                            " - 账号类别错误。如果你在使用 Mojang 账号,请将登录方式切换为 Mojang。")
+                    Case McLoginType.Nide
+                        Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
+                                            " - 输入的账号或密码错误。" & vbCrLf &
+                                            " - 密码错误次数过多,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
+                                            If(Data.Input.UserName.Contains("@"), "", " - 登录账号应为邮箱或统一通行证账号,而非游戏角色 ID。" & vbCrLf) &
+                                            " - 只注册了账号,但没有加入对应服务器。")
+                End Select
+            ElseIf AllMessage.Contains("超时") OrElse AllMessage.Contains("imeout") Then
+                Throw New Exception("$登录失败:连接登录服务器超时。" & vbCrLf & "请检查你的网络状况是否良好,或尝试使用 VPN!")
+            ElseIf AllMessage.Contains("网络请求失败") Then
+                Throw New Exception("$登录失败:连接登录服务器失败。" & vbCrLf & "请检查你的网络状况是否良好,或尝试使用 VPN!")
+            ElseIf ex.Message.StartsWith("$") Then
+                Throw
+            Else
+                Throw New Exception("登录失败:" & ex.Message, ex)
+            End If
+            Return False
+        End Try
+    End Function
+
+    '微软登录步骤 1:打开网页认证,获取 OAuth Code
+    Private Function MsLoginStep1(Data As LoaderTask(Of McLoginMs, McLoginResult)) As String
+        McLaunchLog("开始微软登录步骤 1")
+        If OsVersion <= New Version(10, 0, 17763, 0) Then 'TODO: 添加设置以强制使用网页中继登录
+            'Windows 7 或老版 Windows 10 登录
+            MyMsgBox("PCL2 即将打开登录网页。登录后会转到一个空白页面(这代表登录成功了),请将该空白页面的网址复制到 PCL2。" & vbCrLf &
+                     "如果网络环境不佳,登录网页可能一直加载不出来,此时请尝试使用 VPN 或代理服务器,然后再试。", "登录说明", "开始", ForceWait:=True)
+            OpenWebsite(FormLoginOAuth.LoginUrl1)
+            Dim Result As String = MyMsgBoxInput("", New ObjectModel.Collection(Of Validate) From {New ValidateRegex("(?<=code\=)[^&]+", "返回网址应以 https://login.live.com/oauth20_desktop.srf?code= 开头")}, "https://login.live.com/oauth20_desktop.srf?code=XXXXXX", "输入登录返回码", "确定", "取消")
+            If Result Is Nothing Then
+                McLaunchLog("微软登录已在步骤 1 被取消")
+                Throw New ThreadInterruptedException("$$")
+            Else
+                Return RegexSeek(Result, "(?<=code\=)[^&]+")
+            End If
+        Else
+            'Windows 10 登录
+            Dim ReturnCode As String = Nothing
+            Dim ReturnEx As Exception = Nothing
+            Dim IsFinished As LoadState = LoadState.Loading
+            Dim LoginForm As FormLoginOAuth = Nothing
+            RunInUi(Sub()
+                        Try
+                            LoginForm = New FormLoginOAuth
+                            LoginForm.Show()
+                            AddHandler LoginForm.OnLoginSuccess, Sub(Code As String)
+                                                                     ReturnCode = Code
+                                                                     IsFinished = LoadState.Finished
+                                                                 End Sub
+                            AddHandler LoginForm.OnLoginCanceled, Sub() IsFinished = LoadState.Aborted
+                        Catch ex As Exception
+                            ReturnEx = ex
+                            IsFinished = LoadState.Failed
+                        End Try
+                    End Sub)
+            Do While IsFinished = LoadState.Loading AndAlso Not Data.IsAborted
+                Thread.Sleep(20)
+            Loop
+            RunInUi(Sub() If LoginForm IsNot Nothing Then LoginForm.Close())
+            If IsFinished = LoadState.Finished Then
+                Return ReturnCode
+            ElseIf IsFinished = LoadState.Failed Then
+                Throw ReturnEx
+            Else
+                McLaunchLog("微软登录已在步骤 1 被取消")
+                Throw New ThreadInterruptedException("$$")
+            End If
+        End If
+    End Function
+    '微软登录步骤 2:从 OAuth Code 或 OAuth RefreshToken 获取 {OAuth AccessToken, OAuth RefreshToken}
+    Private Function MsLoginStep2(Code As String, IsRefresh As Boolean) As String()
+        McLaunchLog("开始微软登录步骤 2(" & If(IsRefresh, "", "非") & "刷新登录)")
+
+        Dim Request As String
+        If IsRefresh Then
+            Request = "client_id=00000000402b5328" & "&" &
+                      "refresh_token=" & Uri.EscapeDataString(Code) & "&" &
+                      "grant_type=refresh_token" & "&" &
+                      "redirect_uri=" & Uri.EscapeDataString("https://login.live.com/oauth20_desktop.srf") & "&" &
+                      "scope=" & Uri.EscapeDataString("service::user.auth.xboxlive.com::MBI_SSL")
+        Else
+            Request = "client_id=00000000402b5328" & "&" &
+                      "code=" & Uri.EscapeDataString(Code) & "&" &
+                      "grant_type=authorization_code" & "&" &
+                      "redirect_uri=" & Uri.EscapeDataString("https://login.live.com/oauth20_desktop.srf") & "&" &
+                      "scope=" & Uri.EscapeDataString("service::user.auth.xboxlive.com::MBI_SSL")
+        End If
+        Dim Result As String
+        Try
+            Result = NetRequestMuity("https://login.live.com/oauth20_token.srf", "POST", Request, "application/x-www-form-urlencoded", 1)
+        Catch ex As Exception
+            If ex.Message.Contains("must sign in again") Then
+                Return {"Relogin", ""}
+            Else
+                Throw
+            End If
+        End Try
+
+        Dim ResultJson As JObject = GetJson(Result)
+        Dim AccessToken As String = ResultJson("access_token").ToString
+        Dim RefreshToken As String = ResultJson("refresh_token").ToString
+        Return {AccessToken, RefreshToken}
+    End Function
+    '微软登录步骤 3:从 OAuth AccessToken 获取 XBLToken
+    Private Function MsLoginStep3(AccessToken As String) As String
+        McLaunchLog("开始微软登录步骤 3")
+
+        Dim Request As String = "{
+                                    ""Properties"": {
+                                        ""AuthMethod"": ""RPS"",
+                                        ""SiteName"": ""user.auth.xboxlive.com"",
+                                        ""RpsTicket"": """ & AccessToken & """
+                                    },
+                                    ""RelyingParty"": ""http://auth.xboxlive.com"",
+                                    ""TokenType"": ""JWT""
+                                 }"
+        Dim Result As String = NetRequestMuity("https://user.auth.xboxlive.com/user/authenticate", "POST", Request, "application/json", 3)
+
+        Dim ResultJson As JObject = GetJson(Result)
+        Dim XBLToken As String = ResultJson("Token").ToString
+        Return XBLToken
+    End Function
+    '微软登录步骤 4:从 XBLToken 获取 {XSTSToken, UHS}
+    Private Function MsLoginStep4(XBLToken As String) As String()
+        McLaunchLog("开始微软登录步骤 4")
+
+        Dim Request As String = "{
+                                    ""Properties"": {
+                                        ""SandboxId"": ""RETAIL"",
+                                        ""UserTokens"": [
+                                            """ & XBLToken & """
+                                        ]
+                                    },
+                                    ""RelyingParty"": ""rp://api.minecraftservices.com/"",
+                                    ""TokenType"": ""JWT""
+                                 }"
+        Dim Result As String
+        Try
+            Result = NetRequestMuity("https://xsts.auth.xboxlive.com/xsts/authorize", "POST", Request, "application/json", 3)
+        Catch ex As Net.WebException
+            If ex.Message.Contains("2148916233") Then
+                Throw New Exception("$该微软账号尚未购买 Minecraft Java 版或注册 XBox 账户!")
+            ElseIf ex.Message.Contains("2148916238") Then
+                If MyMsgBox("该账号年龄不足,你需要先修改出生日期,然后才能登录。" & vbCrLf &
+                            "该账号目前填写的年龄是否在 13 岁以上?", "登录提示", "13 岁以上", "12 岁以下", "我不知道") = 1 Then
+                    OpenWebsite("https://account.live.com/editprof.aspx")
+                    MyMsgBox("请在打开的网页中修改账号的出生日期(至少改为 18 岁以上)。" & vbCrLf &
+                             "在修改成功后等待一分钟,然后再回到 PCL2,就可以正常登录了!", "登录提示")
+                Else
+                    OpenWebsite("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a")
+                    MyMsgBox("请根据打开的网页的说明,修改账号的出生日期(至少改为 18 岁以上)。" & vbCrLf &
+                             "在修改成功后等待一分钟,然后再回到 PCL2,就可以正常登录了!", "登录提示")
+                End If
+                Throw New Exception("$$")
+            Else
+                Throw
+            End If
+        End Try
+
+        Dim ResultJson As JObject = GetJson(Result)
+        Dim XSTSToken As String = ResultJson("Token").ToString
+        Dim UHS As String = ResultJson("DisplayClaims")("xui")(0)("uhs").ToString
+        Return {XSTSToken, UHS}
+    End Function
+    '微软登录步骤 5:从 {XSTSToken, UHS} 获取 Minecraft AccessToken
+    Private Function MsLoginStep5(Tokens As String()) As String
+        McLaunchLog("开始微软登录步骤 5")
+
+        Dim Request As String = "{""identityToken"": ""XBL3.0 x=" & Tokens(1) & ";" & Tokens(0) & """}"
+        Dim Result As String
+        Try
+            Result = NetRequestMuity("https://api.minecraftservices.com/authentication/login_with_xbox", "POST", Request, "application/json", 2)
+        Catch ex As Net.WebException
+            Dim Message As String = GetString(ex)
+            If Message.Contains("(429)") Then
+                Log(ex, "微软登录第 5 步汇报 429")
+                Throw New Exception("$登录尝试太过频繁,请等待几分钟后再试!")
+            ElseIf Message.Contains("(403)") Then
+                Log(ex, "微软登录第 5 步汇报 403")
+                Throw New Exception("$当前 IP 的登录尝试异常。" & vbCrLf & "如果你使用了 VPN 或加速器,请把它们关掉或更换节点后再试!")
+            Else
+                Throw
+            End If
+        End Try
+
+        Dim ResultJson As JObject = GetJson(Result)
+        Dim AccessToken As String = ResultJson("access_token").ToString
+        Return AccessToken
+    End Function
+    '微软登录步骤 6:从 Minecraft AccessToken 获取 {UUID, UserName, ProfileJson}
+    Private Function MsLoginStep6(AccessToken As String) As String()
+        McLaunchLog("开始微软登录步骤 6")
+
+        Dim Result As String
+        Try
+            Result = NetRequestMuity("https://api.minecraftservices.com/minecraft/profile", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}})
+        Catch ex As Net.WebException
+            Dim Message As String = GetString(ex)
+            If Message.Contains("(429)") Then
+                Log(ex, "微软登录第 6 步汇报 429")
+                Throw New Exception("$登录尝试太过频繁,请等待几分钟后再试!")
+            ElseIf Message.Contains("(404)") Then
+                Log(ex, "微软登录第 6 步汇报 404")
+                Throw New Exception("$你可能没有在购买后去 Minecraft 官网创建游戏档案,或者没有购买 Minecraft。")
+            Else
+                Throw
+            End If
+        End Try
+        Dim ResultJson As JObject = GetJson(Result)
+        Dim UUID As String = ResultJson("id").ToString
+        Dim UserName As String = ResultJson("name").ToString
+        Return {UUID, UserName, Result}
+    End Function
+
+    '根据用户名返回对应 Uuid,需要多线程
+    Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
+        If Name.Trim.Length = 0 Then Return StrFill("", "0", 32)
+        '从缓存获取
+        Dim Uuid As String = ReadIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, "")
+        If Len(Uuid) = 32 Then Return Uuid
+        '从官网获取
+        Try
+            Dim GotJson As JObject = NetGetCodeByRequestRetry("https://api.mojang.com/users/profiles/minecraft/" & Name, IsJson:=True)
+            If GotJson Is Nothing Then Throw New FileNotFoundException("正版玩家档案不存在(" & Name & ")")
+            Uuid = If(GotJson("id"), "")
+        Catch ex As Exception
+            Log(ex, "从官网获取正版 Uuid 失败(" & Name & ")")
+            If Not ThrowOnNotFound AndAlso ex.GetType.Name = "FileNotFoundException" Then
+                Uuid = McLoginLegacyUuid(Name) '玩家档案不存在
+            Else
+                Throw New Exception("从官网获取正版 Uuid 失败", ex)
+            End If
+        End Try
+        '写入缓存
+        If Not Len(Uuid) = 32 Then Throw New Exception("获取的正版 Uuid 长度不足(" & Uuid & ")")
+        WriteIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, Uuid)
+        Return Uuid
+    End Function
+    Public Function McLoginLegacyUuid(Name As String)
+        Dim FullUuid As String = StrFill(Name.Length.ToString("X"), "0", 16) & StrFill(GetHash(Name).ToString("X"), "0", 16)
+        Return FullUuid.Substring(0, 12) & "3" & FullUuid.Substring(13, 3) & "9" & FullUuid.Substring(17, 15)
+    End Function
+
+#End Region
+
+#Region "Java 处理"
+
+    Private McLaunchJavaSelected As JavaEntry = Nothing
+    Private Sub McLaunchJavaValidate()
+        Dim MinVer As New Version(0, 0, 0, 0), MaxVer As New Version(999, 999, 999, 999)
+
+        'MC 大版本检测
+        If (McVersionCurrent.ReleaseTime >= New Date(2021, 11, 16) AndAlso McVersionCurrent.Version.McCodeMain = 99) OrElse
+            (McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain <> 99) Then
+            '1.18 pre2+:至少 Java 17
+            MinVer = New Version(1, 17, 0, 0)
+        ElseIf (McVersionCurrent.ReleaseTime >= New Date(2021, 5, 11) AndAlso McVersionCurrent.Version.McCodeMain = 99) OrElse
+           (McVersionCurrent.Version.McCodeMain >= 17 AndAlso McVersionCurrent.Version.McCodeMain <> 99) Then
+            '21w19a+:至少 Java 16
+            MinVer = New Version(1, 16, 0, 0)
+        ElseIf McVersionCurrent.ReleaseTime.Year >= 2017 Then 'Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了
+            '1.12+:至少 Java 8
+            MinVer = New Version(1, 8, 0, 0)
+        ElseIf McVersionCurrent.ReleaseTime.Year >= 2001 Then '避免某些版本的 1960 癌
+            '1.11-:最高 Java 12
+            MaxVer = New Version(1, 12, 999, 999)
+        End If
+
+        'Forge 检测
+        If McVersionCurrent.Version.HasForge Then
+            If McVersionCurrent.Version.McName = "1.7.2" Then
+                '1.7.2:必须 Java 7
+                MinVer = New Version(1, 7, 0, 0) : MaxVer = New Version(1, 7, 999, 999)
+            ElseIf McVersionCurrent.Version.McCodeMain <= 12 AndAlso McVersionCurrent.Version.McCodeMain <> -1 AndAlso VersionSortBoolean("14.23.5.2855", McVersionCurrent.Version.ForgeVersion) Then
+                '1.12,Forge 14.23.5.2855 及更低:Java 8
+                MaxVer = New Version(1, 8, 999, 999)
+            ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain <> -1 AndAlso VersionSortBoolean("28.2.23", McVersionCurrent.Version.ForgeVersion) Then
+                '1.13 - 1.14,Forge 28.2.23 及更低:Java 8 - 10
+                MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 10, 999, 999)
+            ElseIf McVersionCurrent.Version.McCodeMain <= 16 AndAlso McVersionCurrent.Version.McCodeMain <> -1 Then
+                '1.15 - 1.16:Java 8 - 12, 12 - 15未测试
+                MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 15, 999, 999)
+            End If
+        End If
+
+        '统一通行证检测
+        If Setup.Get("LoginType") = McLoginType.Nide Then
+            '至少 Java 8.101 (1.8.0.101)
+            MinVer = If(New Version(1, 8, 0, 101) > MinVer, New Version(1, 8, 0, 101), MinVer)
+        End If
+
+        '选择 Java
+        McLaunchLog("Java 版本需求:最低 " & MinVer.ToString & ",最高 " & MaxVer.ToString)
+        McLaunchJavaSelected = JavaSelect(MinVer, MaxVer, McVersionCurrent)
+        If McLaunchJavaSelected IsNot Nothing Then
+            McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
+            Exit Sub
+        End If
+
+        '无合适的 Java
+        McLaunchLog("无合适的 Java,取消启动")
+        If MinVer >= New Version(1, 17, 0, 0) Then
+            '缺少 Java 17
+            JavaMissing(17)
+        ElseIf MinVer >= New Version(1, 16, 0, 0) Then
+            '缺少 Java 16
+            JavaMissing(16)
+        ElseIf MaxVer < New Version(1, 8, 0, 0) Then
+            '缺少 Java 7
+            JavaMissing(7)
+        Else
+            '缺少 Java 8 较新版
+            JavaMissing(8)
+        End If
+        Throw New Exception("$$")
+
+    End Sub
+
+#End Region
+
+#Region "启动参数"
+
+    Private McLaunchArgument As String
+
+    '主方法,合并 Jvm、Game、Replace 三部分的参数数据
+    Private Sub McLaunchArgumentMain(Loader As LoaderTask(Of String, List(Of McLibToken)))
+        McLaunchLog("开始获取 Minecraft 启动参数")
+        '获取基准字符串与参数信息
+        Dim Arguments As String
+        If McVersionCurrent.JsonObject("arguments") IsNot Nothing AndAlso McVersionCurrent.JsonObject("arguments")("jvm") IsNot Nothing Then
+            McLaunchLog("获取新版 JVM 参数")
+            Arguments = McLaunchArgumentsJvmNew(McVersionCurrent)
+            McLaunchLog("新版 JVM 参数获取成功:")
+            McLaunchLog(Arguments)
+        Else
+            McLaunchLog("获取旧版 JVM 参数")
+            Arguments = McLaunchArgumentsJvmOld(McVersionCurrent)
+            McLaunchLog("旧版 JVM 参数获取成功:")
+            McLaunchLog(Arguments)
+        End If
+        If McVersionCurrent.JsonObject("minecraftArguments") IsNot Nothing Then
+            McLaunchLog("获取旧版 Game 参数")
+            Arguments += " " & McLaunchArgumentsGameOld(McVersionCurrent)
+            McLaunchLog("旧版 Game 参数获取成功")
+        End If
+        If McVersionCurrent.JsonObject("arguments") IsNot Nothing AndAlso McVersionCurrent.JsonObject("arguments")("game") IsNot Nothing Then
+            McLaunchLog("获取新版 Game 参数")
+            Arguments += " " & McLaunchArgumentsGameNew(McVersionCurrent)
+            McLaunchLog("新版 Game 参数获取成功")
+        End If
+        '替换参数
+        Dim ReplaceArguments = McLaunchArgumentsReplace(McVersionCurrent, Loader)
+        If String.IsNullOrWhiteSpace(ReplaceArguments("${version_type}")) Then
+            '若自定义信息为空,则去掉该部分
+            Arguments = Arguments.Replace(" --versionType ${version_type}", "")
+            ReplaceArguments("${version_type}") = """"""
+        End If
+        For Each entry As KeyValuePair(Of String, String) In ReplaceArguments
+            Arguments = Arguments.Replace(entry.Key, If(entry.Value.Contains(" ") OrElse entry.Value.Contains(":\"), """" & entry.Value & """", entry.Value))
+        Next
+        'MJSB
+        Arguments = Arguments.Replace(" -Dos.name=Windows 10", " -Dos.name=""Windows 10""")
+        '全屏
+        If Setup.Get("LaunchArgumentWindowType") = 0 Then Arguments += " --fullscreen"
+        '进服
+        Dim Server As String = If(String.IsNullOrEmpty(Loader.Input), Setup.Get("VersionServerEnter", McVersionCurrent), Loader.Input)
+        If Server.Length > 0 Then
+            If Server.Contains(":") Then
+                '包含端口号
+                Arguments += " --server " & Server.Split(":")(0) & " --port " & Server.Split(":")(1)
+            Else
+                '不包含端口号
+                Arguments += " --server " & Server & " --port 25565"
+            End If
+            'OptiFine 警告
+            If McVersionCurrent.Version.HasOptiFine Then
+                Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Critical)
+            End If
+        End If
+        '自定义
+        Dim ArgumentGame As String = Setup.Get("VersionAdvanceGame", Version:=McVersionCurrent)
+        Arguments += " " & If(ArgumentGame = "", Setup.Get("LaunchAdvanceGame"), ArgumentGame)
+        '输出
+        McLaunchLog("Minecraft 启动参数:")
+        McLaunchLog(Arguments.Replace(McLoginLoader.Output.AccessToken, McLoginLoader.Output.AccessToken.Substring(0, 22) & "[数据删除]" & McLoginLoader.Output.AccessToken.Substring(30)))
+        McLaunchArgument = Arguments
+    End Sub
+
+    'Jvm 部分(第一段)
+    Private Function McLaunchArgumentsJvmOld(Version As McVersion) As String
+        '存储以空格为间隔的启动参数列表
+        Dim DataList As New List(Of String)
+
+        '输出固定参数
+        DataList.Add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump")
+        Dim ArgumentJvm As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
+        If ArgumentJvm = "" Then ArgumentJvm = Setup.Get("LaunchAdvanceJvm")
+        If Not ArgumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true") Then ArgumentJvm += " -Dlog4j2.formatMsgNoLookups=true"
+        DataList.Insert(0, ArgumentJvm) '可变 JVM 参数
+        DataList.Add("-Xmn256m")
+        DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent) * 1024) & "m")
+        DataList.Add("""-Djava.library.path=" & Version.Path & Version.Name & "-natives""")
+        DataList.Add("-cp ${classpath}") '把支持库添加进启动参数表
+
+        '统一通行证
+        If McLoginLoader.Output.Type = "Nide" Then
+            DataList.Insert(0, "-Dnide8auth.client=true -javaagent:nide8auth.jar=" & Setup.Get("VersionServerNide", Version:=McVersionCurrent))
+        End If
+        'Authlib-Injector
+        If McLoginLoader.Output.Type = "Auth" Then
+            Dim Server As String = Setup.Get("VersionServerAuthServer", Version:=McVersionCurrent)
+            Dim Response As String = NetGetCodeByRequestRetry(Server, Encoding.UTF8)
+            DataList.Insert(0, "-javaagent:authlib-injector.jar=" & Server &
+                              " -Dauthlibinjector.side=client" &
+                              " -Dauthlibinjector.yggdrasil.prefetched=" & Convert.ToBase64String(Encoding.UTF8.GetBytes(Response)))
+        End If
+
+        '添加 MainClass
+        If Version.JsonObject("mainClass") Is Nothing Then
+            Throw New Exception("版本 Json 中没有 mainClass 项!")
+        Else
+            DataList.Add(Version.JsonObject("mainClass"))
+        End If
+
+        Return Join(DataList, " ")
+    End Function
+    Private Function McLaunchArgumentsJvmNew(Version As McVersion) As String
+        Dim DataList As New List(Of String)
+
+        '获取 Json 中的 DataList
+        Dim CurrentVersion As McVersion = Version
+NextVersion:
+        If CurrentVersion.JsonObject("arguments") IsNot Nothing AndAlso CurrentVersion.JsonObject("arguments")("jvm") IsNot Nothing Then
+            For Each SubJson As JToken In CurrentVersion.JsonObject("arguments")("jvm")
+                If SubJson.Type = JTokenType.String Then
+                    '字符串类型
+                    DataList.Add(SubJson.ToString)
+                Else
+                    '非字符串类型
+                    If McJsonRuleCheck(SubJson("rules")) Then
+                        '满足准则
+                        If SubJson("value").Type = JTokenType.String Then
+                            DataList.Add(SubJson("value").ToString)
+                        Else
+                            For Each value As JToken In SubJson("value")
+                                DataList.Add(value.ToString)
+                            Next
+                        End If
+                    End If
+                End If
+            Next
+        End If
+        If CurrentVersion.InheritVersion <> "" Then
+            CurrentVersion = New McVersion(CurrentVersion.InheritVersion)
+            GoTo NextVersion
+        End If
+
+        '内存、Log4j 防御参数等
+        SecretLaunchJvmArgs(DataList)
+
+        '统一通行证
+        If McLoginLoader.Output.Type = "Nide" Then
+            DataList.Insert(0, "-javaagent:nide8auth.jar=" & Setup.Get("VersionServerNide", Version:=McVersionCurrent))
+        End If
+        'Authlib-Injector
+        If McLoginLoader.Output.Type = "Auth" Then
+            Dim Server As String = Setup.Get("VersionServerAuthServer", Version:=McVersionCurrent)
+            Try
+                Dim Response As String = NetGetCodeByRequestRetry(Server, Encoding.UTF8)
+                DataList.Insert(0, "-javaagent:authlib-injector.jar=" & Server &
+                              " -Dauthlibinjector.side=client" &
+                              " -Dauthlibinjector.yggdrasil.prefetched=" & Convert.ToBase64String(Encoding.UTF8.GetBytes(Response)))
+            Catch ex As Exception
+                Throw New Exception("无法连接到第三方登录服务器(" & If(Server, Nothing) & ")")
+            End Try
+        End If
+
+        '将 "-XXX" 与后面 "XXX" 合并到一起
+        '如果不合并,会导致 Forge 1.17 启动无效,它有两个 --add-exports,进一步导致其中一个在后面被去重
+        Dim DeDuplicateDataList As New List(Of String)
+        For i = 0 To DataList.Count - 1
+            Dim CurrentEntry As String = DataList(i)
+            If DataList(i).StartsWith("-") Then
+                Do While i < DataList.Count - 1
+                    If DataList(i + 1).StartsWith("-") Then
+                        Exit Do
+                    Else
+                        i += 1
+                        CurrentEntry += " " + DataList(i)
+                    End If
+                Loop
+            End If
+            DeDuplicateDataList.Add(CurrentEntry.Trim.Replace("McEmu= ", "McEmu="))
+        Next
+
+        '去重
+        Dim Result As String = Join(ArrayNoDouble(DeDuplicateDataList), " ")
+
+        '添加 MainClass
+        If Version.JsonObject("mainClass") Is Nothing Then
+            Throw New Exception("版本 Json 中没有 mainClass 项!")
+        Else
+            Result += " " & Version.JsonObject("mainClass").ToString
+        End If
+
+        Return Result
+    End Function
+
+    'Game 部分(第二段)
+    Private Function McLaunchArgumentsGameOld(Version As McVersion) As String
+        Dim DataList As New List(Of String)
+
+        '本地化 Minecraft 启动信息
+        Dim BasicString As String = Version.JsonObject("minecraftArguments").ToString
+        If Not BasicString.Contains("--height") Then BasicString += " --height ${resolution_height} --width ${resolution_width}"
+        DataList.Add(BasicString)
+
+        McLaunchArgumentsGameOld = Join(DataList, " ")
+
+        '特别改变 OptiFineTweaker
+        If (Version.Version.HasForge OrElse Version.Version.HasLiteLoader) AndAlso Version.Version.HasOptiFine Then
+            '把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
+            If McLaunchArgumentsGameOld.Contains("--tweakClass optifine.OptiFineForgeTweaker") Then
+                Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" & McLaunchArgumentsGameOld)
+                McLaunchArgumentsGameOld = McLaunchArgumentsGameOld.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "").Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") & " --tweakClass optifine.OptiFineForgeTweaker"
+            End If
+            If McLaunchArgumentsGameOld.Contains("--tweakClass optifine.OptiFineTweaker") Then
+                Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" & McLaunchArgumentsGameOld)
+                McLaunchArgumentsGameOld = McLaunchArgumentsGameOld.Replace(" --tweakClass optifine.OptiFineTweaker", "").Replace("--tweakClass optifine.OptiFineTweaker ", "") & " --tweakClass optifine.OptiFineForgeTweaker"
+                Try
+                    WriteFile(Version.Path & Version.Name & ".json", ReadFile(Version.Path & Version.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
+                Catch ex As Exception
+                    Log(ex, "替换 OptiFineForge TweakClass 失败")
+                End Try
+            End If
+        End If
+    End Function
+    Private Function McLaunchArgumentsGameNew(Version As McVersion) As String
+        Dim DataList As New List(Of String)
+
+        '获取 Json 中的 DataList
+        Dim CurrentVersion As McVersion = Version
+NextVersion:
+        If CurrentVersion.JsonObject("arguments") IsNot Nothing AndAlso CurrentVersion.JsonObject("arguments")("game") IsNot Nothing Then
+            For Each SubJson As JToken In CurrentVersion.JsonObject("arguments")("game")
+                If SubJson.Type = JTokenType.String Then
+                    '字符串类型
+                    DataList.Add(SubJson.ToString)
+                Else
+                    '非字符串类型
+                    If McJsonRuleCheck(SubJson("rules")) Then
+                        '满足准则
+                        If SubJson("value").Type = JTokenType.String Then
+                            DataList.Add(SubJson("value").ToString)
+                        Else
+                            For Each value As JToken In SubJson("value")
+                                DataList.Add(value.ToString)
+                            Next
+                        End If
+                    End If
+                End If
+            Next
+        End If
+        If CurrentVersion.InheritVersion <> "" Then
+            CurrentVersion = New McVersion(CurrentVersion.InheritVersion)
+            GoTo NextVersion
+        End If
+
+        '将 "-XXX" 与后面 "XXX" 合并到一起
+        '如果不进行合并 Impact 会启动无效,它有两个 --tweakclass
+        Dim DeDuplicateDataList As New List(Of String)
+        For i = 0 To DataList.Count - 1
+            Dim CurrentEntry As String = DataList(i)
+            If DataList(i).StartsWith("-") Then
+                Do While i < DataList.Count - 1
+                    If DataList(i + 1).StartsWith("-") Then
+                        Exit Do
+                    Else
+                        i += 1
+                        CurrentEntry += " " + DataList(i)
+                    End If
+                Loop
+            End If
+            DeDuplicateDataList.Add(CurrentEntry)
+        Next
+        '去重
+        McLaunchArgumentsGameNew = Join(ArrayNoDouble(DeDuplicateDataList), " ")
+
+        '特别改变 OptiFineTweaker
+        If (Version.Version.HasForge OrElse Version.Version.HasLiteLoader) AndAlso Version.Version.HasOptiFine Then
+            '把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
+            If McLaunchArgumentsGameNew.Contains("--tweakClass optifine.OptiFineForgeTweaker") Then
+                Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" & McLaunchArgumentsGameNew)
+                McLaunchArgumentsGameNew = McLaunchArgumentsGameNew.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "").Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") & " --tweakClass optifine.OptiFineForgeTweaker"
+            End If
+            If McLaunchArgumentsGameNew.Contains("--tweakClass optifine.OptiFineTweaker") Then
+                Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" & McLaunchArgumentsGameNew)
+                McLaunchArgumentsGameNew = McLaunchArgumentsGameNew.Replace(" --tweakClass optifine.OptiFineTweaker", "").Replace("--tweakClass optifine.OptiFineTweaker ", "") & " --tweakClass optifine.OptiFineForgeTweaker"
+                Try
+                    WriteFile(Version.Path & Version.Name & ".json", ReadFile(Version.Path & Version.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
+                Catch ex As Exception
+                    Log(ex, "替换 OptiFineForge TweakClass 失败")
+                End Try
+            End If
+        End If
+    End Function
+
+    '替换 Arguments
+    Private Function McLaunchArgumentsReplace(Version As McVersion, ByRef Loader As LoaderTask(Of String, List(Of McLibToken))) As Dictionary(Of String, String)
+        Dim GameArguments As New Dictionary(Of String, String)
+
+        '基础参数
+        GameArguments.Add("${classpath_separator}", ";")
+        GameArguments.Add("${natives_directory}", Version.Path & Version.Name & "-natives")
+        GameArguments.Add("${library_directory}", PathMcFolder & "libraries")
+        GameArguments.Add("${libraries_directory}", PathMcFolder & "libraries")
+        GameArguments.Add("${launcher_name}", "PCL2")
+        GameArguments.Add("${launcher_version}", VersionCode)
+        GameArguments.Add("${version_name}", Version.Name)
+        Dim ArgumentInfo As String = Setup.Get("VersionArgumentInfo", Version:=McVersionCurrent)
+        GameArguments.Add("${version_type}", If(ArgumentInfo = "", Setup.Get("LaunchArgumentInfo"), ArgumentInfo))
+        GameArguments.Add("${game_directory}", Left(McVersionCurrent.PathIndie, McVersionCurrent.PathIndie.Count - 1))
+        GameArguments.Add("${assets_root}", PathMcFolder & "assets")
+        GameArguments.Add("${user_properties}", "{}")
+        GameArguments.Add("${auth_player_name}", McLoginLoader.Output.Name)
+        GameArguments.Add("${auth_uuid}", McLoginLoader.Output.Uuid)
+        GameArguments.Add("${auth_access_token}", McLoginLoader.Output.AccessToken)
+        GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken)
+        GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken)
+        GameArguments.Add("${user_type}", If(McLoginLoader.Output.Type = "Legacy", "Legacy", "Mojang"))
+        Dim GameSize As Size = Setup.GetLaunchArgumentWindowSize()
+        GameArguments.Add("${resolution_width}", GameSize.Width)
+        GameArguments.Add("${resolution_height}", GameSize.Height)
+
+        'Assets 相关参数
+        GameArguments.Add("${game_assets}", PathMcFolder & "assets\virtual\legacy") '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
+        GameArguments.Add("${assets_index_name}", McAssetsGetIndexName(Version))
+
+        '支持库参数
+        Dim LibList As List(Of McLibToken) = McLibListGet(Version, Not (Version.Version.HasForge AndAlso Version.Version.McCodeMain >= 17))
+        Loader.Output = LibList
+        Dim CpStrings As New List(Of String)
+        Dim OptiFineCp As String = Nothing
+        For Each Library As McLibToken In LibList
+            If Library.IsNatives Then Continue For
+            If Library.Name IsNot Nothing AndAlso Library.Name = "optifine:OptiFine" Then
+                OptiFineCp = Library.LocalPath
+            Else
+                CpStrings.Add(Library.LocalPath)
+            End If
+        Next
+        If OptiFineCp IsNot Nothing Then CpStrings.Insert(CpStrings.Count - 2, OptiFineCp)
+        GameArguments.Add("${classpath}", Join(CpStrings, ";"))
+
+        Return GameArguments
+    End Function
+
+#End Region
+
+#Region "解压 Natives"
+
+    Private Sub McLaunchNatives(Loader As LoaderTask(Of List(Of McLibToken), Integer))
+
+        '创建文件夹
+        Directory.CreateDirectory(McVersionCurrent.Path & McVersionCurrent.Name & "-natives")
+
+        '解压文件
+        McLaunchLog("正在解压 Natives 文件")
+        Dim ExistFiles As New List(Of String)
+        For Each Native As McLibToken In Loader.Input
+            If Not Native.IsNatives Then Continue For
+            Dim Zip As ZipArchive
+            Try
+                Zip = New ZipArchive(New FileStream(Native.LocalPath, FileMode.Open))
+            Catch ex As InvalidDataException
+                Log(ex, "打开 Natives 文件失败(" & Native.LocalPath & ")")
+                File.Delete(Native.LocalPath)
+                Throw New Exception("无法打开 Natives 文件(" & Native.LocalPath & "),该文件可能已损坏,请重新尝试启动游戏")
+            End Try
+            For Each Entry In Zip.Entries
+                Dim FileName As String = Entry.FullName
+                If FileName.EndsWith(".dll") Then
+                    '实际解压文件的步骤
+                    Dim FilePath As String = McVersionCurrent.Path & McVersionCurrent.Name & "-natives\" & FileName
+                    ExistFiles.Add(FilePath)
+                    Dim OriginalFile As New FileInfo(FilePath)
+                    If OriginalFile.Exists Then
+                        If OriginalFile.Length = Entry.Length Then
+                            If ModeDebug Then McLaunchLog("无需解压:" & FilePath)
+                            Continue For
+                        End If
+                        '删除原文件
+                        Try
+                            File.Delete(FilePath)
+                        Catch ex As UnauthorizedAccessException
+                            McLaunchLog("删除原 dll 访问被拒绝,这通常代表有一个 MC 正在运行,跳过解压:" & FilePath)
+                            McLaunchLog("实际的错误信息:" & GetString(ex))
+                            Exit For
+                        End Try
+                    End If
+                    '解压新文件
+                    WriteFile(FilePath, Entry.Open)
+                    McLaunchLog("已解压:" & FilePath)
+                End If
+            Next
+            If Zip IsNot Nothing Then Zip.Dispose()
+        Next
+
+        '删除多余文件
+        For Each FileName As String In Directory.GetFiles(McVersionCurrent.Path & McVersionCurrent.Name & "-natives")
+            If ExistFiles.Contains(FileName) Then Continue For
+            Try
+                McLaunchLog("删除:" & FileName)
+                File.Delete(FileName)
+            Catch ex As UnauthorizedAccessException
+                McLaunchLog("删除多余文件访问被拒绝,跳过删除步骤")
+                McLaunchLog("实际的错误信息:" & GetString(ex))
+                Exit Sub
+            End Try
+        Next
+
+    End Sub
+
+#End Region
+
+#Region "启动与前后处理"
+
+    Private Sub McLaunchPrerun()
+
+        '更新 launcher_profiles.json
+        Try
+            '确保可用
+            If Not (McLoginLoader.Output.Type = "Mojang" OrElse McLoginLoader.Output.Type = "Microsoft") Then Exit Try
+            McFolderLauncherProfilesJsonCreate(PathMcFolder)
+            '构建需要替换的 Json 对象
+            Dim ReplaceJsonString As String = "
+            {
+              ""authenticationDatabase"": {
+                ""00000111112222233333444445555566"": {
+                  ""accessToken"": """ & McLoginLoader.Output.AccessToken & """,
+                  ""username"": """ & If(McLoginLoader.Output.Email, McLoginLoader.Output.Name).Replace("""", "-") & """,
+                  ""profiles"": {
+                    ""66666555554444433333222221111100"": {
+                        ""displayName"": """ & McLoginLoader.Output.Name & """
+                    }
+                  }
+                }
+              },
+              ""clientToken"": """ & McLoginLoader.Output.ClientToken & """,
+              ""selectedUser"": {
+                ""account"": ""00000111112222233333444445555566"", 
+                ""profile"": ""66666555554444433333222221111100""
+              }
+            }"
+            Dim ReplaceJson As JObject = GetJson(ReplaceJsonString)
+            '更新文件
+            Dim Profiles As JObject = GetJson(ReadFile(PathMcFolder & "launcher_profiles.json"))
+            Profiles.Merge(ReplaceJson)
+            WriteFile(PathMcFolder & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.Default)
+            McLaunchLog("已更新 launcher_profiles.json")
+        Catch ex As Exception
+            Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试")
+            Try
+                File.Delete(PathMcFolder & "launcher_profiles.json")
+                McFolderLauncherProfilesJsonCreate(PathMcFolder)
+                '构建需要替换的 Json 对象
+                Dim ReplaceJsonString As String = "
+                    {
+                      ""authenticationDatabase"": {
+                        ""00000111112222233333444445555566"": {
+                          ""accessToken"": """ & McLoginLoader.Output.AccessToken & """,
+                          ""username"": """ & If(McLoginLoader.Output.Email, McLoginLoader.Output.Name).Replace("""", "-") & """,
+                          ""profiles"": {
+                            ""66666555554444433333222221111100"": {
+                                ""displayName"": """ & McLoginLoader.Output.Name & """
+                            }
+                          }
+                        }
+                      },
+                      ""clientToken"": """ & McLoginLoader.Output.ClientToken & """,
+                      ""selectedUser"": {
+                        ""account"": ""00000111112222233333444445555566"", 
+                        ""profile"": ""66666555554444433333222221111100""
+                      }
+                    }"
+                Dim ReplaceJson As JObject = GetJson(ReplaceJsonString)
+                '更新文件
+                Dim Profiles As JObject = GetJson(ReadFile(PathMcFolder & "launcher_profiles.json"))
+                Profiles.Merge(ReplaceJson)
+                WriteFile(PathMcFolder & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.Default)
+                McLaunchLog("已在删除后更新 launcher_profiles.json")
+            Catch exx As Exception
+                Log(exx, "更新 launcher_profiles.json 失败", LogLevel.Feedback)
+            End Try
+        End Try
+
+        '更新 options.txt
+        Dim SetupFileAddress As String = McVersionCurrent.PathIndie & "options.txt"
+        Try
+            '语言
+            If Setup.Get("ToolHelpChinese") Then
+                If Not File.Exists(SetupFileAddress) OrElse Not Directory.Exists(McVersionCurrent.PathIndie & "saves") Then
+                    McLaunchLog("已根据设置自动修改语言为中文")
+                    WriteIni(SetupFileAddress, "lang", "-") '触发缓存更改,避免删除后重新下载残留缓存
+                    If McVersionCurrent.Version.McCodeMain >= 12 Then
+                        WriteIni(SetupFileAddress, "lang", "zh_cn")
+                    Else
+                        WriteIni(SetupFileAddress, "lang", "zh_CN")
+                    End If
+                    WriteIni(SetupFileAddress, "forceUnicodeFont", "true")
+                Else
+                    McLaunchLog("并非首次启动,不修改语言")
+                End If
+            End If
+            '窗口
+            Select Case Setup.Get("LaunchArgumentWindowType")
+                Case 0 '全屏
+                    WriteIni(SetupFileAddress, "fullscreen", "true")
+                Case 5 '保持默认
+                Case Else '其他
+                    WriteIni(SetupFileAddress, "fullscreen", "false")
+            End Select
+        Catch ex As Exception
+            Log(ex, "更新 options.txt 失败", LogLevel.Hint)
+        End Try
+
+        '离线皮肤 Alex 警告
+        Try
+            If McVersionCurrent.Version.McCodeMain <= 7 AndAlso McVersionCurrent.Version.McCodeMain >= 2 AndAlso '1.7 ~ 1.2
+               McLoginLoader.Input.Type = McLoginType.Legacy AndAlso '离线登录
+               (Setup.Get("LaunchSkinType") = 2 OrElse '强制 Alex
+               (Setup.Get("LaunchSkinType") = 4 AndAlso Setup.Get("LaunchSkinSlim"))) Then '或选用 Alex 皮肤
+                Hint("此 Minecraft 版本尚不支持 Alex 皮肤,你的皮肤可能会显示为 Steve!", HintType.Critical)
+            End If
+        Catch ex As Exception
+            Log(ex, "检查离线皮肤失效失败")
+        End Try
+
+        '离线皮肤资源包
+        Try
+            Directory.CreateDirectory(McVersionCurrent.PathIndie & "resourcepacks\")
+            Dim ZipFileAddress As String = McVersionCurrent.PathIndie & "resourcepacks\PCL2 Skin.zip"
+            Dim NewTypeSetup As Boolean = McVersionCurrent.Version.McCodeMain >= 13 OrElse McVersionCurrent.Version.McCodeMain < 6
+            If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Setup.Get("LaunchSkinType") = 4 AndAlso File.Exists(PathTemp & "CustomSkin.png") Then
+                Directory.CreateDirectory(PathTemp)
+                Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
+                Dim PackPicAddress As String = PathTemp & "pack.png"
+                Dim PackFormat As Integer
+                Select Case McVersionCurrent.Version.McCodeMain
+                    Case 0, 1, 2, 3, 4, 5
+                        '更早的版本没有资源包;如果判断失败该值为 -1,不会跑到这
+                        McLaunchLog("Minecraft 版本过老,尚不支持自定义离线皮肤")
+                        GoTo IgnoreCustomSkin
+                    Case 6, 7, 8
+                        PackFormat = 1
+                    Case 9, 10
+                        PackFormat = 2
+                    Case 11, 12
+                        PackFormat = 3
+                    Case 13, 14
+                        PackFormat = 4
+                    Case 15
+                        PackFormat = 5
+                    Case 16
+                        PackFormat = 6
+                    Case 17
+                        PackFormat = 7
+                    Case 18
+                        If McVersionCurrent.Version.McCodeSub <= 2 Then
+                            PackFormat = 8
+                        Else
+                            PackFormat = 9
+                        End If
+                    Case 19
+                        PackFormat = 9
+                    Case Else
+                        PackFormat = 10
+                End Select
+                McLaunchLog("正在构建自定义皮肤资源包,格式为:" & PackFormat)
+                '准备文件
+                Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
+                Bit.Save(PackPicAddress)
+                WriteFile(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL2 自定义离线皮肤资源包""}}")
+                Dim Skin As New MyBitmap(PathTemp & "CustomSkin.png")
+                If (McVersionCurrent.Version.McCodeMain = 6 OrElse McVersionCurrent.Version.McCodeMain = 7) AndAlso Skin.Pic.Height = 64 Then
+                    McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
+                    Skin = Skin.Clip(New System.Drawing.Rectangle(0, 0, 64, 32))
+                End If
+                Skin.Save(Path & "PCL\CustomSkin_Cliped.png")
+                '构建压缩文件
+                Using ZipFile As New FileStream(ZipFileAddress, FileMode.Create)
+                    Using ZipAr As New ZipArchive(ZipFile, ZipArchiveMode.Create)
+                        ZipAr.CreateEntryFromFile(MetaFileAddress, "pack.mcmeta")
+                        ZipAr.CreateEntryFromFile(PackPicAddress, "pack.png")
+                        ZipAr.CreateEntryFromFile(Path & "PCL\CustomSkin_Cliped.png", "assets/minecraft/textures/entity/" & If(Setup.Get("LaunchSkinSlim"), "alex.png", "steve.png"))
+                    End Using
+                End Using
+                File.Delete(Path & "PCL\CustomSkin_Cliped.png")
+                '更改设置文件
+                IniClearCache(SetupFileAddress)
+                Dim EnabledResourcePack As String = ReadIni(SetupFileAddress, "resourcePacks", "[]").TrimStart("[").TrimEnd("]")
+                If NewTypeSetup Then
+                    If EnabledResourcePack = "" Then EnabledResourcePack = """vanilla"""
+                    Dim EnabledResourcePacks As New List(Of String)(EnabledResourcePack.Split(","))
+                    Dim NewResourcePacks As New List(Of String)
+                    For Each Res In EnabledResourcePacks
+                        If Res <> """file/PCL2 Skin.zip""" AndAlso Res <> "" Then NewResourcePacks.Add(Res)
+                    Next
+                    NewResourcePacks.Add("""file/PCL2 Skin.zip""")
+                    Dim Result As String = "[" & Join(NewResourcePacks, ",") & "]"
+                    WriteIni(SetupFileAddress, "resourcePacks", Result)
+                Else
+                    Dim EnabledResourcePacks As New List(Of String)(EnabledResourcePack.Split(","))
+                    Dim NewResourcePacks As New List(Of String)
+                    For Each Res In EnabledResourcePacks
+                        If Res <> """PCL2 Skin.zip""" AndAlso Res <> "" Then NewResourcePacks.Add(Res)
+                    Next
+                    NewResourcePacks.Add("""PCL2 Skin.zip""")
+                    Dim Result As String = "[" & Join(NewResourcePacks, ",") & "]"
+                    WriteIni(SetupFileAddress, "resourcePacks", Result)
+                End If
+IgnoreCustomSkin:
+            ElseIf File.Exists(ZipFileAddress) Then
+                McLaunchLog("正在清空自定义皮肤资源包")
+                '删除压缩文件
+                File.Delete(ZipFileAddress)
+                '更改设置文件
+                IniClearCache(SetupFileAddress)
+                Dim EnabledResourcePack As String = ReadIni(SetupFileAddress, "resourcePacks", "[]").TrimStart("[").TrimEnd("]")
+                If NewTypeSetup Then
+                    If EnabledResourcePack = "" Then EnabledResourcePack = """vanilla"""
+                    Dim EnabledResourcePacks As New List(Of String)(EnabledResourcePack.Split(","))
+                    EnabledResourcePacks.Remove("""file/PCL2 Skin.zip""")
+                    Dim Result As String = "[" & Join(EnabledResourcePacks, ",") & "]"
+                    WriteIni(SetupFileAddress, "resourcePacks", Result)
+                Else
+                    Dim EnabledResourcePacks As New List(Of String)(EnabledResourcePack.Split(","))
+                    EnabledResourcePacks.Remove("""PCL2 Skin.zip""")
+                    Dim Result As String = "[" & Join(EnabledResourcePacks, ",") & "]"
+                    WriteIni(SetupFileAddress, "resourcePacks", Result)
+                End If
+            End If
+        Catch ex As Exception
+            Log(ex, "离线皮肤资源包设置失败", LogLevel.Hint)
+        End Try
+
+    End Sub
+    Private Sub McLaunchRun(Loader As LoaderTask(Of Integer, Process))
+
+        '启动信息
+        Dim GameProcess = New Process()
+        Dim StartInfo As New ProcessStartInfo(McLaunchJavaSelected.PathJavaw)
+
+        '设置环境变量
+        If StartInfo.EnvironmentVariables.ContainsKey("appdata") Then
+            StartInfo.EnvironmentVariables("appdata") = PathMcFolder
+        Else
+            StartInfo.EnvironmentVariables.Add("appdata", PathMcFolder)
+        End If
+        Dim Paths As New List(Of String)(StartInfo.EnvironmentVariables("Path").Split(";"))
+        Paths.Add(McLaunchJavaSelected.PathFolder)
+        StartInfo.EnvironmentVariables("Path") = Join(ArrayNoDouble(Paths), ";")
+
+        '设置其他参数
+        StartInfo.WorkingDirectory = McVersionCurrent.PathIndie
+        StartInfo.UseShellExecute = False
+        StartInfo.RedirectStandardOutput = True
+        StartInfo.RedirectStandardError = True
+        StartInfo.CreateNoWindow = False
+        StartInfo.Arguments = McLaunchArgument
+        GameProcess.StartInfo = StartInfo
+
+        '开始进程
+        GameProcess.Start()
+        McLaunchLog("已启动游戏进程:" & McLaunchJavaSelected.PathJavaw)
+        Loader.Output = GameProcess
+        McLaunchProcess = GameProcess
+
+        '输出 bat
+        Try
+            Dim CmdString As String =
+                "@echo off" & vbCrLf &
+                "title 启动 - " & McVersionCurrent.Name & vbCrLf &
+                "echo 游戏正在启动,请稍候。" & vbCrLf &
+                "set APPDATA=""" & PathMcFolder & """" & vbCrLf &
+                "cd /D """ & PathMcFolder & """" & vbCrLf &
+                """" & McLaunchJavaSelected.PathJava & """ " & McLaunchArgument & vbCrLf &
+                "echo 游戏已退出。" & vbCrLf &
+                "pause"
+            WriteFile(Path & "PCL\LatestLaunch.bat", CmdString, Encoding:=Encoding.Default)
+        Catch ex As Exception
+            Log(ex, "输出启动脚本失败")
+        End Try
+
+        '进程优先级处理
+        Try
+            If GameProcess.HasExited Then Exit Try '可能在启动游戏进程的同时刚好取消了启动
+            GameProcess.PriorityBoostEnabled = True
+            Select Case Setup.Get("LaunchArgumentPriority")
+                Case 0 '高
+                    GameProcess.PriorityClass = ProcessPriorityClass.AboveNormal
+                Case 2 '低
+                    GameProcess.PriorityClass = ProcessPriorityClass.BelowNormal
+                Case Else '中
+            End Select
+        Catch ex As Exception
+            Log(ex, "设置进程优先级失败", LogLevel.Feedback)
+        End Try
+
+    End Sub
+    Private Sub McLaunchWait(Loader As LoaderTask(Of Process, Integer))
+
+        '输出信息
+        McLaunchLog("")
+        McLaunchLog("~ 基础参数 ~")
+        McLaunchLog("PCL2 版本:" & VersionDisplayName & " (" & VersionCode & ")")
+        McLaunchLog("游戏版本:" & McVersionCurrent.Version.ToString & "(" & McVersionCurrent.Version.McCodeMain & "." & McVersionCurrent.Version.McCodeSub & ")")
+        McLaunchLog("资源版本:" & McAssetsGetIndexName(McVersionCurrent))
+        McLaunchLog("版本继承:" & If(McVersionCurrent.InheritVersion = "", "无", McVersionCurrent.InheritVersion))
+        McLaunchLog("分配的内存:" & PageVersionSetup.GetRam(McVersionCurrent) & " GB(" & Math.Round(PageVersionSetup.GetRam(McVersionCurrent) * 1024) & " MB)")
+        McLaunchLog("MC 文件夹:" & PathMcFolder)
+        McLaunchLog("版本文件夹:" & McVersionCurrent.Path)
+        McLaunchLog("版本隔离:" & (McVersionCurrent.PathIndie = McVersionCurrent.Path))
+        McLaunchLog("HMCL 格式:" & McVersionCurrent.IsHmclFormatJson)
+        McLaunchLog("Java 信息:" & If(McLaunchJavaSelected IsNot Nothing, McLaunchJavaSelected.ToString, "无可用 Java"))
+        McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
+        McLaunchLog("")
+        McLaunchLog("~ 登录参数 ~")
+        McLaunchLog("玩家用户名:" & McLoginLoader.Output.Name)
+        McLaunchLog("AccessToken:" & McLoginLoader.Output.AccessToken.Substring(0, 22) & "[数据删除]" & McLoginLoader.Output.AccessToken.Substring(30))
+        McLaunchLog("ClientToken:" & McLoginLoader.Output.ClientToken)
+        McLaunchLog("UUID:" & McLoginLoader.Output.Uuid)
+        McLaunchLog("登录方式:" & McLoginLoader.Output.Type)
+        McLaunchLog("")
+
+        '获取窗口标题
+        Dim WindowTitle As String = Setup.Get("VersionArgumentTitle", Version:=McVersionCurrent)
+        If WindowTitle = "" Then WindowTitle = Setup.Get("LaunchArgumentTitle")
+        WindowTitle = ArgumentReplace(WindowTitle)
+
+        '初始化等待
+        Dim Watcher As New Watcher(Loader, McVersionCurrent, WindowTitle)
+        McLaunchWatcher = Watcher
+
+        '等待
+        Do While Watcher.State = Watcher.MinecraftState.Loading
+            Thread.Sleep(100)
+        Loop
+        If Watcher.State = Watcher.MinecraftState.Crashed Then
+            Throw New Exception("$$")
+        End If
+
+    End Sub
+    Private Sub McLaunchEnd()
+        McLaunchLog("开始启动结束处理")
+
+        '暂停或开始音乐播放
+        If Setup.Get("UiMusicStop") Then
+            RunInUi(Sub() If MusicPause() Then Log("[Music] 已根据设置,在启动后暂停音乐播放"))
+        ElseIf Setup.Get("UiMusicStart") Then
+            RunInUi(Sub() If MusicResume() Then Log("[Music] 已根据设置,在启动后开始音乐播放"))
+        End If
+
+        '启动器可见性
+        McLaunchLog("启动器可见性:" & Setup.Get("LaunchArgumentVisible"))
+        Select Case Setup.Get("LaunchArgumentVisible")
+            Case 0
+                '直接关闭
+                McLaunchLog("已根据设置,在启动后关闭启动器")
+                RunInUi(Sub() FrmMain.EndProgram(False))
+            Case 2, 3
+                '隐藏
+                McLaunchLog("已根据设置,在启动后隐藏启动器")
+                RunInUi(Sub() FrmMain.Hidden = True)
+            Case 4
+                '最小化
+                McLaunchLog("已根据设置,在启动后最小化启动器")
+                RunInUi(Sub() FrmMain.WindowState = WindowState.Minimized)
+            Case 5
+                '啥都不干
+        End Select
+
+        '启动计数
+        Setup.Set("SystemLaunchCount", Setup.Get("SystemLaunchCount") + 1)
+
+    End Sub
+
+#End Region
+
+End Module

+ 3835 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -0,0 +1,3835 @@
+Imports System.IO.Compression
+
+Public Module ModMinecraft
+
+#Region "Java"
+
+    '列表初始化
+
+    Public JavaListCacheVersion As Integer = 1
+    ''' <summary>
+    ''' 目前所有可用的 Java。
+    ''' </summary>
+    Public JavaList As List(Of JavaEntry)
+    ''' <summary>
+    ''' 初始化 Java 列表,但除非没有 Java,否则不进行检查。
+    ''' </summary>
+    Public Sub JavaListInit()
+        JavaList = New List(Of JavaEntry)
+        Try
+            If Setup.Get("CacheJavaListVersion") < JavaListCacheVersion Then
+                '不使用缓存
+                Log("[Java] 要求 Java 列表缓存更新")
+                Setup.Set("CacheJavaListVersion", JavaListCacheVersion)
+            Else
+                '使用缓存
+                For Each JsonEntry In GetJson(Setup.Get("LaunchArgumentJavaAll"))
+                    JavaList.Add(JavaEntry.FromJson(JsonEntry))
+                Next
+            End If
+            If JavaList.Count = 0 Then
+                Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", LogLevel.Developer)
+                JavaSearchLoader.Start(0)
+            Else
+                Log("[Java] 缓存中有 " & JavaList.Count & " 个可用的 Java")
+            End If
+        Catch ex As Exception
+            Log(ex, "初始化 Java 列表失败", LogLevel.Feedback)
+            Setup.Set("LaunchArgumentJavaAll", "[]")
+        End Try
+    End Sub
+
+    '基础环境
+
+    ''' <summary>
+    ''' Path 环境变量。
+    ''' </summary>
+    Private ReadOnly Property PathEnv As String
+        Get
+            If _PathEnv Is Nothing Then _PathEnv = Environment.GetEnvironmentVariable("Path")
+            Return _PathEnv
+        End Get
+    End Property
+    Private _PathEnv As String = Nothing
+
+    Public Class JavaEntry
+
+        '路径
+        ''' <summary>
+        ''' Java.exe 文件的完整路径。
+        ''' </summary>
+        Public ReadOnly Property PathJava As String
+            Get
+                Return PathFolder & "java.exe"
+            End Get
+        End Property
+        ''' <summary>
+        ''' Javaw.exe 文件的完整路径。
+        ''' </summary>
+        Public ReadOnly Property PathJavaw As String
+            Get
+                Return PathFolder & "javaw.exe"
+            End Get
+        End Property
+        ''' <summary>
+        ''' Javaw.exe 文件所在文件夹的路径,以 \ 结尾且只含有 \。
+        ''' </summary>
+        Public PathFolder As String
+        ''' <summary>
+        ''' 是否为用户手动导入的 Java。
+        ''' </summary>
+        Public IsUserImport As Boolean
+
+        '版本信息
+        ''' <summary>
+        ''' Java 的详细版本。若不足 4 位会在前方补 1,例如 1.16.0.1。
+        ''' 其大版本号为 Minor。
+        ''' </summary>
+        Public Version As Version
+        ''' <summary>
+        ''' Java 的大版本号。
+        ''' </summary>
+        Public ReadOnly Property VersionCode As Integer
+            Get
+                Return Version.Minor
+            End Get
+        End Property
+        ''' <summary>
+        ''' 是否为 Java Runtime Environment。
+        ''' </summary>
+        Public IsJre As Boolean
+        ''' <summary>
+        ''' 是否为 64 位 Java。
+        ''' </summary>
+        Public Is64Bit As Boolean
+        ''' <summary>
+        ''' 是否已设置环境变量。
+        ''' </summary>
+        Public ReadOnly Property HasEnvironment As Boolean
+            Get
+                If PathFolder Is Nothing OrElse PathEnv Is Nothing Then Return False
+                Return PathEnv.Replace("\", "").Replace("/", "").ToLower.Contains(PathFolder.Replace("\", "").ToLower)
+            End Get
+        End Property
+
+        '序列化
+        Public Function ToJson() As JObject
+            Return New JObject({New JProperty("Path", PathFolder), New JProperty("VersionString", Version.ToString), New JProperty("IsJre", IsJre), New JProperty("Is64Bit", Is64Bit), New JProperty("IsUserImport", IsUserImport)})
+        End Function
+        Public Shared Function FromJson(Data As JObject) As JavaEntry
+            Return New JavaEntry(Data("Path"), Data("IsUserImport")) With {.Version = New Version(Data("VersionString")), .IsJre = Data("IsJre"), .Is64Bit = Data("Is64Bit")}
+        End Function
+        ''' <summary>
+        ''' 转化为用户友好的字符串输出。
+        ''' </summary>
+        Public Overrides Function ToString() As String
+            Dim VersionString = Version.ToString
+            If VersionString.StartsWith("1.") Then VersionString = Mid(VersionString, 3)
+            Return If(IsJre, "Java ", "JDK ") & VersionCode & " (" & VersionString & ")," & If(Is64Bit, "64", "32") & " 位" & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
+        End Function
+
+        '构造
+        ''' <summary>
+        ''' 输入 javaw.exe 文件所在文件夹的路径,不限制结尾。
+        ''' </summary>
+        Public Sub New(Folder As String, IsUserImport As Boolean)
+            If Not Folder.EndsWith("\") Then Folder += "\"
+            PathFolder = Folder.Replace("/", "\")
+            Me.IsUserImport = IsUserImport
+        End Sub
+
+        '方法
+        Private IsChecked As Boolean = False
+        ''' <summary>
+        ''' 检查并获取 Java 详细信息。在 Java 存在异常时抛出错误。
+        ''' </summary>
+        Public Sub Check()
+            If IsChecked Then Exit Sub
+            Dim Output As String = Nothing
+            Try
+                '确定文件存在
+                If Not File.Exists(PathJavaw) Then
+                    Throw New FileNotFoundException("未找到 javaw.exe 文件", PathJavaw)
+                End If
+                If Not File.Exists(PathFolder & "java.exe") Then
+                    Throw New FileNotFoundException("未找到 java.exe 文件", PathFolder & "java.exe")
+                End If
+                IsJre = Not File.Exists(PathFolder & "javac.exe")
+                '运行 -version
+                Output = ShellAndGetOutput(PathFolder & "java.exe", "-version", 8000).ToLower
+                If ModeDebug Then Log("[Java] Java 检查输出:" & PathFolder & "java.exe" & vbCrLf & Output)
+                '获取详细信息
+                Dim VersionString = If(RegexSeek(Output, "(?<=version "")[^""]+"), If(RegexSeek(Output, "(?<=openjdk )[0-9]+"), "")).Replace("_", ".").Split("-").First
+                Do While VersionString.Split(".").Count < 4
+                    If VersionString.StartsWith("1.") Then
+                        VersionString = VersionString & ".0"
+                    Else
+                        VersionString = "1." & VersionString
+                    End If
+                Loop
+                Version = New Version(VersionString)
+                If Version.Minor = 0 Then
+                    Log("[Java] 疑似 X.0.X.X 格式版本号:" & Version.ToString)
+                    Version = New Version(1, Version.Major, Version.Build, Version.Revision)
+                End If
+                Is64Bit = Output.Contains("64-bit")
+                If Version.Minor <= 4 OrElse Version.Minor >= 25 Then Throw New Exception("分析详细信息失败,获取的版本为 " & Version.ToString)
+            Catch ex As Exception
+                Log("[Java] 检查失败的 Java 输出:" & PathFolder & "java.exe" & vbCrLf & If(Output, "无程序输出"))
+                ex = New Exception("检查 Java 详细信息失败(" & If(PathJavaw, "Nothing") & ")", ex)
+                Throw ex
+            End Try
+            IsChecked = True
+        End Sub
+
+    End Class
+
+    '搜索
+
+    ''' <summary>
+    ''' 模糊搜索并获取所有可用的 Java,并在结束后更新设置页面显示。输出将直接写入 JavaList。
+    ''' </summary>
+    Public JavaSearchLoader As New LoaderTask(Of Integer, Integer)("Java Search Loader", AddressOf JavaSearchLoaderSub)
+    Private Sub JavaSearchLoaderSub(Loader As LoaderTask(Of Integer, Integer))
+        If FrmSetupLaunch IsNot Nothing Then
+            RunInUiWait(Sub()
+                            FrmSetupLaunch.ComboArgumentJava.Items.Clear()
+                            FrmSetupLaunch.ComboArgumentJava.Items.Add(New ComboBoxItem With {.Content = "加载中……", .IsSelected = True})
+                        End Sub)
+        End If
+        If FrmVersionSetup IsNot Nothing Then
+            RunInUiWait(Sub()
+                            FrmVersionSetup.ComboArgumentJava.Items.Clear()
+                            FrmVersionSetup.ComboArgumentJava.Items.Add(New ComboBoxItem With {.Content = "加载中……", .IsSelected = True})
+                        End Sub)
+        End If
+
+        Try
+
+            '可能包含 Java 的文件夹列表,以 “\” 结尾,且仅包含 “\”
+            'Key:文件夹地址
+            'Value: 是否为玩家手动导入
+            Dim JavaPreList As New Dictionary(Of String, Boolean)
+
+#Region "模糊查找可能可用的 Java"
+
+            '查找环境变量中的 Java
+            For Each PathInEnv As String In Split(PathEnv.Replace("\\", "\").Replace("/", "\"), ";")
+                PathInEnv = PathInEnv.Trim(" """.ToCharArray())
+                If Not PathInEnv.EndsWith("\") Then PathInEnv += "\"
+                '粗略检查有效性
+                If File.Exists(PathInEnv & "javaw.exe") Then DictionaryAdd(JavaPreList, PathInEnv, False)
+            Next
+            '查找磁盘中的 Java
+            For Each Disk As DriveInfo In DriveInfo.GetDrives()
+                JavaSearchFolder(Disk.Name, JavaPreList, False)
+            Next
+            '查找 APPDATA 文件夹中的 Java
+            JavaSearchFolder(PathAppdata, JavaPreList, False)
+            '查找启动器目录中的 Java
+            JavaSearchFolder(Path, JavaPreList, False, IsFullSearch:=True)
+            '查找所选 Minecraft 文件夹中的 Java
+            If Not String.IsNullOrWhiteSpace(PathMcFolder) AndAlso Path <> PathMcFolder Then
+                JavaSearchFolder(PathMcFolder, JavaPreList, False, IsFullSearch:=True)
+            End If
+
+            '若[不全]为符号链接,则清除符号链接的地址
+            Dim JavaWithoutReparse As New Dictionary(Of String, Boolean)
+            For Each Pair In JavaPreList
+                Dim Folder As String = Pair.Key.Replace("\\", "\").Replace("/", "\")
+                Dim Info As FileSystemInfo = New FileInfo(Folder & "javaw.exe")
+                Do
+                    If Info.Attributes.HasFlag(FileAttributes.ReparsePoint) Then
+                        Log("[Java] 位于 " & Folder & " 的 Java 包含符号链接")
+                        Continue For
+                    End If
+                    Info = If(TypeOf Info Is FileInfo, CType(Info, FileInfo).Directory, CType(Info, DirectoryInfo).Parent)
+                Loop While Info IsNot Nothing
+                Log("[Java] 位于 " & Folder & " 的 Java 不含符号链接")
+                JavaWithoutReparse.Add(Pair.Key, Pair.Value)
+            Next
+            If JavaWithoutReparse.Count > 0 Then JavaPreList = JavaWithoutReparse
+
+            '若不全为 javapath_target,则清除二重引用的地址
+            Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
+            For Each Pair In JavaPreList
+                If Pair.Key.Contains("javapath_target_") Then
+                    Log("[Java] 位于 " & Pair.Key & " 的 Java 包含二重引用")
+                    Continue For
+                End If
+                Log("[Java] 位于 " & Pair.Key & " 的 Java 不含二重引用")
+                JavaWithoutInherit.Add(Pair.Key, Pair.Value)
+            Next
+            If JavaWithoutInherit.Count > 0 Then JavaPreList = JavaWithoutInherit
+
+#End Region
+
+#Region "添加玩家手动导入的 Java"
+
+            Dim ImportedJava As String = Setup.Get("LaunchArgumentJavaAll")
+            Try
+                For Each JavaJsonObject In GetJson(ImportedJava)
+                    Dim Entry = JavaEntry.FromJson(JavaJsonObject)
+                    If Entry.IsUserImport Then DictionaryAdd(JavaPreList, Entry.PathFolder, True)
+                Next
+            Catch ex As Exception
+                Log(ex, "Java 列表已损坏", LogLevel.Feedback)
+                Setup.Set("LaunchArgumentJavaAll", "[]")
+            End Try
+
+#End Region
+
+            '确保可用并获取详细信息,转入正式列表
+            Dim NewJavaList As New List(Of JavaEntry)
+            For Each Entry In JavaPreList
+                NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
+            Next
+            NewJavaList = Sort(JavaCheckList(NewJavaList), AddressOf JavaSorter)
+
+            '修改设置项
+            Dim AllList As New JArray
+            For Each Java In NewJavaList
+                AllList.Add(Java.ToJson)
+            Next
+            Setup.Set("LaunchArgumentJavaAll", AllList.ToString(Newtonsoft.Json.Formatting.None))
+            JavaList = NewJavaList
+
+        Catch ex As Exception
+            Log(ex, "搜索 Java 时出错", LogLevel.Feedback)
+            JavaList = New List(Of JavaEntry)
+        End Try
+
+        Log("[Java] Java 搜索完成,发现 " & JavaList.Count & " 个 Java")
+        If FrmSetupLaunch IsNot Nothing Then RunInUi(Sub() FrmSetupLaunch.RefreshJavaComboBox())
+        If FrmVersionSetup IsNot Nothing Then RunInUi(Sub() FrmVersionSetup.RefreshJavaComboBox())
+    End Sub
+
+    ''' <summary>
+    ''' 多线程检查列表中的所有 Java 项。
+    ''' </summary>
+    Private Function JavaCheckList(JavaEntries As List(Of JavaEntry)) As List(Of JavaEntry)
+        Log("[Java] 开始确认列表 Java 状态,共 " & JavaEntries.Count & " 项")
+        JavaCheckList = New List(Of JavaEntry)
+        Dim ListLock As New Object
+
+        '启动检查线程
+        Dim CheckThreads As New List(Of Thread)
+        For Each Entry In JavaEntries
+            Dim CheckThread As New Thread(Sub()
+                                              Try
+                                                  Entry.Check()
+                                                  Log("[Java] " & Entry.ToString)
+                                                  SyncLock ListLock
+                                                      JavaCheckList.Add(Entry)
+                                                  End SyncLock
+                                              Catch ex As Exception
+                                                  If Entry.IsUserImport Then
+                                                      Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", LogLevel.Hint)
+                                                  Else
+                                                      Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
+                                                  End If
+                                              End Try
+                                          End Sub)
+            CheckThreads.Add(CheckThread)
+            CheckThread.Start()
+        Next
+
+        '等待构造线程完成
+Wait:
+        Thread.Sleep(10)
+        For Each CheckThread In CheckThreads
+            If CheckThread.IsAlive Then GoTo Wait
+        Next
+    End Function
+    ''' <summary>
+    ''' 模糊搜索指定文件夹下的 Java,并只进行粗略的检查。这不会搜索全部路径。
+    ''' </summary>
+    ''' <param name="OriginalPath">开始搜索的起始路径,不限制结尾。</param>
+    ''' <param name="IsFullSearch">搜索当前文件夹下的全部文件夹(此参数不会传递到子文件夹)。</param>
+    Private Sub JavaSearchFolder(OriginalPath As String, ByRef Results As Dictionary(Of String, Boolean), Source As Boolean, Optional IsFullSearch As Boolean = False)
+        Try
+            Log("[Java] 开始" & If(IsFullSearch, "完全", "部分") & "遍历查找:" & OriginalPath)
+            JavaSearchFolder(New DirectoryInfo(OriginalPath), Results, Source, IsFullSearch)
+        Catch ex As UnauthorizedAccessException
+            Log("[Java] 遍历查找 Java 时遭遇无权限的文件夹:" & OriginalPath)
+        Catch ex As Exception
+            Log(ex, "遍历查找 Java 时出错(" & OriginalPath & ")")
+        End Try
+    End Sub
+    ''' <summary>
+    ''' 模糊搜索指定文件夹下的 Java,并只进行粗略的检查。这不会搜索全部路径。
+    ''' </summary>
+    ''' <param name="OriginalPath">开始搜索的起始路径,不限制结尾。</param>
+    ''' <param name="IsFullSearch">搜索当前文件夹下的全部文件夹(此参数不会传递到子文件夹)。</param>
+    Private Sub JavaSearchFolder(OriginalPath As DirectoryInfo, ByRef Results As Dictionary(Of String, Boolean), Source As Boolean, Optional IsFullSearch As Boolean = False)
+        Try
+            '确认目录存在
+            If Not OriginalPath.Exists Then Exit Sub
+            Dim Path As String = OriginalPath.FullName.Replace("\\", "\")
+            If Not Path.EndsWith("\") Then Path += "\"
+            '若该目录有 Java,则加入结果
+            If File.Exists(Path & "javaw.exe") Then DictionaryAdd(Results, Path, Source)
+            '查找其下的所有文件夹
+            For Each FolderInfo As DirectoryInfo In OriginalPath.EnumerateDirectories
+                If FolderInfo.Attributes.HasFlag(FileAttributes.ReparsePoint) Then Continue For '跳过符号链接
+                Dim SearchEntry = GetFolderNameFromPath(FolderInfo.Name).ToLower '用于搜索的字符串
+                If IsFullSearch OrElse
+                        FolderInfo.Parent.Name.ToLower = "users" OrElse
+                        SearchEntry.Contains("java") OrElse SearchEntry.Contains("jdk") OrElse SearchEntry.Contains("env") OrElse
+                        SearchEntry.Contains("环境") OrElse SearchEntry.Contains("run") OrElse SearchEntry.Contains("软件") OrElse
+                        SearchEntry.Contains("jre") OrElse SearchEntry = "bin" OrElse SearchEntry.Contains("mc") OrElse
+                        SearchEntry.Contains("software") OrElse SearchEntry.Contains("cache") OrElse SearchEntry.Contains("temp") OrElse
+                        SearchEntry.Contains("corretto") OrElse SearchEntry.Contains("roaming") OrElse SearchEntry.Contains("users") OrElse
+                        SearchEntry.Contains("craft") OrElse SearchEntry.Contains("program") OrElse SearchEntry.Contains("世界") OrElse
+                        SearchEntry.Contains("net") OrElse SearchEntry.Contains("游戏") OrElse SearchEntry.Contains("oracle") OrElse
+                        SearchEntry.Contains("game") OrElse SearchEntry.Contains("file") OrElse SearchEntry.Contains("data") OrElse
+                        SearchEntry.Contains("jvm") OrElse SearchEntry.Contains("服务") OrElse SearchEntry.Contains("server") OrElse
+                        SearchEntry.Contains("客户") OrElse SearchEntry.Contains("client") OrElse SearchEntry.Contains("整合") OrElse
+                        SearchEntry.Contains("应用") OrElse SearchEntry.Contains("运行") OrElse SearchEntry.Contains("前置") OrElse
+                        SearchEntry.Contains("mojang") OrElse SearchEntry.Contains("官启") OrElse SearchEntry.Contains("新建文件夹") OrElse
+                        SearchEntry.Contains("eclipse") OrElse SearchEntry.Contains("microsoft") OrElse SearchEntry.Contains("hotspot") OrElse
+                        SearchEntry.Contains("runtime") OrElse SearchEntry.Contains("x86") OrElse SearchEntry.Contains("x64") OrElse
+                        SearchEntry.Contains("forge") OrElse SearchEntry.Contains("原版") OrElse SearchEntry.Contains("optifine") OrElse
+                        SearchEntry.Contains("官方") OrElse SearchEntry.Contains("启动") OrElse SearchEntry.Contains("hmcl") OrElse
+                        SearchEntry.Contains("mod") OrElse SearchEntry.Contains("高清") OrElse SearchEntry.Contains("download") OrElse
+                        SearchEntry.Contains("launch") OrElse SearchEntry.Contains("程序") OrElse SearchEntry.Contains("path") OrElse
+                        SearchEntry.Contains("国服") OrElse SearchEntry.Contains("网易") OrElse SearchEntry.Contains("ext") OrElse '网易 Java 文件夹名
+                        SearchEntry.Contains("netease") OrElse SearchEntry.Contains("1.") OrElse SearchEntry.Contains("启动") Then
+                    JavaSearchFolder(FolderInfo, Results, Source)
+                End If
+            Next
+        Catch ex As UnauthorizedAccessException
+            Log("[Java] 遍历查找 Java 时遭遇无权限的文件夹:" & OriginalPath.FullName)
+        Catch ex As Exception
+            Log(ex, "遍历查找 Java 时出错(" & OriginalPath.FullName & ")")
+        End Try
+    End Sub
+    Public PathAppdata As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\"
+
+    '获取
+
+    ''' <summary>
+    ''' 根据要求返回最适合的 Java,若找不到则返回 Nothing。
+    ''' 最小与最大版本在与输入相同时也会通过。
+    ''' 必须在工作线程调用。
+    ''' </summary>
+    Public Function JavaSelect(Optional MinVersion As Version = Nothing, Optional MaxVersion As Version = Nothing, Optional RelatedVersion As McVersion = Nothing) As JavaEntry
+        Try
+            Dim AllowedJavaList As New List(Of JavaEntry)
+
+            '添加特定的 Java
+            Dim JavaPreList As New Dictionary(Of String, Boolean)
+            If PathMcFolder.Split("\").Count > 3 Then JavaSearchFolder(GetPathFromFullPath(PathMcFolder), JavaPreList, False, True) 'Minecraft 文件夹的父文件夹(如果不是根目录的话)
+            JavaSearchFolder(PathMcFolder, JavaPreList, False, True) 'Minecraft 文件夹
+            If RelatedVersion IsNot Nothing Then JavaSearchFolder(RelatedVersion.Path, JavaPreList, False, True) '所选版本文件夹
+            Dim TargetJavaList As New List(Of JavaEntry)
+            For Each Entry In JavaPreList
+                TargetJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
+            Next
+            If TargetJavaList.Count > 0 Then
+                TargetJavaList = JavaCheckList(TargetJavaList)
+                Log("[Java] 检查后找到 " & TargetJavaList.Count & " 个特定目录下的 Java")
+            End If
+
+RetryGet:
+            '等待进行中的搜索结束
+            If JavaSearchLoader.State <> LoadState.Finished AndAlso JavaSearchLoader.State <> LoadState.Waiting Then JavaSearchLoader.WaitForExit()
+            Select Case JavaSearchLoader.State
+                Case LoadState.Failed
+                    Throw JavaSearchLoader.Error
+                Case LoadState.Aborted
+                    Throw New ThreadInterruptedException("Java 搜索加载器已中断")
+            End Select
+            Dim AllJavaList As New List(Of JavaEntry)
+            AllJavaList.AddRange(TargetJavaList)
+            AllJavaList.AddRange(JavaList)
+
+            '根据选定条件进行过滤
+            For Each Java In AllJavaList
+                If MinVersion IsNot Nothing AndAlso Java.Version < MinVersion Then Continue For
+                If MaxVersion IsNot Nothing AndAlso Java.Version > MaxVersion Then Continue For
+                If Java.Is64Bit AndAlso Is32BitSystem Then Continue For
+                AllowedJavaList.Add(Java)
+            Next
+
+            '若未找到适合的 Java,尝试触发搜索
+            If AllowedJavaList.Count = 0 AndAlso JavaSearchLoader.State = LoadState.Waiting Then
+                Log("[Java] 未找到满足条件的 Java,尝试进行搜索")
+                JavaSearchLoader.Start()
+                GoTo RetryGet
+            End If
+
+            '若依然未找到适合的 Java,直接返回
+            If AllowedJavaList.Count = 0 Then Return Nothing
+
+            '检查用户指定的 Java
+            Dim UserSetup As String = Setup.Get("LaunchArgumentJavaSelect")
+            If RelatedVersion IsNot Nothing Then
+                Dim UserSetupVersion As String = Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion)
+                If UserSetupVersion <> "使用全局设置" Then UserSetup = UserSetupVersion
+            End If
+            If UserSetup <> "" Then
+                Dim UserJava As JavaEntry
+                Try
+                    UserJava = JavaEntry.FromJson(GetJson(UserSetup))
+                Catch ex As Exception
+                    Setup.Set("LaunchArgumentJavaSelect", "")
+                    Log(ex, "获取储存的 Java 失败")
+                    GoTo UserPass
+                End Try
+                For Each Java In AllowedJavaList
+                    If Java.PathFolder = UserJava.PathFolder Then
+                        '直接使用指定的 Java
+                        AllowedJavaList = New List(Of JavaEntry) From {UserJava}
+                        GoTo UserPass
+                    End If
+                Next
+                Log("[Java] 发现用户指定的不兼容 Java:" & UserJava.ToString)
+                If MyMsgBox("你在启动设置中指定了使用下列 Java:" & vbCrLf &
+                            " - " & UserJava.ToString & vbCrLf &
+                            vbCrLf &
+                            "该 Java 可能不兼容当前游戏版本,游戏存在崩溃风险。是否要强制使用该 Java?", "Java 兼容性警告", "取消,让 PCL2 自动选择", "继续") = 2 Then
+                    '强制使用指定的 Java
+                    Log("[Java] 已强制使用用户指定的不兼容 Java")
+                    AllowedJavaList = New List(Of JavaEntry) From {UserJava}
+                End If
+            End If
+
+            '检查特定的 Java
+            For Each Java In AllowedJavaList
+                If TargetJavaList.Contains(Java) Then
+                    '直接使用指定的 Java
+                    AllowedJavaList = New List(Of JavaEntry) From {Java}
+                    Log("[Java] 使用特定目录下的 Java:" & Java.ToString)
+                    GoTo UserPass
+                End If
+            Next
+UserPass:
+
+            '对适合的 Java 进行排序
+            AllowedJavaList = Sort(AllowedJavaList, AddressOf JavaSorter)
+
+            '检查选定的 Java,若测试失败则尝试进行搜索
+            Dim SelectedJava = AllowedJavaList.First
+            Try
+                SelectedJava.Check()
+            Catch ex As Exception
+                Log(ex, "找到的 Java 已无法使用,尝试进行搜索")
+                JavaSearchLoader.Start(IsForceRestart:=True)
+                GoTo RetryGet
+            End Try
+
+            '返回
+            Log("[Java] 选定的 Java:" & SelectedJava.ToString)
+            Return SelectedJava
+
+        Catch ex As ThreadInterruptedException
+            Log(ex, "查找符合条件的 Java 时出现加载器中断")
+            Return Nothing
+        Catch ex As Exception
+            Log(ex, "查找符合条件的 Java 失败", LogLevel.Feedback)
+            Return Nothing
+        End Try
+    End Function
+    ''' <summary>
+    ''' 返回是否安装了 64 位 Java,或在强制指定的时候是否指定 64 位 Java。
+    ''' </summary>
+    Public Function JavaUse64Bit(Optional RelatedVersion As McVersion = Nothing) As Boolean
+        Try
+            '检查强制指定
+            Dim UserSetup As String = Setup.Get("LaunchArgumentJavaSelect")
+            If RelatedVersion IsNot Nothing Then
+                Dim UserSetupVersion As String = Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion)
+                If UserSetupVersion <> "使用全局设置" Then UserSetup = UserSetupVersion
+            End If
+            If UserSetup <> "" Then
+                Dim UserJava = JavaEntry.FromJson(GetJson(UserSetup))
+                For Each Java In JavaList
+                    If Java.PathFolder = UserJava.PathFolder Then Return UserJava.Is64Bit
+                Next
+            End If
+            '检查列表
+            For Each Java In JavaList
+                If Java.Is64Bit Then Return True
+            Next
+            Return False
+        Catch ex As Exception
+            Log(ex, "检查 Java 类别时出错", LogLevel.Feedback)
+            Setup.Set("LaunchArgumentJavaSelect", "")
+            Setup.Set("VersionArgumentJavaSelect", "", Version:=RelatedVersion)
+            Return True
+        End Try
+    End Function
+    ''' <summary>
+    ''' 用于 Java 排序的函数。
+    ''' </summary>
+    Public Function JavaSorter(Left As JavaEntry, Right As JavaEntry) As Boolean
+        '1. 尽量在当前文件夹或当前 Minecraft 文件夹
+        Dim ProgramPathParent As String, MinecraftPathParent As String = ""
+        ProgramPathParent = If(New DirectoryInfo(Path).Parent, New DirectoryInfo(Path)).FullName
+        If PathMcFolder <> "" Then MinecraftPathParent = If(New DirectoryInfo(PathMcFolder).Parent, New DirectoryInfo(PathMcFolder)).FullName
+        If Left.PathFolder.StartsWith(ProgramPathParent) AndAlso Not Right.PathFolder.StartsWith(ProgramPathParent) Then Return True
+        If Not Left.PathFolder.StartsWith(ProgramPathParent) AndAlso Right.PathFolder.StartsWith(ProgramPathParent) Then Return False
+        If PathMcFolder <> "" Then
+            If Left.PathFolder.StartsWith(MinecraftPathParent) AndAlso Not Right.PathFolder.StartsWith(MinecraftPathParent) Then Return True
+            If Not Left.PathFolder.StartsWith(MinecraftPathParent) AndAlso Right.PathFolder.StartsWith(MinecraftPathParent) Then Return False
+        End If
+        '2. 尽量使用 64 位
+        If Left.Is64Bit AndAlso Not Right.Is64Bit Then Return True
+        If Not Left.Is64Bit AndAlso Right.Is64Bit Then Return False
+        '3. 尽量不使用 JDK
+        If Left.IsJre AndAlso Not Right.IsJre Then Return True
+        If Not Left.IsJre AndAlso Right.IsJre Then Return False
+        '4. Java 大版本
+        If Left.VersionCode <> Right.VersionCode Then
+            '                             Java  7   8   9  10  11  12 13 14 15  16  17  18  19
+            Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 29, 10, 11, 12, 13, 9, 8, 7, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28}
+            Return Weight.ElementAtOrDefault(Left.VersionCode) >= Weight.ElementAtOrDefault(Right.VersionCode)
+        End If
+        '5. 最次级版本号更接近 51
+        Return Math.Abs(Left.Version.Revision - 51) <= Math.Abs(Right.Version.Revision - 51)
+    End Function
+
+    '缺失
+    ''' <summary>
+    ''' 提示 Java 缺失并跳转下载。支持 Java 7、8、16。
+    ''' </summary>
+    Public Sub JavaMissing(VersionCode As Integer)
+        Select Case VersionCode
+            Case 7
+                MyMsgBox("PCL2 未找到 Java 7。" & vbCrLf &
+                         "请自行百度安装 Java 7,安装后在 PCL2 的 设置 → 启动设置 → 游戏 Java 中通过搜索或导入,确保安装的 Java 已列入 Java 列表。",
+                         "未找到 Java")
+            Case 8
+                If Is32BitSystem Then
+                    OpenWebsite("https://wwa.lanzoui.com/i7RyXq0jbub")
+                Else
+                    OpenWebsite("https://wwa.lanzoui.com/i2UyMq0jaqb")
+                End If
+                MyMsgBox("PCL2 未找到版本适宜的 Java 8。" & vbCrLf &
+                         "请在打开的网页中下载安装包并安装,安装后在 PCL2 的 设置 → 启动设置 → 游戏 Java 中通过搜索或导入,确保安装的 Java 已列入 Java 列表。",
+                         "未找到 Java")
+            Case 16, 17
+                If Is32BitSystem Then
+                    MyMsgBox("该版本的 MC 已不支持 32 位操作系统。你必须增加内存并重装为 64 位系统才能继续。", "系统兼容性提示")
+                Else
+                    OpenWebsite("https://www.oracle.com/java/technologies/downloads/#jdk17-windows")
+                    MyMsgBox("PCL2 未找到 Java " & VersionCode & "。" & vbCrLf &
+                             "请在打开的网页中选择 x64 Installer,下载并安装。" & vbCrLf &
+                             "安装后在 PCL2 的 设置 → 启动设置 → 游戏 Java 中通过搜索或导入,确保安装的 Java 已列入 Java 列表。",
+                             "未找到 Java")
+                End If
+        End Select
+    End Sub
+
+#End Region
+
+#Region "文件夹"
+
+    ''' <summary>
+    ''' 当前的 Minecraft 文件夹路径,以“\”结尾。
+    ''' </summary>
+    Public PathMcFolder As String
+    ''' <summary>
+    ''' 当前的 Minecraft 文件夹列表。
+    ''' </summary>
+    Public McFolderList As New List(Of McFolder)
+
+    Public Class McFolder '必须是 Class,否则不是引用类型,在 ForEach 中不会得到刷新
+        Public Name As String
+        Public Path As String
+        Public Type As McFolderType
+        Public VersionCount As Integer
+        Public Overrides Function Equals(obj As Object) As Boolean
+            If Not (TypeOf obj Is McFolder) Then
+                Return False
+            End If
+
+            Dim folder = DirectCast(obj, McFolder)
+            Return Name = folder.Name AndAlso
+                   Path = folder.Path AndAlso
+                   Type = folder.Type AndAlso
+                   VersionCount = folder.VersionCount
+        End Function
+        Public Overrides Function ToString() As String
+            Return Path
+        End Function
+    End Class
+    Public Enum McFolderType
+        Original
+        RenamedOriginal
+        Custom
+    End Enum
+
+    ''' <summary>
+    ''' 加载 Minecraft 文件夹列表。
+    ''' </summary>
+    Public McFolderListLoader As New LoaderTask(Of Integer, Integer)("Minecraft Folder List", AddressOf McFolderListLoadSub, Priority:=ThreadPriority.AboveNormal)
+    Private Sub McFolderListLoadSub()
+        Try
+            '初始化
+            Dim CacheMcFolderList = New List(Of McFolder)
+
+#Region "读取默认(Original)文件夹,即当前、官启文件夹,可能没有结果"
+
+            '扫描当前文件夹
+            If CheckPermission(Path) AndAlso Directory.Exists(Path & "versions\") Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Path = Path, .Type = McFolderType.Original})
+            For Each Folder As DirectoryInfo In New DirectoryInfo(Path).GetDirectories
+                If CheckPermission(Folder.FullName) AndAlso Directory.Exists(Folder.FullName & "versions\") OrElse Folder.Name = ".minecraft" Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Path = Folder.FullName & "\", .Type = McFolderType.Original})
+            Next
+
+            '扫描官启文件夹
+            Dim MojangPath As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\"
+            If (CacheMcFolderList.Count = 0 OrElse MojangPath <> CacheMcFolderList(0).Path) AndAlso '当前文件夹不是官启文件夹
+                CheckPermission(MojangPath) AndAlso Directory.Exists(MojangPath & "versions\") Then '具有权限且存在 versions 文件夹
+                CacheMcFolderList.Add(New McFolder With {.Name = "官方启动器文件夹", .Path = MojangPath, .Type = McFolderType.Original})
+            End If
+
+#End Region
+
+#Region "读取自定义(Custom)文件夹,可能没有结果"
+
+            '格式:TMZ 12>C://xxx/xx/|Test>D://xxx/xx/|名称>路径
+            For Each Folder As String In Setup.Get("LaunchFolders").Split("|")
+                If Folder = "" Then Continue For
+                If Not Folder.Contains(">") OrElse Not Folder.EndsWith("\") Then
+                    Hint("无效的 Minecraft 文件夹:" & Folder, HintType.Critical)
+                    Continue For
+                End If
+                Dim Name As String = Folder.Split(">")(0)
+                Dim Path As String = Folder.Split(">")(1)
+                If CheckPermission(Path) Then
+                    '若已有该文件夹,则直接重命名;没有则添加
+                    Dim Renamed As Boolean = False
+                    For Each OriginalFolder As McFolder In CacheMcFolderList
+                        If OriginalFolder.Path = Path Then
+                            OriginalFolder.Name = Name
+                            OriginalFolder.Type = McFolderType.RenamedOriginal
+                            Renamed = True
+                        End If
+                    Next
+                    If Not Renamed Then CacheMcFolderList.Add(New McFolder With {.Name = Name, .Path = Path, .Type = McFolderType.Custom})
+                Else
+                    Hint("无效的 Minecraft 文件夹:" & Path, HintType.Critical)
+                End If
+            Next
+
+            '将自定义文件夹情况同步到设置
+            Dim NewSetup As New List(Of String)
+            For Each Folder As McFolder In CacheMcFolderList
+                If Not Folder.Type = McFolderType.Original Then NewSetup.Add(Folder.Name & ">" & Folder.Path)
+            Next
+            If NewSetup.Count = 0 Then NewSetup.Add("") '防止 0 元素 Join 返回 Nothing
+            Setup.Set("LaunchFolders", Join(NewSetup, "|"))
+
+#End Region
+
+            '若没有可用文件夹,则创建 .minecraft
+            If CacheMcFolderList.Count = 0 Then
+                Directory.CreateDirectory(Path & ".minecraft\versions\")
+                CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Path = Path & ".minecraft\", .Type = McFolderType.Original})
+            End If
+
+            For Each Folder As McFolder In CacheMcFolderList
+#Region "获取可用版本数"
+                If Directory.Exists(Folder.Path & "versions\") Then
+                    Folder.VersionCount = New DirectoryInfo(Folder.Path & "versions\").GetDirectories.Count
+                    '减去隐藏版本的个数
+                    For Each Dir As DirectoryInfo In New DirectoryInfo(Folder.Path & "versions\").GetDirectories
+                        If ReadIni(Dir.FullName & "\PCL\Setup.ini", "Hide", "False") Then Folder.VersionCount -= 1
+                    Next
+                End If
+#End Region
+#Region "更新 launcher_profiles.json"
+                McFolderLauncherProfilesJsonCreate(Folder.Path)
+#End Region
+            Next
+            If Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(200, 2000))
+
+            '回设
+            McFolderList = CacheMcFolderList
+
+        Catch ex As Exception
+            Log(ex, "加载 Minecraft 文件夹列表失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 为 Minecraft 文件夹创建 launcher_profiles.json 文件。
+    ''' </summary>
+    Public Sub McFolderLauncherProfilesJsonCreate(Folder As String)
+        Try
+            If File.Exists(Folder & "launcher_profiles.json") Then Exit Sub
+            Dim ResultJson As String =
+"{
+    ""profiles"":  {
+        ""PCL2"": {
+            ""icon"": ""Grass"",
+            ""name"": ""PCL2"",
+            ""lastVersionId"": ""latest-release"",
+            ""type"": ""latest-release"",
+            ""lastUsed"": """ & Date.Now.ToString("yyyy-MM-dd") & "T" & Date.Now.ToString("HH:mm:ss") & ".0000Z""
+        }
+    },
+    ""selectedProfile"": ""PCL2"",
+    ""clientToken"": ""23323323323323323323323323323333""
+}"
+            WriteFile(Folder & "launcher_profiles.json", ResultJson, Encoding:=Encoding.Default)
+            Log("[Minecraft] 已创建 launcher_profiles.json:" & Folder)
+        Catch ex As Exception
+            Log(ex, "创建 launcher_profiles.json 失败(" & Folder & ")", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "版本处理"
+
+    Public Const McVersionCacheVersion As Integer = 24
+
+    Private _McVersionCurrent As McVersion
+    Private _McVersionLast = 0 '为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
+    ''' <summary>
+    ''' 当前的 Minecraft 版本。
+    ''' </summary>
+    Public Property McVersionCurrent As McVersion
+        Get
+            Return _McVersionCurrent
+        End Get
+        Set(value As McVersion)
+            If ReferenceEquals(_McVersionLast, value) Then Exit Property
+            _McVersionCurrent = value '由于有可能是 Nothing,导致无法初始化,才得这样弄一圈
+            _McVersionLast = value
+            If value Is Nothing Then Exit Property
+            '统一通行证重判
+            If AniControlEnabled = 0 AndAlso
+               Setup.Get("VersionServerNide", Version:=value) <> Setup.Get("CacheNideServer") AndAlso
+               Setup.Get("VersionServerLogin", Version:=value) = 3 Then
+                Setup.Set("CacheNideAccess", "")
+                Log("[Launch] 服务器改变,要求重新登录统一通行证")
+            End If
+            If Setup.Get("VersionServerLogin", Version:=value) = 3 Then
+                Setup.Set("CacheNideServer", Setup.Get("VersionServerNide", Version:=value))
+            End If
+            'Authlib-Injector 重判
+            If AniControlEnabled = 0 AndAlso
+               Setup.Get("VersionServerAuthServer", Version:=value) <> Setup.Get("CacheAuthServerServer") AndAlso
+               Setup.Get("VersionServerLogin", Version:=value) = 4 Then
+                Setup.Set("CacheNideServer", "")
+                Log("[Launch] 服务器改变,要求重新登录 Authlib-Injector")
+            End If
+            If Setup.Get("VersionServerLogin", Version:=value) = 4 Then
+                Setup.Set("CacheAuthServerServer", Setup.Get("VersionServerAuthServer", Version:=value))
+                Setup.Set("CacheAuthServerName", Setup.Get("VersionServerAuthName", Version:=value))
+                Setup.Set("CacheAuthServerRegister", Setup.Get("VersionServerAuthRegister", Version:=value))
+            End If
+        End Set
+    End Property
+
+    Public Class McVersion
+
+        ''' <summary>
+        ''' 该版本的版本文件夹,以“\”结尾。
+        ''' </summary>
+        Public ReadOnly Property Path As String
+        ''' <summary>
+        ''' 应用版本隔离后,该版本所对应的 Minecraft 根文件夹,以“\”结尾。
+        ''' </summary>
+        Public ReadOnly Property PathIndie As String
+            Get
+                Return GetPathIndie(Version.Modable)
+            End Get
+        End Property
+        ''' <summary>
+        ''' 在不加载版本的情况下获取版本隔离目录。
+        ''' </summary>
+        Public Function GetPathIndie(Modable As Boolean) As String
+            Dim IndieType As Integer = Setup.Get("LaunchArgumentIndie")
+            Select Case Setup.Get("VersionArgumentIndie", Version:=Me)
+                Case -1
+                    '尚未判断
+                    Dim ModFolder As New DirectoryInfo(Path & "mods\")
+                    Dim SaveFolder As New DirectoryInfo(Path & "saves\")
+                    If (ModFolder.Exists AndAlso ModFolder.EnumerateFiles.Count > 0) OrElse
+                       (SaveFolder.Exists AndAlso SaveFolder.EnumerateFiles.Count > 0) Then
+                        '自动开启
+                        Setup.Set("VersionArgumentIndie", 1, Version:=Me)
+                        Log("[Setup] 已自动开启单版本隔离:" & Name)
+                        IndieType = 4
+                    Else
+                        '使用全局设置
+                        Setup.Set("VersionArgumentIndie", 0, Version:=Me)
+                        Log("[Setup] 版本隔离使用全局设置:" & Name)
+                    End If
+                Case 0
+                    '使用全局设置
+                Case 1
+                    '开启
+                    IndieType = 4
+                Case 2
+                    '关闭
+                    IndieType = 0
+            End Select
+            Select Case IndieType
+                Case 0 '关闭
+                Case 1 '仅隔离可安装 Mod 的版本
+                    If Modable Then Return Path
+                Case 2 '仅隔离非正式版
+                    If State = McVersionState.Fool OrElse State = McVersionState.Old OrElse State = McVersionState.Snapshot Then Return Path
+                Case 3 '隔离非正式版与可安装 Mod 的版本
+                    If Modable Then Return Path
+                    If State = McVersionState.Fool OrElse State = McVersionState.Old OrElse State = McVersionState.Snapshot Then Return Path
+                Case 4 '隔离所有版本
+                    Return Path
+            End Select
+            Return PathMcFolder
+        End Function
+
+        ''' <summary>
+        ''' 该版本的版本文件夹名称。
+        ''' </summary>
+        Public ReadOnly Property Name As String
+            Get
+                If _Name Is Nothing AndAlso Not Path = "" Then _Name = GetFolderNameFromPath(Path)
+                Return _Name
+            End Get
+        End Property
+        Private _Name As String = Nothing
+
+        ''' <summary>
+        ''' 显示的描述文本。
+        ''' </summary>
+        Public Info As String = "该版本未被加载,请向作者反馈此问题"
+        ''' <summary>
+        ''' 该版本的列表检查原始结果,不受自定义影响。
+        ''' </summary>
+        Public State As McVersionState = McVersionState.Error
+        ''' <summary>
+        ''' 显示的版本图标。
+        ''' </summary>
+        Public Logo As String
+        ''' <summary>
+        ''' 是否为收藏的版本。
+        ''' </summary>
+        Public IsStar As Boolean = False
+        ''' <summary>
+        ''' 强制版本分类,0 为未启用,1 为隐藏,2 及以上为其他普通分类。
+        ''' </summary>
+        Public DisplayType As McVersionCardType = McVersionCardType.Auto
+        ''' <summary>
+        ''' 版本信息。
+        ''' </summary>
+        Public Property Version As McVersionInfo
+            Get
+                If _Version Is Nothing Then
+                    _Version = New McVersionInfo
+#Region "获取游戏版本"
+                    Try
+
+                        '获取发布时间并判断是否为老版本
+                        Try
+                            If JsonObject("releaseTime") Is Nothing Then
+                                ReleaseTime = New Date(1970, 1, 1, 15, 0, 0) '未知版本也可能显示为 1970 年
+                            Else
+                                ReleaseTime = JsonObject("releaseTime").ToObject(Of Date)
+                            End If
+                            If ReleaseTime.Year > 2000 AndAlso ReleaseTime.Year < 2013 Then
+                                _Version.McName = "Old"
+                                GoTo VersionSearchFinish
+                            End If
+                        Catch
+                            ReleaseTime = New Date(1970, 1, 1, 15, 0, 0)
+                        End Try
+                        '实验性快照
+                        If If(JsonObject("type"), "") = "pending" Then
+                            _Version.McName = "pending"
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 JumpLoader 信息中获取版本号
+                        If HasJumpLoader Then
+                            Try
+                                _Version.McName = JsonObject("jumploader")("jars")("minecraft")(0)("gameVersion")
+                                GoTo VersionSearchFinish
+                            Catch
+                            End Try
+                        End If
+                        '从 PCL 下载的版本信息中获取版本号
+                        If JsonObject("clientVersion") IsNot Nothing Then
+                            _Version.McName = JsonObject("clientVersion")
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 HMCL 下载的版本信息中获取版本号
+                        If JsonObject("patches") IsNot Nothing Then
+                            For Each Patch As JObject In JsonObject("patches")
+                                If If(Patch("id"), "").ToString = "game" AndAlso Patch("version") IsNot Nothing Then
+                                    _Version.McName = Patch("version").ToString
+                                    GoTo VersionSearchFinish
+                                End If
+                            Next
+                        End If
+                        '从 Forge Arguments 中获取版本号
+                        If JsonObject("arguments") IsNot Nothing AndAlso JsonObject("arguments")("game") IsNot Nothing Then
+                            Dim Mark As Boolean = False
+                            For Each Argument In JsonObject("arguments")("game")
+                                If Mark Then
+                                    _Version.McName = Argument.ToString
+                                    GoTo VersionSearchFinish
+                                End If
+                                If Argument.ToString = "--fml.mcVersion" Then Mark = True
+                            Next
+                        End If
+                        '从继承版本中获取版本号
+                        If Not InheritVersion = "" Then
+                            _Version.McName = If(JsonObject("jar"), "").ToString 'LiteLoader 优先使用 Jar
+                            If _Version.McName = "" Then _Version.McName = InheritVersion
+                            GoTo VersionSearchFinish
+                        End If
+                        '从下载地址中获取版本号
+                        Dim Regex As String = RegexSeek(If(JsonObject("downloads"), "").ToString, "(?<=launcher.mojang.com/mc/game/)[^/]*")
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 Forge 版本中获取版本号
+                        Dim LibrariesString As String = JsonObject("libraries").ToString
+                        Regex = If(RegexSeek(LibrariesString, "(?<=net.minecraftforge:forge:)1.[0-9+.]+"), RegexSeek(LibrariesString, "(?<=net.minecraftforge:fmlloader:)1.[0-9+.]+"))
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 OptiFine 版本中获取版本号
+                        Regex = RegexSeek(LibrariesString, "(?<=optifine:OptiFine:)1.[0-9+.]+")
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 Fabric 版本中获取版本号
+                        Regex = RegexSeek(LibrariesString, "(?<=((fabricmc)|(quiltmc)):intermediary:)[^""]*")
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        'TODO: [Quilt 支持] 从 Quilt 版本中获取版本号
+                        '从 jar 项中获取版本号
+                        If JsonObject("jar") IsNot Nothing Then
+                            _Version.McName = JsonObject("jar").ToString
+                            GoTo VersionSearchFinish
+                        End If
+                        '非准确的版本判断警告
+                        Log("[Minecraft] 无法完全确认 MC 版本号的版本:" & Name)
+                        '从文件夹名中获取
+                        Regex = RegexSeek(Name, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)")
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        '从 Json 出现的版本号中获取
+                        Dim JsonRaw As JObject = JsonObject.DeepClone()
+                        JsonRaw.Remove("libraries")
+                        Dim JsonRawText As String = JsonRaw.ToString
+                        Regex = RegexSeek(JsonRawText, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)")
+                        If Regex IsNot Nothing Then
+                            _Version.McName = Regex
+                            GoTo VersionSearchFinish
+                        End If
+                        '无法获取
+                        _Version.McName = "Unknown"
+                        Info = "PCL 无法识别该版本,请向作者反馈此问题"
+                    Catch ex As Exception
+                        Log(ex, "识别 Minecraft 版本时出错")
+                        _Version.McName = "Unknown"
+                        Info = "无法识别:" & ex.Message
+                    End Try
+VersionSearchFinish:
+                    '获取版本号
+                    If _Version.McName.StartsWith("1.") Then
+                        Dim SplitVersion = _Version.McName.Split(" "c, "_"c, "-"c, "."c)
+                        Dim SplitResult As String
+                        '分割获取信息
+                        SplitResult = If(SplitVersion.Count >= 2, SplitVersion(1), "0")
+                        _Version.McCodeMain = If(SplitResult.Length <= 2, Val(SplitResult), "0")
+                        SplitResult = If(SplitVersion.Count >= 3, SplitVersion(2), "0")
+                        _Version.McCodeSub = If(SplitResult.Length <= 2, Val(SplitResult), "0")
+                    ElseIf _Version.McName.Contains("w") OrElse _Version.McName = "pending" Then
+                        _Version.McCodeMain = 99
+                        _Version.McCodeSub = 99
+                    End If
+#End Region
+                End If
+                Return _Version
+            End Get
+            Set(value As McVersionInfo)
+                _Version = value
+            End Set
+        End Property
+        Private _Version As McVersionInfo = Nothing
+
+        ''' <summary>
+        ''' 版本的发布时间。
+        ''' </summary>
+        Public ReleaseTime As New Date(1970, 1, 1, 15, 0, 0)
+
+        ''' <summary>
+        ''' 该版本的 Json 文本。
+        ''' </summary>
+        Public Property JsonText As String
+            Get
+                If _JsonText Is Nothing Then
+                    If Not File.Exists(Path & Name & ".json") Then Throw New Exception("未找到版本 Json 文件")
+                    _JsonText = ReadFile(Path & Name & ".json")
+                    '如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
+                    If _JsonText.Length = 0 Then
+                        If RunInUi() Then
+                            Log("[Minecraft] 版本 Json 文件为空或读取失败,由于代码在主线程运行,将不再进行重试", LogLevel.Debug)
+                            Throw New Exception("版本 Json 文件为空或读取失败")
+                        Else
+                            Log("[Minecraft] 版本 Json 文件为空或读取失败,将在 2s 后重试读取(" & Path & Name & ".json)", LogLevel.Debug)
+                            Thread.Sleep(2000)
+                            _JsonText = ReadFile(Path & Name & ".json")
+                            If _JsonText.Length = 0 Then Throw New Exception("版本 Json 文件为空或读取失败")
+                        End If
+                    End If
+                    If _JsonText.Length < 100 Then Throw New Exception("版本 Json 文件有误,内容为:" & _JsonText)
+                End If
+                Return _JsonText
+            End Get
+            Set(ByVal value As String)
+                _JsonText = value
+            End Set
+        End Property
+        Private _JsonText As String = Nothing
+        ''' <summary>
+        ''' 该版本的 Json 对象。
+        ''' 若 Json 存在问题,在获取该属性时即会抛出异常。
+        ''' </summary>
+        Public Property JsonObject As JObject
+            Get
+                If _JsonObject Is Nothing Then
+                    Dim Text As String = JsonText '触发 JsonText 的 Get 事件
+                    Try
+                        _JsonObject = GetJson(Text)
+                        '转换 HMCL 关键项
+                        If _JsonObject.ContainsKey("patches") AndAlso Not _JsonObject.ContainsKey("time") Then
+                            IsHmclFormatJson = True
+                            '合并 Json
+                            'Dim HasOptiFine As Boolean = False, HasForge As Boolean = False
+                            Dim CurrentObject As JObject = Nothing
+                            Dim SubjsonList As New List(Of JObject)
+                            For Each Subjson As JObject In _JsonObject("patches")
+                                SubjsonList.Add(Subjson)
+                            Next
+                            SubjsonList = Sort(SubjsonList, Function(Left As JObject, Right As JObject) As Boolean
+                                                                Return Val(If(Left("priority"), "0").ToString) < Val(If(Right("priority"), "0").ToString)
+                                                            End Function)
+                            For Each Subjson As JObject In SubjsonList
+                                Dim Id As String = Subjson("id")
+                                If Id IsNot Nothing Then
+                                    '合并 Json
+                                    Log("[Minecraft] 合并 HMCL 分支项:" & Id)
+                                    If CurrentObject IsNot Nothing Then
+                                        CurrentObject.Merge(Subjson)
+                                    Else
+                                        CurrentObject = Subjson
+                                    End If
+                                Else
+                                    Log("[Minecraft] 存在为空的 HMCL 分支项")
+                                End If
+                            Next
+                            _JsonObject = CurrentObject
+                            '修改附加项
+                            _JsonObject("id") = Name
+                            If _JsonObject.ContainsKey("inheritsFrom") Then _JsonObject.Remove("inheritsFrom")
+                        End If
+                        '与继承版本合并
+                        Dim InheritVersion = Nothing
+                        Try
+                            InheritVersion = If(_JsonObject("inheritsFrom") Is Nothing, "", _JsonObject("inheritsFrom").ToString)
+                            If InheritVersion = Name Then
+                                Log("[Minecraft] 自引用的继承版本:" & Name, LogLevel.Debug)
+                                InheritVersion = ""
+                                Exit Try
+                            End If
+Recheck:
+                            If InheritVersion <> "" Then
+                                Dim Inherit As New McVersion(InheritVersion)
+                                '继续循环
+                                If Inherit.InheritVersion = InheritVersion Then Throw New Exception("版本依赖项出现嵌套:" & InheritVersion)
+                                InheritVersion = Inherit.InheritVersion
+                                '合并
+                                Inherit.JsonObject.Merge(_JsonObject)
+                                _JsonObject = Inherit.JsonObject
+                                GoTo Recheck
+                            End If
+                        Catch ex As Exception
+                            Log(ex, "合并版本依赖项 Json 失败(" & If(InheritVersion, "null").ToString & ")")
+                        End Try
+                    Catch ex As Exception
+                        Throw New Exception("版本 Json 不规范(" & If(Name, "null") & ")", ex)
+                    End Try
+                    Try
+                        '处理 JumpLoader
+                        If Text.Contains("minecraftforge") AndAlso File.Exists(PathIndie & "config\jumploader.json") Then
+                            For Each ModFile In Directory.EnumerateFiles(PathIndie & "mods")
+                                Dim FileName As String = GetFileNameFromPath(ModFile).ToLower
+                                If FileName.EndsWith(".jar") AndAlso FileName.Contains("jumploader") Then
+                                    Log("[Minecraft] 发现 JumpLoader 分支项:" & FileName)
+                                    HasJumpLoader = True
+                                    Exit For
+                                End If
+                            Next
+                        End If
+                        If HasJumpLoader Then
+                            _JsonObject.Remove("jumploader")
+                            _JsonObject.Add("jumploader", GetJson(ReadFile(PathIndie & "config\jumploader.json")))
+                        End If
+                    Catch ex As Exception
+                        Log(ex, "处理 JumpLoader 失败")
+                    End Try
+                End If
+                Return _JsonObject
+            End Get
+            Set(ByVal value As JObject)
+                _JsonObject = value
+            End Set
+        End Property
+        Private _JsonObject As JObject = Nothing
+        ''' <summary>
+        ''' 是否为旧版 Json 格式。
+        ''' </summary>
+        Public ReadOnly Property IsOldJson As Boolean
+            Get
+                Return JsonObject("minecraftArguments") IsNot Nothing
+            End Get
+        End Property
+        ''' <summary>
+        ''' Json 是否为 HMCL 格式。
+        ''' </summary>
+        Public Property IsHmclFormatJson As Boolean = False
+        ''' <summary>
+        ''' 是否包含 JumpLoader。
+        ''' </summary>
+        Public Property HasJumpLoader As Boolean = False
+
+        ''' <summary>
+        ''' 该版本的依赖版本。若无依赖版本则为空字符串。
+        ''' </summary>
+        Public ReadOnly Property InheritVersion As String
+            Get
+                If _InheritVersion Is Nothing Then
+                    _InheritVersion = If(JsonObject("inheritsFrom"), "").ToString
+                    '由于过老的 LiteLoader 中没有 Inherits(例如 1.5.2),需要手动判断以获取真实继承版本
+                    '此外,由于这里的加载早于版本种类判断,所以需要手动判断是否为 LiteLoader
+                    '如果版本提供了不同的 Jar,代表所需的 Jar 可能已被更改,则跳过 Inherit 替换
+                    If JsonText.Contains("liteloader") AndAlso Version.McName <> Name AndAlso Not JsonText.Contains("logging") Then
+                        If If(JsonObject("jar"), Version.McName).ToString = Version.McName Then _InheritVersion = Version.McName
+                    End If
+                    'HMCL 版本无 Json
+                    If IsHmclFormatJson Then _InheritVersion = ""
+                End If
+                Return _InheritVersion
+            End Get
+        End Property
+        Private _InheritVersion As String = Nothing
+
+        ''' <summary></summary>
+        ''' <param name="Path">版本名,或版本文件夹的完整路径(不规定是否以 \ 结尾)。</param>
+        Public Sub New(Path As String)
+            Me.Path = If(Path.Contains(":"), "", PathMcFolder & "versions\") & '补全完整路径
+                      Path &
+                      If(Path.EndsWith("\"), "", "\") '补全右划线
+        End Sub
+
+        ''' <summary>
+        ''' 检查 Minecraft 版本,若检查通过 State 则为 Original 且返回 True。
+        ''' </summary>
+        Public Function Check() As Boolean
+
+            '检查文件夹
+            If Not Directory.Exists(Path) Then
+                State = McVersionState.Error
+                Info = "该文件夹不存在"
+                Return False
+            End If
+            '检查权限
+            Try
+                Directory.CreateDirectory(Path & "PCL\")
+                CheckPermissionWithException(Path & "PCL\")
+            Catch ex As Exception
+                State = McVersionState.Error
+                Info = "PCL2 没有对该文件夹的访问权限,请右键以管理员身份运行 PCL2"
+                Log(ex, "没有访问版本文件夹的权限")
+                Return False
+            End Try
+            '确认 Json 可用性
+            Try
+                Dim JsonObjCheck = JsonObject
+            Catch ex As Exception
+                Log(ex, "版本 Json 可用性检查失败(" & Name & ")")
+                JsonText = ""
+                JsonObject = Nothing
+                Info = ex.Message
+                State = McVersionState.Error
+                Return False
+            End Try
+            '检查依赖版本
+            Try
+                If Not InheritVersion = "" Then
+                    If Not File.Exists(GetPathFromFullPath(Path) & InheritVersion & "\" & InheritVersion & ".json") Then
+                        State = McVersionState.Error
+                        Info = "需要安装 " & InheritVersion & " 作为前置版本"
+                        Return False
+                    End If
+                End If
+            Catch ex As Exception
+                Log(ex, "依赖版本检查出错(" & Name & ")")
+                State = McVersionState.Error
+                Info = "未知错误:" & GetString(ex)
+                Return False
+            End Try
+
+            State = McVersionState.Original
+            Return True
+        End Function
+        ''' <summary>
+        ''' 加载 Minecraft 版本的详细信息。不使用其缓存,且会更新缓存。
+        ''' </summary>
+        Public Function Load() As McVersion
+            Try
+                Directory.CreateDirectory(Path & "PCL")
+                '检查版本,若出错则跳过数据确定阶段
+                If Not Check() Then GoTo ExitDataLoad
+#Region "确定版本分类"
+                Select Case Version.McName '在获取 Version.Original 对象时会完成它的加载
+                    Case "Unknown"
+                        State = McVersionState.Error
+                    Case "Old"
+                        State = McVersionState.Old
+                    Case Else '根据 API 进行筛选
+                        Dim RealJson As String = If(JsonObject, JsonText).ToString
+                        '愚人节与快照版本
+                        If If(JsonObject("type"), "").ToString = "fool" OrElse GetMcFoolName(Name) <> "" Then
+                            State = McVersionState.Fool
+                        ElseIf Version.McName.ToLower.Contains("w") OrElse Name.ToLower.Contains("combat") OrElse Version.McName.ToLower.Contains("rc") OrElse Version.McName.ToLower.Contains("pre") OrElse Version.McName.Contains("experimental") OrElse If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending" Then
+                            State = McVersionState.Snapshot
+                        End If
+                        'OptiFine
+                        If RealJson.Contains("optifine") Then
+                            State = McVersionState.OptiFine
+                            Version.OptiFineVersion = If(RegexSeek(RealJson, "(?<=HD_U_)[^"":/]+"), "未知版本")
+                            Version.HasOptiFine = True
+                        End If
+                        'LiteLoader
+                        If RealJson.Contains("liteloader") Then
+                            State = McVersionState.LiteLoader
+                            Version.HasLiteLoader = True
+                        End If
+                        'Fabric、Forge
+                        'TODO: [Quilt 支持] 确认这里的玩意儿对不对
+                        If RealJson.Contains("net.fabricmc:fabric-loader") OrElse RealJson.Contains("org.quiltmc:quilt-loader") Then
+                            State = McVersionState.Fabric
+                            Version.FabricVersion = If(RegexSeek(RealJson, "(?<=(net.fabricmc:fabric-loader:)|(org.quiltmc:quilt-loader:))[0-9\.]+(\+build.[0-9]+)?"), "未知版本").Replace("+build", "")
+                            Version.HasFabric = True
+                        ElseIf RealJson.Contains("minecraftforge") Then
+                            State = McVersionState.Forge
+                            Version.ForgeVersion = RegexSeek(RealJson, "(?<=forge:[0-9\.]+(_pre[0-9]*)?\-)[0-9\.]+")
+                            If Version.ForgeVersion Is Nothing Then Version.ForgeVersion = RegexSeek(RealJson, "(?<=net\.minecraftforge:minecraftforge:)[0-9\.]+")
+                            If Version.ForgeVersion Is Nothing Then Version.ForgeVersion = If(RegexSeek(RealJson, "(?<=net\.minecraftforge:fmlloader:[0-9\.]+-)[0-9\.]+"), "未知版本")
+                            Version.HasForge = True
+                        End If
+                        Version.IsApiLoaded = True
+                End Select
+#End Region
+ExitDataLoad:
+                '确定版本图标
+                Logo = ReadIni(Path & "PCL\Setup.ini", "Logo", "")
+                If Logo = "" OrElse ReadIni(Path & "PCL\Setup.ini", "LogoCustom", "False") = "False" Then
+                    Select Case State
+                        Case McVersionState.Original
+                            Logo = "pack://application:,,,/images/Blocks/Grass.png"
+                        Case McVersionState.Snapshot
+                            Logo = "pack://application:,,,/images/Blocks/CommandBlock.png"
+                        Case McVersionState.Old
+                            Logo = "pack://application:,,,/images/Blocks/CobbleStone.png"
+                        Case McVersionState.Forge
+                            Logo = "pack://application:,,,/images/Blocks/Anvil.png"
+                        Case McVersionState.Fabric
+                            Logo = "pack://application:,,,/images/Blocks/Fabric.png"
+                        Case McVersionState.OptiFine
+                            Logo = "pack://application:,,,/images/Blocks/GrassPath.png"
+                        Case McVersionState.LiteLoader
+                            Logo = "pack://application:,,,/images/Blocks/Egg.png"
+                        Case McVersionState.Fool
+                            Logo = "pack://application:,,,/images/Blocks/GoldBlock.png"
+                        Case Else
+                            Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
+                    End Select
+                End If
+                '确定版本描述
+                Dim CustomInfo As String = ReadIni(Path & "PCL\Setup.ini", "CustomInfo")
+                If CustomInfo = "" Then
+                    Select Case State
+                        Case McVersionState.Snapshot
+                            If Version.McName.ToLower.Contains("pre") OrElse Version.McName.ToLower.Contains("rc") Then
+                                Info = "预发布版 " & Version.McName
+                            ElseIf Version.McName.Contains("experimental") OrElse Version.McName = "pending" Then
+                                Info = "实验性快照"
+                            Else
+                                Info = "快照 " & Version.McName
+                            End If
+                        Case McVersionState.Old
+                            Info = "远古版本"
+                        Case McVersionState.Original, McVersionState.Forge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
+                            Info = Version.ToString
+                        Case McVersionState.Fool
+                            Info = GetMcFoolName(Name)
+                        Case McVersionState.Error
+                            '已有错误信息
+                        Case Else
+                            Info = "发生了未知错误,请向作者反馈此问题"
+                    End Select
+                    If Not State = McVersionState.Error Then
+                        If HasJumpLoader Then Info += ", JumpLoader"
+                        If Setup.Get("VersionServerLogin", Version:=Me) = 3 Then Info += ", 统一通行证验证"
+                        If Setup.Get("VersionServerLogin", Version:=Me) = 4 Then Info += ", Authlib 验证"
+                    End If
+                Else
+                    Info = CustomInfo
+                End If
+                '确定版本收藏状态
+                IsStar = ReadIni(Path & "PCL\Setup.ini", "IsStar", False)
+                '确定版本显示种类
+                DisplayType = ReadIni(Path & "PCL\Setup.ini", "DisplayType", McVersionCardType.Auto)
+                '写入缓存
+                WriteIni(Path & "PCL\Setup.ini", "State", State)
+                WriteIni(Path & "PCL\Setup.ini", "Info", Info)
+                WriteIni(Path & "PCL\Setup.ini", "Logo", Logo)
+                If State <> McVersionState.Error Then
+                    WriteIni(Path & "PCL\Setup.ini", "ReleaseTime", ReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"))
+                    WriteIni(Path & "PCL\Setup.ini", "VersionFabric", Version.FabricVersion)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionOptiFine", Version.OptiFineVersion)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionLiteLoader", Version.HasLiteLoader)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionForge", Version.ForgeVersion)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionApiCode", Version.SortCode)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionOriginal", Version.McName)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionOriginalMain", Version.McCodeMain)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionOriginalSub", Version.McCodeSub)
+                End If
+            Catch ex As Exception
+                Info = "未知错误:" & GetString(ex)
+                Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
+                State = McVersionState.Error
+                Log(ex, "加载版本失败(" & Name & ")", LogLevel.Feedback)
+            Finally
+                IsLoaded = True
+            End Try
+            Return Me
+        End Function
+        Public IsLoaded As Boolean = False
+
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim version = TryCast(obj, McVersion)
+            Return version IsNot Nothing AndAlso Path = version.Path
+        End Function
+
+    End Class
+    Public Enum McVersionState
+        [Error]
+        Original
+        Snapshot
+        Fool
+        OptiFine
+        Old
+        Forge
+        LiteLoader
+        Fabric
+    End Enum
+
+    ''' <summary>
+    ''' 某个 Minecraft 实例的版本名、附加组件信息。
+    ''' </summary>
+    Public Class McVersionInfo
+
+        ''' <summary>
+        ''' 版本的 API 信息是否已加载。
+        ''' </summary>
+        Public IsApiLoaded As Boolean = False
+
+        '原版
+
+        ''' <summary>
+        ''' 原版版本名。如 1.12.2,16w01a。
+        ''' </summary>
+        Public McName As String
+        ''' <summary>
+        ''' 原版主版本号,如 12(For 1.12.2),快照则固定为 99。不可用则为 -1。
+        ''' </summary>
+        Public McCodeMain As Integer = -1
+        ''' <summary>
+        ''' 原版次版本号,如 2(For 1.12.2),快照则固定为 99。不可用则为 -1。
+        ''' </summary>
+        Public McCodeSub As Integer = -1
+
+        'OptiFine
+
+        ''' <summary>
+        ''' 该版本是否通过 Json 安装了 OptiFine。
+        ''' </summary>
+        Public HasOptiFine As Boolean = False
+        ''' <summary>
+        ''' OptiFine 版本号,如 C8、C9_pre10。
+        ''' </summary>
+        Public OptiFineVersion As String = ""
+
+        'Forge
+
+        ''' <summary>
+        ''' 该版本是否安装了 Forge。
+        ''' </summary>
+        Public HasForge As Boolean = False
+        ''' <summary>
+        ''' Forge 版本号,如 31.1.2、14.23.5.2847。
+        ''' </summary>
+        Public ForgeVersion As String = ""
+
+        'Fabric
+
+        ''' <summary>
+        ''' 该版本是否安装了 Fabric。
+        ''' </summary>
+        Public HasFabric As Boolean = False
+        ''' <summary>
+        ''' Fabric 版本号,如 0.7.2.175。
+        ''' </summary>
+        Public FabricVersion As String = ""
+
+        'LiteLoader
+
+        ''' <summary>
+        ''' 该版本是否安装了 LiteLoader。
+        ''' </summary>
+        Public HasLiteLoader As Boolean = False
+
+        'API
+
+        ''' <summary>
+        ''' 该版本是否可以安装 Mod。
+        ''' </summary>
+        Public ReadOnly Property Modable As Boolean
+            Get
+                Return HasFabric OrElse HasForge OrElse HasLiteLoader
+            End Get
+        End Property
+
+        ''' <summary>
+        ''' 生成对此版本信息的用户友好的描述性字符串。
+        ''' </summary>
+        Public Overrides Function ToString() As String
+            ToString = ""
+            If HasForge Then ToString += ", Forge" & If(ForgeVersion = "未知版本", "", " " & ForgeVersion)
+            If HasFabric Then ToString += ", Fabric" & If(FabricVersion = "未知版本", "", " " & FabricVersion)
+            If HasOptiFine Then ToString += ", OptiFine" & If(OptiFineVersion = "未知版本", "", " " & OptiFineVersion)
+            If HasLiteLoader Then ToString += ", LiteLoader"
+            If ToString = "" Then
+                Return "原版 " & McName
+            Else
+                Return McName & ToString & If(ModeDebug, " (" & SortCode & "#)", "")
+            End If
+        End Function
+
+        ''' <summary>
+        ''' 用于排序比较的编号。
+        ''' </summary>
+        Public Property SortCode As Integer
+            Get
+                If _SortCode = -2 Then
+                    '初始化
+                    Try
+                        If HasFabric Then
+                            If FabricVersion = "未知版本" Then Return 0
+                            Dim SubVersions = FabricVersion.Split(".")
+                            If SubVersions.Length >= 3 Then
+                                _SortCode = Val(SubVersions(0)) * 10000 + Val(SubVersions(1)) * 100 + Val(SubVersions(2))
+                            Else
+                                Throw New Exception("无效的 Fabric 版本:" & ForgeVersion)
+                            End If
+                        ElseIf HasForge Then
+                            If ForgeVersion = "未知版本" Then Return 0
+                            Dim SubVersions = ForgeVersion.Split(".")
+                            If SubVersions.Length = 4 Then
+                                _SortCode = Val(SubVersions(0)) * 1000000 + Val(SubVersions(1)) * 10000 + Val(SubVersions(3))
+                            ElseIf SubVersions.Length = 3 Then
+                                _SortCode = Val(SubVersions(0)) * 1000000 + Val(SubVersions(1)) * 10000 + Val(SubVersions(2))
+                            Else
+                                Throw New Exception("无效的 Forge 版本:" & ForgeVersion)
+                            End If
+                        ElseIf HasOptiFine Then
+                            If OptiFineVersion = "未知版本" Then Return 0
+                            '由对应原版次级版本号(2 位)、字母(2 位)、末尾数字(2 位)、测试标记(2 位,正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)组成
+                            _SortCode =
+                                If(McCodeSub >= 0, McCodeSub, 0) * 1000000 +                    '第一段:原版次级版本号(2 位)
+                                (Asc(CType(Left(OptiFineVersion.ToUpper, 1), Char)) - Asc("A"c) + 1) * 10000 + '第二段:字母编号(2 位),如 G2 中的 G(7)
+                                Val(RegexSeek(Right(OptiFineVersion, OptiFineVersion.Length - 1), "[0-9]+")) * 100         '第三段:末尾数字(2 位),如 C5 beta4 中的 5
+                            '第三段:测试标记
+                            If OptiFineVersion.ToLower.Contains("pre") Then _SortCode += 50
+                            If OptiFineVersion.ToLower.Contains("pre") OrElse OptiFineVersion.ToLower.Contains("beta") Then
+                                If Val(Right(OptiFineVersion, 1)) = 0 AndAlso Right(OptiFineVersion, 1) <> "0" Then
+                                    _SortCode += 1 '为 pre 或 beta 结尾,视作 1
+                                Else
+                                    _SortCode += Val(RegexSeek(OptiFineVersion.ToLower, "(?<=((pre)|(beta)))[0-9]+"))
+                                End If
+                            Else
+                                _SortCode += 99
+                            End If
+                        Else
+                            _SortCode = -1
+                        End If
+                    Catch ex As Exception
+                        _SortCode = -1
+                        Log(ex, "获取 API 版本信息失败:" & ToString())
+                    End Try
+                End If
+                Return _SortCode
+            End Get
+            Set(ByVal value As Integer)
+                _SortCode = value
+            End Set
+        End Property
+        Private _SortCode As Integer = -2
+
+    End Class
+
+    ''' <summary>
+    ''' 根据版本名获取对应的愚人节版本描述。非愚人节版本会返回空字符串。
+    ''' </summary>
+    Public Function GetMcFoolName(Name As String) As String
+        Name = Name.ToLower
+        If Name.StartsWith("2.0") Then
+            Return "这个秘密计划了两年的更新将游戏推向了一个新高度!"
+        ElseIf Name.StartsWith("20w14inf") OrElse Name = "20w14∞" Then
+            Return "我们加入了 20 亿个新的世界,让无限的想象变成了现实!"
+        ElseIf Name = "15w14a" Then
+            Return "作为一款全年龄向的游戏,我们需要和平,需要爱与拥抱。"
+        ElseIf Name = "1.rv-pre1" Then
+            Return "是时候将现代科技带入 Minecraft 了!"
+        ElseIf Name = "3d shareware v1.34" Then
+            Return "我们从地下室的废墟里找到了这个开发于 1994 年的杰作!"
+        ElseIf Name = "22w13oneblockatatime" Then
+            Return "一次一个方块更新!迎接全新的挖掘、合成与骑乘玩法吧!"
+        Else
+            Return ""
+        End If
+    End Function
+
+    ''' <summary>
+    ''' 当前按卡片分类的所有版本列表。
+    ''' </summary>
+    Public McVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
+#End Region
+
+#Region "版本列表加载"
+
+    ''' <summary>
+    ''' 是否要求本次加载强制刷新版本列表。
+    ''' </summary>
+    Public McVersionListForceRefresh As Boolean = False
+    ''' <summary>
+    ''' 加载 Minecraft 文件夹的版本列表。
+    ''' </summary>
+    Public McVersionListLoader As New LoaderTask(Of String, Integer)("Minecraft Version List", AddressOf McVersionListLoad) With {.ReloadTimeout = 1}
+
+    ''' <summary>
+    ''' 开始加载当前 Minecraft 文件夹的版本列表。
+    ''' </summary>
+    Private Sub McVersionListLoad(Loader As LoaderTask(Of String, Integer))
+        '开始加载
+        Dim Path As String = Loader.Input
+        Try
+            '初始化
+            McVersionList.Clear()
+
+            '检测缓存是否需要更新
+            Dim FolderList As New List(Of String)
+            If Directory.Exists(Path & "versions") Then '不要使用 CheckPermission,会导致写入时间改变,从而使得文件夹被强制刷新
+                Try
+                    For Each Folder As DirectoryInfo In New DirectoryInfo(Path & "versions").GetDirectories
+                        FolderList.Add(Folder.Name)
+                    Next
+                Catch ex As Exception
+                    Throw New Exception("无法读取版本文件夹,可能是由于没有权限(" & Path & "versions)", ex)
+                End Try
+            End If
+            '没有可用版本
+            If FolderList.Count = 0 Then
+                WriteIni(Path & "PCL.ini", "VersionCache", "") '清空缓存
+                GoTo OnLoaded
+            End If
+            '有可用版本
+            Dim FolderListCheck As Integer = GetHash(McVersionCacheVersion & "#" & Join(FolderList.ToArray, "#")) Mod (Integer.MaxValue - 1) '根据文件夹名列表生成辨识码
+            If Not McVersionListForceRefresh AndAlso Val(ReadIni(Path & "PCL.ini", "VersionCache")) = FolderListCheck Then
+                '可以使用缓存
+                Dim Result = McVersionListLoadCache(Path)
+                If Result Is Nothing Then
+                    GoTo Reload
+                Else
+                    McVersionList = Result
+                End If
+            Else
+                '文件夹列表不符
+Reload:
+                McVersionListForceRefresh = False
+                Log("[Minecraft] 文件夹列表变更,重载所有版本")
+                WriteIni(Path & "PCL.ini", "VersionCache", FolderListCheck)
+                McVersionList = McVersionListLoadNoCache(Path)
+            End If
+            If Loader.IsAborted Then Exit Sub
+
+            '改变当前选择的版本
+OnLoaded:
+            If McVersionList.Count = 0 Then
+                McVersionCurrent = Nothing
+                Setup.Set("LaunchVersionSelect", "")
+                Log("[Minecraft] 未找到可用 Minecraft 版本")
+            Else
+                '尝试读取已储存的选择
+                Dim SavedSelection As String = ReadIni(Path & "PCL.ini", "Version")
+                If Not SavedSelection = "" Then
+                    For Each Card As KeyValuePair(Of McVersionCardType, List(Of McVersion)) In McVersionList
+                        For Each Version As McVersion In Card.Value
+                            If Version.Name = SavedSelection AndAlso Not Version.State = McVersionState.Error Then
+                                '使用已储存的选择
+                                McVersionCurrent = Version
+                                Setup.Set("LaunchVersionSelect", McVersionCurrent.Name)
+                                Log("[Minecraft] 选择该文件夹储存的 Minecraft 版本:" & McVersionCurrent.Path)
+                                Exit Sub
+                            End If
+                        Next
+                    Next
+                End If
+                If Not McVersionList.First.Value(0).State = McVersionState.Error Then
+                    '自动选择第一项
+                    McVersionCurrent = McVersionList.First.Value(0)
+                    Setup.Set("LaunchVersionSelect", McVersionCurrent.Name)
+                    Log("[Launch] 自动选择 Minecraft 版本:" & McVersionCurrent.Path)
+                End If
+            End If
+            If Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(200, 3000))
+        Catch ex As ThreadInterruptedException
+        Catch ex As Exception
+            WriteIni(Path & "PCL.ini", "VersionCache", "") '要求下次重新加载
+            Log(ex, "加载 .minecraft 版本列表失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    '获取版本列表
+    Private Function McVersionListLoadCache(Path As String) As Dictionary(Of McVersionCardType, List(Of McVersion))
+        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+        Try
+            Dim CardCount As Integer = ReadIni(Path & "PCL.ini", "CardCount", -1)
+            If CardCount = -1 Then Return Nothing
+            For i = 0 To CardCount - 1
+                Dim CardType As McVersionCardType = ReadIni(Path & "PCL.ini", "CardKey" & (i + 1), ":")
+                Dim VersionList As New List(Of McVersion)
+
+                '循环读取版本
+                For Each Folder As String In ReadIni(Path & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
+                    If Folder = "" Then Continue For
+                    Try
+
+                        '读取单个版本
+                        Dim Version As New McVersion(Path & "versions\" & Folder & "\")
+                        VersionList.Add(Version)
+                        Version.Info = ReadIni(Version.Path & "PCL\Setup.ini", "CustomInfo", "")
+                        If Version.Info = "" Then Version.Info = ReadIni(Version.Path & "PCL\Setup.ini", "Info", Version.Info)
+                        Version.Logo = ReadIni(Version.Path & "PCL\Setup.ini", "Logo", Version.Logo)
+                        Version.ReleaseTime = ReadIni(Version.Path & "PCL\Setup.ini", "ReleaseTime", Version.ReleaseTime)
+                        Version.State = ReadIni(Version.Path & "PCL\Setup.ini", "State", Version.State)
+                        Version.IsStar = ReadIni(Version.Path & "PCL\Setup.ini", "IsStar", False)
+                        Version.DisplayType = ReadIni(Path & "PCL\Setup.ini", "DisplayType", McVersionCardType.Auto)
+                        If Version.State <> McVersionState.Error Then
+                            Dim VersionInfo As New McVersionInfo With {
+                                .FabricVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionFabric", ""),
+                                .ForgeVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionForge", ""),
+                                .OptiFineVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOptiFine", ""),
+                                .HasLiteLoader = ReadIni(Version.Path & "PCL\Setup.ini", "VersionLiteLoader", False),
+                                .SortCode = ReadIni(Version.Path & "PCL\Setup.ini", "VersionApiCode", -1),
+                                .McName = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOriginal", "Unknown"),
+                                .McCodeMain = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOriginalMain", -1),
+                                .McCodeSub = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOriginalSub", -1),
+                                .IsApiLoaded = True
+                            }
+                            VersionInfo.HasFabric = VersionInfo.FabricVersion.Count > 1
+                            VersionInfo.HasForge = VersionInfo.ForgeVersion.Count > 1
+                            VersionInfo.HasOptiFine = VersionInfo.OptiFineVersion.Count > 1
+                            Version.Version = VersionInfo
+                        End If
+
+                        '重新检查错误版本
+                        If Version.State = McVersionState.Error Then
+                            '重新获取版本错误信息
+                            Dim OldDesc As String = Version.Info
+                            Version.State = McVersionState.Original
+                            Version.Check()
+                            '校验错误原因是否改变
+                            Dim CustomInfo As String = ReadIni(Version.Path & "PCL\Setup.ini", "CustomInfo")
+                            If Version.State = McVersionState.Original OrElse (CustomInfo = "" AndAlso Not OldDesc = Version.Info) Then
+                                Log("[Minecraft] 版本 " & Version.Name & " 的错误状态已变更,新的状态为:" & Version.Info)
+                                Return Nothing
+                            End If
+                        End If
+
+                        '校验未加载的版本
+                        If Version.Logo = "" Then
+                            Log("[Minecraft] 版本 " & Version.Name & " 未被加载")
+                            Return Nothing
+                        End If
+
+                    Catch ex As Exception
+                        Log(ex, "读取版本加载缓存失败(" & Folder & ")", LogLevel.Debug)
+                        Return Nothing
+                    End Try
+                Next
+
+                ResultVersionList.Add(CardType, VersionList)
+            Next
+            Return ResultVersionList
+        Catch ex As Exception
+            Log(ex, "读取版本缓存失败")
+            Return Nothing
+        End Try
+    End Function
+    Private Function McVersionListLoadNoCache(Path As String) As Dictionary(Of McVersionCardType, List(Of McVersion))
+        Dim VersionList As New List(Of McVersion)
+        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+#Region "循环加载每个版本的信息"
+        Dim Dirs As DirectoryInfo() = New DirectoryInfo(Path & "versions").GetDirectories
+        For Each Folder As DirectoryInfo In Dirs
+            If (Folder.Name = "cache" OrElse Folder.Name = "BLClient" OrElse Folder.Name = "PCL") AndAlso Not File.Exists(Folder.FullName & "\" & Folder.Name & ".json") Then
+                Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
+                Continue For
+            End If
+            Dim Version As New McVersion(Folder.FullName & "\")
+            VersionList.Add(Version)
+            Version.Load()
+        Next
+#End Region
+#Region "将版本分类到各个卡片"
+        Try
+
+            '未经过自定义的版本列表
+            Dim VersionListOriginal As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
+            '单独列出收藏的版本
+            Dim StaredVersions As New List(Of McVersion)
+            For Each Version As McVersion In VersionList
+                If Version.IsStar AndAlso Not Version.DisplayType = McVersionCardType.Hidden Then StaredVersions.Add(Version)
+            Next
+            If StaredVersions.Count > 0 Then VersionListOriginal.Add(McVersionCardType.Star, StaredVersions)
+
+            '预先筛选出愚人节的版本
+            McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Error}, McVersionCardType.Error)
+            McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Fool}, McVersionCardType.Fool)
+
+            '筛选 API 版本
+            McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Forge, McVersionState.LiteLoader, McVersionState.Fabric}, McVersionCardType.API)
+
+            '将老版本预先分类入不常用,只剩余原版、快照、OptiFine
+            Dim VersionUseful As New List(Of McVersion)
+            Dim VersionRubbish As New List(Of McVersion)
+            McVersionFilter(VersionList, {McVersionState.Old}, VersionRubbish)
+
+            '确认最新版本,若为快照则加入常用列表
+            Dim LargestVersion As McVersion = Nothing '最新的版本
+            For Each Version As McVersion In VersionList
+                If Version.State = McVersionState.Original OrElse Version.State = McVersionState.Snapshot Then
+                    If LargestVersion Is Nothing OrElse Version.ReleaseTime > LargestVersion.ReleaseTime Then LargestVersion = Version
+                End If
+            Next
+            If LargestVersion IsNot Nothing AndAlso LargestVersion.State = McVersionState.Snapshot Then
+                VersionUseful.Add(LargestVersion)
+                VersionList.Remove(LargestVersion)
+            End If
+
+            '将剩余的快照全部拖进不常用列表
+            McVersionFilter(VersionList, {McVersionState.Snapshot}, VersionRubbish)
+
+            '获取每个大版本下最新的原版与 OptiFine
+            Dim NewerVersion As New Dictionary(Of String, McVersion)
+            Dim ExistVersion As New List(Of Integer)
+            For Each Version As McVersion In VersionList
+                If Version.Version.McCodeMain < 2 Then Continue For '未获取成功的版本
+                If Not ExistVersion.Contains(Version.Version.McCodeMain) Then ExistVersion.Add(Version.Version.McCodeMain)
+                If NewerVersion.ContainsKey(Version.Version.McCodeMain & "-" & Version.State) Then
+                    If Version.Version.HasOptiFine Then
+                        'OptiFine 根据排序识别号判断
+                        If Version.Version.SortCode > NewerVersion(Version.Version.McCodeMain & "-" & Version.State).Version.SortCode Then NewerVersion(Version.Version.McCodeMain & "-" & Version.State) = Version
+                    Else
+                        '原版根据发布时间判断
+                        If Version.ReleaseTime > NewerVersion(Version.Version.McCodeMain & "-" & Version.State).ReleaseTime Then NewerVersion(Version.Version.McCodeMain & "-" & Version.State) = Version
+                    End If
+                Else
+                    NewerVersion.Add(Version.Version.McCodeMain & "-" & Version.State, Version)
+                End If
+            Next
+
+            '将每个大版本下的最常规版本加入
+            For Each Code As Integer In ExistVersion
+                If NewerVersion.ContainsKey(Code & "-" & McVersionState.OptiFine) Then
+                    Dim OptiFineVersion As McVersion = NewerVersion(Code & "-" & McVersionState.OptiFine)
+                    If NewerVersion.ContainsKey(Code & "-" & McVersionState.Original) AndAlso Not NewerVersion(Code & "-" & McVersionState.Original).Version.McName = OptiFineVersion.InheritVersion Then
+                        '同时存在 OptiFine 与原版,但 OptiFine 不是该版的依赖版本,则一定为两个不同版本,且原版较新
+                        VersionUseful.Add(NewerVersion(Code & "-" & McVersionState.Original))
+                        VersionList.Remove(NewerVersion(Code & "-" & McVersionState.Original))
+                    End If
+                    VersionUseful.Add(OptiFineVersion)
+                    VersionList.Remove(OptiFineVersion)
+                Else
+                    VersionUseful.Add(NewerVersion(Code & "-" & McVersionState.Original))
+                    VersionList.Remove(NewerVersion(Code & "-" & McVersionState.Original))
+                End If
+            Next
+
+            '将剩余的东西添加进去
+            VersionRubbish.AddRange(VersionList)
+            If VersionUseful.Count > 0 Then VersionListOriginal.Add(McVersionCardType.OriginalLike, VersionUseful)
+            If VersionRubbish.Count > 0 Then VersionListOriginal.Add(McVersionCardType.Rubbish, VersionRubbish)
+
+            '按照自定义版本分类重新添加
+            For Each VersionPair In VersionListOriginal
+                For Each Version As McVersion In VersionPair.Value
+                    Dim RealType = If(Version.DisplayType = 0 OrElse VersionPair.Key = McVersionCardType.Star, VersionPair.Key, Version.DisplayType)
+                    If Not ResultVersionList.ContainsKey(RealType) Then ResultVersionList.Add(RealType, New List(Of McVersion))
+                    ResultVersionList(RealType).Add(Version)
+                Next
+            Next
+
+        Catch ex As Exception
+            ResultVersionList.Clear()
+            Log(ex, "分类版本列表失败", LogLevel.Feedback)
+        End Try
+#End Region
+#Region "对卡片与版本进行排序"
+
+        '对卡片进行整体排序
+        Dim SortedVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+        For Each SortRule As String In {McVersionCardType.Star, McVersionCardType.API, McVersionCardType.OriginalLike, McVersionCardType.Rubbish, McVersionCardType.Fool, McVersionCardType.Error, McVersionCardType.Hidden}
+            If ResultVersionList.ContainsKey(SortRule) Then SortedVersionList.Add(SortRule, ResultVersionList(SortRule))
+        Next
+        ResultVersionList = SortedVersionList
+
+        '常规版本:快照放在最上面,此后按版本号从高到低排序
+        If ResultVersionList.ContainsKey(McVersionCardType.OriginalLike) Then
+            Dim OldList As List(Of McVersion) = ResultVersionList(McVersionCardType.OriginalLike)
+            '提取快照
+            Dim Snapshot As McVersion = Nothing
+            For Each Version As McVersion In OldList
+                If Version.State = McVersionState.Snapshot Then
+                    Snapshot = Version
+                    Exit For
+                End If
+            Next
+            If Not IsNothing(Snapshot) Then OldList.Remove(Snapshot)
+            '按版本号排序
+            Dim NewList As List(Of McVersion) = Sort(OldList, Function(Left As McVersion, Right As McVersion)
+                                                                  Return Left.Version.McCodeMain > Right.Version.McCodeMain
+                                                              End Function)
+            '回设
+            If Not IsNothing(Snapshot) Then NewList.Insert(0, Snapshot)
+            ResultVersionList(McVersionCardType.OriginalLike) = NewList
+        End If
+
+        '不常用版本:按发布时间新旧排序,如果不可用则按名称排序
+        If ResultVersionList.ContainsKey(McVersionCardType.Rubbish) Then
+            ResultVersionList(McVersionCardType.Rubbish) = Sort(ResultVersionList(McVersionCardType.Rubbish), Function(Left As McVersion, Right As McVersion)
+                                                                                                                  Dim LeftYear As Integer = Left.ReleaseTime.Year '+ If(Left.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
+                                                                                                                  Dim RightYear As Integer = Right.ReleaseTime.Year '+ If(Right.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
+                                                                                                                  If LeftYear > 2000 AndAlso RightYear > 2000 Then
+                                                                                                                      If LeftYear <> RightYear Then
+                                                                                                                          Return LeftYear > RightYear
+                                                                                                                      Else
+                                                                                                                          Return Left.ReleaseTime > Right.ReleaseTime
+                                                                                                                      End If
+                                                                                                                  ElseIf LeftYear > 2000 AndAlso RightYear < 2000 Then
+                                                                                                                      Return True
+                                                                                                                  ElseIf LeftYear < 2000 AndAlso RightYear > 2000 Then
+                                                                                                                      Return False
+                                                                                                                  Else
+                                                                                                                      Return Left.Name > Right.Name
+                                                                                                                  End If
+                                                                                                              End Function)
+        End If
+
+        'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
+        If ResultVersionList.ContainsKey(McVersionCardType.API) Then
+            ResultVersionList(McVersionCardType.API) = Sort(ResultVersionList(McVersionCardType.API), Function(Left As McVersion, Right As McVersion)
+                                                                                                          Dim Basic = VersionSortInteger(Left.Version.McName, Right.Version.McName)
+                                                                                                          If Basic <> 0 Then
+                                                                                                              Return Basic > 0
+                                                                                                          Else
+                                                                                                              If Left.Version.HasFabric Xor Right.Version.HasFabric Then
+                                                                                                                  Return Left.Version.HasFabric
+                                                                                                              ElseIf Left.Version.HasForge Xor Right.Version.HasForge Then
+                                                                                                                  Return Left.Version.HasForge
+                                                                                                              ElseIf Not Left.Version.SortCode <> Right.Version.SortCode Then
+                                                                                                                  Return Left.Version.SortCode > Right.Version.SortCode
+                                                                                                              Else
+                                                                                                                  Return Left.Name > Right.Name
+                                                                                                              End If
+                                                                                                          End If
+                                                                                                      End Function)
+        End If
+
+#End Region
+#Region "保存卡片缓存"
+        WriteIni(Path & "PCL.ini", "CardCount", ResultVersionList.Count)
+        For i = 0 To ResultVersionList.Count - 1
+            WriteIni(Path & "PCL.ini", "CardKey" & (i + 1), ResultVersionList.Keys(i))
+            Dim Value As String = ""
+            For Each Version As McVersion In ResultVersionList.Values(i)
+                Value += Version.Name & ":"
+            Next
+            WriteIni(Path & "PCL.ini", "CardValue" & (i + 1), Value)
+        Next
+#End Region
+        Return ResultVersionList
+    End Function
+    ''' <summary>
+    ''' 筛选特定种类的版本,并直接添加为卡片。
+    ''' </summary>
+    ''' <param name="VersionList">用于筛选的列表。</param>
+    ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
+    ''' <param name="CardType">卡片的名称。</param>
+    Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), ByRef Target As Dictionary(Of McVersionCardType, List(Of McVersion)), Formula As McVersionState(), CardType As McVersionCardType)
+        '将某种版本筛选出来
+        Dim KeepList As New List(Of McVersion)
+        For Each Version As McVersion In VersionList
+            For Each Type As Integer In Formula
+                If Type = Version.State Then
+                    KeepList.Add(Version)
+                    GoTo NextVersion
+                End If
+            Next
+NextVersion:
+        Next
+        '加入版本列表,并从剩余中删除
+        If KeepList.Count > 0 Then
+            Target.Add(CardType, KeepList)
+            For Each Version As McVersion In KeepList
+                VersionList.Remove(Version)
+            Next
+        End If
+    End Sub
+    ''' <summary>
+    ''' 筛选特定种类的版本,并增加入一个已有列表中。
+    ''' </summary>
+    ''' <param name="VersionList">用于筛选的列表。</param>
+    ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
+    ''' <param name="KeepList">传入需要增加入的列表。</param>
+    Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), Formula As McVersionState(), ByRef KeepList As List(Of McVersion))
+        For Each Version As McVersion In VersionList
+            For Each Type As McVersionState In Formula
+                If Type <> Version.State Then Continue For
+                KeepList.Add(Version) : Exit For
+            Next
+NextVersion:
+        Next
+        If KeepList.Count > 0 Then
+            For Each Version As McVersion In KeepList
+                VersionList.Remove(Version)
+            Next
+        End If
+    End Sub
+    Public Enum McVersionCardType
+        Star = -1
+        Auto = 0 '仅用于强制版本分类的自动
+        Hidden = 1
+        API = 2
+        OriginalLike = 3
+        Rubbish = 4
+        Fool = 5
+        [Error] = 6
+    End Enum
+
+#End Region
+
+#Region "皮肤"
+
+    Public Structure McSkinInfo
+        Public IsSlim As Boolean
+        Public LocalFile As String
+        Public IsVaild As Boolean
+    End Structure
+    ''' <summary>
+    ''' 要求玩家选择一个皮肤文件,并进行相关校验。
+    ''' </summary>
+    Public Function McSkinSelect(MustHaveTwoLayers As Boolean) As McSkinInfo
+        Dim FileName As String = SelectFile("皮肤文件(*.png)|*.png", "选择皮肤文件")
+
+        '验证有效性
+        If FileName = "" Then Return New McSkinInfo With {.IsVaild = False}
+        Try
+            Dim Image As New MyBitmap(FileName)
+            If MustHaveTwoLayers Then
+                If Image.Pic.Width = 64 AndAlso Image.Pic.Height = 32 Then
+                    Hint("自定义离线皮肤只支持 64x64 像素的双层皮肤!", HintType.Critical)
+                    Return New McSkinInfo With {.IsVaild = False}
+                End If
+                If Image.Pic.Width <> 64 OrElse Image.Pic.Height <> 64 Then
+                    Hint("自定义离线皮肤图片大小应为 64x64 像素!", HintType.Critical)
+                    Return New McSkinInfo With {.IsVaild = False}
+                End If
+            Else
+                If Image.Pic.Width <> 64 OrElse Not (Image.Pic.Height = 32 OrElse Image.Pic.Height = 64) Then
+                    Hint("皮肤图片大小应为 64x32 像素或 64x64 像素!", HintType.Critical)
+                    Return New McSkinInfo With {.IsVaild = False}
+                End If
+            End If
+            Dim FileInfo As New FileInfo(FileName)
+            If FileInfo.Length > 24 * 1024 Then
+                Hint("皮肤文件大小需小于 24 KB,而所选文件大小为 " & Math.Round(FileInfo.Length / 1024, 2) & " KB", HintType.Critical)
+                Return New McSkinInfo With {.IsVaild = False}
+            End If
+        Catch ex As Exception
+            Log(ex, "皮肤文件存在错误", LogLevel.Hint)
+            Return New McSkinInfo With {.IsVaild = False}
+        End Try
+
+        '获取皮肤种类
+        Dim IsSlim As Integer = MyMsgBox("此皮肤为 Steve 模型(粗手臂)还是 Alex 模型(细手臂)?", "选择皮肤种类", "Steve 模型", "Alex 模型", "我不知道", HighLight:=False)
+        If IsSlim = 3 Then
+            Hint("请在皮肤下载页面确认皮肤种类后再使用此皮肤!")
+            Return New McSkinInfo With {.IsVaild = False}
+        End If
+
+        Return New McSkinInfo With {.IsVaild = True, .IsSlim = IsSlim = 2, .LocalFile = FileName}
+    End Function
+
+    ''' <summary>
+    ''' 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。
+    ''' </summary>
+    Public Function McSkinGetAddress(Uuid As String, Type As String) As String
+        If Uuid = "" Then Throw New Exception("Uuid 为空。")
+        If Uuid.StartsWith("000000") Then Throw New Exception("离线 Uuid 无正版皮肤文件。")
+        '尝试读取缓存
+        Dim CacheSkinAddress As String = ReadIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid)
+        If Not CacheSkinAddress = "" Then Return CacheSkinAddress
+        '获取皮肤地址
+        Dim Url As String
+        Select Case Type
+            Case "Mojang", "Ms"
+                Url = "https://sessionserver.mojang.com/session/minecraft/profile/"
+            Case "Nide"
+                Url = "https://auth.mc-user.com:233/" & If(McVersionCurrent Is Nothing, Setup.Get("CacheNideServer"), Setup.Get("VersionServerNide", Version:=McVersionCurrent)) & "/sessionserver/session/minecraft/profile/"
+            Case "Auth"
+                Url = If(McVersionCurrent Is Nothing, Setup.Get("CacheAuthServerServer"), Setup.Get("VersionServerAuthServer", Version:=McVersionCurrent)) & "/sessionserver/session/minecraft/profile/"
+            Case Else
+                Throw New ArgumentException("皮肤地址种类无效:" & If(Type, "null"))
+        End Select
+        Dim SkinString = NetGetCodeByRequestRetry(Url & Uuid)
+        If SkinString = "" Then Throw New Exception("皮肤返回值为空,可能是未设置自定义皮肤的用户")
+        '处理皮肤地址
+        Dim SkinValue As String
+        Try
+            For Each SkinProperty In GetJson(SkinString)("properties")
+                If SkinProperty("name") = "textures" Then
+                    SkinValue = SkinProperty("value")
+                    Exit Try
+                End If
+            Next
+            Throw New Exception("未从皮肤返回值中找到符合条件的 Property")
+        Catch ex As Exception
+            Log(ex, "无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:" & SkinString, LogLevel.Developer)
+            Throw New Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex)
+        End Try
+        SkinString = Encoding.GetEncoding("utf-8").GetString(Convert.FromBase64String(SkinValue))
+        Dim SkinJson As JObject = GetJson(SkinString.ToLower)
+        If SkinJson("textures") Is Nothing OrElse SkinJson("textures")("skin") Is Nothing OrElse SkinJson("textures")("skin")("url") Is Nothing Then
+            Throw New Exception("用户未设置自定义皮肤")
+        Else
+            SkinValue = SkinJson("textures")("skin")("url").ToString
+        End If
+        '保存缓存
+        WriteIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid, SkinValue)
+        Log("[Skin] UUID " & Uuid & " 对应的皮肤文件为 " & SkinValue)
+        Return SkinValue
+    End Function
+
+    Private ReadOnly McSkinDownloadLock As New Object
+    ''' <summary>
+    ''' 从 Url 下载皮肤。返回本地文件路径,失败将抛出异常。
+    ''' </summary>
+    Public Function McSkinDownload(Address As String) As String
+        Dim SkinName As String = GetFileNameFromPath(Address)
+        Dim FileAddress As String = PathTemp & "Cache\Skin\" & GetHash(Address) & ".png"
+        SyncLock McSkinDownloadLock
+            If Not File.Exists(FileAddress) Then
+                NetDownload(Address, FileAddress & NetDownloadEnd)
+                File.Delete(FileAddress)
+                FileSystem.Rename(FileAddress & NetDownloadEnd, FileAddress)
+                Log("[Minecraft] 皮肤下载成功:" & FileAddress)
+            End If
+            Return FileAddress
+        End SyncLock
+    End Function
+
+    ''' <summary>
+    ''' 获取 Uuid 对应的皮肤,返回“Steve”或“Alex”。
+    ''' </summary>
+    Public Function McSkinSex(Uuid As String) As String
+        If Not Uuid.Length = 32 Then Return "Steve"
+        Dim a = Integer.Parse(Uuid(7), Globalization.NumberStyles.AllowHexSpecifier)
+        Dim b = Integer.Parse(Uuid(15), Globalization.NumberStyles.AllowHexSpecifier)
+        Dim c = Integer.Parse(Uuid(23), Globalization.NumberStyles.AllowHexSpecifier)
+        Dim d = Integer.Parse(Uuid(31), Globalization.NumberStyles.AllowHexSpecifier)
+        Return If((a Xor b Xor c Xor d) Mod 2, "Alex", "Steve")
+    End Function
+
+#End Region
+
+#Region "支持库文件(Library)"
+
+    Public Class McLibToken
+        ''' <summary>
+        ''' 文件的完整本地路径。
+        ''' </summary>
+        Public LocalPath As String
+        ''' <summary>
+        ''' 文件大小。若无有效数据即为 0。
+        ''' </summary>
+        Public Size As Long = 0
+        ''' <summary>
+        ''' 是否为 Natives 文件。
+        ''' </summary>
+        Public IsNatives As Boolean = False
+        ''' <summary>
+        ''' 文件的 SHA1。
+        ''' </summary>
+        Public SHA1 As String = Nothing
+        ''' <summary>
+        ''' 由 Json 提供的 URL,若没有则为 Nothing。
+        ''' </summary>
+        Public Property Url As String
+            Get
+                Return _Url
+            End Get
+            Set(ByVal value As String)
+                '孤儿 Forge 作者喜欢把没有的 URL 写个空字符串
+                _Url = If(String.IsNullOrWhiteSpace(value), Nothing, value)
+            End Set
+        End Property
+        Private _Url As String
+        ''' <summary>
+        ''' 原 Json 中 Name 项除去最后一部分版本号的较前部分。可能为 Nothing。
+        ''' </summary>
+        Public ReadOnly Property Name As String
+            Get
+                If OriginalName Is Nothing Then Return Nothing
+                Dim Splited As New List(Of String)(OriginalName.Split(":"))
+                Splited.RemoveAt(Splited.Count - 1)
+                Return Join(Splited, ":")
+            End Get
+        End Property
+        ''' <summary>
+        ''' 原 Json 中 Name 项最后一部分的版本号。
+        ''' </summary>
+        Public ReadOnly Property Version As String
+            Get
+                Dim Splited = OriginalName.Split(":")
+                Return Splited(Splited.Count - 1)
+            End Get
+        End Property
+        ''' <summary>
+        ''' 原 Json 中的 Name 项。
+        ''' </summary>
+        Public OriginalName As String
+        ''' <summary>
+        ''' 是否为 JumpLoader 项。
+        ''' </summary>
+        Public IsJumpLoader As Boolean = False
+
+        Public Overrides Function ToString() As String
+            Return If(IsNatives, "[Native] ", "") & GetString(Size) & " | " & LocalPath
+        End Function
+    End Class
+
+    ''' <summary>
+    ''' 检查是否符合 Json 中的 Rules。
+    ''' </summary>
+    ''' <param name="RuleToken">Json 中的 "rules" 项目。</param>
+    Public Function McJsonRuleCheck(RuleToken As JToken) As Boolean
+        If RuleToken Is Nothing Then Return True
+
+        '初始化
+        Dim Required As Boolean = False
+        For Each Rule As JToken In RuleToken
+
+            '单条条件验证
+            Dim IsRightRule As Boolean = True '是否为正确的规则
+            If Rule("os") IsNot Nothing Then '操作系统
+                If Rule("os")("name") IsNot Nothing Then '操作系统名称
+                    Dim OsName As String = Rule("os")("name").ToString
+                    If OsName = "unknown" Then
+                    ElseIf OsName = "windows" Then
+                        If Rule("os")("version") IsNot Nothing Then '操作系统版本
+                            Dim Cr As String = Rule("os")("version").ToString
+                            IsRightRule = IsRightRule AndAlso RegexCheck(OSVersion, Cr)
+                        End If
+                    Else
+                        IsRightRule = False
+                    End If
+                End If
+                If Rule("os")("arch") IsNot Nothing Then '操作系统架构
+                    IsRightRule = IsRightRule AndAlso ((Rule("os")("arch").ToString = "x86") = Is32BitSystem)
+                End If
+            End If
+            If Not IsNothing(Rule("features")) Then '标签
+                IsRightRule = IsRightRule AndAlso IsNothing(Rule("features")("is_demo_user")) '只反选是否为 Demo 用户
+            End If
+
+            '反选确认
+            If Rule("action").ToString = "allow" Then
+                If IsRightRule Then Required = True 'allow
+            Else
+                If IsRightRule Then Required = False 'disallow
+            End If
+
+        Next
+        Return Required
+    End Function
+    Private OSVersion As String = My.Computer.Info.OSVersion
+
+    ''' <summary>
+    ''' 递归获取 Minecraft 某一版本的完整支持库列表。
+    ''' </summary>
+    Public Function McLibListGet(Version As McVersion, IncludeVersionJar As Boolean) As List(Of McLibToken)
+
+        '获取当前支持库列表
+        Log("[Minecraft] 获取支持库列表:" & Version.Name)
+        McLibListGet = McLibListGetWithJson(Version.JsonObject, JumpLoaderFolder:=Version.PathIndie & ".jumploader\")
+
+        '需要添加原版 Jar
+        If IncludeVersionJar Then
+            Dim RealVersion As McVersion
+            Dim RequiredJar As String = Version.JsonObject("jar")?.ToString
+            If Version.IsHmclFormatJson OrElse RequiredJar Is Nothing Then
+                '根据 Inherit 获取最深层版本
+                '此外,HMCL 项直接使用自身的 Jar
+                Dim OriginalVersion As McVersion = Version
+                Do Until OriginalVersion.InheritVersion = ""
+                    If OriginalVersion.InheritVersion = OriginalVersion.Name Then Exit Do
+                    OriginalVersion = New McVersion(PathMcFolder & "versions\" & OriginalVersion.InheritVersion & "\")
+                Loop
+                '需要新建对象,否则后面的 Check 会导致 McVersionCurrent 的 State 变回 Original
+                '复现:启动一个 Snapshot 版本
+                RealVersion = New McVersion(OriginalVersion.Path)
+            Else
+                'Json 已提供 Jar 字段,使用该字段的信息
+                RealVersion = New McVersion(RequiredJar)
+            End If
+            Dim ClientUrl As String, ClientSHA1 As String
+            '判断需求的版本是否存在
+            '不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在
+            If Not File.Exists(RealVersion.Path & RealVersion.Name & ".json") Then
+                RealVersion = Version
+                Log("[Minecraft] 可能缺少前置版本 " & RealVersion.Name & ",找不到对应的 Json 文件", LogLevel.Debug)
+            End If
+            '获取详细下载信息
+            If RealVersion.JsonObject("downloads") IsNot Nothing AndAlso RealVersion.JsonObject("downloads")("client") IsNot Nothing Then
+                ClientUrl = RealVersion.JsonObject("downloads")("client")("url")
+                ClientSHA1 = RealVersion.JsonObject("downloads")("client")("sha1")
+            Else
+                ClientUrl = Nothing
+                ClientSHA1 = Nothing
+            End If
+            '把所需的原版 Jar 添加进去
+            McLibListGet.Add(New McLibToken With {.LocalPath = RealVersion.Path & RealVersion.Name & ".jar", .Size = 0, .IsNatives = False, .Url = ClientUrl, .SHA1 = ClientSHA1})
+        End If
+
+    End Function
+    ''' <summary>
+    ''' 获取 Minecraft 某一版本忽视继承的支持库列表,即结果中没有继承项。
+    ''' </summary>
+    Public Function McLibListGetWithJson(JsonObject As JObject, Optional KeepSameNameDifferentVersionResult As Boolean = False, Optional CustomMcFolder As String = Nothing, Optional JumpLoaderFolder As String = Nothing) As List(Of McLibToken)
+        CustomMcFolder = If(CustomMcFolder, PathMcFolder)
+        Dim BasicArray As New List(Of McLibToken)
+
+        '添加基础 Json 项
+        Dim AllLibs As JArray = JsonObject("libraries")
+        '添加 JumpLoader Json 项
+        If JsonObject("jumploader") IsNot Nothing AndAlso JsonObject("jumploader")("jars") IsNot Nothing AndAlso JsonObject("jumploader")("jars")("maven") IsNot Nothing Then
+            For Each JumpLoaderToken In JsonObject("jumploader")("jars")("maven")
+                AllLibs.Add(JumpLoaderToken)
+            Next
+        End If
+
+        '转换为 LibToken
+        For Each Library As JObject In AllLibs.Children
+
+            '检查是否需要(Rules)
+            If Not McJsonRuleCheck(Library("rules")) Then Continue For
+
+            '检查 JumpLoader
+            Dim IsJumpLoader As Boolean = False
+            If Library("mavenPath") IsNot Nothing Then
+                IsJumpLoader = True
+                If Library("name") Is Nothing Then Library.Add("name", Library("mavenPath")) '这里的修改会导致原 Json 内容改变
+                If Library("repoUrl") IsNot Nothing AndAlso Library("url") Is Nothing Then Library.Add("url", Library("repoUrl"))
+            End If
+
+            '获取根节点下的 url
+            Dim RootUrl As String = Library("url")
+            If RootUrl IsNot Nothing Then
+                RootUrl += McLibGet(Library("name"), False, True, CustomMcFolder).Replace("\", "/")
+            End If
+
+            '根据是否本地化处理(Natives)
+            If Library("natives") Is Nothing Then
+                '没有 Natives
+                Dim LocalPath As String
+                If IsJumpLoader Then
+                    LocalPath = McLibGet(Library("name"), CustomMcFolder:=If(JumpLoaderFolder, CustomMcFolder))
+                Else
+                    LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder)
+                End If
+                Try
+                    If Library("downloads") IsNot Nothing AndAlso Library("downloads")("artifact") IsNot Nothing Then
+                        BasicArray.Add(New McLibToken With {
+                                                       .IsJumpLoader = IsJumpLoader,
+                                                       .OriginalName = Library("name"),
+                                                       .Url = If(RootUrl, Library("downloads")("artifact")("url")),
+                                                       .LocalPath = If(Library("downloads")("artifact")("path") Is Nothing, McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder), CustomMcFolder & "libraries\" & Library("downloads")("artifact")("path").ToString.Replace("/", "\")),
+                                                       .Size = Val(Library("downloads")("artifact")("size").ToString),
+                                                       .IsNatives = False,
+                                                       .SHA1 = Library("downloads")("artifact")("sha1")?.ToString})
+                    Else
+                        BasicArray.Add(New McLibToken With {.IsJumpLoader = IsJumpLoader, .OriginalName = Library("name"), .Url = RootUrl, .LocalPath = LocalPath, .Size = 0, .IsNatives = False, .SHA1 = Nothing})
+                    End If
+                Catch ex As Exception
+                    Log(ex, "处理实际支持库列表失败(无 Natives," & If(Library("name"), "Nothing").ToString & ")")
+                    BasicArray.Add(New McLibToken With {.IsJumpLoader = IsJumpLoader, .OriginalName = Library("name"), .Url = RootUrl, .LocalPath = LocalPath, .Size = 0, .IsNatives = False, .SHA1 = Nothing})
+                End Try
+            Else
+                '有 Natives
+                If Library("natives")("windows") IsNot Nothing Then
+                    Try
+                        If Library("downloads") IsNot Nothing AndAlso Library("downloads")("classifiers") IsNot Nothing AndAlso Library("downloads")("classifiers")("natives-windows") IsNot Nothing Then
+                            BasicArray.Add(New McLibToken With {
+                                                       .IsJumpLoader = IsJumpLoader,
+                                                       .OriginalName = Library("name"),
+                                                       .Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
+                                                       .LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
+                                                           McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").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),
+                                                       .IsNatives = True,
+                                                       .SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
+                        Else
+                            BasicArray.Add(New McLibToken With {.IsJumpLoader = IsJumpLoader, .OriginalName = Library("name"), .Url = RootUrl, .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")), .Size = 0, .IsNatives = True, .SHA1 = Nothing})
+                        End If
+                    Catch ex As Exception
+                        Log(ex, "处理实际支持库列表失败(有 Natives," & If(Library("name"), "Nothing").ToString & ")")
+                        BasicArray.Add(New McLibToken With {.IsJumpLoader = IsJumpLoader, .OriginalName = Library("name"), .Url = RootUrl, .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")), .Size = 0, .IsNatives = True, .SHA1 = Nothing})
+                    End Try
+                End If
+            End If
+
+        Next
+
+        '去重
+        Dim ResultArray As New Dictionary(Of String, McLibToken)
+        For i = 0 To BasicArray.Count - 1
+            Dim Key As String = BasicArray(i).Name & BasicArray(i).IsNatives.ToString & BasicArray(i).IsJumpLoader.ToString
+            If ResultArray.ContainsKey(Key) Then
+                If BasicArray(i).Version <> ResultArray(Key).Version AndAlso
+                    (KeepSameNameDifferentVersionResult OrElse BasicArray(i).Version.Contains("natives-windows")) Then
+                    'Contains("natives-windows") 源于 1.19-Pre1 开始,lwjgl-3.3.1-natives-windows 与 lwjgl-3.3.1-natives-windows-x86 重复
+                    ResultArray.Add(Key & GetUuid(), BasicArray(i))
+                ElseIf VersionSortBoolean(BasicArray(i).Version, ResultArray(Key).Version) Then
+                    ResultArray(Key) = BasicArray(i)
+                End If
+            Else
+                ResultArray.Add(Key, BasicArray(i))
+            End If
+        Next i
+        Return ResultArray.Values.ToList
+    End Function
+
+    ''' <summary>
+    ''' 获取版本缺失的支持库文件所对应的 NetTaskFile。
+    ''' </summary>
+    Public Function McLibFix(Version As McVersion, Optional CoreJarOnly As Boolean = False) As List(Of NetFile)
+        If Not Version.IsLoaded Then Version.Load() '确保例如 JumpLoader 等项被合并入 Json
+        Dim Result As New List(Of NetFile)
+
+        '更新此方法时需要同步更新 Forge 新版自动安装方法!
+
+        '主 Jar 文件
+        Try
+            Dim MainJar As NetFile = DlClientJarGet(Version, True)
+            If MainJar IsNot Nothing Then Result.Add(MainJar)
+        Catch ex As Exception
+            Log(ex, "版本缺失主 Jar 文件所必须的信息", LogLevel.Developer)
+        End Try
+        If CoreJarOnly Then Return Result
+
+        '是否跳过校验
+        Dim IsSetupSkip As Boolean = Setup.Get("LaunchAdvanceAssets")
+        Select Case Setup.Get("VersionAdvanceAssets", Version:=Version)
+            Case 0 '使用全局设置
+            Case 1 '开启校验
+                IsSetupSkip = False
+            Case 2 '关闭校验
+                IsSetupSkip = True
+        End Select
+
+        'Library 文件
+        Result.AddRange(McLibFixFromLibToken(McLibListGet(Version, False), JumpLoaderFolder:=Version.PathIndie & ".jumploader\", AllowUnsameFile:=IsSetupSkip))
+
+        '统一通行证文件
+        If Setup.Get("VersionServerLogin", Version:=Version) = 3 Then
+            Dim TargetFile = Version.PathIndie & "nide8auth.jar"
+            Dim Checker As New FileChecker(MinSize:=173000)
+            If (IsSetupSkip AndAlso File.Exists(TargetFile)) OrElse Checker.Check(TargetFile) IsNot Nothing Then
+                Result.Add(New NetFile({"https://login.mc-user.com:233/index/jar"}, TargetFile, Checker))
+            End If
+        End If
+        'Authlib-Injector 文件
+        If Setup.Get("VersionServerLogin", Version:=Version) = 4 Then
+            Dim TargetFile = Version.PathIndie & "authlib-injector.jar"
+            Dim DownloadInfo As JObject = Nothing
+            '获取下载信息
+            Try
+                Log("[Minecraft] 开始获取 Authlib-Injector 下载信息")
+                DownloadInfo = GetJson(NetGetCodeByDownload({"https://download.mcbbs.net/mirrors/authlib-injector/artifact/latest.json", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json"}, IsJson:=True))
+            Catch ex As Exception
+                Log("获取 Authlib-Injector 下载信息失败", LogLevel.Hint)
+            End Try
+            '校验文件
+            If DownloadInfo IsNot Nothing Then
+                Dim Checker As New FileChecker(Hash:=DownloadInfo("checksums")("sha256").ToString)
+                If (IsSetupSkip AndAlso File.Exists(TargetFile)) OrElse Checker.Check(TargetFile) IsNot Nothing Then
+                    '开始下载
+                    Dim DownloadAddress As String = DownloadInfo("download_url")
+                    Log("[Minecraft] Authlib-Injector 需要更新:" & DownloadAddress)
+                    Result.Add(New NetFile({
+                            DownloadAddress.Replace("bmclapi2.bangbang93.com", "download.mcbbs.net"), DownloadAddress
+                        }, TargetFile, New FileChecker(Hash:=DownloadInfo("checksums")("sha256").ToString)))
+                End If
+            End If
+        End If
+
+        Return Result
+    End Function
+    ''' <summary>
+    ''' 将 McLibToken 列表转换为 NetFile。无需下载的文件会被自动过滤。
+    ''' </summary>
+    Public Function McLibFixFromLibToken(Libs As List(Of McLibToken), Optional CustomMcFolder As String = Nothing, Optional JumpLoaderFolder As String = Nothing, Optional AllowUnsameFile As Boolean = False) As List(Of NetFile)
+        CustomMcFolder = If(CustomMcFolder, PathMcFolder)
+        Dim Result As New List(Of NetFile)
+        '获取
+        For Each Token As McLibToken In Libs
+            '检查文件
+            Dim Checker As FileChecker
+            If AllowUnsameFile Then '只要文件存在则通过检查,用于放宽完整性校验的情况
+                Checker = New FileChecker(MinSize:=1)
+            Else
+                Checker = New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1)
+            End If
+            If Checker.Check(Token.LocalPath) Is Nothing Then Continue For
+            '文件不符合,添加下载
+            Dim Urls As New List(Of String)
+            If Token.Url IsNot Nothing Then
+                '获取 Url 的真实地址
+                Dim Url As String = Token.Url
+                Url = Url.Replace("https://files.minecraftforge.net", "http://files.minecraftforge.net") '这个鬼东西不让 https
+                Urls.Add(Url)
+                If Token.Url.Contains("launcher.mojang.com/v1/objects") Then Urls = DlSourceLauncherOrMetaGet(Token.Url).ToList() 'Mappings
+                If Token.Url.Contains("maven") Then Urls.Insert(0, Token.Url.Replace(Mid(Token.Url, 1, Token.Url.IndexOf("maven")), "https://download.mcbbs.net/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven"))
+            End If
+            If Token.LocalPath.Contains("transformer-discovery-service") Then
+                'Transformer 文件释放
+                If Not File.Exists(Token.LocalPath) Then WriteFile(Token.LocalPath, GetResources("Transformer"))
+                Log("[Download] 已自动释放 Transformer Discovery Service", LogLevel.Developer)
+                Continue For
+            ElseIf Token.LocalPath.Contains("optifine\OptiFine") Then
+                'OptiFine 主 Jar
+                Dim OptiFineBase As String = Token.LocalPath.Replace(If(Token.IsJumpLoader, JumpLoaderFolder, CustomMcFolder) & "libraries\optifine\OptiFine\", "").Split("_")(0) & "/" & GetFileNameFromPath(Token.LocalPath).Replace("-", "_")
+                OptiFineBase = "/maven/com/optifine/" & OptiFineBase
+                If OptiFineBase.Contains("_pre") Then OptiFineBase = OptiFineBase.Replace("com/optifine/", "com/optifine/preview_")
+                Urls.Add("http://download.mcbbs.net" & OptiFineBase)
+                Urls.Add("http://bmclapi2.bangbang93.com" & OptiFineBase)
+            ElseIf Urls.Count <= 2 Then
+                '普通文件
+                Urls.AddRange(DlSourceLibraryGet("https://libraries.minecraft.net" & Token.LocalPath.Replace(If(Token.IsJumpLoader, JumpLoaderFolder, CustomMcFolder) & "libraries", "").Replace("\", "/")))
+            End If
+            Result.Add(New NetFile(ArrayNoDouble(Urls.ToArray()), Token.LocalPath, Checker))
+        Next
+        '去重并返回
+        Return ArrayNoDouble(Result, Function(Left As NetFile, Right As NetFile)
+                                         Return Left.LocalPath = Right.LocalPath
+                                     End Function)
+    End Function
+    ''' <summary>
+    ''' 获取对应的支持库文件地址。
+    ''' </summary>
+    ''' <param name="Original">原始地址,如 com.mumfrey:liteloader:1.12.2-SNAPSHOT。</param>
+    ''' <param name="WithHead">是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。</param>
+    Public Function McLibGet(Original As String, Optional WithHead As Boolean = True, Optional IgnoreLiteLoader As Boolean = False, Optional CustomMcFolder As String = Nothing) As String
+        CustomMcFolder = If(CustomMcFolder, PathMcFolder)
+        Dim Splited = Original.Split(":")
+        'If Original.ToLower.Contains("xray") OrElse Original.ToLower.Contains("rift") OrElse Original.ToLower.Contains("mixin") OrElse Original.Contains("net.minecraftforge:forge") OrElse Splited(2).Contains("beta") OrElse (IgnoreLiteLoader AndAlso Original.Contains("liteloader")) Then
+        'ElseIf Splited(2).Contains("-") Then
+        '    Log("[Launch] 可能存在分段问题的支持库:" & Original)
+        'End If
+        McLibGet = If(WithHead, CustomMcFolder & "libraries\", "") &
+                   Splited(0).Replace(".", "\") & "\" & Splited(1) & "\" & Splited(2) & "\" & Splited(1) & "-" & Splited(2) & ".jar"
+        '判断 OptiFine 是否应该使用 installer
+        If McLibGet.Contains("optifine\OptiFine\1.12") AndAlso '仅在 1.12 OptiFine 可重现
+           File.Exists(CustomMcFolder & "libraries\" & Splited(0).Replace(".", "\") & "\" & Splited(1) & "\" & Splited(2) & "\" & Splited(1) & "-" & Splited(2) & "-installer.jar") Then
+            Log("[Launch] 已将 " & Original & " 特判替换为对应的 Installer 文件", LogLevel.Debug)
+            McLibGet = McLibGet.Replace(".jar", "-installer.jar")
+        End If
+    End Function
+
+#End Region
+
+#Region "资源文件(Assets)"
+
+    '获取索引
+    ''' <summary>
+    ''' 获取某版本资源文件索引的对应 Json 项,详见版本 Json 中的 assetIndex 项。失败会抛出异常。
+    ''' </summary>
+    Public Function McAssetsGetIndex(Version As McVersion, Optional ReturnLegacyOnError As Boolean = False, Optional CheckURLEmpty As Boolean = False) As JToken
+        Dim AssetsName As String
+        Try
+            Do While True
+                Dim Index As JToken = Version.JsonObject("assetIndex")
+                If Index IsNot Nothing AndAlso Index("id") IsNot Nothing Then Return Index
+                If Version.JsonObject("assets") IsNot Nothing Then AssetsName = Version.JsonObject("assets").ToString
+                If CheckURLEmpty AndAlso Index("url") IsNot Nothing Then Return Index
+                '下一个版本
+                If Version.InheritVersion = "" Then Exit Do
+                Version = New McVersion(PathMcFolder & "versions\" & Version.InheritVersion)
+            Loop
+        Catch ex As Exception
+            Log(ex, "获取资源文件索引下载地址失败")
+        End Try
+        '无法获取到下载地址
+        If ReturnLegacyOnError Then
+            '返回 assets 文件名会由于没有下载地址导致全局失败
+            'If AssetsName IsNot Nothing AndAlso AssetsName <> "legacy" Then
+            '    Log("[Minecraft] 无法获取资源文件索引下载地址,使用 assets 项提供的资源文件名:" & AssetsName)
+            '    Return GetJson("{""id"": """ & AssetsName & """}")
+            'Else
+            Log("[Minecraft] 无法获取资源文件索引下载地址,使用默认的 legacy 下载地址")
+            Return GetJson("
+                    {
+                        ""id"": ""legacy"",
+                        ""sha1"": ""c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729"",
+                        ""size"": 134284,
+                        ""url"": ""https://launchermeta.mojang.com/mc-staging/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"",
+                        ""totalSize"": 111220701
+                    }")
+            'End If
+        Else
+            Throw New Exception("该版本不存在资源文件索引信息")
+        End If
+    End Function
+    ''' <summary>
+    ''' 获取某版本资源文件索引名,优先使用 assetIndex,其次使用 assets。失败会返回 legacy。
+    ''' </summary>
+    Public Function McAssetsGetIndexName(Version As McVersion) As String
+        Try
+            Do While True
+                If Version.JsonObject("assetIndex") IsNot Nothing AndAlso Version.JsonObject("assetIndex")("id") IsNot Nothing Then
+                    Return Version.JsonObject("assetIndex")("id").ToString
+                End If
+                If Version.JsonObject("assets") IsNot Nothing Then
+                    Return Version.JsonObject("assets").ToString
+                End If
+                If Version.InheritVersion = "" Then Exit Do
+                Version = New McVersion(PathMcFolder & "versions\" & Version.InheritVersion)
+            Loop
+        Catch ex As Exception
+            Log(ex, "获取资源文件索引名失败")
+        End Try
+        Return "legacy"
+    End Function
+
+    '获取列表
+    Private Structure McAssetsToken
+        ''' <summary>
+        ''' 文件的完整本地路径。
+        ''' </summary>
+        Public LocalPath As String
+        ''' <summary>
+        ''' Json 中书写的源路径。例如 minecraft/sounds/mob/stray/death2.ogg 。
+        ''' </summary>
+        Public SourcePath As String
+        ''' <summary>
+        ''' 文件大小。若无有效数据即为 0。
+        ''' </summary>
+        Public Size As Long
+        ''' <summary>
+        ''' 是否为 Virtual 资源文件。
+        ''' </summary>
+        Public IsVirtual As Boolean
+        ''' <summary>
+        ''' 文件的 Hash 校验码。
+        ''' </summary>
+        Public Hash As String
+
+        Public Overrides Function ToString() As String
+            Return If(IsVirtual, "[Virtual] ", "") & GetString(Size) & " | " & LocalPath
+        End Function
+    End Structure
+    ''' <summary>
+    ''' 获取 Minecraft 的资源文件列表。失败会抛出异常。
+    ''' </summary>
+    ''' <param name="Name">版本的资源名称,如“1.13.1”。</param>
+    Private Function McAssetsListGet(Name As String) As List(Of McAssetsToken)
+        Try
+
+            '初始化
+            If Not File.Exists(PathMcFolder & "assets\indexes\" & Name & ".json") Then Throw New FileNotFoundException("Assets 索引文件未找到", PathMcFolder & "assets\indexes\" & Name & ".json")
+            McAssetsListGet = New List(Of McAssetsToken)
+            Dim Json = GetJson(ReadFile(PathMcFolder & "assets\indexes\" & Name & ".json"))
+
+            '确认 Virtual 与 Map 状态
+            Dim IsVirtual As Boolean = False
+            If Json("virtual") IsNot Nothing Then IsVirtual = Json("virtual").ToString
+            Dim IsMap As Boolean = False
+            If Json("map_to_resources") IsNot Nothing Then IsMap = Json("map_to_resources").ToString
+
+            '加载列表
+            If IsVirtual OrElse IsMap Then
+                For Each File As JProperty In Json("objects").Children
+                    McAssetsListGet.Add(New McAssetsToken With {
+                                        .IsVirtual = True,
+                                        .LocalPath = PathMcFolder & "assets\virtual\legacy\" & File.Name.Replace("/", "\"),
+                                        .SourcePath = File.Name,
+                                        .Hash = File.Value("hash").ToString,
+                                        .Size = File.Value("size").ToString
+                                    })
+                Next
+            Else
+                For Each File As JProperty In Json("objects").Children
+                    McAssetsListGet.Add(New McAssetsToken With {
+                                        .IsVirtual = False,
+                                        .LocalPath = PathMcFolder & "assets\objects\" & Left(File.Value("hash").ToString, 2) & "\" & File.Value("hash").ToString,
+                                        .SourcePath = File.Name,
+                                        .Hash = File.Value("hash").ToString,
+                                        .Size = File.Value("size").ToString
+                                    })
+                Next
+            End If
+
+        Catch ex As Exception
+            Log(ex, "获取资源文件列表失败:" & Name)
+            Throw
+        End Try
+    End Function
+
+    '获取缺失列表
+    ''' <summary>
+    ''' 获取版本缺失的资源文件所对应的 NetTaskFile。
+    ''' </summary>
+    Public Function McAssetsFixList(IndexAddress As String, CheckHash As Boolean, Optional ByRef ProgressFeed As LoaderBase = Nothing) As List(Of NetFile)
+        Dim Result As New List(Of NetFile)
+
+        Dim AssetsList As List(Of McAssetsToken)
+        Try
+            AssetsList = McAssetsListGet(IndexAddress)
+            Dim Token As McAssetsToken
+            If ProgressFeed IsNot Nothing Then ProgressFeed.Progress = 0.04
+            For i = 0 To AssetsList.Count - 1
+                '初始化
+                Token = AssetsList(i)
+                If ProgressFeed IsNot Nothing Then ProgressFeed.Progress = 0.05 + 0.94 * i / AssetsList.Count
+                '检查文件是否存在
+                Dim File As New FileInfo(Token.LocalPath)
+                If File.Exists AndAlso (Token.Size = 0 OrElse Token.Size = File.Length) AndAlso
+                    (Not CheckHash OrElse Token.Hash Is Nothing OrElse Token.Hash = GetAuthSHA1(Token.LocalPath)) Then Continue For
+                '文件不存在,添加下载
+                Result.Add(New NetFile(DlSourceResourceGet("http://resources.download.minecraft.net/" & Left(Token.Hash, 2) & "/" & Token.Hash), Token.LocalPath, New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.Hash)))
+            Next
+        Catch ex As Exception
+            Log(ex, "获取版本缺失的资源文件下载列表失败")
+        End Try
+        If ProgressFeed IsNot Nothing Then ProgressFeed.Progress = 0.99
+
+        Return Result
+    End Function
+
+#End Region
+
+#Region "Mod"
+
+    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 的完整文件名。
+        ''' </summary>
+        Public ReadOnly Property FileName As String
+            Get
+                Return GetFileNameFromPath(Path)
+            End Get
+        End Property
+
+        ''' <summary>
+        ''' Mod 的状态。
+        ''' </summary>
+        Public ReadOnly Property State As McModState
+            Get
+                Load()
+                If Not IsFileAvailable Then
+                    Return McModState.Unavaliable
+                ElseIf Path.EndsWith(".disabled") Then
+                    Return McModState.Disabled
+                Else
+                    Return McModState.Fine
+                End If
+            End Get
+        End Property
+        Public Enum McModState As Integer
+            Fine = 0
+            Disabled = 1
+            Unavaliable = 2
+        End Enum
+
+#End Region
+
+#Region "信息项"
+
+        ''' <summary>
+        ''' Mod 的名称。若不可用则为 ModID 或无扩展的文件名。
+        ''' </summary>
+        Public Property Name As String
+            Get
+                If _Name Is Nothing Then Load()
+                If _Name Is Nothing Then _Name = _ModId
+                If _Name Is Nothing Then _Name = GetFileNameWithoutExtentionFromPath(Path)
+                Return _Name
+            End Get
+            Set(value As String)
+                If _Name Is Nothing AndAlso value IsNot Nothing AndAlso
+                   value.ToLower <> "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 的描述信息。
+        ''' </summary>
+        Public Property Description As String
+            Get
+                If _Description Is Nothing Then Load()
+                If _Description Is Nothing AndAlso FileUnavailableReason IsNot Nothing Then _Description = FileUnavailableReason.Message
+                'If _Description Is Nothing Then _Description = Path
+                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.ToLower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
+                End If
+            End Set
+        End Property
+        Private _Description As String = Nothing
+
+        ''' <summary>
+        ''' Mod 的版本,不保证符合版本格式规范。
+        ''' </summary>
+        Public Property Version As String
+            Get
+                If _Version Is Nothing Then Load()
+                Return _Version
+            End Get
+            Set(value As String)
+                If _Version IsNot Nothing AndAlso (_Version.Contains(".") OrElse _Version.Contains("-")) Then Exit Property
+                If value IsNot Nothing AndAlso value.ToLower.Contains("version") Then value = "version" '需要修改的标识
+                _Version = value
+            End Set
+        End Property
+        Private _Version As String = Nothing
+
+        ''' <summary>
+        ''' 用于依赖检查的 ModID。
+        ''' </summary>
+        Public Property ModId As String
+            Get
+                If _ModId Is Nothing Then Load()
+                Return _ModId
+            End Get
+            Set(value As String)
+                If value Is Nothing Then Exit Property
+                value = RegexSeek(value.ToLower, "[0-9a-z_]+")
+                If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
+                    If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
+                    If _ModId Is Nothing Then _ModId = value
+                End If
+            End Set
+        End Property
+        Private _ModId As String = Nothing
+        ''' <summary>
+        ''' 其他可能的 ModID。
+        ''' </summary>
+        Public PossibleModId As New List(Of String)
+
+        ''' <summary>
+        ''' Mod 的主页。
+        ''' </summary>
+        Public Property Url As String
+            Get
+                If _Url Is Nothing Then Load()
+                Return _Url
+            End Get
+            Set(value As String)
+                If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWith("http") Then
+                    _Url = value
+                End If
+            End Set
+        End Property
+        Private _Url As String = Nothing
+
+        ''' <summary>
+        ''' Mod 的作者列表。
+        ''' </summary>
+        Public Property Authors As String
+            Get
+                If _Authors Is Nothing Then Load()
+                Return _Authors
+            End Get
+            Set(value As String)
+                If _Authors Is Nothing AndAlso Not String.IsNullOrWhiteSpace(value) Then
+                    _Authors = value
+                End If
+            End Set
+        End Property
+        Private _Authors As String = Nothing
+
+        ''' <summary>
+        ''' 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。
+        ''' </summary>
+        Public ReadOnly Property Dependencies As Dictionary(Of String, String)
+            Get
+                Load()
+                Return _Dependencies
+            End Get
+        End Property
+        Private _Dependencies As New Dictionary(Of String, String)
+        Private Sub AddDependency(ModID As String, Optional VersionRequirement As String = Nothing)
+            '确保信息正确
+            If ModID Is Nothing OrElse ModID.Count < 2 Then Exit Sub
+            ModID = ModID.ToLower
+            If ModID = "name" OrElse Val(ModID).ToString = ModID Then Exit Sub '跳过 name 与纯数字 id
+            If VersionRequirement Is Nothing OrElse ((Not VersionRequirement.Contains(".")) AndAlso (Not VersionRequirement.Contains("-"))) OrElse VersionRequirement.Contains("$") Then
+                VersionRequirement = Nothing
+            Else
+                If (Not VersionRequirement.StartsWith("[")) AndAlso (Not VersionRequirement.StartsWith("(")) AndAlso (Not VersionRequirement.EndsWith("]")) AndAlso (Not VersionRequirement.EndsWith(")")) Then VersionRequirement = "[" & VersionRequirement & ",)"
+            End If
+            '向依赖项中添加
+            If _Dependencies.ContainsKey(ModID) Then
+                If _Dependencies(ModID) Is Nothing Then _Dependencies(ModID) = VersionRequirement
+            Else
+                _Dependencies.Add(ModID, VersionRequirement)
+            End If
+        End Sub
+
+#End Region
+
+#Region "加载步骤标记"
+
+        '1. 进行文件可用性检查
+        '   成功:继续第二步。
+        '   失败:标记 FileUnavailableReason, 并停止后续加载。
+        ''' <summary>
+        ''' 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步)
+        ''' </summary>
+        Private IsLoaded As Boolean = False
+        ''' <summary>
+        ''' Mod 文件是否可被正常读取。
+        ''' </summary>
+        Public ReadOnly Property IsFileAvailable As Boolean
+            Get
+                Load()
+                Return FileUnavailableReason Is Nothing
+            End Get
+        End Property
+        ''' <summary>
+        ''' Mod 文件出错的原因。若无错误,则为 Nothing。
+        ''' </summary>
+        Public ReadOnly Property FileUnavailableReason As Exception
+            Get
+                Load()
+                Return _FileUnavailableReason
+            End Get
+        End Property
+        Private _FileUnavailableReason As Exception = Nothing
+
+        '2. 进行 .class 以外的信息获取
+        '   成功:标记 IsInfoWithoutClassAvailable。
+        '   失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。
+        ''' <summary>
+        ''' 是否已在不获取 .class 文件的前提下完成了所需信息的加载。
+        ''' </summary>
+        Private IsInfoWithoutClassAvailable As Boolean = False
+
+        '3. 尝试从 .class 文件中获取信息
+        '   成功:标记 IsInfoWithClassAvailable。
+        '   失败:什么也不干。
+        ''' <summary>
+        ''' 是否已进行 .class 文件的信息获取。
+        ''' </summary>
+        Private IsInfoWithClassLoaded As Boolean = False
+        ''' <summary>
+        ''' 是否已在 .class 文件中完成了所需信息的加载。
+        ''' </summary>
+        Private IsInfoWithClassAvailable As Boolean = False
+
+#End Region
+
+#Region "加载"
+
+        ''' <summary>
+        ''' 初始化所有数据。
+        ''' </summary>
+        Private Sub Init()
+            _Name = Nothing
+            _Description = Nothing
+            _Version = Nothing
+            _ModId = Nothing
+            PossibleModId = New List(Of String)
+            _Dependencies = New Dictionary(Of String, String)
+            IsLoaded = False
+            _FileUnavailableReason = Nothing
+            IsInfoWithoutClassAvailable = False
+            IsInfoWithClassLoaded = False
+            IsInfoWithClassAvailable = False
+        End Sub
+
+        ''' <summary>
+        ''' 进行文件可用性检查与 .class 以外的信息获取。
+        ''' </summary>
+        Public Sub Load(Optional ForceReload As Boolean = False)
+            If IsLoaded AndAlso Not ForceReload Then Exit Sub
+            '初始化
+            Init()
+            '阶段 1:基础可用性检查
+            Dim Jar As ZipArchive
+            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))
+                If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空")
+            Catch ex As UnauthorizedAccessException
+                Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
+                _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL2", ex)
+                Jar = Nothing
+            Catch ex As Exception
+                Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
+                _FileUnavailableReason = ex
+                Jar = Nothing
+            End Try
+            '阶段 2:信息获取
+            If Jar IsNot Nothing Then LoadWithoutClass(Jar)
+            '阶段 3: Class 信息获取
+            If Jar IsNot Nothing AndAlso Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar)
+            '释放文件
+            Try
+                If Jar IsNot Nothing Then Jar.Dispose()
+            Catch
+            End Try
+            '完成标记
+            IsLoaded = True
+        End Sub
+
+        ''' <summary>
+        ''' 进行不使用 .class 文件的信息获取。
+        ''' </summary>
+        Private Sub LoadWithoutClass(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 信息项
+                Name = InfoObject("name")
+                Description = InfoObject("description")
+                Version = InfoObject("version")
+                Url = InfoObject("url")
+                ModId = InfoObject("modid")
+                Dim AuthorJson As JArray = InfoObject("authorList")
+                If AuthorJson IsNot Nothing Then
+                    Dim Author As New List(Of String)
+                    For Each Token In AuthorJson
+                        Author.Add(Token.ToString)
+                    Next
+                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                End If
+                Dim Reqs As JArray = InfoObject("requiredMods")
+                If Reqs IsNot Nothing Then
+                    For Each Token As String In Reqs
+                        If Not String.IsNullOrEmpty(Token) Then
+                            Token = Token.Substring(Token.IndexOf(":") + 1)
+                            If Token.Contains("@") Then
+                                AddDependency(Token.Split("@")(0), Token.Split("@")(1))
+                            Else
+                                AddDependency(Token)
+                            End If
+                        End If
+                    Next
+                End If
+                Reqs = InfoObject("dependancies")
+                If Reqs IsNot Nothing Then
+                    For Each Token As String In Reqs
+                        If Not String.IsNullOrEmpty(Token) Then
+                            Token = Token.Substring(Token.IndexOf(":") + 1)
+                            If Token.Contains("@") Then
+                                AddDependency(Token.Split("@")(0), Token.Split("@")(1))
+                            Else
+                                AddDependency(Token)
+                            End If
+                        End If
+                    Next
+                End If
+            Catch ex As Exception
+                Log(ex, "读取 mcmod.info 时出现未知错误(" & 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.StartsWith("#") Then '去除注释
+                        Continue For
+                    ElseIf Line.Contains("#") Then
+                        Line = Line.Substring(0, Line.IndexOf("#"))
+                    End If
+                    Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
+                    If Line.Count > 0 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.StartsWith("[[") AndAlso Line.EndsWith("]]") Then
+                        '段落标记
+                        Dim Header = Line.Replace("[", "").Replace("]", "")
+                        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.IndexOf("=")).TrimEnd(New Char() {" "c, "	"c, " "c})
+                        Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, "	"c, " "c})
+                        Dim Value As Object
+                        If RawValue.StartsWith("""") AndAlso RawValue.EndsWith("""") Then
+                            '单行字符串
+                            Value = RawValue.Trim("""")
+                        ElseIf RawValue.StartsWith("'''") Then
+                            '多行字符串
+                            Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
+                            Do Until i >= Lines.Count - 1
+                                i += 1
+                                Dim ValueLine As String = Lines(i)
+                                If ValueLine.EndsWith("'''") Then
+                                    ValueLines.Add(ValueLine.TrimEnd("'"))
+                                    Exit Do
+                                Else
+                                    ValueLines.Add(ValueLine)
+                                End If
+                            Loop
+                            Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
+                        ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then
+                            '布尔型
+                            Value = (RawValue.ToLower = "true")
+                        ElseIf Val(RawValue).ToString = RawValue Then
+                            '数字型
+                            Value = Val(RawValue)
+                        Else
+                            '不知道是个啥玩意儿,直接存储
+                            Value = RawValue
+                        End If
+                        DictionaryAdd(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
+                ModId = ModEntry("modId")
+                If _ModId Is Nothing Then Exit Try '设置了无效的 ModID
+                If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName")
+                If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
+                If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
+                If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL")
+                If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors")
+                For Each TomlSubData In TomlData
+                    If TomlSubData.Key.StartsWith("dependencies") Then
+                        Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value
+                        If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso
+                           DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then
+                            AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing))
+                        End If
+                    End If
+                Next
+                '加载成功
+                GoTo Finished
+            Catch ex As Exception
+                Log(ex, "读取 mods.toml 时出现未知错误(" & 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 Name = FabricObject("name")
+                If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
+                If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
+                If FabricObject.ContainsKey("id") Then ModId = FabricObject("id")
+                If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "")
+                Dim AuthorJson As JArray = FabricObject("authors")
+                If AuthorJson IsNot Nothing Then
+                    Dim Author As New List(Of String)
+                    For Each Token In AuthorJson
+                        Author.Add(Token.ToString)
+                    Next
+                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                End If
+                'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
+                '    '添加 Minecraft 依赖
+                '    Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "")
+                '    If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
+                '    '添加其他依赖
+                '    Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "")
+                '    If Deps <> "" Then
+                '        For Each Dep In Deps.Split(";")
+                '            If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
+                '            Dep = Dep.Substring(Dep.IndexOf(":") + 1)
+                '            If Dep.Contains("@") Then
+                '                AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
+                '            Else
+                '                AddDependency(Dep)
+                '            End If
+                '        Next
+                '    End If
+                'End If
+                '加载成功
+                GoTo Finished
+            Catch ex As Exception
+                Log(ex, "读取 fabric.mod.json 时出现未知错误(" & 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("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then
+                    '要求使用 mcmod.info 中的信息
+                    Dim value As String = FmlObject("modid")("value")
+                    If value Is Nothing Then Exit Try
+                    value = RegexSeek(value.ToLower, "[0-9a-z_]+")
+                    If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
+                        If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
+                    End If
+                    Exit Try
+                End If
+                If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value")
+                If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
+                If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value")
+                If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
+                    '添加 Minecraft 依赖
+                    Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "")
+                    If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
+                    '添加其他依赖
+                    Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "")
+                    If Deps <> "" Then
+                        For Each Dep In Deps.Split(";")
+                            If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
+                            Dep = Dep.Substring(Dep.IndexOf(":") + 1)
+                            If Dep.Contains("@") Then
+                                AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
+                            Else
+                                AddDependency(Dep)
+                            End If
+                        Next
+                    End If
+                End If
+            Catch ex As Exception
+                Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
+            End Try
+#End Region
+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.IndexOf("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
+
+            IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
+        End Sub
+
+        ''' <summary>
+        ''' 进行使用 .class 文件的信息获取。
+        ''' </summary>
+        Private Sub LoadWithClass(Jar As ZipArchive)
+            Try
+                '查找入口点文件
+                Dim ModClass As String = Nothing
+                Dim ModClassNotBest As String = Nothing '非完美匹配
+                For Each Entry In Jar.Entries
+                    If Entry.Name.EndsWith(".class") Then
+                        Dim Temp As String = ReadFile(Entry.Open()).ToLower
+                        If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then
+                            ModClass = Temp
+                            Exit For
+                        ElseIf Temp.Contains("modid") Then
+                            ModClassNotBest = Temp
+                        End If
+                    End If
+                Next
+                If ModClass Is Nothing Then ModClass = ModClassNotBest
+                If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点")
+                ModClass = ModClass.Replace("ljava/lang/string;", "")
+                If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte
+                Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer
+                '获取 ModID
+                If _ModId Is Nothing Then
+                    IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1
+                    IncreaseCount = 0
+                    Do While Convert.ToInt32(ModClass(IndexHead)) < 32
+                        IndexHead += 1
+                        IncreaseCount += 1
+                        If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败")
+                    Loop
+                    IndexTail = IndexHead + 1
+                    IncreaseCount = 0
+                    Do Until Convert.ToInt32(ModClass(IndexTail)) < 32
+                        IndexTail += 1
+                        IncreaseCount += 1
+                        If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败")
+                    Loop
+                    ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead)
+                End If
+                '获取 Version
+                If _Version Is Nothing AndAlso ModClass.Contains("version") Then
+                    IndexHead = ModClass.IndexOf("version") + "version".Length + 1
+                    IncreaseCount = 0
+                    Do While Convert.ToInt32(ModClass(IndexHead)) < 32
+                        IndexHead += 1
+                        IncreaseCount += 1
+                        If IncreaseCount > 10 Then GoTo VersionFindFail
+                    Loop
+                    IndexTail = IndexHead + 1
+                    IncreaseCount = 0
+                    Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse
+                         (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse
+                         (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c)
+                        IndexTail += 1
+                        IncreaseCount += 1
+                        If IncreaseCount > 50 Then GoTo VersionFindFail
+                    Loop
+                    _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead)
+                End If
+VersionFindFail:
+                '获取 Dependencies
+                IndexHead = ModClass.IndexOf("dependencies")
+                If IndexHead > 0 Then
+                    If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299)
+                    Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?")
+                    For Each Token As String In Deps
+                        If Not String.IsNullOrEmpty(Token) Then
+                            If Token.Contains("@") Then
+                                AddDependency(Token.Split("@")(0), Token.Split("@")(1))
+                            Else
+                                AddDependency(Token)
+                            End If
+                        End If
+                    Next
+                End If
+            Catch ex As Exception
+                Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal)
+            End Try
+            IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
+        End Sub
+
+#End Region
+
+        ''' <summary>
+        ''' 是否可能为前置 Mod。
+        ''' </summary>
+        Public Function IsPresetMod() As Boolean
+            Return Dependencies.Count = 0 AndAlso (Name IsNot Nothing) AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.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.ToLower
+            If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse
+               Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") 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 FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False)
+
+            '获取 Mod 文件夹下的可用文件列表
+            Dim ModFileList As New List(Of FileInfo)
+            If Directory.Exists(Loader.Input) Then
+                For Each File As FileInfo In New DirectoryInfo(Loader.Input).EnumerateFiles("*.*", SearchOption.AllDirectories)
+                    Dim DirName As String = File.DirectoryName.ToLower
+                    If DirName.StartsWith(Loader.Input & "memory_repo") OrElse DirName.StartsWith(".") OrElse
+                       ((DirName & "\") <> Loader.Input AndAlso DirName.Contains("voxel")) Then Continue For
+                    If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
+                Next
+            End If
+
+            '确定是否显示进度
+            Loader.Progress = 0.03
+            If ModFileList.Count > 100 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True)
+
+            '加载为 Mod 列表
+            Dim ModList As New List(Of McMod)
+            For Each ModFile As FileInfo In ModFileList
+                Dim ModEntity As New McMod(ModFile.FullName)
+                ModEntity.Load()
+                ModList.Add(ModEntity)
+                Loader.Progress += 0.9 / ModFileList.Count
+            Next
+            Loader.Progress = 0.93
+
+            '排序
+            ModList = Sort(ModList, Function(Left As McMod, Right As McMod) As Boolean
+                                        If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then
+                                            Return Left.State = McMod.McModState.Unavaliable
+                                        Else
+                                            Return Left.FileName < Right.FileName
+                                        End If
+                                    End Function)
+            Loader.Progress = 0.98
+
+            '回设
+            If Loader.IsAborted Then Exit Sub
+            Loader.Output = ModList
+
+        Catch ex As Exception
+            Log(ex, "Mod 列表加载失败", LogLevel.Debug)
+            Throw
+        End Try
+    End Sub
+
+#If DEBUG Then
+    ''' <summary>
+    ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。
+    ''' </summary>
+    Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String)
+        Dim Result As New List(Of String)
+        '令所有 Mod 进行基础检查,并归纳需要检查的 Mod
+        Dim CurrentModList As New List(Of McMod)
+        For Each ModEntity In Mods
+            If Not ModEntity.IsFileAvailable Then
+                Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path)
+                Continue For
+            End If
+            If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity)
+        Next
+        '添加默认依赖
+        Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path}
+        If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"})
+        CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"})
+        '检查重复的 Mod,并添加对应的依赖
+        For Each ModEntity In CurrentModList
+            For Each PossibleModId In ModEntity.PossibleModId
+                If CurrentDependencies.ContainsKey(PossibleModId) Then
+                    If CurrentDependencies(PossibleModId)(2) = 1 Then
+                        Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf &
+                                       " - " & ModEntity.FileName & vbCrLf &
+                                       " - " & CurrentDependencies(PossibleModId)(1))
+                    Else
+                        Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf &
+                                       " - " & ModEntity.FileName & vbCrLf &
+                                       " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
+                    End If
+                Else
+                    CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count})
+                End If
+            Next
+        Next
+        '检查依赖
+        For Each ModEntity In CurrentModList
+            Try
+                For Each Dependency In ModEntity.Dependencies
+                    Dim ReqId As String = Dependency.Key
+                    If ReqId.Count < 2 Then Continue For '确保正常
+                    If ReqId = ModEntity.ModId Then Continue For '跳过自体引用
+                    If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用
+                    If Dependency.Value IsNot Nothing Then
+                        '获取分段后的详细版本信息
+                        Dim ReqVersion As String = Dependency.Value
+                        Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[")
+                        Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]")
+                        Dim ReqVersionHead As String
+                        Dim ReqVersionTail As String
+                        If ReqVersion.Contains(",") Then
+                            ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray())
+                            ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray())
+                        Else
+                            ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray())
+                            ReqVersionTail = ReqVersionHead
+                            If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then
+                                ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1)
+                                ReqVersionTailCanEqual = False
+                            End If
+                        End If
+                        If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1)
+                        If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1)
+                        '获取报错描述文本
+                        Dim VersionRequire As String
+                        If ReqVersionHead = ReqVersionTail Then
+                            VersionRequire = "应为 " & ReqVersionHead
+                        ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then
+                            VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail
+                        ElseIf ReqVersionHead.Contains(".") Then
+                            If ReqVersionHeadCanEqual Then
+                                VersionRequire = "最低应为 " & ReqVersionHead
+                            Else
+                                VersionRequire = "应高于 " & ReqVersionHead
+                            End If
+                        ElseIf ReqVersionTail.Contains(".") Then
+                            If ReqVersionTailCanEqual Then
+                                VersionRequire = "最高应为 " & ReqVersionHead
+                            Else
+                                VersionRequire = "应低于 " & ReqVersionHead
+                            End If
+                        Else
+                            VersionRequire = ""
+                        End If
+                        '检查前置 Mod 是否存在,并获取其版本
+                        If Not CurrentDependencies.ContainsKey(ReqId) Then
+                            Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName)
+                            Continue For
+                        End If
+                        Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0")
+                        If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1)
+                        '对比前置 Mod 头部版本
+                        If ReqVersionHead.Contains(".") Then
+                            If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then
+                                Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
+                                           " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
+                                Continue For
+                            End If
+                        End If
+                        '对比前置 Mod 尾部版本
+                        If ReqVersionTail.Contains(".") Then
+                            If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then
+                                Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
+                                           " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
+                                Continue For
+                            End If
+                        End If
+                    Else
+                        If Not CurrentDependencies.ContainsKey(Dependency.Key) Then
+                            Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName)
+                            Continue For
+                        End If
+                    End If
+                Next
+            Catch ex As Exception
+                Result.Add("检查 Mod 时出错:" & GetString(ex) & vbCrLf & " - " & ModEntity.FileName)
+                Log(ex, "检查 Mod 时出错")
+            End Try
+        Next
+        If Result.Count = 0 Then
+            Log("[Minecraft] Mod 检查未发现异常")
+        Else
+            Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf))
+        End If
+        Return Result
+    End Function
+#End If
+
+#End Region
+
+    ''' <summary>
+    ''' 发送 Minecraft 更新提示。
+    ''' </summary>
+    Public Sub McDownloadClientUpdateHint(VersionName As String, Json As JObject)
+        Try
+
+            '获取对应版本
+            Dim Version As JToken = Nothing
+            For Each Token In Json("versions")
+                If Token("id") IsNot Nothing AndAlso Token("id").ToString = VersionName Then
+                    Version = Token
+                    Exit For
+                End If
+            Next
+            '进行提示
+            If Version Is Nothing Then Exit Sub
+            Dim Time As Date = Version("releaseTime")
+            Dim MsgResult As Integer
+            If (Date.Now - Time).TotalDays > 1 Then
+                '1 天以上
+                MsgResult = MyMsgBox("新版本:" & VersionName & vbCrLf & "更新时间:" & Time.ToString, "Minecraft 更新提示", "下载", "更新日志", "取消")
+            ElseIf (Date.Now - Time).TotalHours > 3 Then
+                '1 天 ~ 3 小时
+                MsgResult = MyMsgBox("新版本:" & VersionName & vbCrLf & "更新于:" & GetTimeSpanString(Time - Date.Now), "Minecraft 更新提示", "下载", "更新日志", "取消")
+            Else
+                '不到 3 小时
+                MsgResult = MyMsgBox("新版本:" & VersionName & vbCrLf & "更新于:" & GetTimeSpanString(Time - Date.Now), "Minecraft 更新提示", "下载", "取消")
+                If MsgResult = 2 Then MsgResult = 3 '把 “取消” 移位
+            End If
+            '弹窗结果
+            Select Case MsgResult
+                Case 1
+                    '下载
+                    McDownloadClient(NetPreDownloadBehaviour.HintWhileExists, VersionName, Version("url").ToString)
+                Case 2
+                    '更新日志
+                    McUpdateLogShow(Version)
+            End Select
+
+        Catch ex As Exception
+            Log(ex, "Minecraft 更新提示发送失败(" & If(VersionName, "Nothing") & ")", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 比较两个版本名的排序,若 Left 较新则返回 True。无法比较两个 Pre 的大小。
+    ''' 支持的格式范例:未知版本, 1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6
+    ''' </summary>
+    Public Function VersionSortBoolean(Left As String, Right As String) As Boolean
+        Return VersionSortInteger(Left, Right) >= 0
+    End Function
+    ''' <summary>
+    ''' 比较两个版本名的排序,若 Left 较新则返回 1,相同则返回 0,Right 较新则返回 -1。
+    ''' 支持的格式范例:未知版本, 1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6
+    ''' </summary>
+    Public Function VersionSortInteger(Left As String, Right As String) As Integer
+        If Left = "未知版本" OrElse Right = "未知版本" Then
+            If Left = "未知版本" AndAlso Right <> "未知版本" Then Return 1
+            If Left = "未知版本" AndAlso Right = "未知版本" Then Return 0
+            If Left <> "未知版本" AndAlso Right = "未知版本" Then Return -1
+        End If
+        Dim Lefts = RegexSearch(Left.ToLower, "[a-z]+|[0-9]+")
+        Dim Rights = RegexSearch(Right.ToLower, "[a-z]+|[0-9]+")
+        Dim i As Integer = 0
+        While True
+            '两边均缺失,感觉是一个东西
+            If Lefts.Count - 1 < i AndAlso Rights.Count - 1 < i Then
+                If Left > Right Then
+                    Return 1
+                ElseIf Left < Right Then
+                    Return -1
+                Else
+                    Return 0
+                End If
+            End If
+            '确定两边的数值
+            Dim LeftValue As String = If(Lefts.Count - 1 < i, "-1", Lefts(i))
+            Dim RightValue As String = If(Rights.Count - 1 < i, "-1", Rights(i))
+            If LeftValue = RightValue Then GoTo NextEntry
+            If LeftValue = "pre" OrElse LeftValue = "snapshot" Then LeftValue = "-3"
+            If LeftValue = "rc" Then LeftValue = "-2"
+            If LeftValue = "experimental" Then LeftValue = "-4"
+            Dim LeftValValue = Val(LeftValue)
+            If RightValue = "pre" OrElse RightValue = "snapshot" Then RightValue = "-3"
+            If RightValue = "rc" Then RightValue = "-2"
+            If RightValue = "experimental" Then RightValue = "-4"
+            Dim RightValValue = Val(RightValue)
+            If LeftValValue = 0 AndAlso RightValValue = 0 Then
+                '如果没有数值则直接比较字符串
+                If LeftValue > RightValue Then
+                    Return 1
+                ElseIf LeftValue < RightValue Then
+                    Return -1
+                End If
+            Else
+                '如果有数值则比较数值
+                '这会使得一边是数字一边是字母时数字方更大
+                If LeftValValue > RightValValue Then
+                    Return 1
+                ElseIf LeftValValue < RightValValue Then
+                    Return -1
+                End If
+            End If
+NextEntry:
+            i += 1
+        End While
+        Return 0
+    End Function
+    ''' <summary>
+    ''' 比较两个版本名的排序器。
+    ''' </summary>
+    Public Class VersionSorter
+        Implements IComparer(Of String)
+        Public IsDecreased As Boolean = True
+        Public Function Compare(x As String, y As String) As Integer Implements IComparer(Of String).Compare
+            Return VersionSortInteger(x, y) * If(IsDecreased, -1, 1)
+        End Function
+        Public Sub New(Optional IsDecreased As Boolean = True)
+            Me.IsDecreased = IsDecreased
+        End Sub
+    End Class
+
+    ''' <summary>
+    ''' 为邮箱地址或手机号账号进行部分打码。
+    ''' </summary>
+    Public Function AccountFilter(Account As String) As String
+        If Account.Contains("@") Then
+            '是邮箱
+            Dim Splits = Account.Split("@")
+            'If Splits(0).Count >= 6 Then
+            '    '前半部分至少 6 位,屏蔽后 4 位
+            '    Return Mid(Splits(0), 1, Splits(0).Count - 4) & "****" & "@" & Splits(1)
+            'Else
+            '前半部分不到 6 位,返回全 *
+            Return "".PadLeft(Splits(0).Count, "*") & "@" & Splits(1)
+            'End If
+        ElseIf Account.Count >= 6 Then
+            '至少 6 位,屏蔽后 4 位
+            Return Mid(Account, 1, Account.Count - 4) & "****"
+        Else
+            '不到 6 位,返回全 *
+            Return "".PadLeft(Account.Count, "*")
+        End If
+    End Function
+    ''' <summary>
+    ''' 对 PCL 约定的替换标记进行处理。
+    ''' </summary>
+    Public Function ArgumentReplace(Raw As String) As String
+        If Raw Is Nothing Then Return Nothing
+        Raw = Raw.Replace("{user}", If(IsNothing(McLoginLoader.Output), "尚未登录", McLoginLoader.Output.Name))
+        If IsNothing(McLoginLoader.Input) Then
+            Raw = Raw.Replace("{login}", "尚未登录")
+        Else
+            Select Case McLoginLoader.Input.Type
+                Case McLoginType.Legacy
+                    Raw = Raw.Replace("{login}", "离线")
+                Case McLoginType.Mojang
+                    Raw = Raw.Replace("{login}", "Mojang 正版")
+                Case McLoginType.Ms
+                    Raw = Raw.Replace("{login}", "微软正版")
+                Case McLoginType.Nide
+                    Raw = Raw.Replace("{login}", "统一通行证")
+                Case McLoginType.Auth
+                    Raw = Raw.Replace("{login}", "Authlib-Injector")
+            End Select
+        End If
+        If McVersionCurrent Is Nothing Then
+            Raw = Raw.Replace("{name}", "无可用版本")
+            Raw = Raw.Replace("{version}", "无可用版本")
+        Else
+            Raw = Raw.Replace("{name}", McVersionCurrent.Name)
+            If Not McVersionCurrent.IsLoaded Then McVersionCurrent.Load()
+            If {"unknown", "old", "pending"}.Contains(McVersionCurrent.Version.McName.ToLower) Then
+                Raw = Raw.Replace("{version}", McVersionCurrent.Name)
+            Else
+                Raw = Raw.Replace("{version}", McVersionCurrent.Version.McName)
+            End If
+        End If
+        Raw = Raw.Replace("{path}", Path)
+        Return Raw
+    End Function
+
+End Module

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません