龙腾猫跃 1 an în urmă
părinte
comite
503244a0de
62 a modificat fișierele cu 2434 adăugiri și 1724 ștergeri
  1. 29 28
      Plain Craft Launcher 2/App.config
  2. 6 0
      Plain Craft Launcher 2/Application.xaml
  3. 1 1
      Plain Craft Launcher 2/Controls/MyButton.xaml
  4. 14 6
      Plain Craft Launcher 2/Controls/MyButton.xaml.vb
  5. 23 14
      Plain Craft Launcher 2/Controls/MyCard.vb
  6. 1 1
      Plain Craft Launcher 2/Controls/MyCheckBox.xaml
  7. 74 61
      Plain Craft Launcher 2/Controls/MyCheckBox.xaml.vb
  8. 33 0
      Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml
  9. 158 0
      Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.vb
  10. 1 1
      Plain Craft Launcher 2/Controls/MyHint.xaml
  11. 9 1
      Plain Craft Launcher 2/Controls/MyHint.xaml.vb
  12. 12 4
      Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb
  13. 1 1
      Plain Craft Launcher 2/Controls/MyImage.vb
  14. 16 24
      Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
  15. 16 7
      Plain Craft Launcher 2/Controls/MyPageRight.vb
  16. 1 1
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml
  17. 82 74
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb
  18. 10 1
      Plain Craft Launcher 2/Controls/MyRadioButton.xaml.vb
  19. 1 1
      Plain Craft Launcher 2/FormMain.xaml
  20. 62 149
      Plain Craft Launcher 2/FormMain.xaml.vb
  21. 204 41
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  22. 8 5
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  23. 41 12
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  24. 6 10
      Plain Craft Launcher 2/Modules/Base/ModValidate.vb
  25. 16 6
      Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
  26. 4 18
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  27. 9 9
      Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
  28. 3 3
      Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
  29. 45 42
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  30. 62 45
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  31. 50 399
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  32. 18 8
      Plain Craft Launcher 2/Modules/ModEvent.vb
  33. 48 0
      Plain Craft Launcher 2/Modules/ModMain.vb
  34. 11 4
      Plain Craft Launcher 2/Modules/ModSecret.vb
  35. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  36. 25 40
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  37. 3 4
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompDetail.xaml.vb
  38. 5 3
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  39. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  40. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  41. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  42. 1 0
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb
  43. 186 111
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  44. 19 15
      Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb
  45. 3 0
      Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb
  46. 6 6
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.vb
  47. 0 5
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb
  48. 2 2
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  49. 4 4
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  50. 257 107
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml
  51. 670 297
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb
  52. 7 2
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml
  53. 15 1
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml.vb
  54. 2 4
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb
  55. 4 4
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb
  56. 5 5
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml.vb
  57. 10 3
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  58. 5 5
      Plain Craft Launcher 2/Resources/Custom.xaml
  59. BIN
      Plain Craft Launcher 2/Resources/Help.zip
  60. 118 118
      Plain Craft Launcher 2/Resources/ModData.txt
  61. 6 5
      Plain Craft Launcher 2/app.manifest
  62. BIN
      最新正式版.zip

+ 29 - 28
Plain Craft Launcher 2/App.config

@@ -1,34 +1,35 @@
 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
-  <system.net>
-    <settings>
-      <httpWebRequest useUnsafeHeaderParsing="true"/>
-    </settings>
-  </system.net>
+    <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" />
-	<AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/> <!-- #5337 -->
-    <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>
+    <runtime>
+		<AppContextSwitchOverrides value="Switch.System.Net.DontEnableSchUseStrongCrypto=false" />
+        <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/> <!-- #5337 -->
+		<AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=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>

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

@@ -13,6 +13,12 @@
                 <ResourceDictionary Source="pack://application:,,,/Resources/Language/zh_CN.xaml" />
                 <ResourceDictionary Source="pack://application:,,,/Resources/Language/zh_CN.xaml" />
             </ResourceDictionary.MergedDictionaries>
+            
+            <!-- Converter -->
+            <local:AdditionConverter x:Key="AdditionConverter"/>
+            <local:MultiplicationConverter x:Key="MultiplicationConverter"/>
+            <local:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter"/>
+            <local:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
 
             <!-- 颜色表 -->
             <SolidColorBrush x:Key="ColorBrush1">#343d4a</SolidColorBrush>

+ 1 - 1
Plain Craft Launcher 2/Controls/MyButton.xaml

@@ -7,7 +7,7 @@
         <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 TextOptions.TextFormattingMode="Display" SnapsToDevicePixels="True" TextTrimming="None" 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>

+ 14 - 6
Plain Craft Launcher 2/Controls/MyButton.xaml.vb

@@ -1,22 +1,30 @@
-Public Class MyButton
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyButton
 
     '声明
     Public Event Click(sender As Object, e As MouseButtonEventArgs) '自定义事件
 
     '自定义属性
     Public Uuid As Integer = GetUuid()
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
-            Return LabText.Text
+            Return GetValue(TextProperty)
         End Get
         Set(value As String)
-            LabText.Text = value
+            SetValue(TextProperty, 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)))
+    Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+        If sender IsNot Nothing Then CType(sender, MyButton).LabText.Text = e.NewValue
+    End Sub)))
     Public Property TextPadding As Thickness
         Get
             Return LabText.Padding

+ 23 - 14
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -36,6 +36,11 @@
 
     '属性
     Public Uuid As Integer = GetUuid()
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return MainTextBlock.Inlines
+        End Get
+    End Property
     Public Property Title As String
         Get
             Return GetValue(TitleProperty)
@@ -62,7 +67,7 @@
         If IsLoad Then Exit Sub
         IsLoad = True
         '初次加载限定
-        If Title <> "" AndAlso MainTextBlock Is Nothing Then
+        If MainTextBlock Is Nothing Then
             MainTextBlock = New TextBlock With {.HorizontalAlignment = HorizontalAlignment.Left, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(15, 12, 0, 0), .FontWeight = FontWeights.Bold, .FontSize = 13, .IsHitTestVisible = False}
             MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
             MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
@@ -222,20 +227,21 @@
             '高度增加较大,使用弹起动画
             Dim Delta As Double = MathClamp(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))
-                })
+                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, MathClamp(Math.Abs(DeltaHeight) * 4, 150, 250),, New AniEaseOutFluent)
-                    })
+                AaHeight(Me, DeltaHeight, MathClamp(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))
+        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
@@ -253,7 +259,8 @@
 
 #End Region
 
-    '折叠
+#Region "折叠"
+
     '若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠
     '这是因为不能直接在 XAML 中设置 SwapControl
     Public SwapControl As Object
@@ -294,7 +301,7 @@
     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
-            (SwapControl Is Nothing OrElse Pos > SwapedHeight OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Exit Sub '检测点击位置;或已经不在可视树上的误判
+            (SwapControl Is Nothing OrElse Pos > If(IsSwaped, SwapedHeight, SwapedHeight - 6) 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
@@ -303,7 +310,7 @@
 
         Dim Pos As Double = Mouse.GetPosition(Me).Y
         If Not IsSwaped AndAlso
-            (SwapControl Is Nothing OrElse Pos > SwapedHeight OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Exit Sub '检测点击位置;或已经不在可视树上的误判
+            (SwapControl Is Nothing OrElse Pos > If(IsSwaped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Exit Sub '检测点击位置;或已经不在可视树上的误判
 
         Dim ee = New RouteEventArgs(True)
         RaiseEvent PreviewSwap(Me, ee)
@@ -320,6 +327,8 @@
         IsMouseDown = False
     End Sub
 
+#End Region
+
 End Class
 Partial Public Module ModAnimation
     Public Sub AniDispose(Control As MyCard, RemoveFromChildren As Boolean, Optional CallBack As ParameterizedThreadStart = Nothing)

+ 1 - 1
Plain Craft Launcher 2/Controls/MyCheckBox.xaml

@@ -4,7 +4,7 @@
              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" />
+    <TextBlock IsHitTestVisible="False" FontSize="13" x:FieldModifier="Public" x:Name="LabText" Padding="0" Margin="26,0,0,0" 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>

+ 74 - 61
Plain Craft Launcher 2/Controls/MyCheckBox.xaml.vb

@@ -1,4 +1,7 @@
-Public Class MyCheckBox
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyCheckBox
 
     '基础
 
@@ -14,29 +17,32 @@
     End Sub '使外部程序引发本控件的 Change 事件
 
     '自定义属性
-
-    Private _Checked As Boolean = False '是否选中
     Public Property Checked As Boolean
         Get
-            Return _Checked
+            Return GetValue(CheckedProperty)
         End Get
         Set(value As Boolean)
-            SetChecked(value, False, True)
+            SetChecked(value, False)
         End Set
     End Property
+    Public Shared ReadOnly CheckedProperty As DependencyProperty =
+        DependencyProperty.Register("Checked", GetType(Boolean), GetType(MyCheckBox), New PropertyMetadata(False,
+        Sub(d As MyCheckBox, e As DependencyPropertyChangedEventArgs)
+            '在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变
+            If Not d.IsLoaded Then d.SyncUI()
+        End Sub))
+
     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)
+    Public Sub SetChecked(value As Boolean, user As Boolean)
         Try
-            If value = _Checked Then Exit Sub
+            If value = Checked Then Exit Sub
 
             'Preview 事件
-
             If value AndAlso user Then
                 Dim e = New RouteEventArgs(user)
                 RaiseEvent PreviewChange(Me, e)
@@ -48,60 +54,67 @@
                 End If
             End If
 
-            _Checked = value
+            SetValue(CheckedProperty, 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
-
+            SyncUI()
         Catch ex As Exception
             Log(ex, "设置 Checked 失败")
         End Try
     End Sub
+    Private Sub SyncUI()
+        If AniControlEnabled = 0 AndAlso IsLoaded 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
+    End Sub
+
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
             Return GetValue(TextProperty)
@@ -110,10 +123,10 @@
             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)))
+    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)))
 
     '点击事件
 
@@ -123,7 +136,7 @@
         If Not MouseDowned Then Exit Sub
         Log("[Control] 按下复选框(" & (Not Checked).ToString & "):" & Text)
         MouseDowned = False
-        SetChecked(Not Checked, True, True)
+        SetChecked(Not Checked, True)
         AniStart(AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), "MyCheckBox Background " & Uuid)
     End Sub
     Private Sub Checkbox_MouseDown() Handles Me.MouseLeftButtonDown

+ 33 - 0
Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml

@@ -0,0 +1,33 @@
+<Grid
+    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" 
+    x:Name="PanBack" mc:Ignorable="d" x:Class="MyExtraTextButton"
+    RenderTransformOrigin="0.5,0.5" ToolTipService.Placement="Left" ToolTipService.VerticalOffset="16" ToolTipService.HorizontalOffset="-8" Height="52">
+    <Grid.RenderTransform>
+        <ScaleTransform ScaleX="0" ScaleY="0" />
+    </Grid.RenderTransform>
+    <Border x:Name="PanClick" BorderThickness="0.001" Background="{StaticResource ColorBrushSemiTransparent}" 
+            CornerRadius="{Binding ActualHeight, ConverterParameter=0.4, Converter={StaticResource MultiplicationConverter}, ElementName=PanBack, Mode=OneWay}"/>
+    <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" BorderThickness="0.001" Background="{DynamicResource ColorBrush3}"
+                CornerRadius="{Binding ActualHeight, ConverterParameter=0.4, Converter={StaticResource MultiplicationConverter}, ElementName=PanBack, Mode=OneWay}">
+            <Border.Effect>
+                <DropShadowEffect ShadowDepth="0" BlurRadius="10" Opacity="0.2" />
+            </Border.Effect>
+        </Border>
+        <StackPanel Orientation="Horizontal" Margin="24,0" SnapsToDevicePixels="False" UseLayoutRounding="False">
+            <Path x:Name="Path" Stretch="Uniform" Margin="0,12" Fill="{DynamicResource ColorBrush8}" RenderTransformOrigin="0.5,0.5">
+                <Path.RenderTransform>
+                    <ScaleTransform />
+                </Path.RenderTransform>
+            </Path>
+            <TextBlock x:Name="LabText" VerticalAlignment="Center" Foreground="{DynamicResource ColorBrush8}" FontSize="16" 
+                       Margin="11,0,-2,1.2" />
+        </StackPanel>
+    </Grid>
+</Grid>

+ 158 - 0
Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.vb

@@ -0,0 +1,158 @@
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyExtraTextButton
+
+    '声明
+    Public Event Click(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 Path IsNot Nothing Then Path.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale}
+        End Set
+    End Property
+    '显示文本
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
+    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(MyExtraTextButton), New PropertyMetadata(New PropertyChangedCallback(
+    Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+        If sender IsNot Nothing Then CType(sender, MyExtraTextButton).LabText.Text = e.NewValue
+    End Sub)))
+
+    '动画
+    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
+                    '有了
+                    Opacity = 0
+                    AniStart({
+                        AaOpacity(Me, 1 - Opacity, 80, 50),
+                        AaScaleTransform(Me, 0.15 - CType(RenderTransform, ScaleTransform).ScaleX, 400, 50, New AniEaseOutBack),
+                        AaScaleTransform(Me, 0.85, 160, 50, New AniEaseOutFluent(AniEasePower.Middle))
+                    }, "MyExtraTextButton MainScale " & Uuid)
+                Else
+                    '没了
+                    AniStart({
+                        AaOpacity(Me, -Opacity, 50, 50),
+                        AaScaleTransform(Me, -CType(RenderTransform, ScaleTransform).ScaleX, 100,, New AniEaseInFluent(AniEasePower.Weak))
+                    }, "MyExtraTextButton MainScale " & Uuid)
+                End If
+                IsHitTestVisible = value '防止缩放动画中依然可以点进去
+            End Sub)
+        End Set
+    End Property
+
+    '触发点击事件
+    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
+
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsLeftMouseHeld As Boolean = False
+    Private Sub Button_LeftMouseDown(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseLeftButtonDown
+        If Not IsLeftMouseHeld Then
+            AniStart({
+                     AaScaleTransform(PanScale, 0.85 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 800,, New AniEaseOutFluent(AniEasePower.Strong)),
+                     AaScaleTransform(PanScale, -0.05, 60,, New AniEaseOutFluent)
+                     }, "MyExtraTextButton Scale " & Uuid)
+        End If
+        IsLeftMouseHeld = True
+        Focus()
+    End Sub
+    Private Sub Button_LeftMouseUp() Handles PanClick.MouseLeftButtonUp
+        AniStart({
+                     AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 300,, New AniEaseOutBack)
+                 }, "MyExtraTextButton Scale " & Uuid)
+        IsLeftMouseHeld = False
+        RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+    Private Sub Button_RightMouseUp() Handles PanClick.MouseRightButtonUp
+        If Not IsLeftMouseHeld Then
+            AniStart({
+                         AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 300,, New AniEaseOutBack)
+                     }, "MyExtraTextButton Scale " & Uuid)
+        End If
+        RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
+    End Sub
+    Private Sub Button_MouseLeave() Handles PanClick.MouseLeave
+        IsLeftMouseHeld = False
+        AniStart({
+                     AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 500,, New AniEaseOutFluent)
+                 }, "MyExtraTextButton 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), "MyExtraTextButton Color " & Uuid)
+                Else
+                    '普通
+                    AniStart(AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut), "MyExtraTextButton Color " & Uuid)
+                End If
+
+            Else
+
+                AniStop("MyExtraTextButton Color " & Uuid)
+                If IsMouseOver Then
+                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4")
+                Else
+                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3")
+                End If
+
+            End If
+        Catch ex As Exception
+            Log(ex, "刷新图标按钮颜色出错")
+        End Try
+    End Sub
+
+End Class

+ 1 - 1
Plain Craft Launcher 2/Controls/MyHint.xaml

@@ -17,7 +17,7 @@
             <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" />
+        <TextBlock IsHitTestVisible="False" Grid.Column="1" x:Name="LabText" Margin="0,0.5,0,0" Padding="6,6,8,6" 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>

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

@@ -1,4 +1,7 @@
-Public Class MyHint
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyHint
 
     Public Uuid As Integer = GetUuid()
 
@@ -30,6 +33,11 @@
         End Set
     End Property
 
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
             Return LabText.Text

+ 12 - 4
Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb

@@ -1,4 +1,7 @@
-Public Class MyIconTextButton
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyIconTextButton
 
     '基础
 
@@ -30,6 +33,11 @@
         End Set
     End Property
 
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
             Return GetValue(TextProperty)
@@ -39,9 +47,9 @@
         End Set
     End Property '内容
     Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(New PropertyChangedCallback(
-                                                                                                                                                               Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
-                                                                                                                                                                   If Not IsNothing(sender) Then CType(sender, MyIconTextButton).LabText.Text = e.NewValue
-                                                                                                                                                               End Sub)))
+    Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
+        If Not IsNothing(sender) Then CType(sender, MyIconTextButton).LabText.Text = e.NewValue
+    End Sub)))
     Public Enum ColorState
         Black
         Highlight

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

@@ -138,7 +138,7 @@ RetryStart:
                 ElseIf EnableCache Then
                     '保存缓存并显示
                     If File.Exists(TempPath) Then File.Delete(TempPath)
-                    Rename(TempDownloadingPath, TempPath)
+                    FileSystem.Rename(TempDownloadingPath, TempPath)
                     RunInUi(Sub() ActualSource = TempPath)
                 Else
                     '直接显示

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

@@ -1,4 +1,7 @@
-Public Class MyListItem
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyListItem
     Implements IMyRadio
 
     Public Event Click(sender As Object, e As MouseButtonEventArgs)
@@ -161,6 +164,11 @@
     End Property
 
     '标题
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabTitle.Inlines
+        End Get
+    End Property
     Public Property Title As String
         Get
             Return GetValue(TitleProperty)
@@ -337,16 +345,7 @@
             Return _Checked
         End Get
         Set(value As Boolean)
-            SetChecked(value, False, True, _Half)
-        End Set
-    End Property
-    Private _Half As Boolean = False
-    Public Property Half As Boolean
-        Get
-            Return _Half
-        End Get
-        Set(value As Boolean)
-            SetChecked(_Checked, False, True, value)
+            SetChecked(value, False, True)
         End Set
     End Property
     ''' <summary>
@@ -355,8 +354,7 @@
     ''' <param name="value">新的 Checked 属性。</param>
     ''' <param name="user">是否由用户引发。</param>
     ''' <param name="anime">是否执行动画。</param>
-    ''' <param name="half">是否为部分选中。</param>
-    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean, Optional half As Boolean = False)
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
         Try
 
             '自定义属性基础
@@ -374,9 +372,8 @@
                 End If
                 _Checked = value
             Else
-                If value = _Checked AndAlso half = _Half Then Exit Sub
+                If value = _Checked Then Exit Sub
                 _Checked = value
-                _Half = half
                 If IsInitialized Then
                     RaiseEvent Changed(Me, ChangedEventArgs)
                     If ChangedEventArgs.Handled Then
@@ -440,9 +437,9 @@
                     '由无变有
                     If Not IsNothing(RectCheck) Then
                         Dim Delta = ActualHeight - RectCheck.ActualHeight - 12
-                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.2, 0.4), 200,, New AniEaseOutFluent(AniEasePower.Weak)))
-                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.3, 0.6), 300,, New AniEaseOutBack(AniEasePower.Weak)))
-                        Anim.Add(AaOpacity(RectCheck, If(half, 0.5, 1) - RectCheck.Opacity, 30))
+                        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
@@ -461,17 +458,12 @@
             Else
                 '不使用动画
                 AniStop("MyListItem Checked " & Uuid)
-
-                If Checked OrElse half Then
+                If Checked Then
                     If Not IsNothing(RectCheck) Then
                         RectCheck.Height = Double.NaN
                         RectCheck.Margin = New Thickness(-1, 6, 0, 6)
                         RectCheck.Opacity = 1
                         RectCheck.VerticalAlignment = VerticalAlignment.Stretch
-                        If half Then
-                            RectCheck.Height = Height * 0.4
-                            RectCheck.Opacity = 0.5
-                        End If
                     End If
                     SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2"))
                 Else

+ 16 - 7
Plain Craft Launcher 2/Controls/MyPageRight.vb

@@ -353,6 +353,10 @@
                 If Control.RenderTransform IsNot Nothing AndAlso TypeOf Control.RenderTransform Is TranslateTransform Then Control.RenderTransform = Nothing
             Next
             For Each Control As FrameworkElement In GetAllAnimControls(Element)
+                If TypeOf Control Is MyExtraTextButton Then
+                    CType(Control, MyExtraTextButton).Show = True
+                    Continue For
+                End If
                 Control.Opacity = 0
                 Control.RenderTransform = New TranslateTransform(0, -16)
                 AniList.Add(AaOpacity(Control, 1, 150, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
@@ -379,6 +383,10 @@
         Dim Delay As Integer = 0
         For Each Element In RealElements
             For Each Control As FrameworkElement In GetAllAnimControls(Element)
+                If TypeOf Control Is MyExtraTextButton Then
+                    CType(Control, MyExtraTextButton).Show = False
+                    Continue For
+                End If
                 Control.IsHitTestVisible = False
                 AniList.Add(AaOpacity(Control, -1, 90, Delay))
                 AniList.Add(AaTranslateY(Control, -6, 90, Delay))
@@ -392,12 +400,13 @@
             AniList.Add(AaTranslateX(Scroll, 10 - CType(Scroll.RenderTransform, TranslateTransform).X, 90, 0, New AniEaseInFluent))
         End If
         '结束
-        AniList.Add(AaCode(Sub()
-                               For Each Element In RealElements
-                                   Element.Visibility = Visibility.Collapsed
-                               Next
-                               PageOnExitAnimationFinished()
-                           End Sub,, True))
+        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
 
@@ -409,7 +418,7 @@
     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 OrElse TypeOf Element Is TextBlock OrElse TypeOf Element Is MyTextButton Then
+        If TypeOf Element Is MyCard OrElse TypeOf Element Is MyHint OrElse TypeOf Element Is MyExtraTextButton OrElse TypeOf Element Is TextBlock OrElse TypeOf Element Is MyTextButton Then
             AllControls.Add(Element)
         ElseIf TypeOf Element Is ContentControl Then
             Dim Content = CType(Element, ContentControl).Content

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

@@ -3,7 +3,7 @@
              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"/>
+    <TextBlock IsHitTestVisible="False" x:Name="LabText" FontSize="13" Padding="0" Margin="26,0,0,0" 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>

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

@@ -1,4 +1,7 @@
-Public Class MyRadioBox
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyRadioBox
     Implements IMyRadio
 
     '基础
@@ -10,27 +13,29 @@
     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
+            Return GetValue(CheckedProperty)
         End Get
         Set(value As Boolean)
-            SetChecked(value, False, True)
+            SetChecked(value, False)
         End Set
     End Property
+    Public Shared ReadOnly CheckedProperty As DependencyProperty =
+        DependencyProperty.Register("Checked", GetType(Boolean), GetType(MyRadioBox), New PropertyMetadata(False,
+        Sub(d As MyRadioBox, e As DependencyPropertyChangedEventArgs)
+            '在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变
+            If Not d.IsLoaded Then d.SyncUI()
+        End Sub))
+
     ''' <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)
+    Public Sub SetChecked(value As Boolean, user As Boolean)
         Try
-
             'Preview 事件
-
             If value AndAlso user Then
                 Dim e = New RouteEventArgs(user)
                 RaiseEvent PreviewCheck(Me, e)
@@ -41,28 +46,24 @@
             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
+            If IsLoaded AndAlso Not value = Checked Then RaiseEvent PreviewChange(Me, New RouteEventArgs(user))
+            If Not value = Checked Then
+                SetValue(CheckedProperty, 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
+            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
+            Select Case CheckedCount '判断选中情况
                 Case 0
                     '没有任何单选框被选中,选择第一个
                     RadioboxList(0).Checked = True
@@ -87,67 +88,74 @@
                         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))
+            If IsChanged Then
+                If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
+                RaiseEvent Changed(Me, New RouteEventArgs(user))
+            End If
 
+            '更改动画
+            SyncUI()
         Catch ex As Exception
             Log(ex, "单选框勾选改变错误", LogLevel.Hint)
         End Try
     End Sub
+    Private Sub SyncUI()
+        If AniControlEnabled = 0 AndAlso IsLoaded 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
+    End Sub
+
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
             Return GetValue(TextProperty)
@@ -165,9 +173,9 @@
     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)
+        SetChecked(True, True)
+        MouseDowned = False
         AniStart(AaColor(ShapeBorder, Ellipse.FillProperty, "ColorBrushHalfWhite", 100), "MyRadioBox Background " & Uuid)
     End Sub
     Private Sub Radiobox_MouseDown() Handles Me.MouseLeftButtonDown

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

@@ -1,4 +1,7 @@
-Public Class MyRadioButton
+Imports System.Windows.Markup
+
+<ContentProperty("Inlines")>
+Public Class MyRadioButton
 
     '基础
 
@@ -108,6 +111,12 @@
             Log(ex, "单选按钮勾选改变错误", LogLevel.Hint)
         End Try
     End Sub
+
+    Public ReadOnly Property Inlines As InlineCollection
+        Get
+            Return LabText.Inlines
+        End Get
+    End Property
     Public Property Text As String
         Get
             Return GetValue(TextProperty)

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

@@ -133,7 +133,7 @@
                     </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" />
+                                             LogoScale="0.87" Logo="M1097 584 250 584 562 896C591 925 591 972 562 1001 533 1030 487 1030 458 1001L21 565C6 550-0 531 0 511L0 511 0 511C-0 492 6 472 21 457L458 21C487-7 533-7 562 21 591 50 591 97 562 126L250 438 1097 438C1137 438 1170 471 1170 511 1170 551 1137 584 1097 584L1097 584Z" />
                         <TextBlock SnapsToDevicePixels="False" IsHitTestVisible="False" Name="LabTitleInner" VerticalAlignment="Center" Margin="47,1,60,0" HorizontalAlignment="Left" Foreground="{DynamicResource ColorBrush8}" FontSize="15" />
                     </Grid>
                 </Grid>

+ 62 - 149
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -10,201 +10,112 @@ Public Class FormMain
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
 #If BETA Then
+        If LastVersion < 352 Then 'Release 2.9.1
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:导出整合包功能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:支持在超长路径下安装、启动游戏"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 1.21 OptiFine"))
+            FeatureCount += 23
+            BugCount += 10
+        End If
+        If LastVersion < 349 Then 'Release 2.8.13
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络环境不佳时,Mod、整合包页面的加载速度"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:部分电脑无法打开程序"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:可能无法识别、更新 Mod"))
+            FeatureCount += 6
+            BugCount += 14
+        End If
         If LastVersion < 347 Then 'Release 2.8.12
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 管理页面添加下载 Mod、安装 Mod 选项"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 详情页面支持按加载器、游戏版本进行分类和筛选"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持安装同时包含 modpack 文件和启动器的懒人包"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化整合包导入流程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:Mod 管理页面添加下载 Mod、安装 Mod 选项"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:Mod 详情页面支持按加载器、游戏版本进行分类和筛选"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:支持安装同时包含 modpack 文件和启动器的懒人包"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:整合包导入流程"))
             FeatureCount += 43
             BugCount += 37
         End If
         If LastVersion < 342 Then 'Release 2.8.9
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "支持下载原版服务端"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "本地 Mod 的标题支持选择显示 Mod 原始文件名"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复搜索后启用/禁用 Mod 时出错的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:支持下载原版服务端"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:本地 Mod 的标题支持选择显示 Mod 原始文件名"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:搜索后启用/禁用 Mod 时出错"))
             FeatureCount += 17
             BugCount += 13
         End If
         If LastVersion < 340 Then 'Release 2.8.8
-            If LastVersion = 338 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复数个与新正版登录相关的严重 Bug"))
+            If LastVersion = 338 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:数个与新正版登录相关的严重 Bug"))
             FeatureCount += 3
             BugCount += 7
         End If
         If LastVersion < 338 Then 'Release 2.8.7
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "使用新的正版登录方式,以提高安全性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化安装整合包、检索 Mod 的稳定性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法加载部分 Mod 的图标的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复在 Mod 管理页面删除 Mod 导致报错的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化:使用新的正版登录方式,以提高安全性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:安装整合包、检索 Mod 的稳定性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法加载部分 Mod 的图标"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:在 Mod 管理页面删除 Mod 导致报错"))
             FeatureCount += 11
             BugCount += 21
         End If
-        If LastVersion < 336 Then 'Release 2.8.6
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "下载 Mod 时会使用 MCIM 国内镜像源"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 管理页面允许筛选可更新/启用/禁用的 Mod"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "打开 PCL 时会自动安装同目录下的 modpack.zip"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "爱发电域名迁移至 afdian.com"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复 1.20.1+ 离线登录使用正版皮肤时无法保存游戏的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装的 1.14~1.15 Forge+OptiFine 无法进入世界的 Bug"))
-            FeatureCount += 19
-            BugCount += 24
-        End If
-        If LastVersion < 332 Then 'Release 2.8.3
-            If LastVersion = 330 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复部分玩家无法启动 MC 的 Bug"))
-        End If
-        If LastVersion < 330 Then 'Release 2.8.2
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "NeoForge 兼容与自动安装"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持编译、运行 PCL 开源代码"))
-            FeatureCount += 15
-            BugCount += 22
-        End If
-        If LastVersion < 326 Then 'Release 2.7.4
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "会自动隐藏明显不可用的自动安装选项"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化正版登录流程和 MC 性能"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复正版登录时弹出脚本错误提示的 Bug"))
-            FeatureCount += 17
-            BugCount += 19
-        End If
-        If LastVersion < 323 Then 'Release 2.7.3
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "添加 启动游戏前进行内存优化 设置"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化 MC 性能"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装 OptiFine 有概率失败的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动 Fabric 1.20.5+ 时无法正确选择 Java 的 Bug"))
-            FeatureCount += 22
-            BugCount += 21
-        End If
-        If LastVersion < 321 Then 'Release 2.7.1
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动部分整合包导致设置丢失的 Bug"))
-            BugCount += 1
-        End If
-        If LastVersion < 319 Then 'Release 2.7.0
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持更新 Mod"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持查看可更新的 Mod 的更新日志"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持滑动鼠标快速选中、取消选中多个 Mod"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法启动 MC 24w14a+ 的 Bug"))
-            FeatureCount += 10
-            BugCount += 10
-        End If
 #Else
         '5:          FEAT+
         '4:     IMP+ FEAT*
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '1:BUG-
-        If LastVersion < 346 Then 'Snapshot 2.8.13
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化网络环境不佳时,Mod、整合包页面的加载速度"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复部分电脑无法打开程序的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复可能无法识别、更新 Mod 的 Bug"))
+        If LastVersion < 351 Then 'Snapshot 2.9.1
+            If LastVersion = 350 Then
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化:对导出整合包功能进行了 7 项优化,详见完整更新日志"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法进行第三方登录"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:在部分版本下,打开导出页面会导致启动器崩溃"))
+                BugCount += 1
+            End If
+            BugCount += 2
+        End If
+        If LastVersion < 350 Then 'Snapshot 2.9.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:导出整合包功能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:支持在超长路径下安装、启动游戏"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 1.21 OptiFine"))
+            FeatureCount += 21
+            BugCount += 10
+        End If
+        If LastVersion < 348 Then 'Snapshot 2.8.13
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络环境不佳时,Mod、整合包页面的加载速度"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:部分电脑无法打开程序"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:可能无法识别、更新 Mod"))
             FeatureCount += 6
             BugCount += 14
         End If
         If LastVersion < 346 Then 'Snapshot 2.8.12
-            If LastVersion = 345 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复帮助页面报错的 Bug"))
+            If LastVersion = 345 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:帮助页面报错"))
         End If
         If LastVersion < 345 Then 'Snapshot 2.8.11
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 管理页面添加下载 Mod、安装 Mod 选项"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 详情页面支持按加载器、游戏版本进行分类和筛选"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:Mod 管理页面添加下载 Mod、安装 Mod 选项"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:Mod 详情页面支持按加载器、游戏版本进行分类和筛选"))
             FeatureCount += 23
             BugCount += 21
         End If
         If LastVersion < 343 Then 'Snapshot 2.8.10
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持安装同时包含 modpack 文件和启动器的懒人包"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化整合包导入流程"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:支持安装同时包含 modpack 文件和启动器的懒人包"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:整合包导入流程"))
             FeatureCount += 20
             BugCount += 16
         End If
         If LastVersion < 341 Then 'Snapshot 2.8.9
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "支持下载原版服务端"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "本地 Mod 的标题支持选择显示 Mod 原始文件名"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复搜索后启用/禁用 Mod 时出错的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:支持下载原版服务端"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:本地 Mod 的标题支持选择显示 Mod 原始文件名"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:搜索后启用/禁用 Mod 时出错"))
             FeatureCount += 17
             BugCount += 13
         End If
         If LastVersion < 339 Then 'Snapshot 2.8.8
-            If LastVersion = 337 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复数个与新正版登录相关的严重 Bug"))
+            If LastVersion = 337 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:数个与新正版登录相关的严重 Bug"))
             FeatureCount += 3
             BugCount += 7
         End If
         If LastVersion < 337 Then 'Snapshot 2.8.7
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "使用新的正版登录方式,以提高安全性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化安装整合包、检索 Mod 的稳定性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法加载部分 Mod 的图标的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复在 Mod 管理页面删除 Mod 导致报错的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "优化:使用新的正版登录方式,以提高安全性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:安装整合包、检索 Mod 的稳定性"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法加载部分 Mod 的图标"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:在 Mod 管理页面删除 Mod 导致报错"))
             FeatureCount += 11
             BugCount += 21
         End If
-        If LastVersion < 335 Then 'Snapshot 2.8.6
-            BugCount += 2
-        End If
-        If LastVersion < 334 Then 'Snapshot 2.8.5
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "Mod 管理页面允许筛选可更新/启用/禁用的 Mod"))
-            If LastVersion = 333 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法安装愚人节和预发布版本的 Bug"))
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法导出错误报告的 Bug"))
-            End If
-            FeatureCount += 6
-            BugCount += 7
-        End If
-        If LastVersion < 333 Then 'Snapshot 2.8.4
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "下载 Mod 时会使用 MCIM 国内镜像源"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "打开 PCL 时会自动安装同目录下的 modpack.zip"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "爱发电域名迁移至 afdian.com"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复 1.20.1+ 离线登录使用正版皮肤时无法保存游戏的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装的 1.14~1.15 Forge+OptiFine 无法进入世界的 Bug"))
-            FeatureCount += 13
-            BugCount += 17
-        End If
-        If LastVersion < 331 Then 'Snapshot 2.8.3
-            If LastVersion = 329 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复部分玩家无法启动 MC 的 Bug"))
-        End If
-        If LastVersion < 329 Then 'Snapshot 2.8.2
-            If LastVersion >= 327 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法安装 Beta 版 NeoForge 的整合包的 Bug"))
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复自动安装无法选择部分 OptiFine 的 Bug"))
-            End If
-            FeatureCount += 4
-            BugCount += 8
-        End If
-        If LastVersion < 328 Then 'Snapshot 2.8.1
-            If LastVersion = 327 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法安装 Forge 1.12.2- 的 Bug"))
-            If LastVersion = 327 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法输入解锁码的 Bug"))
-            If LastVersion = 327 Then BugCount += 1
-        End If
-        If LastVersion < 327 Then 'Snapshot 2.8.0
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "NeoForge 兼容与自动安装"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持编译、运行 PCL 开源代码"))
-            FeatureCount += 11
-            BugCount += 14
-        End If
-        If LastVersion < 325 Then 'Snapshot 2.7.4
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "会自动隐藏明显不可用的自动安装选项"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化正版登录流程和 MC 性能"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复正版登录时弹出脚本错误提示的 Bug"))
-            FeatureCount += 17
-            BugCount += 19
-        End If
-        If LastVersion < 324 Then 'Snapshot 2.7.3
-            FeatureCount += 4
-            BugCount += 3
-        End If
-        If LastVersion < 322 Then 'Snapshot 2.7.2
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "添加 启动游戏前进行内存优化 设置"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装 OptiFine 有概率失败的 Bug"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动 Fabric 1.20.5+ 时无法正确选择 Java 的 Bug"))
-            FeatureCount += 18
-            BugCount += 18
-        End If
-        If LastVersion < 320 Then 'Snapshot 2.7.1
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动部分整合包导致设置丢失的 Bug"))
-            BugCount += 1
-        End If
-        If LastVersion < 318 Then 'Snapshot 2.7.0
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持更新 Mod"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持查看可更新的 Mod 的更新日志"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持滑动鼠标快速选中、取消选中多个 Mod"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复无法启动 MC 24w14a+ 的 Bug"))
-            FeatureCount += 10
-            BugCount += 10
-        End If
 #End If
         '整理更新日志文本
         Dim ContentList As New List(Of String)
@@ -366,6 +277,7 @@ Public Class FormMain
                 DlClientListMojangLoader.Start(1)
                 RunCountSub()
                 ServerLoader.Start(1)
+                RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
             Catch ex As Exception
                 Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
             End Try
@@ -466,7 +378,7 @@ Public Class FormMain
             Log("[Start] 已从老版本迁移微软登录结果")
         End If
         '输出更新日志
-        If LastVersionCode = 0 Then Exit Sub
+        If LastVersionCode <= 0 Then Exit Sub
         If LowerVersionCode >= VersionCode Then Exit Sub
         ShowUpdateLog(LowerVersionCode)
     End Sub
@@ -657,6 +569,7 @@ Public Class FormMain
     End Sub
     Private Sub FormMain_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseDown
         '鼠标侧键返回上一级
+        If FrmMain.PanMsg.Children.Count > 0 OrElse WaitingMyMsgBox.Any Then Exit Sub '弹窗中(#5513)
         If e.ChangedButton = MouseButton.XButton1 OrElse e.ChangedButton = MouseButton.XButton2 Then TriggerPageBack()
     End Sub
     Private Sub TriggerPageBack()

+ 204 - 41
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -12,13 +12,13 @@ Public Module ModBase
 #Region "声明"
 
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.8.13" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.8.13." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.9.1" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.9.1." & VersionBranchCode '标准格式的四段式版本号
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
 #If BETA Then
-    Public Const VersionCode As Integer = 347 'Release
+    Public Const VersionCode As Integer = 352 'Release
 #Else
-    Public Const VersionCode As Integer = 348 'Snapshot
+    Public Const VersionCode As Integer = 351 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -180,10 +180,6 @@ Public Module ModBase
         ''' </summary>
         Public Const IconButtonServer As String = "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"
         ''' <summary>
-        ''' 图标,向右箭头,1x
-        ''' </summary>
-        Public Const IconButtonRight As String = "M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z"
-        ''' <summary>
         ''' 图标,音符,1x
         ''' </summary>
         Public Const IconMusic As String = "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"
@@ -961,17 +957,17 @@ Public Module ModBase
     ''' <param name="FileFilter">要求的格式。如:“常用图片文件(*.png;*.jpg)|*.png;*.jpg”。</param>
     ''' <param name="Title">弹窗的标题。</param>
     ''' <param name="FileName">默认的文件名。</param>
-    Public Function SelectAs(Title As String, FileName As String, Optional FileFilter As String = Nothing, Optional DefaultDir As String = Nothing) As String
+    Public Function SelectSaveFile(Title As String, FileName As String, Optional FileFilter As String = Nothing, Optional InitialDirectory As String = Nothing) As String
         Using fileDialog As New Forms.SaveFileDialog
             fileDialog.AddExtension = True
             fileDialog.AutoUpgradeEnabled = True
             fileDialog.Title = Title
             fileDialog.FileName = FileName
             If FileFilter IsNot Nothing Then fileDialog.Filter = FileFilter
-            If DefaultDir IsNot Nothing Then fileDialog.InitialDirectory = DefaultDir
+            If Not String.IsNullOrEmpty(InitialDirectory) AndAlso Directory.Exists(InitialDirectory) Then fileDialog.InitialDirectory = InitialDirectory
             fileDialog.ShowDialog()
-            SelectAs = If(fileDialog.FileName.Contains(":\"), fileDialog.FileName, "")
-            Log("[UI] 选择文件返回:" & SelectAs)
+            SelectSaveFile = If(fileDialog.FileName.Contains(":\"), fileDialog.FileName, "")
+            Log("[UI] 选择文件返回:" & SelectSaveFile)
         End Using
     End Function
     ''' <summary>
@@ -979,7 +975,7 @@ Public Module ModBase
     ''' </summary>
     ''' <param name="FileFilter">要求的格式。如:“常用图片文件(*.png;*.jpg)|*.png;*.jpg”。</param>
     ''' <param name="Title">弹窗的标题。</param>
-    Public Function SelectFile(FileFilter As String, Title As String) As String
+    Public Function SelectFile(FileFilter As String, Title As String, Optional InitialDirectory As String = Nothing) As String
         Using fileDialog As New Forms.OpenFileDialog
             fileDialog.AddExtension = True
             fileDialog.AutoUpgradeEnabled = True
@@ -988,6 +984,7 @@ Public Module ModBase
             fileDialog.Multiselect = False
             fileDialog.Title = Title
             fileDialog.ValidateNames = True
+            If Not String.IsNullOrEmpty(InitialDirectory) AndAlso Directory.Exists(InitialDirectory) Then fileDialog.InitialDirectory = InitialDirectory
             fileDialog.ShowDialog()
             Log("[UI] 选择单个文件返回:" & fileDialog.FileName)
             Return fileDialog.FileName
@@ -1084,6 +1081,37 @@ Re:
         End Try
     End Function
     ''' <summary>
+    ''' 获取文件 SHA512,若失败则返回空字符串。
+    ''' </summary>
+    Public Function GetFileSHA512(FilePath As String) As String
+        Dim Retry As Boolean = False
+Re:
+        Try
+            ''检测该文件是否在下载中,若在下载则放弃检测
+            'If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
+            '获取 SHA512
+            Dim file As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
+            Dim sha512 As SHA512 = New SHA512CryptoServiceProvider()
+            Dim retval As Byte() = sha512.ComputeHash(file)
+            file.Close()
+            Dim Result As New StringBuilder()
+            For i As Integer = 0 To retval.Length - 1
+                Result.Append(retval(i).ToString("x2"))
+            Next
+            Return Result.ToString
+        Catch ex As Exception
+            If Retry OrElse TypeOf ex Is FileNotFoundException Then
+                Log(ex, "获取文件 SHA512 失败:" & FilePath)
+                Return ""
+            Else
+                Retry = True
+                Log(ex, "获取文件 SHA512 可重试失败:" & FilePath, LogLevel.Normal)
+                Thread.Sleep(RandomInteger(200, 500))
+                GoTo Re
+            End If
+        End Try
+    End Function
+    ''' <summary>
     ''' 获取文件 SHA256,若失败则返回空字符串。
     ''' </summary>
     Public Function GetFileSHA256(FilePath As String) As String
@@ -1094,8 +1122,8 @@ Re:
             'If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
             '获取 SHA256
             Dim file As New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
-            Dim sha1 As SHA256 = New SHA256CryptoServiceProvider()
-            Dim retval As Byte() = sha1.ComputeHash(file)
+            Dim sha256 As SHA256 = New SHA256CryptoServiceProvider()
+            Dim retval As Byte() = sha256.ComputeHash(file)
             file.Close()
             Dim Result As New StringBuilder()
             For i As Integer = 0 To retval.Length - 1
@@ -1144,7 +1172,7 @@ Re:
         End Try
     End Function
     ''' <summary>
-    ''' 获取流 SHA1,若失败则返回空字符串。
+    ''' 获取流的 SHA1,若失败则返回空字符串。
     ''' </summary>
     Public Function GetAuthSHA1(Stream As Stream) As String
         Try
@@ -1336,11 +1364,22 @@ RetryDir:
     ''' 遍历文件夹中的所有文件。
     ''' </summary>
     Public Function EnumerateFiles(Directory As String) As IEnumerable(Of FileInfo)
-        Dim Info As New DirectoryInfo(Directory)
+        Dim Info As New DirectoryInfo(ShortenPath(Directory))
         If Not Info.Exists Then Return New List(Of FileInfo)
         Return Info.EnumerateFiles("*", SearchOption.AllDirectories)
     End Function
 
+    ''' <summary>
+    ''' 若路径长度大于指定值,则将长路径转换为短路径。
+    ''' </summary>
+    Public Function ShortenPath(LongPath As String, Optional ShortenThreshold As Integer = 247) As String
+        If LongPath.Length <= ShortenThreshold Then Return LongPath
+        Dim ShortPath As New StringBuilder(260)
+        GetShortPathName(LongPath, ShortPath, 260)
+        Return ShortPath.ToString
+    End Function
+    Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" (ByVal lpszLongPath As String, ByVal lpszShortPath As StringBuilder, ByVal cchBuffer As Integer) As Integer
+
 #End Region
 
 #Region "文本"
@@ -1350,8 +1389,8 @@ RetryDir:
     ''' <summary>
     ''' 提取 Exception 的具体描述与堆栈。
     ''' </summary>
-    ''' <param name="ShowAllTrace">是否必须显示所有堆栈。通常用于判定堆栈信息。</param>
-    Public Function GetExceptionDetail(Ex As Exception, Optional ShowAllTrace As Boolean = False) As String
+    ''' <param name="ShowAllStacks">是否必须显示所有堆栈。通常用于判定堆栈信息。</param>
+    Public Function GetExceptionDetail(Ex As Exception, Optional ShowAllStacks As Boolean = False) As String
         If Ex Is Nothing Then Return "无可用错误信息!"
 
         '获取最底层的异常
@@ -1362,19 +1401,20 @@ RetryDir:
 
         '获取各级错误的描述与堆栈信息
         Dim DescList As New List(Of String)
-        Dim StackList As New List(Of String)
+        Dim IsInner As Boolean = False
         Do Until Ex Is Nothing
-            DescList.Add(Ex.Message.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr & vbCr, vbCr).Replace(vbCr, vbCrLf))
+            DescList.Add(If(IsInner, "→ ", "") & Ex.Message.Replace(vbLf, vbCr).Replace(vbCr & vbCr, vbCr).Replace(vbCr, vbCrLf))
             If Ex.StackTrace IsNot Nothing Then
-                For Each St As String In Ex.StackTrace.Split(vbCrLf)
-                    If ShowAllTrace OrElse St.ToLower.Contains("pcl") Then StackList.Add(St.Replace(vbCr, String.Empty).Replace(vbLf, String.Empty))
+                For Each Stack As String In Ex.StackTrace.Split(vbCrLf.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
+                    If ShowAllStacks OrElse Stack.ContainsF("pcl", True) Then
+                        DescList.Add(Stack.Replace(vbCr, String.Empty).Replace(vbLf, String.Empty))
+                    End If
                 Next
             End If
+            If Ex.GetType.FullName <> "System.Exception" Then DescList.Add("   错误类型:" & Ex.GetType.FullName)
             Ex = Ex.InnerException
+            IsInner = True
         Loop
-        DescList = DescList.Distinct.ToList
-        Dim Desc As String = Join(DescList, vbCrLf & "→ ")
-        Dim Stack As String = If(StackList.Any, vbCrLf & Join(StackList, vbCrLf), "")
 
         '常见错误(记得同时修改下面的)
         Dim CommonReason As String = Nothing
@@ -1387,20 +1427,15 @@ RetryDir:
         ElseIf TypeOf InnerEx Is Runtime.InteropServices.COMException Then
             CommonReason = "由于操作系统或显卡存在问题,导致出现错误。请尝试重启 PCL。"
         ElseIf {"远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称", "由于目标计算机积极拒绝",
-                "操作已超时", "操作超时", "服务器超时", "连接超时"}.Any(Function(s) Desc.Contains(s)) Then
+                "操作已超时", "操作超时", "服务器超时", "连接超时"}.Any(Function(s) DescList.Any(Function(l) l.Contains(s))) Then
             CommonReason = "你的网络环境不佳,导致难以连接到服务器。请检查网络,多重试几次,或尝试使用 VPN。"
         End If
 
-        '获取错误类型
-        Dim TypeDesc As String = If(InnerEx.GetType.FullName = "System.Exception", "", vbCrLf & "错误类型:" & InnerEx.GetType.FullName)
-
         '构造输出信息
         If CommonReason Is Nothing Then
-            Return Desc & Stack & TypeDesc
+            Return DescList.Join(vbCrLf)
         Else
-            Dim Result As String = CommonReason & vbCrLf & DescList.First & vbCrLf & "————————————" & vbCrLf
-            DescList(0) = "详细错误信息:"
-            Return Result & Join(DescList, vbCrLf & "→ ") & Stack & TypeDesc
+            Return CommonReason & vbCrLf & vbCrLf & "————————————" & vbCrLf & "详细错误信息:" & vbCrLf & DescList.Join(vbCrLf)
         End If
     End Function
     ''' <summary>
@@ -1444,7 +1479,7 @@ RetryDir:
             Return CommonReason & "详细错误:" & DescList.First
         Else
             DescList.Reverse() '让最深层错误在最左边
-            Return Join(DescList, " → ")
+            Return Join(DescList, " ← ")
         End If
     End Function
 
@@ -1722,6 +1757,21 @@ RetryDir:
             Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("'", "&apos;").
             Replace("""", "&quot;").Replace(vbCrLf, "&#xa;")
     End Function
+    ''' <summary>
+    ''' 为字符串进行 Like 关键字转义。
+    ''' </summary>
+    Public Function EscapeLikePattern(input As String) As String
+        Dim sb As New StringBuilder()
+        For Each c As Char In input
+            Select Case c
+                Case "["c, "]"c, "*"c, "?"c, "#"c
+                    sb.Append("["c).Append(c).Append("]"c)
+                Case Else
+                    sb.Append(c)
+            End Select
+        Next
+        Return sb.ToString()
+    End Function
 
     '正则
     ''' <summary>
@@ -2134,6 +2184,7 @@ NextElement:
     ''' <param name="FileName">文件名。可以为“notepad”等缩写。</param>
     ''' <param name="Arguments">运行参数。</param>
     Public Sub ShellOnly(FileName As String, Optional Arguments As String = "")
+        FileName = ShortenPath(FileName)
         Using Program As New Process
             Program.StartInfo.Arguments = Arguments
             Program.StartInfo.FileName = FileName
@@ -2179,7 +2230,7 @@ NextElement:
             .CreateNoWindow = True,
             .RedirectStandardError = True,
             .RedirectStandardOutput = True,
-            .WorkingDirectory = If(WorkingDirectory, Path.TrimEnd("\"c))
+            .WorkingDirectory = ShortenPath(If(WorkingDirectory, Path.TrimEnd("\"c)))
         }
         If WorkingDirectory IsNot Nothing Then
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
@@ -2288,6 +2339,27 @@ NextElement:
         Return New List(Of T)(list)
     End Function
 
+    ''' <summary>
+    ''' 尝试从字典中获取某项,如果该项不存在,则返回默认值。
+    ''' </summary>
+    <Extension> Public Function GetOrDefault(Of TKey, TValue)(Dict As Dictionary(Of TKey, TValue), Key As TKey, Optional DefaultValue As TValue = Nothing) As TValue
+        If Dict.ContainsKey(Key) Then
+            Return Dict(Key)
+        Else
+            Return DefaultValue
+        End If
+    End Function
+    ''' <summary>
+    ''' 将某项添加到以列表作为值的字典中。
+    ''' </summary>
+    <Extension> Public Sub AddToList(Of TKey, TValue)(Dict As Dictionary(Of TKey, List(Of TValue)), Key As TKey, Value As TValue)
+        If Dict.ContainsKey(Key) Then
+            Dict(Key).Add(Value)
+        Else
+            Dict.Add(Key, New List(Of TValue) From {Value})
+        End If
+    End Sub
+
     ''' <summary>
     ''' 获取程序启动参数。
     ''' </summary>
@@ -2337,11 +2409,18 @@ NextElement:
         End Try
     End Sub
     ''' <summary>
-    ''' 打开 explorer。注意参数中的路径要尽量加上双引号!
+    ''' 打开 explorer。
+    ''' 若不以 \ 结尾,则将视作文件路径,打开并选中此文件。
     ''' </summary>
-    Public Sub OpenExplorer(Argument As String)
+    Public Sub OpenExplorer(Location As String)
         Try
-            ShellOnly("explorer.exe", Argument)
+            Location = ShortenPath(Location.Replace("/", "\").Trim(" "c, """"c))
+            Log("[System] 正在打开资源管理器:" & Location)
+            If Location.EndsWith("\") Then
+                ShellOnly(Location)
+            Else
+                ShellOnly("explorer", $"/select,""{Location}""")
+            End If
         Catch ex As Exception
             Log(ex, "打开资源管理器失败,请尝试关闭安全软件(如 360 安全卫士)", LogLevel.Msgbox)
         End Try
@@ -2792,7 +2871,7 @@ Retry:
         On Error Resume Next
         FeedbackInfo()
         If ForceOpenLog OrElse (ShowMsgbox AndAlso MyMsgBox("若你在汇报一个 Bug,请点击 打开文件夹 按钮,并上传 Log(1~5).txt 中包含错误信息的文件。" & vbCrLf & "游戏崩溃一般与启动器无关,请不要因为游戏崩溃而提交反馈。", "反馈提交提醒", "打开文件夹", "不需要") = 1) Then
-            OpenExplorer("""" & Path & "PCL\""")
+            OpenExplorer(Path & "PCL\Log1.txt")
         End If
         OpenWebsite("https://github.com/Hex-Dragon/PCL2/issues/")
     End Sub
@@ -2868,4 +2947,88 @@ Retry:
 
 #End Region
 
-End Module
+End Module
+
+#Region "WPF"
+
+''' <summary>
+''' 对数据绑定进行加法运算,使用参数决定加数。
+''' </summary>
+Public Class AdditionConverter
+    Implements IValueConverter
+    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
+        If value Is Nothing Then Return 0
+        Dim before As Double
+        If Not Double.TryParse(value.ToString(), before) Then Return 0
+        Dim scale As Double = 1
+        If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
+        Return before + scale
+    End Function
+    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
+        If value Is Nothing Then Return Binding.DoNothing
+        Dim before As Double
+        If Not Double.TryParse(value.ToString(), before) Then Return Binding.DoNothing
+        Dim scale As Double = 1
+        If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
+        If scale = 0 Then Return Binding.DoNothing
+        Return before - scale
+    End Function
+End Class
+
+''' <summary>
+''' 对数据绑定进行乘法运算,使用参数决定乘数。
+''' </summary>
+Public Class MultiplicationConverter
+    Implements IValueConverter
+    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
+        If value Is Nothing Then Return 0
+        Dim before As Double
+        If Not Double.TryParse(value.ToString(), before) Then Return 0
+        Dim scale As Double = 1
+        If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
+        Return before * scale
+    End Function
+    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
+        If value Is Nothing Then Return Binding.DoNothing
+        Dim before As Double
+        If Not Double.TryParse(value.ToString(), before) Then Return Binding.DoNothing
+        Dim scale As Double = 1
+        If parameter IsNot Nothing Then Double.TryParse(parameter.ToString(), scale)
+        If scale = 0 Then Return Binding.DoNothing
+        Return before / scale
+    End Function
+End Class
+
+''' <summary>
+''' 将取反的 Boolean 绑定到 Visibility。
+''' </summary>
+Public Class InverseBooleanToVisibilityConverter
+    Implements IValueConverter
+    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
+        If value Is Nothing Then Return Visibility.Visible
+        Dim boolValue As Boolean
+        Return If(Boolean.TryParse(value.ToString(), boolValue), If(boolValue, Visibility.Collapsed, Visibility.Visible), Visibility.Visible)
+    End Function
+    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
+        If value Is Nothing Then Return False
+        Return If(TypeOf value Is Visibility, value <> Visibility.Visible, False)
+    End Function
+End Class
+
+''' <summary>
+''' 将 Boolean 取反。
+''' </summary>
+Public Class InverseBooleanConverter
+    Implements IValueConverter
+    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
+        If value Is Nothing Then Return False
+        Dim boolValue As Boolean
+        Return If(Boolean.TryParse(value.ToString(), boolValue), Not boolValue, False)
+    End Function
+    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
+        If value Is Nothing Then Return False
+        Return If(Boolean.TryParse(value.ToString(), value), Not value, False)
+    End Function
+End Class
+
+#End Region

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

@@ -315,23 +315,26 @@
                 Sub()
                     Try
                         IsForceRestarting = IsForceRestart
-                        If ModeDebug Then Log("[Loader] 加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已" & If(IsForceRestarting, "强制", "") & "启动")
+                        If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
                         LoadDelegate(Me)
                         If IsAborted Then
-                            Log("[Loader] 加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已中断但线程正常运行至结束,输出被弃用(最新线程:" & If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId) & ")", LogLevel.Developer)
+                            Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
                         Else
                             RaisePreviewFinish()
                             State = LoadState.Finished
                             LastFinishedTime = GetTimeTick() '未中断,本次输出有效
                         End If
+                    Catch ex As CancelledException
+                        If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发取消中断,已完成 {Math.Round(Progress * 100)}%")
+                        If Not IsAborted Then State = LoadState.Aborted
                     Catch ex As ThreadInterruptedException
-                        If ModeDebug Then Log(ex, "加载线程 " & Name & " (" & Thread.CurrentThread.ManagedThreadId & ") 已触发线程中断")
+                        If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发线程中断,已完成 {Math.Round(Progress * 100)}%")
                         '如果线程是因为判断到 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)
+                        Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
                         [Error] = ex
                         State = LoadState.Failed
                     End Try
@@ -347,7 +350,7 @@
         End Sub
         Private Sub TriggerThreadAbort()
             If LastRunningThread Is Nothing Then Exit Sub
-            If ModeDebug Then Log("[Loader] 加载线程 " & Name & " (" & LastRunningThread.ManagedThreadId & ") 已中断")
+            If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({LastRunningThread.ManagedThreadId}) 已中断")
             If LastRunningThread.IsAlive Then LastRunningThread.Interrupt()
             LastRunningThread = Nothing
         End Sub

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

@@ -220,12 +220,12 @@ RequestFinished:
     ''' 以多线程下载网页文件的方式获取网页源代码。
     ''' </summary>
     ''' <param name="Url">网页的 Url。</param>
-    Public Function NetGetCodeByDownload(Url As String, Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
+    Public Function NetGetCodeByLoader(Url As String, Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
         Dim Temp As String = PathTemp & "Cache\Code\" & Url.GetHashCode() & "_" & GetUuid()
         Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile({Url}, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)})
         Try
             NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(" & Url & ")")
-            NetGetCodeByDownload = ReadFile(Temp)
+            NetGetCodeByLoader = ReadFile(Temp)
             File.Delete(Temp)
         Finally
             NewTask.Abort()
@@ -235,12 +235,12 @@ RequestFinished:
     ''' 以多线程下载网页文件的方式获取网页源代码。
     ''' </summary>
     ''' <param name="Urls">网页的 Url 列表。</param>
-    Public Function NetGetCodeByDownload(Urls As IEnumerable(Of String), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
+    Public Function NetGetCodeByLoader(Urls As IEnumerable(Of String), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
         Dim Temp As String = PathTemp & "Cache\Code\" & Urls.First.GetHashCode() & "_" & GetUuid()
         Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)})
         Try
             NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls.First & ")")
-            NetGetCodeByDownload = ReadFile(Temp)
+            NetGetCodeByLoader = ReadFile(Temp)
             File.Delete(Temp)
         Finally
             NewTask.Abort()
@@ -248,13 +248,12 @@ RequestFinished:
     End Function
 
     ''' <summary>
-    ''' 从网络中直接下载文件。这不能下载 CDN 中的文件。
+    ''' 使用 WebClient 从网络中下载文件。这不能下载 CDN 中的文件。
     ''' </summary>
     ''' <param name="Url">网络 Url。</param>
     ''' <param name="LocalFile">下载的本地地址。</param>
-    Public Sub NetDownload(Url As String, LocalFile As String, Optional UseBrowserUserAgent As Boolean = False)
+    Public Sub NetDownloadByClient(Url As String, LocalFile As String, Optional UseBrowserUserAgent As Boolean = False)
         Log("[Net] 直接下载文件:" & Url)
-
         '初始化
         Try
             '建立目录
@@ -262,9 +261,8 @@ RequestFinished:
             '尝试删除原文件
             File.Delete(LocalFile)
         Catch ex As Exception
-            Throw New WebException("预处理下载文件路径失败(" & LocalFile & ")。", ex)
+            Throw New WebException($"预处理下载文件路径失败({LocalFile})", ex)
         End Try
-
         '下载
         Using Client As New WebClient
             Try
@@ -272,10 +270,41 @@ RequestFinished:
                 Client.DownloadFile(Url, LocalFile)
             Catch ex As Exception
                 File.Delete(LocalFile)
-                Throw New WebException("直接下载文件失败(" & Url & ")。", ex)
+                Throw New WebException($"直接下载文件失败({Url})", ex)
             End Try
         End Using
+    End Sub
 
+    ''' <summary>
+    ''' 简单的多线程下载文件。可以下载 CDN 中的文件。
+    ''' </summary>
+    ''' <param name="Url">文件的 Url。</param>
+    ''' <param name="LocalFile">下载的本地地址。</param>
+    Public Sub NetDownloadByLoader(Url As String, LocalFile As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False)
+        Dim NewTask As New LoaderDownload("文件下载 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile({Url}, LocalFile, Check, UseBrowserUserAgent)})
+        Try
+            NewTask.WaitForExit(LoaderToSyncProgress:=LoaderToSyncProgress)
+        Catch ex As Exception
+            Throw New WebException($"多线程直接下载文件失败({Url})", ex)
+        Finally
+            NewTask.Abort()
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 简单的多线程下载文件。可以下载 CDN 中的文件。
+    ''' </summary>
+    ''' <param name="Urls">文件的 Url 列表。</param>
+    ''' <param name="LocalFile">下载的本地地址。</param>
+    Public Sub NetDownloadByLoader(Urls As IEnumerable(Of String), LocalFile As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False)
+        Dim NewTask As New LoaderDownload("文件下载 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, LocalFile, Check, UseBrowserUserAgent)})
+        Try
+            NewTask.WaitForExit(LoaderToSyncProgress:=LoaderToSyncProgress)
+        Catch ex As Exception
+            Throw New WebException($"多线程直接下载文件失败(第一下载源:" & Urls.First() & ")", ex)
+        Finally
+            NewTask.Abort()
+        End Try
     End Sub
 
     ''' <summary>
@@ -1885,7 +1914,7 @@ Retry:
                         For Each File As NetFile In WaitingFiles
                             If NetTaskThreadCount >= NetTaskThreadLimit Then Continue While '最大线程数检查
                             Dim NewThread = File.TryBeginThread()
-                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(100) '减少 BMCLAPI 请求频率,避免 Too Many Requests
+                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(30) '减少 BMCLAPI 请求频率(目前每分钟限制 4000 次)
                         Next
                         '为进行中的文件追加线程
                         If Speed >= NetTaskSpeedLimitLow Then Continue While '下载速度足够,无需新增
@@ -1905,7 +1934,7 @@ Retry:
                             '新增线程
                             If PreparingCount > DownloadingCount Then Continue For '准备中的线程已多于下载中的线程,不再新增
                             Dim NewThread = File.TryBeginThread()
-                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(100) '减少 BMCLAPI 请求频率,避免 Too Many Requests
+                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(30) '减少 BMCLAPI 请求频率(目前每分钟限制 4000 次)
                         Next
                     End While
                 Catch ex As Exception

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

@@ -262,7 +262,6 @@ Public Class ValidateFileName
     Public Property IgnoreCase As Boolean = True
     Public Property ParentFolder As String = Nothing
     Public Property RequireParentFolderExists = True
-    Public Property AllowNull As Boolean = False
     Public Sub New()
     End Sub
     Public Sub New(Name As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
@@ -272,16 +271,14 @@ Public Class ValidateFileName
     End Sub
     Public Overrides Function Validate(Str As String) As String
         Try
-            If Not AllowNull Then
-                '检查是否为空
-                Dim NullCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-                If Not NullCheck = "" Then Return NullCheck
-            End If
+            '检查是否为空
+            Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
+            If Not LengthCheck = "" Then Return LengthCheck
             '检查空格
             If Str.StartsWithF(" ") Then Return "文件名不能以空格开头!"
             If Str.EndsWithF(" ") Then Return "文件名不能以空格结尾!"
             '检查长度
-            Dim LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 253).Validate(Str & If(ParentFolder, ""))
+            LengthCheck = New ValidateLength(1, 253).Validate(Str & If(ParentFolder, ""))
             If Not LengthCheck = "" Then Return LengthCheck
             '检查尾部小数点
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
@@ -318,7 +315,6 @@ End Class
 Public Class ValidateFolderPath
     Inherits Validate
     Public Property UseMinecraftCharCheck As Boolean = True
-    Public Property AllowNull As Boolean = False
     Public Sub New()
     End Sub
     Public Sub New(UseMinecraftCharCheck As Boolean)
@@ -330,9 +326,9 @@ Public Class ValidateFolderPath
         If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
         '检查是否为空
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-        If Not AllowNull AndAlso Not LengthCheck = "" Then Return LengthCheck
+        If Not LengthCheck = "" Then Return LengthCheck
         '检查长度
-        LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 254).Validate(Str)
+        LengthCheck = New ValidateLength(1, 254).Validate(Str)
         If Not LengthCheck = "" Then Return LengthCheck
         '检查开头
         If Str.StartsWithF("\\Mac\") Then GoTo Fin

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

@@ -360,7 +360,7 @@
                     'Tags & ModLoaders
                     Tags = New List(Of String)
                     ModLoaders = New List(Of CompModLoaderType)
-                    If Data?("loaders") IsNot Nothing Then
+                    If Data.ContainsKey("loaders") Then
                         For Each Category In Data("loaders").Select(Function(t) t.ToString)
                             Select Case Category
                                 Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
@@ -402,7 +402,7 @@
                             Case "technology" : Tags.Add("科技")
                             Case "magic" : Tags.Add("魔法")
                             Case "adventure" : Tags.Add("冒险")
-                            Case "kitchen-sink" : Tags.Add("大杂烩")
+                            Case "kitchen-sink" : Tags.Add("水槽包/大杂烩")
                             Case "lightweight" : Tags.Add("轻量")
                                 'FUTURE: Res
                         End Select
@@ -1274,10 +1274,7 @@ Retry:
                     Dim Url = Data("downloadUrl").ToString
                     If Url = "" Then Url = $"https://media.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}"
                     Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码
-                    DownloadUrls = (New List(Of String) From {Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"),
-                                       Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
-                                       Url.Replace("://edge", "://media"),
-                                       Url}).Distinct.ToList '对脑残 CurseForge 的下载地址进行多种修正
+                    DownloadUrls = HandleCurseForgeDownloadUrls(Url) '对脑残 CurseForge 的下载地址进行多种修正
                     DownloadUrls.AddRange(DownloadUrls.Select(Function(u) DlSourceModGet(u)).ToList) '添加镜像源,这个写法是为了让镜像源排在后面
                     DownloadUrls = DownloadUrls.Distinct.ToList '最终去重
                     'Dependencies
@@ -1352,6 +1349,19 @@ Retry:
                 End If
             End If
         End Sub
+
+        ''' <summary>
+        ''' 重新整理 CurseForge 的下载地址。
+        ''' </summary>
+        Public Shared Function HandleCurseForgeDownloadUrls(Url As String) As List(Of String)
+            Return {
+                Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"),
+                Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
+                Url.Replace("://edge", "://media"),
+                Url
+            }.Distinct.ToList
+        End Function
+
         ''' <summary>
         ''' 将当前实例转为可用于保存缓存的 Json。
         ''' </summary>

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

@@ -1,24 +1,10 @@
 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)
+        TempFolder = RequestTaskTempFolder()
         Directory.CreateDirectory(TempFolder & "Temp\")
         Directory.CreateDirectory(TempFolder & "Report\")
         Log("[Crash] 崩溃分析暂存文件夹:" & TempFolder)
@@ -873,7 +859,7 @@ NextStack:
             Sub()
                 '弹窗选择:查看日志
                 If File.Exists(DirectFile.Value.Key) Then
-                    ShellOnly("notepad", DirectFile.Value.Key)
+                    ShellOnly(DirectFile.Value.Key)
                 Else
                     Dim FilePath As String = PathTemp & "Crash.txt"
                     WriteFile(FilePath, Join(DirectFile.Value.Value, vbCrLf))
@@ -885,7 +871,7 @@ NextStack:
                 Dim FileAddress As String = Nothing
                 Try
                     '获取文件路径
-                    RunInUiWait(Sub() FileAddress = SelectAs("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
+                    RunInUiWait(Sub() FileAddress = SelectSaveFile("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
                     If String.IsNullOrEmpty(FileAddress) Then Exit Sub
                     Directory.CreateDirectory(GetPathFromFullPath(FileAddress))
                     If File.Exists(FileAddress) Then File.Delete(FileAddress)
@@ -923,7 +909,7 @@ NextStack:
                     Log(ex, "导出错误报告失败", LogLevel.Feedback)
                     Exit Sub
                 End Try
-                OpenExplorer("/select,""" & FileAddress & """")
+                OpenExplorer(FileAddress)
         End Select
     End Sub
     ''' <summary>

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

@@ -622,7 +622,7 @@
     Public Sub DlForgeVersionOfficialMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)))
         Dim Result As String
         Try
-            Result = NetGetCodeByDownload("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" &
+            Result = NetGetCodeByLoader("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" &
                                           Loader.Input.Replace("-", "_") & '兼容 Forge 1.7.10-pre4,#4057
                                           ".html", UseBrowserUserAgent:=True)
         Catch ex As Exception
@@ -829,8 +829,8 @@
     Public DlNeoForgeListOfficialLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Official", AddressOf DlNeoForgeListOfficialMain)
     Private Sub DlNeoForgeListOfficialMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult))
         '获取版本列表 JSON
-        Dim ResultLatest As String = NetGetCodeByDownload("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
-        Dim ResultLegacy As String = NetGetCodeByDownload("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True)
+        Dim ResultLatest As String = NetGetCodeByLoader("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
+        Dim ResultLegacy As String = NetGetCodeByLoader("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True)
         If ResultLatest.Length < 100 OrElse ResultLegacy.Length < 100 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")")
         '解析
         Try
@@ -847,8 +847,8 @@
     Public DlNeoForgeListBmclapiLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Bmclapi", AddressOf DlNeoForgeListBmclapiMain)
     Public Sub DlNeoForgeListBmclapiMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult))
         '获取版本列表 JSON
-        Dim ResultLatest As String = NetGetCodeByDownload("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
-        Dim ResultLegacy As String = NetGetCodeByDownload("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True)
+        Dim ResultLatest As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
+        Dim ResultLegacy As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True)
         If ResultLatest.Length < 100 OrElse ResultLegacy.Length < 100 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")")
         '解析
         Try
@@ -1095,12 +1095,12 @@
 
     ''' <summary>
     ''' 对可能涉及 Mod 镜像源的请求进行处理,返回字符串或 JObject。
-    ''' 调用 NetGetCodeByRequest。
+    ''' 调用 NetGetCodeByRequest,会进行重试。
     ''' </summary>
     Public Function DlModRequest(Url As String, Optional IsJson As Boolean = False) As Object
         Dim Urls As New List(Of KeyValuePair(Of String, Integer))
         Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5))
-        Urls.Add(New KeyValuePair(Of String, Integer)(Url, 15))
+        Urls.Add(New KeyValuePair(Of String, Integer)(Url, 20))
         'Dim McimUrl As String = DlSourceModGet(Url)
         'If McimUrl <> Url Then
         '    Select Case Setup.Get("ToolDownloadMod")
@@ -1132,12 +1132,12 @@
 
     ''' <summary>
     ''' 对可能涉及 Mod 镜像源的请求进行处理。
-    ''' 调用 NetRequest。
+    ''' 调用 NetRequest,会进行重试。
     ''' </summary>
     Public Function DlModRequest(Url As String, Method As String, Data As String, ContentType As String) As String
         Dim Urls As New List(Of KeyValuePair(Of String, Integer))
         Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5))
-        Urls.Add(New KeyValuePair(Of String, Integer)(Url, 15))
+        Urls.Add(New KeyValuePair(Of String, Integer)(Url, 20))
         'Dim McimUrl As String = DlSourceModGet(Url)
         'If McimUrl <> Url Then
         '   Select Case Setup.Get("ToolDownloadMod")

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

@@ -653,7 +653,7 @@ Wait:
     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)
+            JavaSearchFolder(New DirectoryInfo(ShortenPath(OriginalPath)), Results, Source, IsFullSearch)
         Catch ex As UnauthorizedAccessException
             Log("[Java] 遍历查找 Java 时遭遇无权限的文件夹:" & OriginalPath)
         Catch ex As Exception
@@ -741,7 +741,7 @@ Wait:
     Private LastJavaBaseDir As String = Nothing '用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java
     Private Sub JavaFileList(Loader As LoaderTask(Of Integer, List(Of NetFile)))
         Log("[Java] 开始获取 Java 下载信息")
-        Dim IndexFileStr As String = NetGetCodeByDownload(
+        Dim IndexFileStr As String = NetGetCodeByLoader(
             {"https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json",
              "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"},
         IsJson:=True)
@@ -753,7 +753,7 @@ Wait:
         Dim Address As String = TargetEntry.Value("manifest")("url")
         Log($"[Java] 准备下载 Java {TargetEntry.Value("version")("name")}({TargetEntry.Key}):{Address}")
         '获取文件列表
-        Dim ListFileStr As String = NetGetCodeByDownload({Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com"), Address}, IsJson:=True)
+        Dim ListFileStr As String = NetGetCodeByLoader({Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com"), Address}, IsJson:=True)
         LastJavaBaseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\runtime\" & TargetEntry.Key & "\"
         Dim Results As New List(Of NetFile)
         For Each File As JProperty In CType(GetJson(ListFileStr), JObject)("files")

+ 45 - 42
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -574,7 +574,7 @@ SkipLogin:
     End Sub
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
         Dim Input As McLoginServer = Data.Input
-        Dim NeedRefresh As Boolean = False
+        Dim NeedRefresh As Boolean = False, WasRefreshed As Boolean = False
         Dim LogUsername As String = Input.UserName
         If LogUsername.Contains("@") AndAlso Setup.Get("UiLauncherEmail") Then
             LogUsername = AccountFilter(LogUsername)
@@ -595,7 +595,7 @@ SkipLogin:
                 McLoginRequestValidate(Data)
                 GoTo LoginFinish
             Catch ex As Exception
-                Dim AllMessage = GetExceptionSummary(ex)
+                Dim AllMessage = GetExceptionDetail(ex)
                 McLaunchLog("验证登录失败:" & AllMessage)
                 If (AllMessage.Contains("超时") OrElse AllMessage.Contains("imeout")) AndAlso Not AllMessage.Contains("403") Then
                     McLaunchLog("已触发超时登录失败")
@@ -610,7 +610,8 @@ Refresh:
                 McLoginRequestRefresh(Data, NeedRefresh)
                 GoTo LoginFinish
             Catch ex As Exception
-                McLaunchLog("刷新登录失败:" & GetExceptionSummary(ex))
+                McLaunchLog("刷新登录失败:" & GetExceptionDetail(ex))
+                If WasRefreshed Then Throw New Exception("二轮刷新登录失败", ex)
             End Try
             Data.Progress = If(NeedRefresh, 0.85, 0.45)
         End If
@@ -619,10 +620,12 @@ Refresh:
             If Data.IsAborted Then Throw New ThreadInterruptedException
             NeedRefresh = McLoginRequestLogin(Data)
         Catch ex As Exception
-            McLaunchLog("登录失败:" & GetExceptionSummary(ex))
+            McLaunchLog("登录失败:" & GetExceptionDetail(ex))
             Throw
         End Try
         If NeedRefresh Then
+            McLaunchLog("重新进行刷新登录")
+            WasRefreshed = True
             Data.Progress = 0.65
             GoTo Refresh
         End If
@@ -699,19 +702,17 @@ LoginFinish:
     End Sub
     Private Sub McLoginRequestRefresh(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult), RequestUser As Boolean)
         McLaunchLog("刷新登录开始(Refresh, " & Data.Input.Token & ")")
-        
-        Dim RefreshInfo As JObject = New JObject
-        Dim SelectProfile As JObject = New JObject()
-        SelectProfile.Add(New JProperty("name", Setup.Get("Cache" & Data.Input.Token & "Name")))
-        SelectProfile.Add(New JProperty("id", Setup.Get("Cache" & Data.Input.Token & "Uuid")))
-        RefreshInfo.Add("selectedProfile", SelectProfile)
-        RefreshInfo.Add(New JProperty("accessToken", Setup.Get("Cache" & Data.Input.Token & "Access")))
-        RefreshInfo.Add(New JProperty("requestUser", True))
-
         Dim LoginJson As JObject = GetJson(NetRequestRetry(
                Url:=Data.Input.BaseUrl & "/refresh",
                Method:="POST",
-               Data:=RefreshInfo.ToString(0),
+               Data:=New JObject(
+                   New JProperty("selectedProfile", New JObject(
+                       New JProperty("name", Setup.Get($"Cache{Data.Input.Token}Name")),
+                       New JProperty("id", Setup.Get($"Cache{Data.Input.Token}Uuid"))
+                   )),
+                   New JProperty("accessToken", Setup.Get($"Cache{Data.Input.Token}Access")),
+                   New JProperty("requestUser", True)
+               ).ToString(Newtonsoft.Json.Formatting.None),
                Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh-CN"}},
                ContentType:="application/json; charset=utf-8"))
         '将登录结果输出
@@ -768,17 +769,18 @@ LoginFinish:
                 '缓存无效,要求玩家选择
                 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)
+                    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
@@ -1072,7 +1074,8 @@ Retry:
             Case 3
                 '使用正版用户名
                 Try
-                    If SkinName <> "" AndAlso McVersionCurrent.Version.McCodeMain < 20 Then '1.20+ 或快照版不能使用该项(#3746)
+                    If SkinName <> "" AndAlso
+                        McVersionCurrent IsNot Nothing AndAlso McVersionCurrent.Version.McCodeMain < 20 Then '1.20+ 或快照版不能使用该项(#3746)
                         Log("[Skin] 由于离线皮肤设置,使用正版 UUID:" & SkinName)
                         Uuid = McLoginMojangUuid(SkinName, False)
                     End If
@@ -1629,16 +1632,16 @@ NextVersion:
 
         '基础参数
         GameArguments.Add("${classpath_separator}", ";")
-        GameArguments.Add("${natives_directory}", GetNativesFolder())
-        GameArguments.Add("${library_directory}", PathMcFolder & "libraries")
-        GameArguments.Add("${libraries_directory}", PathMcFolder & "libraries")
+        GameArguments.Add("${natives_directory}", ShortenPath(GetNativesFolder()))
+        GameArguments.Add("${library_directory}", ShortenPath(PathMcFolder & "libraries"))
+        GameArguments.Add("${libraries_directory}", ShortenPath(PathMcFolder & "libraries"))
         GameArguments.Add("${launcher_name}", "PCL")
         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("${game_directory}", ShortenPath(Left(McVersionCurrent.PathIndie, McVersionCurrent.PathIndie.Count - 1)))
+        GameArguments.Add("${assets_root}", ShortenPath(PathMcFolder & "assets"))
         GameArguments.Add("${user_properties}", "{}")
         GameArguments.Add("${auth_player_name}", McLoginLoader.Output.Name)
         GameArguments.Add("${auth_uuid}", McLoginLoader.Output.Uuid)
@@ -1672,7 +1675,7 @@ NextVersion:
         GameArguments.Add("${resolution_height}", Math.Round(GameSize.Height))
 
         'Assets 相关参数
-        GameArguments.Add("${game_assets}", PathMcFolder & "assets\virtual\legacy") '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
+        GameArguments.Add("${game_assets}", ShortenPath(PathMcFolder & "assets\virtual\legacy")) '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
         GameArguments.Add("${assets_index_name}", McAssetsGetIndexName(Version))
 
         '支持库参数
@@ -1689,7 +1692,7 @@ NextVersion:
             End If
         Next
         If OptiFineCp IsNot Nothing Then CpStrings.Insert(CpStrings.Count - 2, OptiFineCp)
-        GameArguments.Add("${classpath}", Join(CpStrings, ";"))
+        GameArguments.Add("${classpath}", Join(CpStrings.Select(Function(c) ShortenPath(c)), ";"))
 
         Return GameArguments
     End Function
@@ -2050,8 +2053,8 @@ IgnoreCustomSkin:
                 "@echo off" & vbCrLf &
                 "title 启动 - " & McVersionCurrent.Name & vbCrLf &
                 "echo 游戏正在启动,请稍候。" & vbCrLf &
-                "set APPDATA=""" & McVersionCurrent.PathIndie & """" & vbCrLf &
-                "cd /D """ & McVersionCurrent.PathIndie & """" & vbCrLf &
+                "set APPDATA=""" & ShortenPath(McVersionCurrent.PathIndie) & """" & vbCrLf &
+                "cd /D """ & ShortenPath(McVersionCurrent.PathIndie) & """" & vbCrLf &
                 CustomCommandGlobal & vbCrLf &
                 CustomCommandVersion & vbCrLf &
                 """" & McLaunchJavaSelected.PathJava & """ " & McLaunchArgument & vbCrLf &
@@ -2062,7 +2065,7 @@ IgnoreCustomSkin:
             If CurrentLaunchOptions.SaveBatch IsNot Nothing Then
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
                 AbortHint = "导出启动脚本成功!"
-                OpenExplorer("/select,""" & CurrentLaunchOptions.SaveBatch & """")
+                OpenExplorer(CurrentLaunchOptions.SaveBatch)
                 Loader.Parent.Abort()
                 Exit Sub '导出脚本完成
             End If
@@ -2078,7 +2081,7 @@ IgnoreCustomSkin:
             Try
                 CustomProcess.StartInfo.FileName = "cmd.exe"
                 CustomProcess.StartInfo.Arguments = "/c """ & CustomCommandGlobal & """"
-                CustomProcess.StartInfo.WorkingDirectory = PathMcFolder
+                CustomProcess.StartInfo.WorkingDirectory = ShortenPath(PathMcFolder)
                 CustomProcess.StartInfo.UseShellExecute = False
                 CustomProcess.StartInfo.CreateNoWindow = True
                 CustomProcess.Start()
@@ -2102,7 +2105,7 @@ IgnoreCustomSkin:
             Try
                 CustomProcess.StartInfo.FileName = "cmd.exe"
                 CustomProcess.StartInfo.Arguments = "/c """ & CustomCommandVersion & """"
-                CustomProcess.StartInfo.WorkingDirectory = PathMcFolder
+                CustomProcess.StartInfo.WorkingDirectory = ShortenPath(PathMcFolder)
                 CustomProcess.StartInfo.UseShellExecute = False
                 CustomProcess.StartInfo.CreateNoWindow = True
                 CustomProcess.Start()
@@ -2130,18 +2133,18 @@ IgnoreCustomSkin:
 
         '设置环境变量
         If StartInfo.EnvironmentVariables.ContainsKey("appdata") Then
-            StartInfo.EnvironmentVariables("appdata") = PathMcFolder
+            StartInfo.EnvironmentVariables("appdata") = ShortenPath(PathMcFolder)
         Else
-            StartInfo.EnvironmentVariables.Add("appdata", PathMcFolder)
+            StartInfo.EnvironmentVariables.Add("appdata", ShortenPath(PathMcFolder))
         End If
         Dim Paths As New List(Of String)(StartInfo.EnvironmentVariables("Path").Split(";"))
-        Paths.Add(McLaunchJavaSelected.PathFolder)
+        Paths.Add(ShortenPath(McLaunchJavaSelected.PathFolder))
         StartInfo.EnvironmentVariables("Path") = Join(Paths.Distinct.ToList, ";")
 
         '设置其他参数
         StartInfo.StandardErrorEncoding = Encoding.UTF8
         StartInfo.StandardOutputEncoding = Encoding.UTF8
-        StartInfo.WorkingDirectory = McVersionCurrent.PathIndie
+        StartInfo.WorkingDirectory = ShortenPath(McVersionCurrent.PathIndie)
         StartInfo.UseShellExecute = False
         StartInfo.RedirectStandardOutput = True
         StartInfo.RedirectStandardError = True

+ 62 - 45
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -73,7 +73,8 @@ Public Module ModMinecraft
                 End If
                 Dim Name As String = Folder.Split(">")(0)
                 Dim Path As String = Folder.Split(">")(1)
-                If CheckPermission(Path) Then
+                Try
+                    CheckPermissionWithException(Path)
                     '若已有该文件夹,则直接重命名;没有则添加
                     Dim Renamed As Boolean = False
                     For Each OriginalFolder As McFolder In CacheMcFolderList
@@ -84,9 +85,10 @@ Public Module ModMinecraft
                         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
+                Catch ex As Exception
+                    MyMsgBox("失效的 Minecraft 文件夹:" & vbCrLf & Path & vbCrLf & vbCrLf & GetExceptionSummary(ex), "Minecraft 文件夹失效", IsWarn:=True)
+                    Log(ex, $"无法访问 Minecraft 文件夹 {Path}")
+                End Try
             Next
 
             '将自定义文件夹情况同步到设置
@@ -482,17 +484,27 @@ VersionSearchFinish:
                     Return TrimedJson.StartsWithF("{") AndAlso TrimedJson.EndsWithF("}")
                 End Function
                 If _JsonText Is Nothing Then
-                    If Not File.Exists(Path & Name & ".json") Then Throw New Exception($"未找到版本 JSON 文件:{Path}{Name}.json")
-                    _JsonText = ReadFile(Path & Name & ".json")
+                    Dim JsonPath As String = Path & Name & ".json"
+                    If Not File.Exists(JsonPath) Then
+                        '如果文件夹下只有一个 JSON 文件,则将其作为版本 JSON
+                        Dim JsonFiles As String() = Directory.GetFiles(Path, "*.json")
+                        If JsonFiles.Count = 1 Then
+                            JsonPath = JsonFiles(0)
+                            Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, LogLevel.Debug)
+                        Else
+                            Throw New Exception($"未找到版本 JSON 文件:{Path}{Name}.json")
+                        End If
+                    End If
+                    _JsonText = ReadFile(JsonPath)
                     '如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
                     If Not FastJsonCheck(_JsonText) Then
                         If RunInUi() Then
                             Log("[Minecraft] 版本 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", LogLevel.Debug)
                             GetJson(_JsonText) '触发异常
                         Else
-                            Log("[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取(" & Path & Name & ".json)", LogLevel.Debug)
+                            Log($"[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", LogLevel.Debug)
                             Thread.Sleep(2000)
-                            _JsonText = ReadFile(Path & Name & ".json")
+                            _JsonText = ReadFile(JsonPath)
                             If Not FastJsonCheck(_JsonText) Then GetJson(_JsonText) '触发异常
                         End If
                     End If
@@ -779,37 +791,7 @@ ExitDataLoad:
                 End If
                 '确定版本描述
                 Dim CustomInfo As String = ReadIni(Path & "PCL\Setup.ini", "CustomInfo")
-                If CustomInfo = "" Then
-                    Select Case State
-                        Case McVersionState.Snapshot
-                            If Version.McName.ContainsF("pre", True) Then
-                                Info = "预发布版 " & Version.McName
-                            ElseIf Version.McName.ContainsF("rc", True) 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.NeoForge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
-                            Info = Version.ToString
-                        Case McVersionState.Fool
-                            Info = GetMcFoolName(Version.McName)
-                        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
+                Info = If(CustomInfo <> "", CustomInfo, GetDefaultDescription())
                 '确定版本收藏状态
                 IsStar = ReadIni(Path & "PCL\Setup.ini", "IsStar", False)
                 '确定版本显示种类
@@ -842,6 +824,41 @@ ExitDataLoad:
             End Try
             Return Me
         End Function
+        ''' <summary>
+        ''' 获取版本的默认描述。
+        ''' </summary>
+        Public Function GetDefaultDescription() As String
+            Dim Info As String = ""
+            Select Case State
+                Case McVersionState.Snapshot
+                    If Version.McName.ContainsF("pre", True) Then
+                        Info = "预发布版 " & Version.McName
+                    ElseIf Version.McName.ContainsF("rc", True) 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.NeoForge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
+                    Info = Version.ToString
+                Case McVersionState.Fool
+                    Info = GetMcFoolName(Version.McName)
+                Case McVersionState.Error
+                    Return Me.Info '已有错误信息
+                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
+            Return Info
+        End Function
+
         Public IsLoaded As Boolean = False
 
         Public Overrides Function Equals(obj As Object) As Boolean
@@ -1123,10 +1140,10 @@ Reload:
                 McVersionList = McVersionListLoadNoCache(Path)
             End If
             IsFirstMcVersionListLoad = False
-            If Loader.IsAborted Then Exit Sub
 
             '改变当前选择的版本
 OnLoaded:
+            If Loader.IsAborted Then Exit Sub
             If McVersionList.Any(Function(v) v.Key <> McVersionCardType.Error) Then
                 '尝试读取已储存的选择
                 Dim SavedSelection As String = ReadIni(Path & "PCL.ini", "Version")
@@ -1619,7 +1636,7 @@ OnLoaded:
         Dim FileAddress As String = PathTemp & "Cache\Skin\" & GetHash(Address) & ".png"
         SyncLock McSkinDownloadLock
             If Not File.Exists(FileAddress) Then
-                NetDownload(Address, FileAddress & NetDownloadEnd)
+                NetDownloadByClient(Address, FileAddress & NetDownloadEnd)
                 File.Delete(FileAddress)
                 FileSystem.Rename(FileAddress & NetDownloadEnd, FileAddress)
                 Log("[Minecraft] 皮肤下载成功:" & FileAddress)
@@ -1962,7 +1979,7 @@ OnLoaded:
             Try
                 Log("[Minecraft] 开始获取统一通行证下载信息")
                 '测试链接:https://auth.mc-user.com:233/00000000000000000000000000000000/
-                DownloadInfo = GetJson(NetGetCodeByDownload({
+                DownloadInfo = GetJson(NetGetCodeByLoader({
                         "https://auth.mc-user.com:233/" & Setup.Get("VersionServerNide", Version:=Version)}, IsJson:=True))
             Catch ex As Exception
                 Log(ex, "获取统一通行证下载信息失败")
@@ -1985,7 +2002,7 @@ OnLoaded:
             '获取下载信息
             Try
                 Log("[Minecraft] 开始获取 Authlib-Injector 下载信息")
-                DownloadInfo = GetJson(NetGetCodeByDownload({
+                DownloadInfo = GetJson(NetGetCodeByLoader({
                         "https://authlib-injector.yushi.moe/artifact/latest.json",
                         "https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json"
                     }, IsJson:=True))
@@ -2085,8 +2102,8 @@ OnLoaded:
                    Splited(0).Replace(".", "\") & "\" & Splited(1) & "\" & Splited(2) & "\" & Splited(1) & "-" & Splited(2) & ".jar"
         '判断 OptiFine 是否应该使用 installer
         If McLibGet.Contains("optifine\OptiFine\1.") AndAlso Splited(2).Split(".").Count > 1 Then
-            Dim MajorVersion As Integer = Val(Splited(2).Split(".")(1))
-            Dim MinorVersion As Integer = If(Splited(2).Split(".").Count > 1, Val(Splited(2).Split(".")(2).BeforeFirst("_")), 0)
+            Dim MajorVersion As Integer = Val(Splited(2).Split(".")(1).BeforeFirst("_"))
+            Dim MinorVersion As Integer = If(Splited(2).Split(".").Count > 2, Val(Splited(2).Split(".")(2).BeforeFirst("_")), 0)
             If (MajorVersion = 12 OrElse (MajorVersion = 20 AndAlso MinorVersion >= 4) OrElse MajorVersion >= 21) AndAlso '仅在 1.12 (无法追溯) 和 1.20.4+ (#5376) 遇到此问题
                 File.Exists($"{CustomMcFolder}libraries\{Splited(0).Replace(".", "\")}\{Splited(1)}\{Splited(2)}\{Splited(1)}-{Splited(2)}-installer.jar") Then
                 McLaunchLog("已将 " & Original & " 替换为对应的 Installer 文件")

+ 50 - 399
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -1,9 +1,7 @@
-Imports System.IO.Compression
-Imports System.Linq.Expressions
+Imports System.IO.Compression
 
 Public Module ModModpack
 
-#Region "安装"
     '触发整合包安装的外部接口
     ''' <summary>
     ''' 弹窗要求选择一个整合包文件并进行安装。
@@ -31,6 +29,9 @@ Public Module ModModpack
         Dim Archive As ZipArchive = Nothing
         Dim ArchiveBaseFolder As String = ""
         Try
+            '字符校验
+            Dim TargetFolder As String = $"{PathMcFolder}versions\{VersionName}\"
+            If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("游戏路径中不能含有感叹号或分号:" & TargetFolder, HintType.Critical) : Throw New CancelledException
             '获取整合包种类与关键 Json
             Dim PackType As Integer = -1
             Try
@@ -108,29 +109,7 @@ Public Module ModModpack
         End Try
     End Function
 
-    '整合包缓存清理
-    Private IsInstallCacheCleared As Boolean = False
-    Private IsInstallCacheClearing As Boolean = False
     Private Sub ExtractModpackFiles(InstallTemp As String, FileAddress As String, Loader As LoaderBase, LoaderProgressDelta As Double)
-        '清理缓存文件夹
-        If Not IsInstallCacheCleared Then
-            IsInstallCacheCleared = True
-            IsInstallCacheClearing = True
-            Try
-                Log("[ModPack] 开始清理整合包安装缓存")
-                DeleteDirectory(PathTemp & "PackInstall\")
-                Log("[ModPack] 已清理整合包安装缓存")
-            Catch ex As Exception
-                Log(ex, "清理整合包安装缓存失败")
-            Finally
-                IsInstallCacheClearing = False
-            End Try
-        ElseIf IsInstallCacheClearing Then
-            '等待另一个整合包安装的清理步骤完成
-            Do While IsInstallCacheClearing
-                Thread.Sleep(1)
-            Loop
-        End If
         '解压文件
         Dim RetryCount As Integer = 1
         Dim Encode = Encoding.GetEncoding("GB18030")
@@ -204,7 +183,7 @@ Retry:
             End If
         Next
         '解压与配置文件
-        Dim InstallTemp As String = PathTemp & "PackInstall\" & RandomInteger(0, 100000) & "\"
+        Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim OverrideHome As String = If(Json("overrides"), "")
         If OverrideHome <> "" Then
@@ -212,13 +191,15 @@ Retry:
             Sub(Task As LoaderTask(Of String, Integer))
                 ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.6)
                 Task.Progress = 0.6
-                Dim OverridePath As String = InstallTemp & ArchiveBaseFolder & OverrideHome
                 '复制结果
-                If Directory.Exists(OverridePath) Then
-                    CopyDirectory(OverridePath, PathMcFolder & "versions\" & VersionName, Sub(Delta) Task.Progress += Delta * 0.35)
-                    Log($"[ModPack] 整合包 override 复制:{OverridePath} -> {PathMcFolder & "versions\" & VersionName}")
+                Log("[ModPack] 整合包 override 目标:" & OverrideHome)
+                Dim OverrideRoot = InstallTemp & ArchiveBaseFolder & If(OverrideHome = "." OrElse OverrideHome = "./", "", OverrideHome) '#5613
+                If Directory.Exists(OverrideRoot) Then
+                    Dim CopyTarget = $"{PathMcFolder}versions\{VersionName}"
+                    CopyDirectory(OverrideRoot, CopyTarget, Sub(Delta) Task.Progress += Delta * 0.35)
+                    Log($"[ModPack] 整合包 override 复制:{OverrideRoot} -> {CopyTarget}")
                 Else
-                    Log($"[ModPack] 整合包中未找到 override 文件夹:{OverridePath}")
+                    Log($"[ModPack] 整合包中未找到 override 文件夹:{OverrideRoot}")
                 End If
                 Task.Progress = 0.95
                 '开启版本隔离
@@ -314,7 +295,7 @@ Retry:
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Sub(Task As LoaderTask(Of String, String))
             '设置图标
-            Dim VersionFolder As String = PathMcFolder & "versions\" & VersionName & "\"
+            Dim VersionFolder As String = $"{PathMcFolder}versions\{VersionName}\"
             If Logo IsNot Nothing AndAlso File.Exists(Logo) Then
                 File.Copy(Logo, VersionFolder & "PCL\Logo.png", True)
                 WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
@@ -328,6 +309,10 @@ Retry:
                     File.Delete(Target)
                 End If
             Next
+            If File.Exists(FileAddress) AndAlso GetFileNameWithoutExtentionFromPath(FileAddress) = "modpack" Then
+                Log("[ModPack] 删除安装整合包文件:" & FileAddress)
+                File.Delete(FileAddress)
+            End If
         End Sub) With {.ProgressWeight = 0.1, .Show = False})
 
         '重复任务检查
@@ -392,7 +377,7 @@ Retry:
             If String.IsNullOrEmpty(VersionName) Then Throw New CancelledException
         End If
         '解压和配置文件
-        Dim InstallTemp As String = PathTemp & "PackInstall\" & RandomInteger(0, 100000) & "\"
+        Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
@@ -421,7 +406,7 @@ Retry:
             If File("env") IsNot Nothing Then
                 Select Case File("env")("client").ToString
                     Case "optional"
-                        If MyMsgBox("是否要下载整合包中的可选文件 " & GetFileNameFromPath(File("path").ToString) & "?",
+                        If MyMsgBox("是否要下载可选文件 " & GetFileNameFromPath(File("path").ToString) & "?",
                                     "下载可选文件", "是", "否") = 2 Then
                             Continue For
                         End If
@@ -476,10 +461,14 @@ Retry:
                     File.Delete(Target)
                 End If
             Next
+            If File.Exists(FileAddress) AndAlso GetFileNameWithoutExtentionFromPath(FileAddress) = "modpack" Then
+                Log("[ModPack] 删除安装整合包文件:" & FileAddress)
+                File.Delete(FileAddress)
+            End If
         End Sub) With {.ProgressWeight = 0.1, .Show = False})
 
         '重复任务检查
-        Dim LoaderName As String = "Modrinth 整合包安装:" & VersionName & " "
+        Dim LoaderName As String = $"Modrinth 整合包安装:{VersionName} "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
             Hint("该整合包正在安装中!", HintType.Critical)
             Throw New CancelledException
@@ -510,7 +499,7 @@ Retry:
         If VersionName = "" Then VersionName = MyMsgBoxInput("输入版本名称", "", "", New ObjectModel.Collection(Of Validate) From {Validate})
         If String.IsNullOrEmpty(VersionName) Then Throw New CancelledException
         '解压与配置文件
-        Dim InstallTemp As String = PathTemp & "PackInstall\" & RandomInteger(0, 100000) & "\"
+        Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
@@ -593,7 +582,7 @@ Retry:
         If VersionName = "" Then VersionName = MyMsgBoxInput("输入版本名称", "", "", New ObjectModel.Collection(Of Validate) From {Validate})
         If String.IsNullOrEmpty(VersionName) Then Throw New CancelledException
         '解压、配置设置文件
-        Dim InstallTemp As String = $"{PathTemp}PackInstall\{RandomInteger(0, 100000)}\"
+        Dim InstallTemp As String = RequestTaskTempFolder()
         Dim SetupFile As String = $"{PathMcFolder}versions\{VersionName}\PCL\Setup.ini"
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
@@ -613,21 +602,14 @@ Retry:
             Try
                 Dim MMCSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
                 If File.Exists(MMCSetupFile) Then
-                    '部分等号替换为冒号,以符合 ini 文件格式
-                    '为JvmArgs添加排除项,防止参数中的等号被错误的替换
-                    Dim MMCSetupFileLines() As String = File.ReadAllLines(MMCSetupFile)
-                    For i As Integer = 0 To MMCSetupFileLines.Length - 1
-                        '按行读取,检查开头是否为"JvmArgs="
-                        If MMCSetupFileLines(i).StartsWith("JvmArgs=") Then
-                            '有则只替换这个等号,忽略参数部分
-                            MMCSetupFileLines(i) = "JvmArgs:" & MMCSetupFileLines(i).Substring("JvmArgs=".Length)
-                        Else
-                            '无则全部替换
-                            MMCSetupFileLines(i) = MMCSetupFileLines(i).Replace("=", ":")
-                        End If
+                    '将其中的等号替换为冒号,以符合 ini 文件格式
+                    Dim Lines As New List(Of String)
+                    For Each Line In ReadFile(MMCSetupFile).Split({vbCr, vbLf}, StringSplitOptions.RemoveEmptyEntries)
+                        If Not Line.Contains("=") Then Continue For
+                        Lines.Add(Line.BeforeFirst("=") & ":" & Line.AfterFirst("="))
                     Next
-                    '修改后写回文件
-                    File.WriteAllLines(MMCSetupFile, MMCSetupFileLines)
+                    WriteFile(MMCSetupFile, Join(Lines, vbCrLf))
+                    '读取文件
                     If ReadIni(MMCSetupFile, "OverrideCommands", False) Then
                         Dim PreLaunchCommand As String = ReadIni(MMCSetupFile, "PreLaunchCommand")
                         If PreLaunchCommand <> "" Then
@@ -656,16 +638,17 @@ Retry:
                         CopyFile($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png", $"{PathMcFolder}versions\{VersionName}\PCL\Logo.png")
                         Log($"[ModPack] 迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
                     End If
-                    'JvmArgs参数
+                    'JVM 参数
                     Dim JvmArgs As String = ReadIni(MMCSetupFile, "JvmArgs", "")
                     If JvmArgs <> "" Then
-                        WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
-                        Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数:" & JvmArgs)
-                    End If
-                    '这段我看参数有就给加上了...
-                    If ReadIni(MMCSetupFile, "OverrideJavaArgs", False) Then
-                        WriteIni(SetupFile, "VersionAdvanceJavaArgs", True)
-                        Log("[ModPack] 迁移 MultiMC 版本独立设置:覆盖 JavaArgs")
+                        If ReadIni(MMCSetupFile, "OverrideJavaArgs", False) Then
+                            WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
+                            Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数(覆盖):" & JvmArgs)
+                        Else
+                            JvmArgs += " " & Setup.Get("LaunchAdvanceJvm")
+                            WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
+                            Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数(追加):" & JvmArgs)
+                        End If
                     End If
                 End If
             Catch ex As Exception
@@ -740,7 +723,7 @@ Retry:
             If String.IsNullOrEmpty(VersionName) Then Throw New CancelledException
         End If
         '解压与配置文件
-        Dim InstallTemp As String = PathTemp & "PackInstall\" & RandomInteger(0, 100000) & "\"
+        Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
@@ -811,7 +794,6 @@ Retry:
         MyMsgBox("接下来请选择一个空文件夹,它会被安装到这个文件夹里。", "安装", "继续", ForceWait:=True)
         Dim TargetFolder As String = SelectFolder("选择安装目标(必须是一个空文件夹)")
         If String.IsNullOrEmpty(TargetFolder) Then Throw New CancelledException
-        If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Critical) : Throw New CancelledException
         If Directory.GetFileSystemEntries(TargetFolder).Length > 0 Then Hint("请选择一个空文件夹作为安装目标!", HintType.Critical) : Throw New CancelledException
         '解压
         Dim Loader As New LoaderCombo(Of String)("解压压缩包", {
@@ -839,8 +821,9 @@ Retry:
                 '尝试使用附带的启动器打开
                 If Launcher IsNot Nothing Then
                     Log("[Modpack] 找到压缩包中附带的启动器:" & Launcher)
-                    If MyMsgBox($"整合包中似乎自带了启动器,是否换用它继续安装?{vbCrLf}通常推荐这样做,以获得最佳体验。{vbCrLf}即将打开:{Launcher}", "换用整合包启动器?", "继续", "取消") = 1 Then
-                        ShellOnly(Launcher, "--wait")
+                    If MyMsgBox($"整合包里似乎自带了启动器,是否换用它继续安装?{vbCrLf}即将打开:{Launcher}", "换用整合包启动器?", "换用", "不换用") = 1 Then
+                        OpenExplorer(TargetFolder)
+                        ShellOnly(Launcher, "--wait") '要求等待已有的 PCL 退出
                         Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序")
                         FrmMain.EndProgram(False)
                         Return
@@ -848,15 +831,17 @@ Retry:
                 Else
                     Log("[Modpack] 未找到压缩包中附带的启动器")
                 End If
+                OpenExplorer(TargetFolder)
                 '加入文件夹列表
                 Dim VersionName As String = GetFolderNameFromPath(TargetFolder)
+                Directory.CreateDirectory(TargetFolder & ".minecraft\")
                 PageSelectLeft.AddFolder(
-                    TargetFolder & ArchiveBaseFolder.Replace("/", "\").TrimStart("\"), '格式例如:包裹文件夹\.minecraft\(最短为空字符串)
+                    TargetFolder & ".minecraft\" & ArchiveBaseFolder.Replace("/", "\").TrimStart("\"), '格式例如:包裹文件夹\.minecraft\(最短为空字符串)
                     VersionName, False)
                 '调用 modpack 文件进行安装
                 Dim ModpackFile = Directory.GetFiles(TargetFolder, "modpack.*", SearchOption.AllDirectories).First
                 Log("[Modpack] 调用 modpack 文件继续安装:" & ModpackFile)
-                ModpackInstall(ModpackFile, VersionName)
+                ModpackInstall(ModpackFile)
             End Sub)
         })
         Loader.Start(TargetFolder)
@@ -906,340 +891,6 @@ Retry:
         Return Loader
     End Function
 
-#End Region
-#End Region
-
-#Region "导出"
-    Private ExpTempDir As String = PathTemp & "PackExport\"
-    ''' <summary>
-    ''' 整合包类型。
-    ''' </summary>
-    Public Enum ModpackType
-        Modrinth
-    End Enum
-    ''' <summary>
-    ''' 导出整合包的选项。
-    ''' </summary>
-    Public Class ExportOptions
-        ''' <summary>
-        ''' 要导出的版本。
-        ''' </summary>
-        Public Version As McVersion
-        ''' <summary>
-        ''' 要保存整合包的位置。
-        ''' </summary>
-        Public Dest As String
-        ''' <summary>
-        ''' 是否包括 PCL 文件。
-        ''' </summary>
-        Public IncludePCL As Boolean
-        ''' <summary>
-        ''' 整合包类型。
-        ''' </summary>
-        Public Type As ModpackType
-        ''' <summary>
-        ''' 要保留到整合包的文件。
-        ''' </summary>
-        Public Additional As String()
-        ''' <summary>
-        ''' 是否需要保留 PCL 全局设置。仅在包括 PCL 时有效。
-        ''' </summary>
-        Public PCLSetupGlobal As Boolean = True
-        ''' <summary>
-        ''' 整合包名称。
-        ''' </summary>
-        Public Name As String = ""
-        ''' <summary>
-        ''' 整合包描述。
-        ''' </summary>
-        Public Desc As String = ""
-        ''' <summary>
-        ''' 版本号。
-        ''' </summary>
-        Public VerID As String = ""
-        Public Sub New(Version As McVersion,
-                       Dest As String,
-                       IncludePCL As Boolean,
-                       Additional As String(),
-                       Optional PCLSetupGlobal As Boolean = True,
-                       Optional Name As String = "",
-                       Optional Desc As String = "",
-                       Optional VerID As String = "")
-            Me.Version = Version
-            Me.Dest = Dest
-            Me.IncludePCL = IncludePCL
-            Me.Additional = Additional
-            Me.PCLSetupGlobal = PCLSetupGlobal
-            Me.Name = Name
-            Me.Desc = Desc
-            Me.VerID = VerID
-        End Sub
-    End Class
-    ''' <summary>
-    ''' 导出整合包,返回是否成功。
-    ''' </summary>
-    Public Function ModpackExport(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
-        If Task.Input.IncludePCL Then Return ExportCompressed(Task)
-        Return ExportModrinth(Task)
-    End Function
-
-#Region "冗余"
-    Private VersionRedundant As String() = {"^screenshots", "^backups", "^command_history\.txt$", '个人文件
-        "^.*-natives", "^server-resource-packs", "^user.*cache\.json$", "^\.optifine", "^\.fabric", "^\.mixin\.out", '缓存
-        "^.*\.jar$", "^downloads", "^realms_persistence.json$", "^\$\{natives_directory\}", "^essential", '可联网更新
-        "^logs", "^crash-reports", ".*\.log$", "^debug", '日志
-        ".*\.dat_old$", ".*\.old$", '备份
-        "^\$\{quickPlayPath\}", '服务器
-        "^\.replay_cache", "^replay_recordings", "^replay_videos", 'ReplayMod
-        "^irisUpdateInfo\.json$", 'Iris
-        "^modernfix", 'ModernFix 模组
-        "^modtranslations", 'Mod 翻译
-        "^schematics", 'schematics 模组
-        "^journeymap\\data", 'JourneyMap 模组
-        "^mods\\\.connector", '信雅互联
-        "^.*\.BakaCoreInfo$", 'BakaXL 配置
-        "^hmclversion\.cfg$", "^log4j2\.xml$", 'HMCL 配置
-        "^assets$", "^libraries$", "^\$natives$", "^launcher_profiles\.json$", "^versions$" '开启版本隔离时排除的文件
-    }
-    Public Function IsVerRedundant(FilePath As String, PathIndie As String) As Boolean
-        FilePath = FilePath.Replace("/", "\")
-        For Each regex In VersionRedundant
-            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) Then Return True
-        Next
-        Return False
-    End Function
-    Private MustExport As String() = {
-        "mods", "PCL", ".*\.json"
-    }
-    Public Function IsMustExport(FilePath As String, PathIndie As String) As Boolean
-        For Each regex In MustExport
-            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) AndAlso Not (FilePath.EndsWithF(".zip") OrElse FilePath.EndsWithF("\")) Then Return True
-        Next
-        Return False
-    End Function
-
-#End Region
-
-#Region "不同类型整合包的导出方法"
-    Private Function ExportModrinth(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
-        Try
-            Dim Version = Task.Input.Version
-            Dim DestPath = Task.Input.Dest
-            Dim Additional = Task.Input.Additional
-            Dim Name = Task.Input.Name
-            Dim Description = Task.Input.Desc
-            Dim VerID = Task.Input.VerID
-            Log($"[Export] 导出整合包(Modrinth):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个")
-            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
-            Log($"[Export] 缓存文件夹:{tempDir}")
-            DeleteDirectory(tempDir)
-            Directory.CreateDirectory(tempDir)
-
-#Region "0.04-0.40:从 Modrinth 获取 Mod 工程信息,得到 URL"
-            Dim Mods As New Dictionary(Of String, McMod)
-            If Directory.Exists(Version.Path & "mods\") Then
-                For Each m In Directory.EnumerateFiles(Version.Path & "mods\")
-                    If m.EndsWithF(".jar") Then Mods.Add(GetFileNameFromPath(m), New McMod(m))
-                Next
-            End If
-
-            '从 Modrinth 获取信息
-            Dim ModrinthMapping As New Dictionary(Of String, JObject) 'Modrinth 获取到的 Mod
-            Dim CurseForgeMapping As New Dictionary(Of String, JObject) 'CurseForge 获取到的 Mod
-            Dim Failed = Mods.ToArray().ToDictionary(Function(a) a.Key, Function(a) a.Value) '获取失败的 Mod
-            Dim ModrinthHashes = Mods.Select(Function(m) m.Value.ModrinthHash).ToList()
-            If Mods.Count = 0 Then GoTo JumpMod
-            Dim ModrinthRaw = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files", "POST",
-    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
-            Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地 Mod 的对应信息")
-            Task.Progress = 0.35
-
-            For Each Entry In Mods
-                If (Not ModrinthRaw.ContainsKey(Entry.Value.ModrinthHash)) OrElse
-                   (ModrinthRaw(Entry.Value.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.Value.ModrinthHash) Then
-                    Continue For
-                End If
-                Failed.Remove(Entry.Key)
-                ModrinthMapping.Add(Entry.Key, ModrinthRaw(Entry.Value.ModrinthHash)("files")(0))
-            Next
-            Task.Progress = 0.4
-#End Region
-
-#Region "0.40-0.80:从 CurseForge 继续获取"
-            Dim CurseForgeHashes = Mods.Select(Function(m) m.Value.CurseForgeHash).ToList()
-            Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
-                                    $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
-            Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
-            Task.Progress = 0.7
-
-            For Each m In CurseForgeRaw
-                Dim hash As String = ""
-                For Each h In m("file")("hashes") '获取 Modrinth Hash
-                    If h("algo").ToString = 1 Then
-                        hash = h("value")
-                        Exit For
-                    End If
-                Next
-                If String.IsNullOrEmpty(hash) OrElse String.IsNullOrEmpty(m("file")("downloadUrl")) Then Continue For
-
-                m("file")("ModrinthHash") = hash
-                Dim f = Mods.ToArray()
-                For Each file In f
-                    If file.Value.ModrinthHash = hash Then
-                        CurseForgeMapping.Add(file.Key, m("file"))
-                        Failed.Remove(file.Key)
-                        Exit For
-                    End If
-                Next
-            Next
-            Task.Progress = 0.8
-#End Region
-
-JumpMod:
-#Region "0.80-0.90:合并下载链接,复制 Logo,写入 Json 文件"
-            '获取作为检查目标的加载器和版本
-            Dim ModLoaders = GetTargetModLoaders()
-            Dim McVersion = Version.Version.McName
-            Log($"[Export] 目标加载器:{ModLoaders.Join("/")},版本:{McVersion}")
-
-            '合并下载链接
-            Dim BothUrls As New Dictionary(Of String, String()) '两个下载源都有 URL 的 Mod
-            For Each modrinth In ModrinthMapping
-                Dim curseforge As New JObject
-                If CurseForgeMapping.TryGetValue(modrinth.Key, curseforge) Then
-                    BothUrls.Add(modrinth.Key, {modrinth.Value("url"), curseforge("downloadUrl")})
-                End If
-            Next
-            Task.Progress = 0.83
-
-            Dim files As New JArray
-            For Each m In ModrinthMapping
-                files.Add(New JObject From {
-                    {"path", $"mods/{m.Key}"},
-                    {"hashes", m.Value("hashes")},
-                    {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
-                    {"downloads", If(BothUrls.ContainsKey(m.Key), JArray.FromObject(BothUrls(m.Key)), New JArray From {m.Value("url")})},
-                    {"fileSize", m.Value("size")}
-                })
-            Next
-            For Each m In CurseForgeMapping
-                If Not BothUrls.ContainsKey(m.Key) Then
-                    files.Add(New JObject From {
-                        {"path", $"mods/{m.Key}"},
-                        {"hashes", New JObject From {{"sha1", m.Value("ModrinthHash").ToString}}},
-                        {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
-                        {"downloads", New JArray From {m.Value("downloadUrl")}},
-                        {"fileSize", m.Value("fileLength")}
-                    })
-                End If
-            Next
-            Task.Progress = 0.86
-            'If Not String.IsNullOrEmpty(Logo) Then
-            '    Dim LogoObj As New MyBitmap(Logo)
-            '    LogoObj.Save(tempDir & "Logo.png")
-            'End If
-            'Task.Progress = 0.88
-
-            Dim depend As New JObject From {{"minecraft", McVersion}}
-            If Version.Version.HasForge Then depend.Add("forge", Version.Version.ForgeVersion)
-            If Version.Version.HasFabric Then depend.Add("fabric-loader", Version.Version.FabricVersion)
-            If Version.Version.HasNeoForge Then depend.Add("neoforge", Version.Version.NeoForgeVersion)
-
-            Dim json As New JObject From {
-                {"game", "minecraft"},
-                {"formatVersion", 1},
-                {"versionId", VerID},
-                {"name", If(String.IsNullOrEmpty(Name), Version.Name, Name)},
-                {"summary", Description},
-                {"files", files},
-                {"dependencies", depend}
-            }
-            'If Not String.IsNullOrEmpty(Logo) Then json.Add("logo", tempDir & "Logo.png")
-
-            File.WriteAllText(tempDir & "modrinth.index.json", json.ToString)
-            Task.Progress = 0.9
-#End Region
-
-#Region "0.90-0.99:将其余文件保存到 overrides 目录"
-            For Each m In Failed
-                CopyFile(m.Value.Path, tempDir & "overrides\mods\" & m.Value.FileName)
-            Next
-            Task.Progress = 0.92
-
-            '额外文件
-            For Each p In Additional
-                If String.IsNullOrWhiteSpace(p) Then Continue For '传空字串进去会直接把整个版本文件夹拷过去
-                If Not p.StartsWithF(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), IgnoreCase:=True) Then Continue For
-                Dim relative As String = p.Replace(Version.Path, "").Replace(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), "")
-                If File.Exists(p) AndAlso Not IsVerRedundant(p, Version.PathIndie) Then
-                    CopyFile(p, $"{tempDir}overrides\{relative}")
-                End If
-            Next
-            Task.Progress = 0.96
-
-            '如果 resourcepacks shaderpacks 文件夹中有文件夹格式的包,则先将文件夹压缩成 Zip 文件后再打包入对应位置
-            '这可以避免安装时出现路径长度限制而出错
-            If Directory.Exists($"{tempDir}overrides\resourcepacks\") Then
-                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\resourcepacks\")
-                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
-                    DeleteDirectory(d)
-                Next
-            End If
-            If Directory.Exists($"{tempDir}overrides\shaderpacks\") Then
-                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\shaderpacks\")
-                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
-                    DeleteDirectory(d)
-                Next
-            End If
-#End Region
-
-            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
-            ZipFile.CreateFromDirectory(tempDir, DestPath)
-            DeleteDirectory(tempDir)
-            Return True
-        Catch ex As Exception
-            Log(ex, "导出整合包失败", LogLevel.Msgbox)
-            Return False
-        End Try
-    End Function
-    Private Function ExportCompressed(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
-        Try
-            Dim Version = Task.Input.Version
-            Dim DestPath = Task.Input.Dest
-            Dim Additional = Task.Input.Additional
-            Dim Name = Task.Input.Name
-            Dim VerID = Task.Input.VerID
-            Dim PCLSetupGlobal = Task.Input.PCLSetupGlobal
-            Log($"[Export] 导出整合包(含启动器):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个,全局设置 {If(PCLSetupGlobal, "导出", "不导出")}")
-            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
-            Log($"[Export] 最终压缩包的缓存文件夹:{tempDir}")
-            Directory.CreateDirectory(tempDir)
-
-            Task.Progress = 0.04
-
-            Log("[Export] 开始导出 Modrinth 整合包")
-            Task.Input.Dest = $"{tempDir}modpack.mrpack"
-            ExportModrinth(Task)
-
-            Log($"[Export] 正在复制 PCL 本体")
-            CopyFile(PathWithName, tempDir & GetFileNameFromPath(PathWithName))
-
-            If PCLSetupGlobal Then
-                Log($"[Export] 正在复制 PCL 全局配置")
-                CopyFile(Path & "PCL\Setup.ini", tempDir & "PCL\Setup.ini")
-            End If
-
-            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
-            ZipFile.CreateFromDirectory(tempDir, DestPath)
-            DeleteDirectory(tempDir)
-            Return True
-        Catch ex As Exception
-            Log(ex, "导出整合包失败", LogLevel.Msgbox)
-            Return False
-        End Try
-    End Function
-#End Region
-
 #End Region
 
 End Module

+ 18 - 8
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -20,7 +20,7 @@
                     Hint("正在开启中,请稍候……")
                     OpenWebsite(Data(0))
 
-                Case "打开文件", "打开帮助"
+                Case "打开文件", "打开帮助", "执行命令"
                     RunInThread(
                     Sub()
                         Try
@@ -29,15 +29,25 @@
                             Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
                             Log($"[Control] 打开类自定义事件实际路径:{Location},工作目录:{WorkingDir}")
                             '执行
-                            If Type = "打开文件" Then
+                            If Type = "打开帮助" Then
+                                PageOtherHelp.EnterHelpPage(Location)
+                            Else
+                                If Not Setup.Get("HintCustomCommand") Then
+                                    Select Case MyMsgBox(
+                                    "即将执行:" & Location & If(Data.Length >= 2, " " & Data(1), "") & vbCrLf &
+                                    "请在确认该操作没有安全隐患后继续。", "执行确认", "继续", "继续且今后不再要求确认", "取消")
+                                        Case 2
+                                            Setup.Set("HintCustomCommand", True)
+                                        Case 3
+                                            Exit Sub
+                                    End Select
+                                End If
                                 Dim Info As New ProcessStartInfo With {
                                     .Arguments = If(Data.Length >= 2, Data(1), ""),
                                     .FileName = Location,
-                                    .WorkingDirectory = WorkingDir
+                                    .WorkingDirectory = ShortenPath(WorkingDir)
                                 }
                                 Process.Start(Info)
-                            Else '打开帮助
-                                PageOtherHelp.EnterHelpPage(Location)
                             End If
                         Catch ex As Exception
                             Log(ex, "执行打开类自定义事件失败", LogLevel.Msgbox)
@@ -140,8 +150,8 @@
             Dim LocalTemp2 As String = PathTemp & "CustomEvent\" & RawFileName.Replace(".json", ".xaml")
             Log("[Event] 转换网络资源:" & RelativeUrl & " -> " & LocalTemp1)
             Try
-                NetDownload(RelativeUrl, LocalTemp1)
-                NetDownload(RelativeUrl.Replace(".json", ".xaml"), LocalTemp1.Replace(".json", ".xaml"))
+                NetDownloadByClient(RelativeUrl, LocalTemp1)
+                NetDownloadByClient(RelativeUrl.Replace(".json", ".xaml"), LocalTemp1.Replace(".json", ".xaml"))
             Catch ex As Exception
                 Throw New Exception("下载指定的文件失败!" & vbCrLf &
                                     "注意,联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" & vbCrLf &
@@ -174,7 +184,7 @@
             Location = PathTemp & "Help\" & RelativeUrl
             WorkingDir = PathTemp & "Help\"
             Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" & EventType & ":" & Location)
-        ElseIf EventType = "打开文件" Then
+        ElseIf EventType = "打开文件" OrElse EventType = "执行命令" Then
             '直接使用原有路径启动程序
             Location = RelativeUrl
             Log("[Control] 自定义事件中直接" & EventType & ":" & Location)

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

@@ -811,6 +811,54 @@ NextFile:
     Public Declare Function SetForegroundWindow Lib "user32" (hWnd As IntPtr) As Integer
     Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" (hWnd As IntPtr, msg As UInteger, wParam As Long, lParam As Long) As Boolean
 
+#End Region
+
+#Region "任务缓存"
+
+    Private IsTaskTempCleared As Boolean = False
+    Private IsTaskTempClearing As Boolean = False
+
+    ''' <summary>
+    ''' 尝试清理任务缓存文件夹。
+    ''' 在整次运行中只会实际清理一次。
+    ''' </summary>
+    Public Sub TryClearTaskTemp()
+        If Not IsTaskTempCleared Then
+            IsTaskTempCleared = True
+            IsTaskTempClearing = True
+            Try
+                Log("[System] 开始清理任务缓存文件夹")
+                DeleteDirectory($"{OsDrive}ProgramData\PCL\TaskTemp\")
+                DeleteDirectory($"{PathTemp}TaskTemp\")
+                Log("[System] 已清理任务缓存文件夹")
+            Catch ex As Exception
+                Log(ex, "清理任务缓存文件夹失败")
+            Finally
+                IsTaskTempClearing = False
+            End Try
+        ElseIf IsTaskTempClearing Then
+            '等待另一个清理步骤完成
+            Do While IsTaskTempClearing
+                Thread.Sleep(1)
+            Loop
+        End If
+    End Sub
+
+    ''' <summary>
+    ''' 申请一个可用于任务缓存的临时文件夹,以 \ 结尾。
+    ''' 这些文件夹无需进行后续清理。
+    ''' </summary>
+    ''' <param name="RequireNonSpace">是否要求路径不包含空格。</param>
+    Public Function RequestTaskTempFolder(Optional RequireNonSpace As Boolean = False) As String
+        TryClearTaskTemp()
+        If RequireNonSpace Then
+            RequestTaskTempFolder = $"{OsDrive}ProgramData\PCL\Install\{GetUuid()}-{RandomInteger(0, 1000000)}\"
+        Else
+            RequestTaskTempFolder = $"{PathTemp}TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
+        End If
+        Directory.CreateDirectory(RequestTaskTempFolder)
+    End Function
+
 #End Region
 
     Public DragControl = Nothing

+ 11 - 4
Plain Craft Launcher 2/Modules/ModSecret.vb

@@ -246,18 +246,18 @@ Friend Module ModSecret
             FrmMain.PanForm.Background.Freeze()
         End Sub)
     End Sub
-    Public Sub ThemeCheckAll(EffectSetup As Boolean)
+    Friend Sub ThemeCheckAll(EffectSetup As Boolean)
     End Sub
-    Public Function ThemeCheckOne(Id As Integer) As Boolean
+    Friend Function ThemeCheckOne(Id As Integer) As Boolean
         Return True
     End Function
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
         Return False
     End Function
-    Public Function ThemeCheckGold(Optional Code As String = Nothing) As Boolean
+    Friend Function ThemeCheckGold(Optional Code As String = Nothing) As Boolean
         Return False
     End Function
-    Public Function DonateCodeInput() As Boolean?
+    Friend Function DonateCodeInput() As Boolean?
         Return Nothing
     End Function
 
@@ -276,6 +276,13 @@ Friend Module ModSecret
     End Sub
     Public Sub UpdateReplace(ProcessId As Integer, OldFileName As String, NewFileName As String, TriggerRestart As Boolean)
     End Sub
+    ''' <summary>
+    ''' 确保 PathTemp 下的 Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
+    ''' 如果不是,则下载一个。
+    ''' </summary>
+    Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
+        '注意:如果要自行实现这个功能,请换用另一个文件路径,以免与官方版本冲突
+    End Sub
 
 #End Region
 

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

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

+ 25 - 40
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -87,6 +87,7 @@ Public Module ModDownloadLib
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析原版支持库文件(副加载器)",
         Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+            Thread.Sleep(50) '等待 JSON 文件实际写入硬盘(#3710)
             Log("[Download] 开始分析原版支持库文件:" & VersionFolder)
             Task.Output = McLibFix(New McVersion(VersionFolder))
         End Sub) With {.ProgressWeight = 1, .Show = False})
@@ -271,7 +272,7 @@ pause"
             Loaders.Add(New LoaderDownload("下载服务端文件", New List(Of NetFile)) With {.ProgressWeight = 5})
 
             '启动
-            Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 服务端下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 服务端下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(Id)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -316,7 +317,7 @@ pause"
             Loaders.Add(New LoaderDownload("下载核心 JAR 文件", New List(Of NetFile)) With {.ProgressWeight = 5})
 
             '启动
-            Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(Id)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -394,7 +395,7 @@ pause"
     Private Sub McDownloadOptiFineSave(DownloadInfo As DlOptiFineListEntry)
         Try
             Dim Id As String = DownloadInfo.NameVersion
-            Dim Target As String = SelectAs("选择保存位置", DownloadInfo.NameFile, "OptiFine Jar (*.jar)|*.jar")
+            Dim Target As String = SelectSaveFile("选择保存位置", DownloadInfo.NameFile, "OptiFine Jar (*.jar)|*.jar")
             If Not Target.Contains("\") Then Exit Sub
 
             '重复任务检查
@@ -404,7 +405,7 @@ pause"
                 Exit Sub
             Next
 
-            Dim Loader As New LoaderCombo(Of DlOptiFineListEntry)("OptiFine " & DownloadInfo.NameDisplay & " 下载", McDownloadOptiFineSaveLoader(DownloadInfo, Target)) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader As New LoaderCombo(Of DlOptiFineListEntry)("OptiFine " & DownloadInfo.NameDisplay & " 下载", McDownloadOptiFineSaveLoader(DownloadInfo, Target)) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(DownloadInfo)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -454,7 +455,7 @@ pause"
                 .CreateNoWindow = True,
                 .RedirectStandardError = True,
                 .RedirectStandardOutput = True,
-                .WorkingDirectory = BaseMcFolderHome
+                .WorkingDirectory = ShortenPath(BaseMcFolderHome)
             }
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
                 Info.EnvironmentVariables("appdata") = BaseMcFolderHome
@@ -857,7 +858,7 @@ Retry:
     Private Sub McDownloadLiteLoaderSave(DownloadInfo As DlLiteLoaderListEntry)
         Try
             Dim Id As String = DownloadInfo.Inherit
-            Dim Target As String = SelectAs("选择保存位置", DownloadInfo.FileName.Replace("-SNAPSHOT", ""), "LiteLoader 安装器 (*.jar)|*.jar")
+            Dim Target As String = SelectSaveFile("选择保存位置", DownloadInfo.FileName.Replace("-SNAPSHOT", ""), "LiteLoader 安装器 (*.jar)|*.jar")
             If Not Target.Contains("\") Then Exit Sub
 
             '重复任务检查
@@ -893,7 +894,7 @@ Retry:
             End If
             Loaders.Add(New LoaderDownload("下载主文件", New List(Of NetFile) From {New NetFile(Address.ToArray, Target, New FileChecker(MinSize:=1024 * 1024))}) With {.ProgressWeight = 15})
             '启动
-            Dim Loader As New LoaderCombo(Of DlLiteLoaderListEntry)("LiteLoader " & Id & " 安装器下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader As New LoaderCombo(Of DlLiteLoaderListEntry)("LiteLoader " & Id & " 安装器下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(DownloadInfo)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -1037,7 +1038,7 @@ Retry:
 
     Public Sub McDownloadForgelikeSave(Info As DlForgelikeEntry)
         Try
-            Dim Target As String = SelectAs("选择保存位置", $"{Info.LoaderName}-{Info.Inherit}-{Info.VersionName}.{Info.FileExtension}",
+            Dim Target As String = SelectSaveFile("选择保存位置", $"{Info.LoaderName}-{Info.Inherit}-{Info.VersionName}.{Info.FileExtension}",
                                             $"{Info.LoaderName} 安装器 (*.{Info.FileExtension})|*.{Info.FileExtension}")
             Dim DisplayName As String = $"{Info.LoaderName} {Info.Inherit} - {Info.VersionName}"
             If Not Target.Contains("\") Then Exit Sub
@@ -1072,7 +1073,7 @@ Retry:
             Loaders.Add(New LoaderDownload("下载主文件", Files) With {.ProgressWeight = 6})
 
             '启动
-            Dim Loader = New LoaderCombo(Of DlForgelikeEntry)(DisplayName & " 下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader = New LoaderCombo(Of DlForgelikeEntry)(DisplayName & " 下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(Info)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -1339,7 +1340,7 @@ Retry:
                     If Json("data") IsNot Nothing AndAlso Json("data")("MOJMAPS") IsNot Nothing Then
                         '下载原版 Json 文件
                         Task.Progress = 0.4
-                        Dim RawJson As JObject = GetJson(NetGetCodeByDownload(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
+                        Dim RawJson As JObject = GetJson(NetGetCodeByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg]
                         Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@")
                         Dim Address = McLibGet(OriginalName).Replace(".jar", "-mappings." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
@@ -1641,7 +1642,7 @@ Retry:
         RunInNewThread(Sub()
                            Try
                                Log("[Download] 刷新 Forge 推荐版本缓存开始")
-                               Dim Result As String = NetGetCodeByDownload("https://bmclapi2.bangbang93.com/forge/promos")
+                               Dim Result As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/forge/promos")
                                If Result.Length < 1000 Then Throw New Exception("获取的结果过短(" & Result & ")")
                                Dim ResultJson As JContainer = GetJson(Result)
                                '获取所有推荐版本列表
@@ -1789,7 +1790,7 @@ Retry:
             Dim Url As String = DownloadInfo("url").ToString
             Dim FileName As String = GetFileNameFromPath(Url)
             Dim Version As String = GetFileNameFromPath(DownloadInfo("version").ToString)
-            Dim Target As String = SelectAs("选择保存位置", FileName, "Fabric 安装器 (*.jar)|*.jar")
+            Dim Target As String = SelectSaveFile("选择保存位置", FileName, "Fabric 安装器 (*.jar)|*.jar")
             If Not Target.Contains("\") Then Exit Sub
 
             '重复任务检查
@@ -1807,7 +1808,7 @@ Retry:
             Address.Add(Url)
             Loaders.Add(New LoaderDownload("下载主文件", New List(Of NetFile) From {New NetFile(Address.ToArray, Target, New FileChecker(MinSize:=1024 * 64))}) With {.ProgressWeight = 15})
             '启动
-            Dim Loader As New LoaderCombo(Of JObject)("Fabric " & Version & " 安装器下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader As New LoaderCombo(Of JObject)("Fabric " & Version & " 安装器下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Loader.Start(DownloadInfo)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
@@ -1975,29 +1976,26 @@ Retry:
     End Class
 
     ''' <summary>
-    ''' 安装加载器状态改变后进行提示和重载文件夹列表的方法。
+    ''' 在加载器状态改变后显示一条提示。
+    ''' 不会进行任何其他操作。
     ''' </summary>
-    Public Sub McInstallState(Loader)
+    Public Sub LoaderStateChangedHintOnly(Loader)
         Select Case Loader.State
             Case LoadState.Finished
-                WriteIni(PathMcFolder & "PCL.ini", "VersionCache", "") '清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存)
                 Hint(Loader.Name & "成功!", HintType.Finish)
             Case LoadState.Failed
                 Hint(Loader.Name & "失败:" & GetExceptionSummary(Loader.Error), HintType.Critical)
             Case LoadState.Aborted
                 Hint(Loader.Name & "已取消!", HintType.Info)
-            Case LoadState.Loading
-                Exit Sub '不重新加载版本列表
         End Select
-        McInstallFailedClearFolder(Loader)
-        LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
     End Sub
     ''' <summary>
-    ''' 安装加载器状态改变后进行提示的方法。它不会在失败时删除文件夹,也不会刷新 MC 文件夹。
+    ''' 安装加载器状态改变后进行提示和重载文件夹列表的方法。
     ''' </summary>
-    Public Sub DownloadStateSave(Loader)
+    Public Sub McInstallState(Loader)
         Select Case Loader.State
             Case LoadState.Finished
+                WriteIni(PathMcFolder & "PCL.ini", "VersionCache", "") '清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存)
                 Hint(Loader.Name & "成功!", HintType.Finish)
             Case LoadState.Failed
                 Hint(Loader.Name & "失败:" & GetExceptionSummary(Loader.Error), HintType.Critical)
@@ -2006,6 +2004,8 @@ Retry:
             Case LoadState.Loading
                 Exit Sub '不重新加载版本列表
         End Select
+        McInstallFailedClearFolder(Loader)
+        LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
     End Sub
     Public Sub McInstallFailedClearFolder(Loader)
         Try
@@ -2053,26 +2053,12 @@ Retry:
     ''' <exception cref="CancelledException" />
     Public Function McInstallLoader(Request As McInstallRequest, Optional DontFixLibraries As Boolean = False) As List(Of LoaderBase)
         '获取缓存目录
-        Dim PathInstallTemp As String
-        If PathTemp.Contains(" ") AndAlso (Request.OptiFineEntry IsNot Nothing OrElse Request.ForgeEntry IsNot Nothing OrElse Request.NeoForgeEntry IsNot Nothing) Then
-            PathInstallTemp = OsDrive & "ProgramData\PCL\Install\"
-        Else
-            PathInstallTemp = PathTemp & "Install\"
-        End If
-
-        '清理缓存
-        Try
-            If IsInstallTempCleared Then Exit Try
-            IsInstallTempCleared = True
-            DeleteDirectory(PathInstallTemp, True)
-            Log("[Download] 已清理合并安装缓存")
-        Catch ex As Exception
-            Log(ex, "清理合并安装缓存失败")
-        End Try
+        Dim TempMcFolder As String = RequestTaskTempFolder(
+            PathTemp.Contains(" ") AndAlso '要求路径不包含空格
+            (Request.OptiFineEntry IsNot Nothing OrElse Request.ForgeEntry IsNot Nothing OrElse Request.NeoForgeEntry IsNot Nothing))
 
         '获取参数
         Dim OutputFolder As String = PathMcFolder & "versions\" & Request.TargetVersionName & "\"
-        Dim TempMcFolder As String = PathInstallTemp & Request.TargetVersionName.GetHashCode() & "\"
         If Directory.Exists(TempMcFolder) Then DeleteDirectory(TempMcFolder)
         Dim OptiFineFolder As String = Nothing
         If Request.OptiFineVersion IsNot Nothing Then
@@ -2188,7 +2174,6 @@ Retry:
         '总加载器
         Return LoaderList
     End Function
-    Private IsInstallTempCleared As Boolean = False
 
     ''' <summary>
     ''' 将多个版本 Json 进行合并,如果目标已存在则直接覆盖。失败会抛出异常。

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

@@ -92,7 +92,6 @@
                 NewButton.LabText.Margin = New Thickness(-2, 0, 8, 0)
                 AddHandler NewButton.Check,
                 Sub(sender As MyRadioButton, raiseByMouse As Boolean)
-                    PanScroll.ScrollToHome()
                     VersionFilter = If(sender.Text = "全部", Nothing, sender.Text)
                     UpdateFilterResult()
                 End Sub
@@ -379,7 +378,7 @@
                 Sub()
                     '弹窗要求选择保存位置
                     Dim Target As String
-                    Target = SelectAs("选择保存位置", FileName,
+                    Target = SelectSaveFile("选择保存位置", FileName,
                         Desc & "文件|" &
                         If(Project.Type = CompType.Mod,
                             If(File.FileName.EndsWith(".litemod"), "*.litemod", "*.jar"),
@@ -391,7 +390,7 @@
                     Dim Loaders As New List(Of LoaderBase)
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})
                     '启动
-                    Dim Loader As New LoaderCombo(Of Integer)(LoaderName, Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+                    Dim Loader As New LoaderCombo(Of Integer)(LoaderName, Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
                     Loader.Start(1)
                     LoaderTaskbarAdd(Loader)
                     FrmMain.BtnExtraDownload.ShowRefresh()
@@ -410,7 +409,7 @@
         OpenWebsite("https://www.mcmod.cn/class/" & Project.WikiId & ".html")
     End Sub
     Private Sub BtnIntroCopy_Click(sender As Object, e As EventArgs) Handles BtnIntroCopy.Click
-        ClipboardSet(CompItem.LabTitle.Text)
+        ClipboardSet(CompItem.LabTitle.Text & CompItem.LabTitleRaw.Text)
     End Sub
 
 End Class

+ 5 - 3
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb

@@ -64,9 +64,11 @@
         End If
 
         '如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复
-        For Each Card In GetAllAnimControls(PanSelect)
-            Card.Opacity = 1
-            Card.RenderTransform = New TranslateTransform
+        For Each Control In GetAllAnimControls(PanSelect)
+            Control.Opacity = 1
+            If Control.RenderTransform Is Nothing OrElse TypeOf Control.RenderTransform Is TranslateTransform Then
+                Control.RenderTransform = New TranslateTransform
+            End If
         Next
 
         '启动 Forge 加载

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

@@ -52,7 +52,7 @@
             Exit Sub
         End If
         Try
-            Dim FileAddress As String = SelectAs("选取保存皮肤的位置", GetFileNameFromPath(Address), "皮肤图片文件(*.png)|*.png")
+            Dim FileAddress As String = SelectSaveFile("选取保存皮肤的位置", GetFileNameFromPath(Address), "皮肤图片文件(*.png)|*.png")
             If FileAddress.Contains("\") Then
                 File.Delete(FileAddress)
                 If Address.StartsWith(PathImage) Then

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

@@ -53,10 +53,10 @@
             '自动整合包安装
             If PackInstallPath IsNot Nothing Then
                 Try
-                    Dim InstallLoader = ModpackInstall(PackInstallPath, GetFolderNameFromPath(Path))
+                    Dim InstallLoader = ModpackInstall(PackInstallPath)
                     Log("[Launch] 自动安装整合包已开始:" & PackInstallPath)
                     InstallLoader.WaitForExit()
-                    If InstallLoader.State = LoadState.Finished Then
+                    If InstallLoader.State = LoadState.Finished AndAlso File.Exists(PackInstallPath) Then
                         Log("[Launch] 自动安装整合包成功,删除安装包:" & PackInstallPath)
                         File.Delete(PackInstallPath)
                     End If

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

@@ -234,7 +234,7 @@ Download:
                 PanCustom.Children.Add(GetObjectFromXML(Content))
             Catch ex As Exception
                 Log("[Page] 加载失败的自定义主页内容:" & vbCrLf & Content)
-                If MyMsgBox($"自定义主页内容编写有误,请根据下列错误信息进行检查:{vbCrLf}{ex.Message}", "加载自定义主页失败", "重试", "取消") = 1 Then
+                If MyMsgBox($"自定义主页内容编写有误,请根据下列错误信息进行检查:{vbCrLf}{GetExceptionSummary(ex)}", "加载自定义主页失败", "重试", "取消") = 1 Then
                     GoTo Refresh '防止 SyncLock 死锁
                 End If
             End Try

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

@@ -113,6 +113,7 @@ Retry:
                 '获取新皮肤地址
                 Log("[Skin] 皮肤修改返回值:" & vbCrLf & Result)
                 Dim ResultJson As JObject = GetJson(Result)
+                If ResultJson.ContainsKey("errorMessage") Then Throw New Exception(ResultJson("errorMessage").ToString) '#5309
                 For Each Skin As JObject In ResultJson("skins")
                     If Skin("state").ToString = "ACTIVE" Then
                         MySkin.ReloadCache(Skin("url"))

+ 186 - 111
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -104,120 +104,195 @@
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="赞助者">
                 <StackPanel Margin="25,40,10,20">
-                    <TextBlock HorizontalAlignment="Left" Text="以下是 2024 年 12 月的高级赞助者们!感谢各位对 PCL 和伟大的咕咕咕事业的支持!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
+                    <TextBlock HorizontalAlignment="Left" Text="以下是 2025 年 1 月的高级赞助者们!感谢各位对 PCL 和伟大的咕咕咕事业的支持!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
                     <WrapPanel Margin="0,0,0,10">
+                        <TextBlock Width="120" Margin="0,1.5" Text="是阿改哦" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="dingyaa1" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="李白" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Xpinger" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="执念" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="张一凡" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="yu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="quer" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="SHALLOW_SUMMER" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="AnLingHai" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="The_Ren" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="阿彪" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="汤圆不胖" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="凑热闹的顶针" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="德凯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="凌肆玖" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="破星辰只寻你" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="飞猫FlyingCat" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Jethro_Y" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="浩天" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="redston_8788" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="羽毛球awa" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="maximum  zombie" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="可塔Kita" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="染桐" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="飞蛙" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="团长" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="妙天纸袋君" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="YJL" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="天水" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Anonymism" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="佳明" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小创世peng" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="炽热的钢铁" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小邓子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="QianYirui" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Korica" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="司徒牺痕" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="流萤" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="神愆" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="44379" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="cool" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白凌" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="弓虽女干亻尔女马" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="是雾霭啊233" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小豆" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="整天摸鱼的Dx" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LEVI" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="虚空" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Mako" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="禾鳥" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="MILA_326" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="韩大仙尊" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="僭苏寒" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="福渡恒" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Yu氚" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="限制_limited" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="麦克斯韦妖" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="承挨" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="略略略zbc" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="CYOSK" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Atmoon" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="DBTB" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Bongocat1337" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="AD_AIR" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="KenshenLove" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="晚柠" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="H.Jane" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="tanolin" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="乐高漫画君" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白起床了" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ClassicNT" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="土地的誓言" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="nanamik" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="阿宸大大" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="渡边贤人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="千羽星语" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="喵~" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="路过的没心人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="牢轮" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Littleduan" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Sta_Rain_Wx" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="大西几" />
                         <TextBlock Width="120" Margin="0,1.5" Text="阿道" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Smile_Cindy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Yi_Zhi" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="耿一闲" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="我是露娜的√" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="EagleEye" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Cherry林" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="心海鱼豆腐" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="墨染" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="物流2051 朱叶" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="二月霜" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="祈喵" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="阿斯塔" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="夜长梦梦多" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="汐洺ao" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="塞巴斯蒂安" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="琅山先生" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="123" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Alans1gn" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="迷茫LTQ" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Raveniume" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="D六菌" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="WHITE" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="warmaQwQ" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Haroinfather" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="赫乐瑢音" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="陈坤城" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="史莱姆SAMA" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ciallp" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="步衷" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雨沫" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Tabula" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="黑宇" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="awawa" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Yi_Joy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="津竹捏" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LinusWu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LizTtxs" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="徐正阳" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Nxvblack" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="夏弥·零" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="诗云白" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="danall" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="TySNow_" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="悲戚霜雪" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="GThistle-Frddy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="INTOMETE." />
+                        <TextBlock Width="120" Margin="0,1.5" Text="千秋" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="原来这里是四宫啊" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="TianDLG" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="隐”" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="瑶龍龘" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小小" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="憨憨游云" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="名字要六个字" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="牢大是我" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="伊卡律泽斯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="stars.夜冰" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="匿名用户" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Fancy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="唯一白色" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="伊利丹的父亲" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="千星yousa" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Axet." />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Ares" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ding_i" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="林逝a" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="wihz" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="11034" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="xi0st" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="空无" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Andywyl" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="g~m" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雁枫" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="YX修行" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雇佣兵v" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="背着氧气的鱼" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="......." />
+                        <TextBlock Width="120" Margin="0,1.5" Text="艾因·斯芬克斯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="威宝" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="真言之归" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="奈斯特" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="青春没有售价,扣一复活牢大" />
                         <TextBlock Width="120" Margin="0,1.5" Text="GuZhuJing" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="情鼠啾啾" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="蛇蛇W" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="蜜汁青柠檬" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="纸先生" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CLOUD079" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="xiaoioax" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CH土块警长" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CHENJUN551" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ezzo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="夜抱?吃火炉" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Rn_Og" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="六个芙桃" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="月月月子喵" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Kuang3lian" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="烨芽" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="王ZEAN" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CCYY" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="19921454753" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="房非缝" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ashburn" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="绯念" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="PseudoStar" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="符之渡尘" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="singegg" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="白泽麒麟" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="拥抱烟霞不太专心" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="风起" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="白羽" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="所求化如愿" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小小抱枕" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Horrizen" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="浮光" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="songhy" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="任我被回忆捉弄" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="飘飘ppiao" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="EnerG4" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="tiredl" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="有梦想的罐头" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Hasturnyth" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="犇犇" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="初晴" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="just ugly" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="宇ovo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="斑马" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="? AXH" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="璃音233" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="凌源羡辰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="雨巷" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="NoDoors" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="幕若" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="星期六" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="感念" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="凉城uwu" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="栀玖夕夏" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="X11aou" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Bobo_s1" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="玛卡巴卡" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鸱影" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="一曲明清" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="曹予" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="kk" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="XDDDD" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="梵星" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="梦师A" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="傻傻" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="西柚味儿小猫" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="樱花成雨(烟雨)" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="瑞云生" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="桑什么鸟" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="桂花" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="stone devil" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="乄心" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="渡白" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鬼鬼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="菠萝飞走惹" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="陆山君" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="张无忧" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="缄默" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="影铉" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="酸菜鱼鱼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="哈哈哈嗝" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鸡逆苔煤" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="许柯" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="ChuaLinyZ" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="墨依小夕" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="柚可喝" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="m1350096708" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="猫南北丶QAQ" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="此时彼刻" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Yukino" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Paveway" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="明月栞那" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="默默无为" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="屑喵" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="枫雪" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="孩子气" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="纳西妲的狗" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Barb" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小镰刀菌" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="DiceA" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="朴振澈" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="YinYang_SHI" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="网底游鱼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="xc_xiaoxian" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="playermin" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="祈听神命" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="嘎子" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="柠檬爱恰柠檬水" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="游戏人生" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Lunaunde" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="UKasumigaoka" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="梦不依" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="绫波丽" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="桃子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="方村明" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="苏离" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="EnergyBeta" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="邵寒洛" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雨下雫" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="HoshinoAlice" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="1121" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="1742" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="奥顿" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LYSGBZL" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="X_Lu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="细雪" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="虎牙该隐" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="坎儿井窖藏咸鱼" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ClarkeTAT" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="matin" />
                     </WrapPanel>
                     <Grid Height="35">
                         <Grid.ColumnDefinitions>

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

@@ -171,6 +171,9 @@
             Log(ex, "添加文件夹失败(" & NewFolder & ")", LogLevel.Feedback)
         End Try
     End Sub
+    ''' <summary>
+    ''' 将指定文件夹添加到 Minecraft 文件夹列表,并选中它。
+    ''' </summary>
     Public Shared Sub AddFolder(FolderPath As String, DisplayName As String, ShowHint As Boolean)
         RunInThread(
         Sub()
@@ -328,23 +331,24 @@
             Next
             Setup.Set("LaunchFolders", If(Not Folders.Any(), "", Join(Folders.ToArray, "|")))
         End If
-        RunInNewThread(Sub()
-                           '删除文件夹
-                           Try
-                               Hint("正在" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Info)
-                               DeleteDirectory(Folder.Path)
-                               If DeleteText = "清空" Then Directory.CreateDirectory(Folder.Path)
-                               Hint("已" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Finish)
-                           Catch ex As Exception
-                               Log(ex, DeleteText & "文件夹 " & Folder.Name & " 失败", LogLevel.Hint)
-                           Finally
-                               '刷新列表
-                               McFolderListLoader.Start(IsForceRestart:=True)
-                           End Try
-                       End Sub, "Folder Delete " & GetUuid(), ThreadPriority.BelowNormal)
+        RunInNewThread(
+        Sub()
+            '删除文件夹
+            Try
+                Hint("正在" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Info)
+                DeleteDirectory(Folder.Path)
+                If DeleteText = "清空" Then Directory.CreateDirectory(Folder.Path)
+                Hint("已" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Finish)
+            Catch ex As Exception
+                Log(ex, DeleteText & "文件夹 " & Folder.Name & " 失败", LogLevel.Hint)
+            Finally
+                '刷新列表
+                McFolderListLoader.Start(IsForceRestart:=True)
+            End Try
+        End Sub, "Folder Delete " & GetUuid(), ThreadPriority.BelowNormal)
     End Sub
     Public Sub Open_Click(sender As Object, e As RoutedEventArgs)
-        OpenExplorer("""" & CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Info & """")
+        OpenExplorer(CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Info)
     End Sub
     Public Sub Refresh_Click(sender As Object, e As RoutedEventArgs)
         Dim Data As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag

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

@@ -20,11 +20,13 @@
         {"HintBuy", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintClearRubbish", New SetupEntry(0, Source:=SetupSource.Registry)},
         {"HintUpdateMod", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintCustomCommand", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"TestSetupReader", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemEula", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"SystemCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLaunchCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLastVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemHighestSavedBetaVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemHighestBetaVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemHighestAlphaVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemSetupVersionReg", New SetupEntry(VersionSetup, Source:=SetupSource.Registry)},
@@ -37,6 +39,7 @@
         {"SystemSystemCache", New SetupEntry("", Source:=SetupSource.Registry)},
         {"SystemSystemUpdate", New SetupEntry(0)},
         {"SystemSystemActivity", New SetupEntry(0)},
+        {"CacheExportConfig", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageUrl", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageVersion", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheMsOAuthRefresh", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},

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

@@ -165,7 +165,7 @@
     Private Sub BtnSkinDelete_Click(sender As Object, e As EventArgs) Handles BtnSkinDelete.Click
         Try
             File.Delete(PathAppdata & "CustomSkin.png")
-            RadioSkinType0.SetChecked(True, True, True)
+            RadioSkinType0.SetChecked(True, True)
             Hint("离线皮肤已清空!", HintType.Finish)
         Catch ex As Exception
             Log(ex, "清空离线皮肤失败", LogLevel.Msgbox)
@@ -194,11 +194,11 @@
         If LabRamGame Is Nothing OrElse LabRamUsed Is Nothing OrElse FrmMain.PageCurrent <> FormMain.PageType.Setup OrElse FrmSetupLeft.PageID <> FormMain.PageSubType.SetupLaunch Then Exit Sub
         '获取内存情况
         Dim RamGame As Double = GetRam(McVersionCurrent, False)
-        Dim RamTotal As Double = Math.Round(My.Computer.Info.TotalPhysicalMemory / 1024 / 1024 / 1024 * 10) / 10
-        Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024 * 10) / 10
-        Dim RamGameActual As Double = Math.Min(RamGame, RamAvailable)
-        Dim RamUsed As Double = RamTotal - RamAvailable
-        Dim RamEmpty As Double = Math.Round(MathClamp(RamTotal - RamUsed - RamGame, 0, 1000) * 10) / 10
+        Dim RamTotal As Double = Math.Round(My.Computer.Info.TotalPhysicalMemory / 1024 / 1024 / 1024, 1)
+        Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024, 1)
+        Dim RamGameActual As Double = Math.Round(Math.Min(RamGame, RamAvailable), 5)
+        Dim RamUsed As Double = Math.Round(RamTotal - RamAvailable, 5)
+        Dim RamEmpty As Double = Math.Round(MathClamp(RamTotal - RamUsed - RamGame, 0, 1000), 1)
         '设置最大可用内存
         If RamTotal <= 1.5 Then
             SliderRamCustom.MaxValue = Math.Max(Math.Floor((RamTotal - 0.3) / 0.1), 1)

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

@@ -45,11 +45,6 @@
     End Sub
 
     Private Sub BtnHiperLog_Click(sender As Object, e As EventArgs) Handles BtnHiperLog.Click
-        If File.Exists(PageLinkHiper.PathHiper & "logs\hiper.log") Then
-            OpenExplorer("/select,""" & PageLinkHiper.PathHiper & "logs\hiper.log""")
-        Else
-            Hint("没有找到 HiPer 联机模块的日志!")
-        End If
     End Sub
 
 End Class

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

@@ -179,7 +179,7 @@
                             <ColumnDefinition />
                         </Grid.ColumnDefinitions>
                         <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="标题栏文本" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextLogoText" Tag="UiLogoText" Grid.Column="1" MaxLength="30" />
+                        <local:MyTextBox x:Name="TextLogoText" Tag="UiLogoText" Grid.Column="1" MaxLength="100" />
                     </Grid>
                     <Grid x:Name="PanLogoChange" Margin="0,14,0,5" Height="35" Visibility="Collapsed">
                         <Grid.ColumnDefinitions>
@@ -247,7 +247,7 @@
                             <local:MyComboBoxItem Content="PCL 主页市场(作者:凌云)" />
                             <local:MyComboBoxItem Content="PCL 新闻速报(作者:Joker2184)" />
                             <local:MyComboBoxItem Content="PCL 新功能说明书(作者:WForst-Breeze)" />
-                            <local:MyComboBoxItem Content="OpenMCIM 仪表盘(作者:SALTWOOD)" />
+                            <local:MyComboBoxItem Content="OpenMCIM 仪表盘(作者:SALTWOOD)" Visibility="Collapsed" />
                             <local:MyComboBoxItem Content="杂志主页(作者:CreeperIsASpy)" />
                         </local:MyComboBox>
                     </Grid>

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

@@ -189,7 +189,7 @@
 
     '背景图片
     Private Sub BtnUIBgOpen_Click(sender As Object, e As EventArgs) Handles BtnBackgroundOpen.Click
-        OpenExplorer("""" & Path & "PCL\Pictures""")
+        OpenExplorer(Path & "PCL\Pictures\")
     End Sub
     Private Sub BtnBackgroundRefresh_Click(sender As Object, e As EventArgs) Handles BtnBackgroundRefresh.Click
         BackgroundRefresh(True, True)
@@ -335,7 +335,7 @@ Refresh:
     Private Sub BtnLogoDelete_Click(sender As Object, e As EventArgs) Handles BtnLogoDelete.Click
         Try
             File.Delete(Path & "PCL\Logo.png")
-            RadioLogoType1.SetChecked(True, True, True)
+            RadioLogoType1.SetChecked(True, True)
             Hint("标题栏图片已清空!", HintType.Finish)
         Catch ex As Exception
             Log(ex, "清空标题栏图片失败", LogLevel.Msgbox)
@@ -344,7 +344,7 @@ Refresh:
 
     '背景音乐
     Private Sub BtnMusicOpen_Click(sender As Object, e As EventArgs) Handles BtnMusicOpen.Click
-        OpenExplorer("""" & Path & "PCL\Musics""")
+        OpenExplorer(Path & "PCL\Musics\")
     End Sub
     Private Sub BtnMusicRefresh_Click(sender As Object, e As EventArgs) Handles BtnMusicRefresh.Click
         MusicRefreshPlay(True)
@@ -407,7 +407,7 @@ Refresh:
             End If
             WriteFile(Path & "PCL\Custom.xaml", GetResources("Custom"))
             Hint("教学文件已生成!", HintType.Finish)
-            OpenExplorer("/select,""" & Path & "PCL\Custom.xaml""")
+            OpenExplorer(Path & "PCL\Custom.xaml")
         Catch ex As Exception
             Log(ex, "生成教学文件失败", LogLevel.Feedback)
         End Try

+ 257 - 107
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml

@@ -5,117 +5,267 @@
     xmlns:System="clr-namespace:System;assembly=mscorlib" 
     mc:Ignorable="d" x:Class="PageVersionExport"
     PanScroll="{Binding ElementName=PanBack}">
-    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
-        <StackPanel Margin="25,10">
-            <local:MyHint Margin="0,10,0,0" IsWarn="False" Text="当前页面导出的整合包是 Modrinth 格式(.mrpack)。" />
-
-            <local:MyCard Margin="0,15" Title="导出预览">
-                <StackPanel Margin="25,38,15,15">
-                    <Grid Margin="-10,-5,10,0">
+    <local:MyPageRight.Resources>
+        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
+    </local:MyPageRight.Resources>
+    <Grid x:Name="PanAllBack">
+        <Grid.Resources>
+            <Style TargetType="local:MyCheckBox">
+                <Setter Property="Height" Value="26" />
+            </Style>
+        </Grid.Resources>
+        <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
+            <StackPanel Margin="25,10,25,65">
+                <local:MyHint Margin="0,15,0,0" Text="受 Modrinth 整合包规范限制,导出的整合包中暂不包含 OptiFine。" IsWarn="False" x:Name="HintOptiFine" />
+                <local:MyCard Margin="0,15">
+                    <Grid Margin="22,15,25,15">
                         <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" />
                             <ColumnDefinition Width="1*" />
-                            <ColumnDefinition Width="150" />
-                        </Grid.ColumnDefinitions>
-                        <Grid Margin="10,7,10,7" Name="PanDisplayItem" Grid.Column="0" />
-                        <local:MyButton x:Name="BtnExportExport" Grid.Column="1" HorizontalAlignment="Right" 
-                                    Text="导出整合包" Width="150" Height="35" ColorType="Highlight" />
-                    </Grid>
-                    <Grid Margin="0,5,10,5">
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
-                            <ColumnDefinition Width="1*" />
-                            <ColumnDefinition Width="60" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="28" />
-                            <RowDefinition Height="9" />
-                            <RowDefinition Height="28" />
-                            <!--<RowDefinition Height="9" />
-                            <RowDefinition Height="28" />-->
-                            <RowDefinition Height="9" />
-                            <RowDefinition Height="28" />
-                        </Grid.RowDefinitions>
-                        <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包名称" Margin="0,0,25,0" />
-                        <local:MyTextBox Grid.Row="0" x:Name="TbExportName" Grid.ColumnSpan="2" Grid.Column="1" MaxLength="100" ShowValidateResult="True" />
-                        <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包版本" Margin="0,0,25,0" />
-                        <local:MyTextBox Grid.Row="2" x:Name="TbExportVersion" HintText="1.0.0" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" MaxLength="100" />
-                        <!--<TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="图标" Margin="0,0,25,0"  />
-                        <local:MyComboBox x:Name="ComboDisplayLogo" Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1">
-                            <local:MyComboBoxItem Content="自动" IsSelected="True" Tag="" />
-                            <local:MyComboBoxItem x:Name="ItemDisplayLogoCustom" Content="自定义..." />
-                            <local:MyComboBoxItem Content="圆石" Tag="pack://application:,,,/images/Blocks/CobbleStone.png" />
-                            <local:MyComboBoxItem Content="命令方块" Tag="pack://application:,,,/images/Blocks/CommandBlock.png" />
-                            <local:MyComboBoxItem Content="金块" Tag="pack://application:,,,/images/Blocks/GoldBlock.png" />
-                            <local:MyComboBoxItem Content="草方块" Tag="pack://application:,,,/images/Blocks/Grass.png" />
-                            <local:MyComboBoxItem Content="草径" Tag="pack://application:,,,/images/Blocks/GrassPath.png" />
-                            <local:MyComboBoxItem Content="铁砧" Tag="pack://application:,,,/images/Blocks/Anvil.png" />
-                            <local:MyComboBoxItem Content="红石块" Tag="pack://application:,,,/images/Blocks/RedstoneBlock.png" />
-                            <local:MyComboBoxItem Content="红石灯(开)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOn.png" />
-                            <local:MyComboBoxItem Content="红石灯(关)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOff.png" />
-                            <local:MyComboBoxItem Content="鸡蛋" Tag="pack://application:,,,/images/Blocks/Egg.png" />
-                            <local:MyComboBoxItem Content="布料(Fabric)" Tag="pack://application:,,,/images/Blocks/Fabric.png" />
-                            <local:MyComboBoxItem Content="狐狸(NeoForge)" Tag="pack://application:,,,/images/Blocks/NeoForge.png" />
-                        </local:MyComboBox>-->
-                        <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="描述" Margin="0,0,25,0" />
-                        <local:MyTextBox Grid.Row="4" x:Name="TbExportDesc" Grid.ColumnSpan="2" Grid.Column="1" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" />
-                    </Grid>
-                </StackPanel>
-            </local:MyCard>
-
-            <local:MyCard x:Name="CardSwitch" Margin="0,0,0,15" Title="基础选项">
-                <StackPanel Margin="25,38,15,15">
-                    <TextBlock x:Name="TbHint" Margin="0,1,0,1" Text="你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" TextWrapping="Wrap" />
-                    <!-- 注意:以上 TextBlock 的 Text 属性可能会在代码中被改动。 -->
-                    <Grid x:Name="PanCommonFiles">
-                        <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="Auto" />
-                            <ColumnDefinition Width="18" />
-                            <ColumnDefinition Width="1.7*" />
-                            <ColumnDefinition Width="1.9*" />
-                            <ColumnDefinition Width="1.4*" />
-                            <ColumnDefinition Width="1.0*" />
-                            <ColumnDefinition Width="1.0*" />
-                            <ColumnDefinition Width="1.0*" />
+                            <ColumnDefinition Width="0.3*" />
                         </Grid.ColumnDefinitions>
                         <Grid.RowDefinitions>
-                            <RowDefinition Height="4" />
-                            <RowDefinition Height="30" />
-                            <RowDefinition Height="30" />
-                            <RowDefinition Height="30" />
-                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="28" />
                         </Grid.RowDefinitions>
-                        <TextBlock Text="游戏配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="1" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="服务器列表"
-                                          Tag="servers.dat" ToolTip="servers.dat 文件" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="Mod 配置"
-                                          Tag="config" ToolTip="config 文件夹" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="游戏配置"
-                                          Tag="options.txt" ToolTip="options.txt 文件" />
-                        <TextBlock Text="游戏文件" VerticalAlignment="Center" Grid.Column="0" Grid.Row="2" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="存档"
-                                          Tag="saves" ToolTip="saves 文件夹" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="资源包"
-                                          Tag="resourcepacks" ToolTip="resourcepacks 文件夹" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="光影包"
-                                          Tag="shaderpacks" ToolTip="shaderpacks 文件夹" />
-                        <TextBlock Text="PCL 配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="3" />
-                        <local:MyCheckBox x:Name="CheckIncludePCL" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="包含启动器"
-                                          Tag="" ToolTip="将 PCL 程序包含在整合包内" />
-                        <local:MyCheckBox x:Name="CheckIncludeSetup" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="PCL 全局配置"
-                                          Tag="" ToolTip="将 PCL 全局配置包含在整合包内" />
+                        <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包名称" Margin="0,0,20,0" />
+                        <local:MyTextBox Grid.Row="0" Grid.Column="1" x:Name="TextExportName" MaxLength="100" />
+                        <TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包版本" Margin="30,0,20,0" />
+                        <local:MyTextBox Grid.Row="0" Grid.Column="3" x:Name="TextExportVersion" Tag="VersionArgumentTitle" MaxLength="100" />
                     </Grid>
-                </StackPanel>
-
-            </local:MyCard>
-            <local:MyCard x:Name="CardMore" Title="高级选项" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-                <StackPanel x:Name="PanFileList" Margin="25,40,23,15">
-                    <TextBlock Margin="0,1,0,10" Text="你可以在此勾选其他需要包含进整合包的内容。&#xa;请注意:部分必须包含在整合包中的内容(如 Mods 等),以及被排除的冗余文件(如游戏日志等),将不会在列表中展示。" TextWrapping="Wrap" />
-                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z" />
-                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M586.9 436h-74.7V62.4H177.1c-20.6 0-37.4 16.7-37.4 37.4v821.9c0 20.6 16.7 37.4 37.4 37.4h672.5c20.6 0 37.4-16.7 37.4-37.4V436H586.9z m0-74.7h297.8c-7.3-57.4-31.2-70.9-58.4-98.1l-66.5-66.5-0.1-0.1 0.1 0.1-0.9-0.9-22.8-22.8-0.9-0.9 0.1 0.1-0.1-0.1-66.5-66.5c-21.8-21.8-38.8-36.5-81.7-41.4l-0.1 297.1z" />
-                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z" />
-                </StackPanel>
-            </local:MyCard>
-
-        </StackPanel>
-    </local:MyScrollViewer>
-</local:MyPageRight>
+                </local:MyCard>
+                <local:MyCard Margin="0,0,0,15" x:Name="CardOptions" MinHeight="39">
+                    <local:MyIconTextButton x:Name="BtnOverrideCancel" Height="24" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0"
+                                            IsHitTestVisible="False" Opacity="0.5"
+                                            Text="重置" Logo="M530 0c287 0 521 229 521 511s-233 511-521 511c-233 0-436-151-500-368a63 63 0 0 1 44-79 65 65 0 0 1 80 43c48 162 200 276 375 276 215 0 390-171 390-383s-174-383-390-383c-103 0-199 39-270 106l21-5a63 63 0 0 1 33 123l-157 42a65 65 0 0 1-90-42l-49-183a65 65 0 1 1 126-33l6 26A524 524 0 0 1 530 0z" LogoScale="0.9"  />
+                    <StackPanel Margin="25,38,15,15" Name="PanOptions">
+                        <local:MyCheckBox IsEnabled="False" x:Name="CheckOptionsBasic">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="游戏本体"
+                                DefaultChecked="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox x:Name="CheckOptionsOptions" Margin="30,0,0,0">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="游戏本体设置"
+                                Description="键位、音量、视频设置等"
+                                Rules="options.txt|configureddefaults/"
+                                DefaultChecked="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox Margin="30,0,0,0">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="游戏本体个人信息"
+                                Description="命令历史、已保存的快捷栏"
+                                Rules="hotbar.nbt|command_history.txt"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox Margin="30,0,0,0">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="OptiFine 设置"
+                                Rules="optionsof.txt|optionsshaders.txt"
+                                DefaultChecked="True"
+                                RequireOptiFine="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox x:Name="CheckOptionsMod">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="Mod"
+                                Description="模组"
+                                Rules="mods/|!mods/*.disabled|!mods/*.old|!mods/.connector/|coremods/|lib/"
+                                DefaultChecked="True"
+                                RequireModLoader="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <StackPanel Margin="30,0,0,0"
+                                Visibility="{Binding Checked, ElementName=CheckOptionsMod, Converter={StaticResource BooleanToVisibilityConverter}}">
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="已禁用的 Mod"
+                                Rules="mods/*.disabled|mods/*.old" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="整合包重要数据"
+                                Description="脚本文件、内置资源包、数据包等"
+                                Rules="addons/|multiblocked/|modpack-update-checker/|global_packs/|global_resource_packs/|global_data_packs/|optional_data_packs/|maps/|icon.png|mods-resourcepacks/|matmos/|resource_assorts/|resource_assorts.json|patchouli_books/|datapacks/|kubejs*/|!kubejs*/probe/|!kubejs*/exported/|!kubejs*/jsconfig.json|!kubejs*/README.txt|openloader/|worldshape/|resources/|scripts/|structures/|fontfiles/|oresources/|packmenu/|craftpresence/|pointblanks/|template*/|!template*/playerdata/|!template*/stats/"
+                                DefaultChecked="True" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="Mod 设置"
+                                Rules="config/|!config/jei/world/|!config/worldedit/|config/worldedit/worldedit.properties|!config/spark/|config/spark/config.json|defaultconfigs/|journeymap/config/|journeymap/server/|TrashSlotSaveState.json|customfov.txt|gg.essential.mod/|essential/|!essential/*/|!essential/*.jar*|!essential/screenshot-checksum-caches.json|!essential/microsoft_accounts.json|paragliderSettings.nbt|local/client_config.json|local/ftbl.json|local/client/sidebar_buttons.json|local/client/ftbutilities.cfg|local/client/ftblib.cfg|local/client/xencraft.cfg|liteloader.properties|default_reference.xml|CustomSkinLoader/CustomSkinLoader.json"
+                                DefaultChecked="True" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="已绘制的地图"
+                                Description="地图类 Mod 为现有的存档、服务器记录的地图、路标点等"
+                                Rules="journeymap/data/|xaero/"
+                                DefaultChecked="False" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="JEI 个人信息"
+                                Description="物品收藏夹等"
+                                Rules="config/jei/world/"
+                                DefaultChecked="False" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="EMI 个人信息"
+                                Description="物品收藏夹、默认配方、合成历史记录等"
+                                Rules="emi.json"
+                                DefaultChecked="False" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                            <local:MyCheckBox>
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="帕秋莉手册个人信息"
+                                Description="教程书的已读记录、书签、阅读历史记录等"
+                                Rules="patchouli_data.json"
+                                DefaultChecked="False" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                        </StackPanel>
+                        <local:MyCheckBox x:Name="CheckOptionsResourcePacks">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="资源包"
+                                Description="纹理包/材质包"
+                                ShowRules="resourcepacks/|texturepacks/"
+                                DefaultChecked="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <StackPanel Margin="30,0,0,0" x:Name="PanOptionsResourcePacks"
+                                Visibility="{Binding Checked, ElementName=CheckOptionsResourcePacks, Converter={StaticResource BooleanToVisibilityConverter}}">
+                        </StackPanel>
+                        <local:MyCheckBox x:Name="CheckOptionsShaderPacks">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="光影包"
+                                ShowRules="shaderpacks/"
+                                DefaultChecked="True"
+                                RequireModLoaderOrOptiFine="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <StackPanel Margin="30,0,0,0" x:Name="PanOptionsShaderPacks"
+                                Visibility="{Binding Checked, ElementName=CheckOptionsShaderPacks, Converter={StaticResource BooleanToVisibilityConverter}}">
+                        </StackPanel>
+                        <local:MyCheckBox>
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="截图"
+                                Rules="screenshots/"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox>
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="导出的结构"
+                                Description="schematics 文件夹"
+                                Rules="schematics/"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox>
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="录像回放"
+                                Description="Replay Mod 的录像文件"
+                                Rules="replay_recordings/|replay_videos/"
+                                RequireModLoader="True"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox x:Name="CheckOptionsSaves">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="单人游戏存档"
+                                Description="世界/地图"
+                                ShowRules="saves/"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <StackPanel Margin="30,0,0,0" x:Name="PanOptionsSaves"
+                                Visibility="{Binding Checked, ElementName=CheckOptionsSaves, Converter={StaticResource BooleanToVisibilityConverter}}">
+                        </StackPanel>
+                        <local:MyCheckBox>
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="多人游戏服务器列表"
+                                Rules="servers.dat"
+                                DefaultChecked="False" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <local:MyCheckBox x:Name="CheckOptionsPcl">
+                            <local:MyCheckBox.Tag>
+                                <local:ExportOption 
+                                Title="PCL 启动器程序"
+                                Description="打包正式版 PCL,以便没有启动器的玩家安装整合包"
+                                DefaultChecked="True" />
+                            </local:MyCheckBox.Tag>
+                        </local:MyCheckBox>
+                        <StackPanel Margin="30,0,0,0"
+                                Visibility="{Binding Checked, ElementName=CheckOptionsPcl, Converter={StaticResource BooleanToVisibilityConverter}}">
+                            <local:MyCheckBox x:Name="CheckOptionsPclCustom">
+                                <local:MyCheckBox.Tag>
+                                    <local:ExportOption 
+                                Title="PCL 个性化内容"
+                                Description="功能隐藏设置、主页、背景音乐和图片等"
+                                DefaultChecked="True" />
+                                </local:MyCheckBox.Tag>
+                            </local:MyCheckBox>
+                        </StackPanel>
+                    </StackPanel>
+                </local:MyCard>
+                <local:MyCard Title="高级选项" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+                    <StackPanel Margin="25,37,23,20">
+                        <local:MyHint Text="建议仅在无法稳定连接 CurseForge 或 Modrinth 时才打包资源文件。&#xa;二次分发可能违反使用协议,请尽量不要公开发布包含资源文件的整合包!"
+                                      IsWarn="False" Margin="0,0,0,4"
+                                      Visibility="{Binding Checked, ElementName=CheckAdvancedInclude, Converter={StaticResource BooleanToVisibilityConverter}}" />
+                        <local:MyCheckBox Text="打包资源文件,以避免在导入时下载" x:Name="CheckAdvancedInclude"
+                                          ToolTip="将 Mod、资源包、光影包的文件直接放入整合包中,这样在导入时就无需联网下载它们。&#xa;建议仅在无法稳定连接 CurseForge 或 Modrinth 时才考虑勾选。" />
+                        <local:MyCheckBox Text="Modrinth 上传模式" x:Name="CheckAdvancedModrinth"
+                                          ToolTip="如果你想要打包上传到 Modrinth,勾选此项会生成完全符合 Modrinth 要求的整合包文件。&#xa;由于 Modrinth 要求,只能从 CurseForge 下载的资源将无法联网下载,会被直接放入整合包中。&#xa;此外,勾选后也不能在整合包中包含 PCL 启动器文件。" />
+                        <Rectangle Height="1" Fill="{StaticResource ColorBrushGray5}" Margin="0,15,0,15" />
+                        <TextBlock Text="通过配置文件,你可以精准地控制导出内容的范围,或是保存当前的配置以便之后使用。" TextWrapping="WrapWithOverflow" />
+                        <Grid Height="35" Margin="0,12,0,0">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                                <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                                <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            </Grid.ColumnDefinitions>
+                            <local:MyButton Grid.Column="0" x:Name="BtnAdvancedImport" MinWidth="140" Text="读取配置" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" />
+                            <local:MyButton Grid.Column="1" x:Name="BtnAdvancedExport" MinWidth="140" Text="保存配置" Padding="13,0" Margin="0,0,20,0" />
+                            <local:MyButton Grid.Column="2" MinWidth="140" Text="整合包制作指南" Padding="13,0" Margin="0,0,20,0" x:Name="BtnAdvancedHelp"
+                                            EventType="打开帮助" />
+                        </Grid>
+                    </StackPanel>
+                </local:MyCard>
+            </StackPanel>
+        </local:MyScrollViewer>
+        <local:MyExtraTextButton HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="20"
+            x:Name="BtnExport" Text="开始导出"
+            LogoScale="1.1" Logo="M511.488 995.328a128.654222 128.654222 0 0 1-57.116444-13.112889L70.769778 791.808a126.833778 126.833778 0 0 1-70.769778-113.777778V311.608889a126.179556 126.179556 0 0 1 15.36-60.103111V248.604444c1.479111-2.901333 3.356444-5.603556 5.518222-8.021333a127.630222 127.630222 0 0 1 49.891556-42.325333L454.371556 13.368889a128.739556 128.739556 0 0 1 112.981333 0l383.601778 190.407111a126.862222 126.862222 0 0 1 72.049777 113.379556v360.874666a126.805333 126.805333 0 0 1-70.769777 115.939556L568.604444 984.32c-17.92 7.964444-37.461333 11.747556-57.116444 11.008z m42.638222-470.897778v370.204445l360.192-178.545778c14.449778-7.253333 23.552-21.987556 23.438222-38.087111v-335.928889L554.097778 524.430222zM85.248 330.666667v347.335111a42.268444 42.268444 0 0 0 23.438222 38.087111l360.192 178.545778V523.576889L85.248 330.666667zM135.537778 260.835556l375.950222 189.952 137.671111-65.564445L286.435556 188.074667 135.537778 260.864z m245.105778-118.471112l363.576888 197.973334 150.897778-71.480889-365.283555-180.224a42.922667 42.922667 0 0 0-37.518223 0l-111.672888 53.731555z" />
+    </Grid>
+</local:MyPageRight>

+ 670 - 297
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb

@@ -1,346 +1,719 @@
-Public Class PageVersionExport
-    Private CurrentVer As String = ""
-    Private Sub PageVersionExport_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
-        Reload()
-    End Sub
-
-    Public ItemVersion As MyListItem
-    Public Sub Reload()
-        AniControlEnabled += 1
+Imports System.IO.Compression
 
-        If CurrentVer <> PageVersionLeft.Version.Path Then
-            '说明切换到另一个版本了,所有的设置都要重置
-            CurrentPath = ""
-            Selected = New List(Of String)
-            CurrentVer = PageVersionLeft.Version.Path
-
-            TbExportName.Text = ""
-            TbExportVersion.Text = ""
-            'ComboDisplayLogo.SelectedIndex = 0
-            CustomLogoVal = Nothing
-            TbExportDesc.Text = ""
-        End If
+Public Class ExportOption
+    Public Property Title As String
+    Public Property Description As String
+    Public Property Rules As String
+    ''' <summary>
+    ''' 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。
+    ''' 如果 ShowRules 也为空,则始终显示。
+    ''' </summary>
+    Public Property ShowRules As String
+    Public Property DefaultChecked As Boolean
+    Public Property RequireModLoader As Boolean = False
+    Public Property RequireOptiFine As Boolean = False
+    Public Property RequireModLoaderOrOptiFine As Boolean = False
+End Class
 
-        TbExportName.ShowValidateResult = True
-        TbExportName.ValidateRules = New ObjectModel.Collection(Of Validate) From {New ValidateFileName() With {.AllowNull = True}}
-        TbExportName.Validate()
-
-        TbExportName.HintText = PageVersionLeft.Version.Name
-        TbExportDesc.HintText = PageVersionLeft.Version.Info
-        PanDisplayItem.Children.Clear()
-        ItemVersion = PageSelectRight.McVersionListItem(PageVersionLeft.Version.Load())
-        ItemVersion.IsHitTestVisible = False
-        If Not String.IsNullOrEmpty(CustomLogo) Then ItemVersion.Logo = CustomLogo
-        PanDisplayItem.Children.Add(ItemVersion)
-        ItemVersion.Title = If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)
-        ItemVersion.Info = If(String.IsNullOrWhiteSpace(TbExportDesc.Text), PageVersionLeft.Version.Info, TbExportDesc.Text)
-#If BETA Then
-        CheckIncludePCL.IsEnabled = True
-#Else
-        CheckIncludePCL.IsEnabled = False
-        TbHint.Text = "你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" & vbCrLf & "你当前的 PCL 不是正式版,有二次分发的限制,因此整合包不能包含 PCL 程序。"
-#End If
-        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
-        ReloadFileList(CurrentPath)
+Public Class PageVersionExport
+    Implements IRefreshable
 
+    Private CurrentVersion As String = ""
+    Private Sub PageVersionExport_Loaded() Handles Me.Loaded
+        AniControlEnabled += 1
+        If CurrentVersion <> PageVersionLeft.Version.Path Then RefreshAll() '切换到了另一个版本,重置页面
+        BtnAdvancedHelp.EventData = If(VersionBranchName = "Release", "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json")
         AniControlEnabled -= 1
     End Sub
+    Public Sub RefreshAll() Implements IRefreshable.Refresh
+        Log($"[Export] 刷新导出页面")
+        HintOptiFine.Visibility = If(PageVersionLeft.Version.Version.HasOptiFine, Visibility.Visible, Visibility.Collapsed)
+        CurrentVersion = PageVersionLeft.Version.Path
+        TextExportName.Text = ""
+        TextExportName.HintText = PageVersionLeft.Version.Name
+        TextExportVersion.Text = ""
+        TextExportVersion.HintText = "1.0.0"
+        GetExportOption(CheckOptionsBasic).Description = PageVersionLeft.Version.GetDefaultDescription()
+        ResetConfigOverrides()
+        ReloadAllSubOptions()
+        RefreshAllOptionsUI()
+        PanBack.ScrollToHome()
+    End Sub
+
+#Region "子选项"
+    Private SubOptionBlackList As String() = {"Quark Programmer Art.zip", "+ EuphoriaPatches_"}
 
-#Region "文件列表"
     ''' <summary>
-    ''' 当前所在的文件夹。注意是绝对路径!以 \ 结尾。
+    ''' 动态生成子文件夹下的选项,例如资源包、存档等。
     ''' </summary>
-    Private CurrentPath As String = ""
-    Private Sub ReloadFileList(Optional NewPath As String = "")
-        If String.IsNullOrWhiteSpace(NewPath) OrElse NewPath = PathMcFolder Then NewPath = PageVersionLeft.Version.Path
-        If Not NewPath.EndsWithF("\") Then NewPath &= "\"
-        Dim tb As TextBlock = PanFileList.Children(0)
-        PanFileList.Children.Clear()
-        PanFileList.Children.Add(tb)
-
-        If NewPath <> PageVersionLeft.Version.Path Then
-            Dim listItem As New MyListItem With {.Title = "返回上一级目录", .Tag = "..", .Height = 35, .Type = MyListItem.CheckType.Clickable}
-            AddHandler listItem.Click, AddressOf ListItem_Click
-            PanFileList.Children.Add(listItem)
-        End If
+    Private Sub ReloadAllSubOptions()
+        ReloadSubOptions(PanOptionsResourcePacks, True, True, "resourcepacks", "texturepacks")
+        ReloadSubOptions(PanOptionsSaves, False, True, "saves")
+        ReloadSubOptions(PanOptionsShaderPacks, True, True, "shaderpacks")
+    End Sub
 
-        Dim fileCount As Integer = 0
-        Dim directories = Directory.EnumerateDirectories(NewPath)
-        Dim files = Directory.EnumerateFiles(NewPath)
-        If (NewPath = PageVersionLeft.Version.Path) AndAlso PageVersionLeft.Version.PathIndie = PathMcFolder Then
-            '未开启版本隔离时 .minecraft 下的文件
-            directories = directories.Concat(Directory.EnumerateDirectories(PathMcFolder))
-            files = files.Concat(Directory.EnumerateFiles(PathMcFolder))
-        End If
+    Private Sub ReloadSubOptions(Panel As StackPanel, AcceptCompressedFile As Boolean, AcceptFolder As Boolean, ParamArray Folders As String())
+        Panel.Children.Clear()
+        For Each Folder In Folders
+            Dim TargetFolder As New DirectoryInfo(PageVersionLeft.Version.PathIndie & Folder)
+            If Not TargetFolder.Exists() Then Continue For
+            '查找文件夹下的对应项
+            If AcceptCompressedFile Then
+                For Each File In TargetFolder.EnumerateFiles("*.zip").Concat(TargetFolder.EnumerateFiles("*.rar"))
+                    If SubOptionBlackList.Any(Function(b) File.Name.ContainsF(b)) Then Continue For
+                    Panel.Children.Add(New MyCheckBox With {. _
+                        Tag = New ExportOption With {.Title = File.Name, .DefaultChecked = True, .Rules = EscapeLikePattern($"{Folder}/{File.Name}")}})
+                Next
+            End If
+            If AcceptFolder Then
+                For Each SubFolder In TargetFolder.EnumerateDirectories().OrderByDescending(Function(f) f.LastWriteTime)
+                    If SubOptionBlackList.Any(Function(b) SubFolder.Name.ContainsF(b)) Then Continue For
+                    If Not SubFolder.EnumerateFileSystemInfos().Any() Then Continue For
+                    Dim NewCheckBox As New MyCheckBox With {. _
+                        Tag = New ExportOption With {.Title = SubFolder.Name, .DefaultChecked = True, .Rules = EscapeLikePattern($"{Folder}/{SubFolder.Name}/")}}
+                    If Panel Is PanOptionsSaves Then GetExportOption(NewCheckBox).Description = SubFolder.LastWriteTime.ToString("yyyy'/'MM'/'dd HH':'mm")
+                    Panel.Children.Add(NewCheckBox)
+                Next
+            End If
+        Next
+    End Sub
+
+#End Region
 
-        For Each d In directories
-            If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
-            If IsVerRedundant(d, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(d, PageVersionLeft.Version.PathIndie) Then Continue For
-            Dim title As String = ""
-            Dim listItem As New MyListItem
-            If Titles.TryGetValue(GetFolderNameFromPath(d), title) Then 'title 作为引用类型传入
-                listItem.Title = title
+#Region "选项"
+
+    ''' <summary>
+    ''' 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。
+    ''' </summary>
+    Private Sub RefreshAllOptionsUI()
+        '预先归纳所有至多二级的文件/文件夹
+        Dim AllEntries As New List(Of String)
+        Dim IsValidDirectory = '检查文件夹不为空
+        Function(Folder As DirectoryInfo) As Boolean
+            Try
+                Return Folder.Exists AndAlso
+                    Folder.EnumerateFileSystemInfos().Any(Function(i) Not SubOptionBlackList.Any(Function(b) i.Name.ContainsF(b)))
+            Catch '一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接)
+                Return False
+            End Try
+        End Function
+        Dim PathInfo As New DirectoryInfo(PageVersionLeft.Version.PathIndie)
+        AllEntries.AddRange(PathInfo.EnumerateFiles().Select(Function(f) f.Name))
+        For Each SubFolder In PathInfo.EnumerateDirectories().Where(IsValidDirectory)
+            AllEntries.Add($"{SubFolder.Name}\")
+            AllEntries.AddRange(SubFolder.EnumerateFiles().Select(Function(f) $"{SubFolder.Name}\{f.Name}"))
+            AllEntries.AddRange(SubFolder.EnumerateDirectories().Where(IsValidDirectory).Select(Function(d) $"{SubFolder.Name}\{d.Name}\"))
+        Next
+        Log($"[Export] 共发现 {AllEntries.Count} 个可行的二级文件/文件夹")
+        '确认选项是否应该被显示
+        Dim IsVisible =
+        Function(TargetOption As ExportOption) As Boolean
+            '检查需要 OptiFine 或 Mod 加载器
+            If TargetOption.RequireOptiFine AndAlso Not PageVersionLeft.Version.Version.HasOptiFine Then Return False
+            If TargetOption.RequireModLoader AndAlso Not PageVersionLeft.Version.Modable Then Return False
+            If TargetOption.RequireModLoaderOrOptiFine AndAlso Not PageVersionLeft.Version.Version.HasOptiFine AndAlso Not PageVersionLeft.Version.Modable Then Return False
+            '粗略检查是否可能有符合规则的文件/文件夹
+            Return StandardizeLines(If(TargetOption.Rules, TargetOption.ShowRules).Split("|"c), True).Any(
+            Function(Rule As String)
+                If Rule.StartsWithF("!") Then Return False '只看正向规则
+                '检查前两级
+                Try
+                    If AllEntries.Any(Function(Entry) Entry Like Rule) Then Return True
+                Catch ex As Exception
+                    Log(ex, $"错误的规则:{Rule}", LogLevel.Hint)
+                    Return False
+                End Try
+                '粗略检查所有级
+                Rule = Rule.Trim("*?".ToCharArray)
+                If Rule.Split({"\"c}, StringSplitOptions.RemoveEmptyEntries).Count >= 3 Then
+                    If Rule.EndsWithF("\") Then
+                        Return IsValidDirectory(New DirectoryInfo(PageVersionLeft.Version.PathIndie & Rule)) '文件夹有效
+                    Else
+                        Return File.Exists(PageVersionLeft.Version.PathIndie & Rule) '文件有效
+                    End If
+                Else
+                    Return False
+                End If
+            End Function)
+        End Function
+        '逐个检查选项
+        For Each CheckBox In GetAllOptions(True)
+            Dim TargetOption = GetExportOption(CheckBox)
+            '名称与简介
+            CheckBox.Inlines.Clear()
+            CheckBox.Inlines.Add(New Run(TargetOption.Title))
+            If Not String.IsNullOrEmpty(TargetOption.Description) Then
+                CheckBox.Inlines.Add(New Run("   " & TargetOption.Description) With {.Foreground = ColorGray5})
+            End If
+            '可见性、默认勾选
+            If String.IsNullOrEmpty(TargetOption.Rules) AndAlso String.IsNullOrEmpty(TargetOption.ShowRules) Then
+                CheckBox.Visibility = Visibility.Visible
+                CheckBox.Checked = TargetOption.DefaultChecked
             Else
-                listItem.Title = GetFolderNameFromPath(d)
+                Dim Pass As Boolean = IsVisible(TargetOption)
+                CheckBox.Visibility = If(Pass, Visibility.Visible, Visibility.Collapsed)
+                CheckBox.Checked = TargetOption.DefaultChecked AndAlso Pass
             End If
-            listItem.Info = d
-            '图标按钮
-            Dim btnSwap As New MyIconButton
-            btnSwap.Logo = Logo.IconButtonRight
-            btnSwap.LogoScale = 1.0
-            btnSwap.Tag = d & "\"
-            AddHandler btnSwap.Click, AddressOf BtnSwap_Click
-            listItem.Buttons = {btnSwap}
-            listItem.Type = MyListItem.CheckType.CheckBox
-            listItem.Height = 35
-            listItem.Logo = Logo.IconButtonOpen
-            listItem.Tag = d & "\"
-            Select Case IsSelected(d & "\")
-                Case 1
-                    listItem.Half = True
-                Case 2
-                    listItem.Checked = True
-            End Select
-            AddHandler listItem.Click, AddressOf ListItem_Click
-            PanFileList.Children.Add(listItem)
-            fileCount += 1
         Next
+    End Sub
 
-        For Each f In files
-            If IsVerRedundant(f, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(f, PageVersionLeft.Version.PathIndie) Then Continue For
-            Dim title As String = ""
-            Dim listItem As New MyListItem
-            If Titles.TryGetValue(f, title) Then 'title 作为引用类型传入
-                listItem.Title = title
-            Else
-                listItem.Title = GetFileNameFromPath(f)
+    ''' <summary>
+    ''' 对文本行进行标准化处理,以便使用 Like 进行匹配。
+    ''' </summary>
+    Private Iterator Function StandardizeLines(Raw As IEnumerable(Of String), AddSuffixStarToFolderPath As Boolean) As IEnumerable(Of String)
+        For Each IgnoreLine In Raw
+            IgnoreLine = IgnoreLine.Trim
+            If IgnoreLine = "" OrElse IgnoreLine.StartsWithF("#") OrElse IgnoreLine.StartsWithF("=") Then Continue For
+            IgnoreLine = IgnoreLine.Replace("/", "\")
+            Yield IgnoreLine & If(IgnoreLine.EndsWithF("\") AndAlso AddSuffixStarToFolderPath, "*", "")
+        Next
+    End Function
+
+    ''' <summary>
+    ''' 获取所有可作为选项的 CheckBox。
+    ''' </summary>
+    Private Iterator Function GetAllOptions(IncludeHidden As Boolean) As IEnumerable(Of MyCheckBox)
+        For Each Element In PanOptions.Children
+            If Not IncludeHidden AndAlso Element.Visibility <> Visibility.Visible Then Continue For
+            If TypeOf Element Is MyCheckBox Then
+                Yield Element
+            ElseIf TypeOf Element Is StackPanel Then
+                For Each SubElement In DirectCast(Element, StackPanel).Children
+                    If Not IncludeHidden AndAlso SubElement.Visibility <> Visibility.Visible Then Continue For
+                    If TypeOf SubElement Is MyCheckBox Then Yield SubElement
+                Next
             End If
-            listItem.Info = f
-            listItem.Type = MyListItem.CheckType.CheckBox
-            listItem.Height = 35
-            listItem.Tag = f
-            listItem.Checked = (IsSelected(f) = 2)
-            AddHandler listItem.Click, AddressOf ListItem_Click
-            PanFileList.Children.Add(listItem)
-            fileCount += 1
         Next
+    End Function
 
-        CurrentPath = NewPath
-        CardMore.Title = If(NewPath = PageVersionLeft.Version.Path, "高级选项", $"高级选项 ({NewPath.Replace(PageVersionLeft.Version.Path, "").TrimEnd("\")})")
-    End Sub
-    Private Titles As New Dictionary(Of String, String) From {
-        {"saves", "存档"},
-        {"resourcepacks", "资源包"},
-        {"config", "Mod 配置"},
-        {"shaderpacks", "光影包"},
-        {"options.txt", "游戏配置"},
-        {"servers.dat", "服务器列表"}
-    }
-    Private Sub BtnSwap_Click(sender As MyIconButton, e As Object)
-        Dim actualPath As String = sender.Tag
-        'Dim relative As String = actualPath.Replace(PageVersionLeft.Version.Path, "")
-
-        If Not Directory.Exists(actualPath) Then
-            Hint($"找不到目录:{actualPath}", HintType.Critical)
-            Exit Sub
-        End If
-        ReloadFileList(actualPath)
-    End Sub
     ''' <summary>
-    ''' 选中的文件列表,绝对路径。
+    ''' 获取该 CheckBox 对应的 ExportOption。
     ''' </summary>
-    Private Selected As New List(Of String)
-    Private Sub SelectedChange(IsAdd As Boolean, TargetPath As String)
-        If File.Exists(TargetPath) Then
-            If IsAdd Then Selected.Add(TargetPath) Else Selected.Remove(TargetPath)
-            Exit Sub
-        End If
-        If Directory.Exists(TargetPath) Then
-            For Each d In Directory.EnumerateDirectories(TargetPath)
-                SelectedChange(IsAdd, d)
+    Private Function GetExportOption(CheckBox As MyCheckBox) As ExportOption
+        Return CType(CheckBox.Tag, ExportOption)
+    End Function
+
+#End Region
+
+#Region "配置文件"
+    Private Const Sperator As String = "=============================================================="
+
+    ' ================ 导出内容段 ================
+
+    ''' <summary>
+    ''' 从配置文件中读取的规则。
+    ''' 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。
+    ''' </summary>
+    Private Property RulesOverrides As List(Of String)
+        Get
+            Return _RulesOverrides
+        End Get
+        Set(value As List(Of String))
+            _RulesOverrides = value
+            If value Is Nothing Then
+                BtnOverrideCancel.Visibility = Visibility.Collapsed
+                PanOptions.Visibility = Visibility.Visible
+                CardOptions.Inlines.Clear()
+                CardOptions.Inlines.Add(New Run("导出内容列表") With {.FontWeight = FontWeights.Bold})
+            Else
+                BtnOverrideCancel.Visibility = Visibility.Visible
+                PanOptions.Visibility = Visibility.Collapsed
+                CardOptions.Inlines.Clear()
+                CardOptions.Inlines.Add(New Run("导出内容列表:    ") With {.FontWeight = FontWeights.Bold})
+                CardOptions.Inlines.Add(New Run("从配置文件中读取") With {.FontWeight = FontWeights.Normal})
+            End If
+        End Set
+    End Property
+    Private _RulesOverrides As List(Of String) = Nothing
+    ''' <summary>
+    ''' 获取当前实际生效的所有规则。
+    ''' </summary>
+    Private Iterator Function GetAllRules() As IEnumerable(Of String)
+        If RulesOverrides IsNot Nothing Then
+            '返回覆盖的列表
+            For Each Rule In RulesOverrides
+                Yield Rule
             Next
-            For Each f In Directory.EnumerateFiles(TargetPath)
-                If IsAdd Then Selected.Add(f) Else Selected.Remove(f)
+        Else
+            '从当前勾选的所有选项中获取所有规则行
+            Yield ""
+            Yield "# 修改下方的规则以控制需要导出的内容。"
+            Yield "# 以 ! 开头以反选。可以使用 *、?、[] 通配符。靠后的行覆盖靠前的。"
+            Yield ""
+            For Each CheckBox In GetAllOptions(False)
+                If Not CheckBox.Checked Then Continue For
+                Dim TargetOption = GetExportOption(CheckBox)
+                If TargetOption.Rules Is Nothing Then Continue For
+                Yield $"# {TargetOption.Title}"
+                For Each Rule In TargetOption.Rules.Split("|"c)
+                    Yield Rule
+                Next
+                Yield ""
             Next
+            Yield "# 排除的文件"
+            Yield "!*.log"
+            Yield "!*.dat_old"
+            Yield "!*.BakaCoreInfo"
+            Yield ""
         End If
-        Selected = Selected.Distinct().ToList()
-    End Sub
+    End Function
+
+    ' ================ 追加内容段 ================
+
+    Private ExtraFiles As List(Of String) = Nothing
     ''' <summary>
-    ''' 获取 TargetPath 是否选中。
+    ''' 获取当前实际生效的追加内容。
     ''' </summary>
-    ''' <returns>全部未选中为 0,部分选中为 1,全部选中为 2。</returns>
-    Private Function IsSelected(TargetPath As String, Optional AllowEmpty As Boolean = False) As Integer
-        If File.Exists(TargetPath) Then
-            Return If(Selected.Contains(TargetPath), 2, 0)
-        End If
-        If Directory.Exists(TargetPath) Then
-            Dim di As New DirectoryInfo(TargetPath)
-            If di.GetDirectories.Length + di.GetFiles.Length = 0 Then Return If(AllowEmpty, 2, 0)
-            '如果有一个文件不在列表,则 hasExclude = True
-            Dim hasExclude As Boolean = False
-            '如果有一个文件在列表,则 hasInclude = True
-            Dim hasInclude As Boolean = False
-            For Each d In Directory.EnumerateDirectories(TargetPath)
-                If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
-                Select Case IsSelected(d, True)
-                    Case 0
-                        hasExclude = True
-                    Case 1
-                        Return 1
-                    Case 2
-                        hasInclude = True
-                End Select
-                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
+    Private Iterator Function GetExtraFileLines() As IEnumerable(Of String)
+        If ExtraFiles IsNot Nothing Then
+            '返回覆盖的列表
+            For Each File In ExtraFiles
+                Yield File
             Next
-            For Each f In Directory.EnumerateFiles(TargetPath)
-                If Selected.Contains(f) Then hasInclude = True Else hasExclude = True
-                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
-            Next
-            If hasExclude AndAlso Not hasInclude Then Return 0
-            If hasInclude AndAlso Not hasExclude Then Return 2
+        Else
+            '从当前勾选的所有选项中获取所有规则行
+            Yield ""
+            Yield "# 如果想将额外的文件自动放到压缩包根目录中,可以将它们的路径写在下方。"
+            Yield "# 必须是完整路径。每行中,若以 \ 结尾则代表是文件夹,不以 \ 结尾则代表是文件。"
+            Yield ""
         End If
-        Return 0
     End Function
-    Private Sub ListItem_Click(sender As MyListItem, e As Object)
-        If sender.Tag = ".." Then '回到上一级目录
-            ReloadFileList(GetPathFromFullPath(CurrentPath))
-            Exit Sub
-        End If
-        SelectedChange(Not sender.Checked, '非常难绷的逻辑,很难描述清楚,自己打断点看 Checked 属性值吧……
-                       sender.Tag)
+
+    ' ================ 重置 ================
+
+    ''' <summary>
+    ''' 重置配置文件所带来的影响。
+    ''' </summary>
+    Private Sub ResetConfigOverrides()
+        RulesOverrides = Nothing
+        ConfigPackPath = Nothing
+        ExtraFiles = Nothing
+        PanBack.ScrollToHome()
+    End Sub
+    Private Sub CardOptions_MouseLeftButtonDown() Handles CardOptions.MouseLeftButtonDown
+        If RulesOverrides Is Nothing Then Return
+        ResetConfigOverrides()
     End Sub
 
-    'Private ReadOnly Property Saves As List(Of String)
-    '    Get
-    '        If Directory.Exists(PageVersionLeft.Version.Path & "saves\") Then
-    '            Return Directory.EnumerateDirectories(PageVersionLeft.Version.Path & "saves\").ToList
-    '        End If
-    '        Return New List(Of String)
-    '    End Get
-    'End Property
-
-    '复选框与高级选项同步
-    Private Sub PanCommonFiles_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanCommonFiles.MouseLeftButtonUp
-        For Each c In PanCommonFiles.Children
-            If String.IsNullOrEmpty(c.Tag) Then Continue For
-            For Each l In PanFileList.Children
-                If TypeOf l Is MyListItem Then
-                    If If(l.Tag.EndsWith("\"), GetFolderNameFromPath(l.Tag), GetFileNameFromPath(l.Tag)) = c.Tag Then
-                        l.Checked = c.Checked
-                        SelectedChange(c.Checked, PageVersionLeft.Version.Path & c.Tag)
-                    End If
-                End If
-            Next
-        Next
+    ' ================ 保存 / 读取 ================
+
+    '保存配置文件
+    Private Sub ExportConfig() Handles BtnAdvancedExport.Click
+        Try
+            Dim ConfigPath As String = SelectSaveFile("选择文件位置", "export_config.txt", "整合包导出配置(*.txt)|*.txt", Setup.Get("CacheExportConfig"))
+            If String.IsNullOrEmpty(ConfigPath) Then Return
+            Setup.Set("CacheExportConfig", ConfigPath)
+            Dim ConfigLines As New List(Of String)
+            'ini 段
+            ConfigLines.Add("Name:" & TextExportName.Text)
+            ConfigLines.Add("Version:" & TextExportVersion.Text)
+            ConfigLines.Add("")
+            ConfigLines.Add("# 是否打包正式版 PCL,以便没有启动器的玩家安装整合包。")
+            ConfigLines.Add("IncludeLauncher:" & CheckOptionsPcl.Checked)
+            ConfigLines.Add("")
+            ConfigLines.Add("# 是否打包 PCL 个性化内容,例如功能隐藏设置、主页、背景音乐和图片等。")
+            ConfigLines.Add("IncludeLauncherCustom:" & CheckOptionsPclCustom.Checked)
+            ConfigLines.Add("")
+            ConfigLines.Add("# 是否将 Mod、资源包、光影包的文件直接放入整合包中,这样在导入时就无需联网下载它们。")
+            ConfigLines.Add("# 建议仅在无法稳定连接 CurseForge 或 Modrinth 时才考虑启用。")
+            ConfigLines.Add("# 二次分发可能违反使用协议,请尽量不要公开发布包含资源文件的整合包!")
+            ConfigLines.Add("DontCheckHostedAssets:" & CheckAdvancedInclude.Checked)
+            ConfigLines.Add("")
+            ConfigLines.Add("# 如果你想要打包上传到 Modrinth,启用此项会生成完全符合 Modrinth 要求的整合包文件。")
+            ConfigLines.Add("# 由于 Modrinth 要求,只能从 CurseForge 下载的资源将无法联网下载,会被直接放入整合包中。")
+            ConfigLines.Add("# 此选项与 IncludeLauncher、IncludeLauncherCustom、DontCheckHostedAssets 冲突。")
+            ConfigLines.Add("ModrinthUploadMode:" & CheckAdvancedModrinth.Checked)
+            ConfigLines.Add("")
+            ConfigLines.Add("# 导出的文件的存放位置。")
+            ConfigLines.Add("# 若设置了此项,在导出时会直接将文件放到此路径,不会弹窗要求选择。")
+            ConfigLines.Add("# 若 IncludeLauncher 为 True,应以 .zip 结尾;若为 False,应以 .mrpack 结尾。")
+            ConfigLines.Add("PackPath:" & If(ConfigPackPath, ""))
+            ConfigLines.Add("")
+            '导出内容段
+            ConfigLines.Add(Sperator)
+            ConfigLines.AddRange(GetAllRules())
+            '追加内容段
+            ConfigLines.Add(Sperator)
+            ConfigLines.AddRange(GetExtraFileLines())
+            '结束
+            WriteFile(ConfigPath, ConfigLines.Join(vbCrLf))
+            Hint("已保存配置文件:" & ConfigPath, HintType.Finish)
+            OpenExplorer(ConfigPath)
+        Catch ex As Exception
+            Log(ex, "保存配置失败", LogLevel.Msgbox)
+        End Try
     End Sub
-    '高级选项与复选框同步
-    Private Sub PanFileList_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanFileList.MouseLeftButtonUp
-        For Each c In PanCommonFiles.Children
-            If TypeOf c Is MyCheckBox Then
-                c.Checked = IsSelected(PageVersionLeft.Version.Path & c.Tag) = 2 OrElse IsSelected(PathMcFolder & c.Tag) = 2
+    '读取配置文件
+    Private Sub ImportConfig() Handles BtnAdvancedImport.Click
+        Try
+            Dim ConfigPath As String = SelectFile("整合包导出配置(*.txt)|*.txt", "选择配置文件", Setup.Get("CacheExportConfig"))
+            If String.IsNullOrEmpty(ConfigPath) Then Return
+            Setup.Set("CacheExportConfig", ConfigPath)
+            Dim Segments As String() = ReadFile(ConfigPath).Split(Sperator)
+            'ini 段
+            Dim Ini As New Dictionary(Of String, String)
+            For Each Line In Segments(0).Split(vbCrLf.ToCharArray())
+                Line = Line.Trim
+                If Line = "" OrElse Line.StartsWithF("#") OrElse Line.StartsWithF("=") Then Continue For
+                Dim Index As Integer = Line.IndexOfF(":")
+                If Index > 0 Then Ini(Line.Substring(0, Index)) = Line.Substring(Index + 1)
+            Next
+            TextExportName.Text = Ini.GetOrDefault("Name", "")
+            TextExportVersion.Text = Ini.GetOrDefault("Version", "")
+            CheckOptionsPcl.Checked = Ini.GetOrDefault("IncludeLauncher", True)
+            CheckOptionsPclCustom.Checked = Ini.GetOrDefault("IncludeLauncherCustom", True)
+            CheckAdvancedModrinth.Checked = Ini.GetOrDefault("ModrinthUploadMode", False)
+            CheckAdvancedInclude.Checked = Ini.GetOrDefault("DontCheckHostedAssets", False)
+            ConfigPackPath = Ini.GetOrDefault("PackPath", Nothing)
+            '导出内容段
+            RulesOverrides = Segments(1).Replace(vbCr, vbLf).Replace(vbLf & vbLf, vbLf).Split(vbLf).ToList
+            '追加内容段
+            If Segments.Length > 2 Then
+                ExtraFiles = Segments(2).Replace(vbCr, vbLf).Replace(vbLf & vbLf, vbLf).Split(vbLf).ToList
+            Else
+                ExtraFiles = Nothing
             End If
-        Next
+            '结束
+            Hint("已读取配置文件:" & ConfigPath, HintType.Finish)
+        Catch ex As Exception
+            Log(ex, "读取配置失败", LogLevel.Msgbox)
+        End Try
     End Sub
+
 #End Region
 
-#Region "整合包信息 & 预览栏"
-    Private Sub TbExportName_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportName.TextChanged
-        If String.IsNullOrEmpty(TbExportName.ValidateResult) Then
-            ItemVersion.Title = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Name, sender.Text)
-        End If
-    End Sub
+#Region "导出"
 
-    Private Sub TbExportDesc_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportDesc.TextChanged
-        ItemVersion.Info = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Info, sender.Text)
-    End Sub
+    ''' <summary>
+    ''' 配置文件中指定的导出位置。
+    ''' </summary>
+    Private ConfigPackPath As String = Nothing
 
-    Private CustomLogoVal As String
     ''' <summary>
-    ''' 自定义 Logo 的路径。
+    ''' 开始导出。
     ''' </summary>
-    Public Property CustomLogo() As String
-        Get
-            Return CustomLogoVal
-        End Get
-        Set(value As String)
-            If AniControlEnabled <> 0 Then Exit Property
-            CustomLogoVal = value
-            ItemVersion.Logo = value
-            Reload()
-        End Set
-    End Property
+    Private Sub StartExport() Handles BtnExport.Click
+        Dim PackName As String = If(String.IsNullOrEmpty(TextExportName.Text), TextExportName.HintText, TextExportName.Text)
+        Dim PackVersion As String = If(String.IsNullOrEmpty(TextExportVersion.Text), "1.0.0", TextExportVersion.Text)
+
+        '重复任务检查
+        Dim LoaderName As String = $"导出整合包:" & PackName
+        For Each OngoingLoader In LoaderTaskbar
+            If OngoingLoader.Name <> LoaderName Then Continue For
+            FrmMain.PageChange(FormMain.PageType.DownloadManager)
+            Return
+        Next
+
+        '确认导出位置
+        Dim Extension As String = If(CheckOptionsPcl.Checked, ".zip", ".mrpack")
+        Dim PackPath As String = Nothing
+        If Not String.IsNullOrWhiteSpace(ConfigPackPath) AndAlso
+           (Not ConfigPackPath.EndsWithF("\") AndAlso Not ConfigPackPath.EndsWithF("/")) Then
+            Try
+                Directory.CreateDirectory(GetPathFromFullPath(ConfigPackPath))
+                PackPath = ConfigPackPath
+                Log($"[Export] 使用配置文件中指定的导出路径:{ConfigPackPath}")
+            Catch ex As Exception
+                Log(ex, $"无法使用配置文件中指定的导出路径({ConfigPackPath})", LogLevel.Debug)
+                If MyMsgBox($"指定的路径:{ConfigPackPath}{vbCrLf}{vbCrLf}{GetExceptionDetail(ex)}", "无法使用配置文件中指定的导出路径", "确定", "取消") = 2 Then Return
+            End Try
+        End If
+        If PackPath Is Nothing Then
+            PackPath = SelectSaveFile("选择导出位置",
+                PackName & If(String.IsNullOrEmpty(TextExportVersion.Text), "", " " & TextExportVersion.Text),
+                $"整合包文件(*{Extension})|*{Extension}")
+            Log($"[Export] 手动指定的导出路径:{PackPath}")
+        End If
+        If String.IsNullOrEmpty(PackPath) Then Return
+
+        '缓存所需参数
+        Dim CacheFolder = RequestTaskTempFolder()
+        Dim OverridesFolder = CacheFolder & "modpack\overrides\"
+        Dim McVersion = PageVersionLeft.Version
+        Dim PathIndie As String = McVersion.PathIndie
+        Dim CheckHostedAssets As Boolean = Not CheckAdvancedInclude.Checked
+        Dim ModrinthUploadMode As Boolean = CheckAdvancedModrinth.Checked
+        Dim IncludePCL As Boolean = CheckOptionsPcl.Checked
+        Dim IncludePCLCustom As Boolean = IncludePCL AndAlso CheckOptionsPclCustom.Checked
+        Dim AllRules = StandardizeLines(GetAllRules(), True).ToList()
+        Dim AllExtraFiles = StandardizeLines(GetExtraFileLines(), False).ToList()
+        Log($"[Export] 准备导出整合包,共有 {AllRules.Count} 条规则,{AllExtraFiles.Count} 条追加内容行")
+
+        '构造步骤加载器
+        Dim Loaders As New List(Of LoaderBase)
+
+#Region "准备 PCL 文件"
+
+#If Not BETA Then
+        If IncludePCL Then
+            Loaders.Add(New LoaderTask(Of Integer, Integer)("下载 PCL 正式版",
+            Sub(Loader As LoaderTask(Of Integer, Integer))
+                DownloadLatestPCL(Loader)
+                CopyFile(PathTemp & "Latest.exe", CacheFolder & "Plain Craft Launcher.exe")
+            End Sub) With {.ProgressWeight = 0.5, .Block = False})
+        End If
+#End If
 
-    'Private Sub ComboDisplayLogo_SelectionChanged(sender As MyComboBox, e As SelectionChangedEventArgs) Handles ComboDisplayLogo.SelectionChanged
-    '    If Not AniControlEnabled = 0 Then Exit Sub
-    '    '选择 自定义 时修改图片
-    '    If ComboDisplayLogo.SelectedItem Is ItemDisplayLogoCustom Then
-    '        Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片")
-    '        If String.IsNullOrWhiteSpace(FileName) Then Exit Sub
-    '        CustomLogo = FileName
-    '    Else
-    '        CustomLogo = sender.SelectedItem.Tag
-    '    End If
-    'End Sub
 #End Region
 
-#Region "基本信息"
-    Private Sub CheckIncludePCL_Change(sender As Object, user As Boolean) Handles CheckIncludePCL.Change
-        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
-    End Sub
+#Region "复制文件"
+
+        Loaders.Add(New LoaderTask(Of Integer, List(Of McMod))("复制导出内容",
+        Sub(Loader As LoaderTask(Of Integer, List(Of McMod)))
+            Loader.Output = New List(Of McMod)
+            '复制版本文件
+            Dim Progress As Integer = 0
+            Dim SearchFolder As Action(Of DirectoryInfo)
+            SearchFolder =
+            Sub(Folder As DirectoryInfo)
+                '文件夹:进一步搜索
+                For Each SubFolder In Folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
+                    '跳过部分又没用文件又多的文件夹,加快搜索
+                    If Folder.FullName = PathIndie AndAlso {"assets", "versions", "libraries"}.Contains(SubFolder.Name) Then Continue For
+                    If {"structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache"}.Contains(SubFolder.Name) Then Continue For
+                    SearchFolder(SubFolder)
+                Next
+                '文件:检查规则并复制
+                For Each Entry In Folder.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
+                    Dim RelativePath As String = Entry.FullName.AfterFirst(PathIndie)
+                    '检查规则
+                    Dim ShouldKeep As Boolean = False
+                    For Each Rule In AllRules
+                        Dim Revert = Rule.StartsWith("!")
+                        If RelativePath Like Rule.TrimStart("!") Then ShouldKeep = Not Revert
+                    Next
+                    If Not ShouldKeep Then Continue For
+                    Dim TargetPath As String = OverridesFolder & RelativePath
+                    CopyFile(Entry.FullName, TargetPath)
+                    '若为压缩包,考虑联网获取路径
+                    If CheckHostedAssets AndAlso
+                       {".zip", ".rar", ".jar", ".disabled", ".old"}.Contains(Entry.Extension.ToLower) AndAlso
+                       {"mods", "packs", "openloader", "resource"}.Any(Function(s) RelativePath.Contains(s)) Then
+                        Dim ModFile As New McMod(TargetPath)
+                        Dim Unused = ModFile.ModrinthHash '提前计算 Hash
+                        Unused = ModFile.CurseForgeHash
+                        Loader.Output.Add(ModFile)
+                    End If
+                    '更新进度(进度并不准确,主要突出一个我还没似)
+                    Progress += 1
+                    If Progress = 25 Then
+                        Loader.Progress += (0.94 - Loader.Progress) * 0.012
+                        Progress = 0
+                    End If
+                Next
+            End Sub
+            SearchFolder(New DirectoryInfo(PathIndie))
+            Log($"[Export] 复制 overrides 文件完成,有 {Loader.Output.Count} 个文件需要联网检查")
+            Loader.Progress = 0.95
+            '复制追加内容到根目录
+            Dim BaseFolder As String = If(IncludePCL, CacheFolder, CacheFolder & "modpack\")
+            For Each Line In AllExtraFiles
+                If Line.EndsWithF("\") OrElse Line.EndsWithF("/") Then
+                    If Directory.Exists(Line) Then
+                        CopyDirectory(Line, BaseFolder & GetFolderNameFromPath(Line) & "\")
+                    Else
+                        Hint($"未找到配置文件中指定的文件夹:{Line}", HintType.Critical)
+                    End If
+                Else
+                    If File.Exists(Line) Then
+                        CopyFile(Line, BaseFolder & GetFileNameFromPath(Line))
+                    Else
+                        Hint($"未找到配置文件中指定的单个文件:{Line}", HintType.Critical)
+                    End If
+                End If
+            Next
+            Loader.Progress = 0.97
+            '复制 PCL 版本设置
+            CopyDirectory(McVersion.Path & "PCL\", OverridesFolder & "PCL\")
+#If BETA Then
+            '复制 PCL 本体
+            If IncludePCL Then CopyFile(PathWithName, CacheFolder & "Plain Craft Launcher.exe")
+#End If
+            '复制 PCL 个性化内容
+            If IncludePCLCustom Then
+                If Directory.Exists(Path & "PCL\Pictures\") Then CopyDirectory(Path & "PCL\Pictures\", CacheFolder & "PCL\Pictures\")
+                If Directory.Exists(Path & "PCL\Musics\") Then CopyDirectory(Path & "PCL\Musics\", CacheFolder & "PCL\Musics\")
+                If Directory.Exists(Path & "PCL\Help\") Then CopyDirectory(Path & "PCL\Help\", CacheFolder & "PCL\Help\")
+                If File.Exists(Path & "PCL\Custom.xaml") Then CopyFile(Path & "PCL\Custom.xaml", CacheFolder & "PCL\Custom.xaml")
+                If File.Exists(Path & "PCL\Setup.ini") Then CopyFile(Path & "PCL\Setup.ini", CacheFolder & "PCL\Setup.ini")
+                If File.Exists(Path & "PCL\hints.txt") Then CopyFile(Path & "PCL\hints.txt", CacheFolder & "PCL\hints.txt")
+                If File.Exists(Path & "PCL\Logo.png") Then CopyFile(Path & "PCL\Logo.png", CacheFolder & "PCL\Logo.png")
+            End If
+        End Sub) With {.ProgressWeight = 5})
+
 #End Region
 
-#Region "导出"
-    'Private Sub ComboExportType_Change(sender As ComboBox, e As SelectionChangedEventArgs) Handles ComboExportType.SelectionChanged
-    '    If AniControlEnabled <> 0 Then Exit Sub
-    '    Type = sender.SelectedIndex
-    '    Select Case Type
-    '        Case ModpackType.Modrinth
-    '            If IncludePCL() Then 'TODO:修改这里的条件
-    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Visible
-    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Collapsed
-    '            Else
-    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Collapsed
-    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Visible
-    '            End If
-    '    End Select
-    '    CardExport.TriggerForceResize()
-    'End Sub
-    Private Sub BtnExportExport_Click() Handles BtnExportExport.Click
-        Dim savePath As String = SelectAs(
-            "选择导出位置", $"整合包 - {If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)}" & If(CheckIncludePCL.Checked, ".zip", ".mrpack"),
-            If(CheckIncludePCL.Checked, "整合包文件(*.zip)|*.zip", "Modrinth 整合包(*.mrpack)|*.mrpack"))
-        If String.IsNullOrEmpty(savePath) Then Exit Sub
-
-        '获取需要包含的文件
-        Dim contains As List(Of String) = Selected
-        For Each c In PanCommonFiles.Children
-            If TypeOf c IsNot MyCheckBox Then Continue For
-            If String.IsNullOrEmpty(c.Tag) Then Continue For
-            If c.Checked Then contains.Add(PageVersionLeft.Version.Path & c.Tag)
-        Next
-        '重复任务检查
-        For Each OngoingLoader In LoaderTaskbar
-            If OngoingLoader.Name <> $"导出整合包 {PageVersionLeft.Version.Name}" Then Continue For
-            Hint("正在处理中,请稍候!", HintType.Critical)
-            Exit Sub
-        Next
-        '启动!
-        Dim Loader As New LoaderTask(Of ExportOptions, Boolean)($"导出整合包 {PageVersionLeft.Version.Name}",
-        Sub(Task As LoaderTask(Of ExportOptions, Boolean))
-            Task.Output = ModpackExport(Task)
-            Task.Progress = 1.0
-            If Task.Output Then
-                Hint($"{Task.Name} 成功!", HintType.Finish)
-                OpenExplorer($"/select,""{Task.Input.Dest}""")
+#Region "联网检查"
+
+        Loaders.Add(New LoaderTask(Of List(Of McMod), Dictionary(Of McMod, List(Of String)))("联网获取文件信息",
+        Sub(Loader As LoaderTask(Of List(Of McMod), Dictionary(Of McMod, List(Of String))))
+            Loader.Output = New Dictionary(Of McMod, List(Of String))
+            If Not CheckHostedAssets Then Log($"[Export] 要求跳过联网获取步骤") : Return
+            If Not Loader.Input.Any Then Log($"[Export] 没有需要联网检查的文件,跳过联网获取步骤") : Return
+
+            '分平台获取下载地址
+            Dim EndedThreadCount As Integer = 0, FailedExceptions As New List(Of Exception)
+
+            '从 Modrinth 获取信息
+            RunInNewThread(
+            Sub()
+                Try
+                    Dim ModrinthHashes = Loader.Input.Select(Function(m) m.ModrinthHash)
+                    Dim ModrinthRaw = CType(GetJson(DlModRequest("https://api.modrinth.com/v2/version_files", "POST",
+                        $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
+                    For Each ModFile In Loader.Input
+                        '查找对应的文件
+                        If Not ModrinthRaw.ContainsKey(ModFile.ModrinthHash) Then Continue For
+                        If ModrinthRaw(ModFile.ModrinthHash)?("files")?(0)("hashes")?("sha1") <> ModFile.ModrinthHash Then Continue For
+                        '写入下载地址
+                        Loader.Output.AddToList(ModFile, ModrinthRaw(ModFile.ModrinthHash)("files")(0)("url"))
+                    Next
+                    Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地资源项的对应信息")
+                Catch ex As Exception
+                    Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
+                    FailedExceptions.Add(ex)
+                Finally
+                    EndedThreadCount += 1
+                    Loader.Progress += 0.45
+                End Try
+            End Sub, "Modrinth - " & LoaderName)
+
+            '从 CurseForge 获取信息
+            RunInNewThread(
+            Sub()
+                Try
+                    If ModrinthUploadMode Then Return 'Modrinth 上传模式下,不能从 CurseForge 获取信息
+                    Dim CurseForgeHashes = Loader.Input.Select(Function(m) m.CurseForgeHash)
+                    Dim CurseForgeRaw = CType(CType(GetJson(DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", "POST",
+                        $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
+                    For Each ResultJson As JObject In CurseForgeRaw
+                        If Not ResultJson.ContainsKey("file") Then Continue For
+                        Dim File As JObject = ResultJson("file")
+                        If String.IsNullOrEmpty(File("downloadUrl")) Then Continue For
+                        '查找对应的文件
+                        Dim ModFile As McMod = Loader.Input.FirstOrDefault(Function(m) m.CurseForgeHash = File("fileFingerprint").ToObject(Of UInteger))
+                        If ModFile Is Nothing Then Continue For
+                        '写入下载地址
+                        For Each Address In CompFile.HandleCurseForgeDownloadUrls(File("downloadUrl").ToString)
+                            Loader.Output.AddToList(ModFile, Address)
+                        Next
+                    Next
+                    Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地资源项的对应信息")
+                Catch ex As Exception
+                    Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
+                    FailedExceptions.Add(ex)
+                Finally
+                    EndedThreadCount += 1
+                    Loader.Progress += 0.45
+                End Try
+            End Sub, "CurseForge - " & LoaderName)
+
+            '等待线程结束
+            Do Until EndedThreadCount = 2
+                If Loader.IsAborted Then Exit Sub
+                Thread.Sleep(10)
+            Loop
+
+            '若失败,确认是否继续
+            If FailedExceptions.Count = 1 Then
+                If MyMsgBox("联网获取部分文件信息失败,是否继续导出?" & vbCrLf & vbCrLf &
+                            "若继续,无法获取信息的文件将被直接打包。" & vbCrLf &
+                            "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!",
+                            "部分文件信息获取失败", "继续", "取消") = 2 Then Throw FailedExceptions.First
+            ElseIf FailedExceptions.Count > 1 Then
+                If MyMsgBox("联网获取文件信息失败,是否继续导出?" & vbCrLf & vbCrLf &
+                            "若继续,所有文件都将被直接打包。" & vbCrLf &
+                            "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!",
+                            "文件信息获取失败", "继续", "取消") = 2 Then Throw FailedExceptions.First
             End If
-        End Sub)
-        Dim Combo = New LoaderCombo(Of ExportOptions)($"导出整合包 {PageVersionLeft.Version.Name}", {Loader})
-        Combo.Start(New ExportOptions(PageVersionLeft.Version, savePath, CheckIncludePCL.Checked, contains.Distinct.ToArray,
-                                       PCLSetupGlobal:=CheckIncludeSetup.Checked, Name:=TbExportName.Text, Desc:=TbExportDesc.Text,
-                                       VerID:=TbExportVersion.Text))
-        LoaderTaskbarAdd(Combo)
+        End Sub) With {.Show = CheckHostedAssets, .ProgressWeight = If(CheckHostedAssets, 2, 0.01)})
+
+#End Region
+
+#Region "生成压缩包"
+
+        Loaders.Add(New LoaderTask(Of Dictionary(Of McMod, List(Of String)), Integer)("生成压缩包",
+        Sub(Loader As LoaderTask(Of Dictionary(Of McMod, List(Of String)), Integer))
+            '整理文件列表
+            Dim Files As New JArray
+            For Each Pair In Loader.Input
+                Dim ModFile As McMod = Pair.Key
+                Files.Add(New JObject From {
+                    {"path", ModFile.Path.AfterFirst(OverridesFolder).Replace("\", "/")},
+                    {"hashes", New JObject From {{"sha1", ModFile.ModrinthHash}, {"sha512", GetFileSHA512(ModFile.Path)}}},
+                    {"downloads", New JArray(Pair.Value.OrderByDescending(Function(u) u.Contains("modrinth.com")))},
+                    {"fileSize", New FileInfo(ModFile.Path).Length}
+                })
+                File.Delete(ModFile.Path)
+            Next
+            Loader.Progress = 0.2
+            '导出最终 JSON 文件
+            Dim Dependencies As New JObject From {{"minecraft", McVersion.Version.McName}}
+            If McVersion.Version.HasForge Then Dependencies.Add("forge", McVersion.Version.ForgeVersion)
+            If McVersion.Version.HasFabric Then Dependencies.Add("fabric-loader", McVersion.Version.FabricVersion)
+            If McVersion.Version.HasNeoForge Then Dependencies.Add("neoforge", McVersion.Version.NeoForgeVersion)
+            Dim ResultJson As New JObject From {
+                {"game", "minecraft"},
+                {"formatVersion", 1},
+                {"versionId", PackVersion},
+                {"name", PackName},
+                {"summary", McVersion.Info},
+                {"files", Files},
+                {"dependencies", Dependencies}
+            }
+            File.WriteAllText(CacheFolder & "modpack\modrinth.index.json", ResultJson.ToString(Newtonsoft.Json.Formatting.Indented))
+            '打包
+            Directory.CreateDirectory(GetPathFromFullPath(PackPath))
+            If File.Exists(PackPath) Then File.Delete(PackPath)
+            If IncludePCL Then
+                '首次压缩整合包
+                ZipFile.CreateFromDirectory(CacheFolder & "modpack\", CacheFolder & "modpack.mrpack")
+                Loader.Progress = 0.5
+                Directory.Delete(CacheFolder & "modpack\", True)
+                Loader.Progress = 0.6
+                '二次压缩整合包
+                ZipFile.CreateFromDirectory(CacheFolder, PackPath)
+                Loader.Progress = 0.9
+            Else
+                '直接压缩整合包
+                ZipFile.CreateFromDirectory(CacheFolder & "modpack\", PackPath)
+                Loader.Progress = 0.8
+            End If
+            Directory.Delete(CacheFolder, True)
+            OpenExplorer(PackPath)
+        End Sub) With {.ProgressWeight = 6})
+
+#End Region
+
+        '启动
+        Dim MainLoader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
+        MainLoader.Start()
+        LoaderTaskbarAdd(MainLoader)
+        FrmMain.BtnExtraDownload.ShowRefresh()
+        FrmMain.BtnExtraDownload.Ribble()
+        FrmMain.PageChange(FormMain.PageType.DownloadManager)
     End Sub
 
 #End Region
 
+    '自动填写整合包名称
+    Private Sub TextExportName_GotFocus() Handles TextExportName.GotFocus
+        If TextExportName.Text = "" Then
+            TextExportName.Text = TextExportName.HintText
+            TextExportName.SelectionStart = TextExportName.Text.Length
+        End If
+    End Sub
+
+    '勾选 Modrinth 上传模式时,禁止打包 PCL
+    Private Sub CheckAdvancedModrinth_Change(sender As Object, user As Boolean) Handles CheckAdvancedModrinth.Change
+        If CheckAdvancedModrinth.Checked Then CheckOptionsPcl.Checked = False
+        CheckOptionsPcl.IsEnabled = Not CheckAdvancedModrinth.Checked
+    End Sub
+
+    '勾选打包资源文件时,禁止开启 Modrinth 上传模式
+    Private Sub CheckAdvancedInclude_Change(sender As Object, user As Boolean) Handles CheckAdvancedInclude.Change
+        If CheckAdvancedInclude.Checked Then CheckAdvancedModrinth.Checked = False
+        CheckAdvancedModrinth.IsEnabled = Not CheckAdvancedInclude.Checked
+    End Sub
+
 End Class

+ 7 - 2
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml

@@ -23,8 +23,13 @@
         </local:MyListItem>
         <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
-        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出整合包"
-                          Logo="M511.488 995.328a128.654222 128.654222 0 0 1-57.116444-13.112889L70.769778 791.808a126.833778 126.833778 0 0 1-70.769778-113.777778V311.608889a126.179556 126.179556 0 0 1 15.36-60.103111V248.604444c1.479111-2.901333 3.356444-5.603556 5.518222-8.021333a127.630222 127.630222 0 0 1 49.891556-42.325333L454.371556 13.368889a128.739556 128.739556 0 0 1 112.981333 0l383.601778 190.407111a126.862222 126.862222 0 0 1 72.049777 113.379556v360.874666a126.805333 126.805333 0 0 1-70.769777 115.939556L568.604444 984.32c-17.92 7.964444-37.461333 11.747556-57.116444 11.008z m42.638222-470.897778v370.204445l360.192-178.545778c14.449778-7.253333 23.552-21.987556 23.438222-38.087111v-335.928889L554.097778 524.430222zM85.248 330.666667v347.335111a42.268444 42.268444 0 0 0 23.438222 38.087111l360.192 178.545778V523.576889L85.248 330.666667zM135.537778 260.835556l375.950222 189.952 137.671111-65.564445L286.435556 188.074667 135.537778 260.864z m245.105778-118.471112l363.576888 197.973334 150.897778-71.480889-365.283555-180.224a42.922667 42.922667 0 0 0-37.518223 0l-111.672888 53.731555z" >
+        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出"
+                          Logo="M511.488 995.328a128.654222 128.654222 0 0 1-57.116444-13.112889L70.769778 791.808a126.833778 126.833778 0 0 1-70.769778-113.777778V311.608889a126.179556 126.179556 0 0 1 15.36-60.103111V248.604444c1.479111-2.901333 3.356444-5.603556 5.518222-8.021333a127.630222 127.630222 0 0 1 49.891556-42.325333L454.371556 13.368889a128.739556 128.739556 0 0 1 112.981333 0l383.601778 190.407111a126.862222 126.862222 0 0 1 72.049777 113.379556v360.874666a126.805333 126.805333 0 0 1-70.769777 115.939556L568.604444 984.32c-17.92 7.964444-37.461333 11.747556-57.116444 11.008z m42.638222-470.897778v370.204445l360.192-178.545778c14.449778-7.253333 23.552-21.987556 23.438222-38.087111v-335.928889L554.097778 524.430222zM85.248 330.666667v347.335111a42.268444 42.268444 0 0 0 23.438222 38.087111l360.192 178.545778V523.576889L85.248 330.666667zM135.537778 260.835556l375.950222 189.952 137.671111-65.564445L286.435556 188.074667 135.537778 260.864z m245.105778-118.471112l363.576888 197.973334 150.897778-71.480889-365.283555-180.224a42.922667 42.922667 0 0 0-37.518223 0l-111.672888 53.731555z">
+            <local:MyListItem.Buttons>
+                <x:Array Type="{x:Type local:MyIconButton}">
+                    <local:MyIconButton Tag="4" ToolTip="刷新" ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1" Click="Refresh" LogoScale="0.85" Logo="M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z" />
+                </x:Array>
+            </local:MyListItem.Buttons>
         </local:MyListItem>
     </StackPanel>
 </local:MyPageLeft>

+ 15 - 1
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml.vb

@@ -1,4 +1,5 @@
 Public Class PageVersionLeft
+    Implements IRefreshable
 
     ''' <summary>
     ''' 当前显示设置的 MC 版本。
@@ -91,7 +92,20 @@
 #End Region
 
     Public Sub Refresh(sender As Object, e As EventArgs) '由边栏按钮匿名调用
-        PageVersionMod.Refresh()
+        Refresh(Val(sender.Tag))
+    End Sub
+    Public Sub Refresh() Implements IRefreshable.Refresh
+        Refresh(FrmMain.PageCurrentSub)
+    End Sub
+    Public Sub Refresh(SubType As FormMain.PageSubType)
+        Select Case SubType
+            Case FormMain.PageSubType.VersionMod
+                PageVersionMod.Refresh()
+                ItemMod.Checked = True
+            Case FormMain.PageSubType.VersionExport
+                If FrmVersionExport IsNot Nothing Then FrmVersionExport.RefreshAll()
+                ItemExport.Checked = True
+        End Select
     End Sub
 
     Public Sub Reset(sender As Object, e As EventArgs)

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

@@ -289,7 +289,7 @@
     Private Sub BtnManageOpen_Click(sender As Object, e As EventArgs) Handles BtnManageOpen.Click, BtnHintOpen.Click
         Try
             Directory.CreateDirectory(PageVersionLeft.Version.PathIndie & "mods\")
-            OpenExplorer("""" & PageVersionLeft.Version.PathIndie & "mods\""")
+            OpenExplorer(PageVersionLeft.Version.PathIndie & "mods\")
         Catch ex As Exception
             Log(ex, "打开 Mods 文件夹失败", LogLevel.Msgbox)
         End Try
@@ -856,10 +856,8 @@ Install:
     '打开文件所在的位置
     Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
         Try
-
             Dim ListItem As MyLocalModItem = sender.Tag
-            OpenExplorer("/select,""" & ListItem.Entry.Path & """")
-
+            OpenExplorer(ListItem.Entry.Path)
         Catch ex As Exception
             Log(ex, "打开 Mod 文件位置失败", LogLevel.Feedback)
         End Try

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

@@ -235,21 +235,21 @@
         OpenVersionFolder(PageVersionLeft.Version)
     End Sub
     Public Shared Sub OpenVersionFolder(Version As McVersion)
-        OpenExplorer("""" & Version.Path & """")
+        OpenExplorer(Version.Path)
     End Sub
 
     '存档文件夹
     Private Sub BtnFolderSaves_Click() Handles BtnFolderSaves.Click
         Dim FolderPath As String = PageVersionLeft.Version.PathIndie & "saves\"
         Directory.CreateDirectory(FolderPath)
-        OpenExplorer("""" & FolderPath & """")
+        OpenExplorer(FolderPath)
     End Sub
 
     'Mod 文件夹
     Private Sub BtnFolderMods_Click() Handles BtnFolderMods.Click
         Dim FolderPath As String = PageVersionLeft.Version.PathIndie & "mods\"
         Directory.CreateDirectory(FolderPath)
-        OpenExplorer("""" & FolderPath & """")
+        OpenExplorer(FolderPath)
     End Sub
 
 #End Region
@@ -260,7 +260,7 @@
     Private Sub BtnManageScript_Click() Handles BtnManageScript.Click
         Try
             '弹窗要求指定脚本的保存位置
-            Dim SavePath As String = SelectAs("选择脚本保存位置", "启动 " & PageVersionLeft.Version.Name & ".bat", "批处理文件(*.bat)|*.bat")
+            Dim SavePath As String = SelectSaveFile("选择脚本保存位置", "启动 " & PageVersionLeft.Version.Name & ".bat", "批处理文件(*.bat)|*.bat")
             If SavePath = "" Then Exit Sub
             '检查中断(等玩家选完弹窗指不定任务就结束了呢……)
             If McLaunchLoader.State = LoadState.Loading Then

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

@@ -153,11 +153,11 @@
         If LabRamGame Is Nothing OrElse LabRamUsed Is Nothing OrElse FrmMain.PageCurrent <> FormMain.PageType.VersionSetup OrElse FrmVersionLeft.PageID <> FormMain.PageSubType.VersionSetup Then Exit Sub
         '获取内存情况
         Dim RamGame As Double = GetRam(PageVersionLeft.Version)
-        Dim RamTotal As Double = Math.Round(My.Computer.Info.TotalPhysicalMemory / 1024 / 1024 / 1024 * 10) / 10
-        Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024 * 10) / 10
-        Dim RamGameActual As Double = Math.Min(RamGame, RamAvailable)
-        Dim RamUsed As Double = RamTotal - RamAvailable
-        Dim RamEmpty As Double = Math.Round(MathClamp(RamTotal - RamUsed - RamGame, 0, 1000) * 10) / 10
+        Dim RamTotal As Double = Math.Round(My.Computer.Info.TotalPhysicalMemory / 1024 / 1024 / 1024, 1)
+        Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024, 1)
+        Dim RamGameActual As Double = Math.Round(Math.Min(RamGame, RamAvailable), 5)
+        Dim RamUsed As Double = Math.Round(RamTotal - RamAvailable, 5)
+        Dim RamEmpty As Double = Math.Round(MathClamp(RamTotal - RamUsed - RamGame, 0, 1000), 1)
         '设置最大可用内存
         If RamTotal <= 1.5 Then
             SliderRamCustom.MaxValue = Math.Max(Math.Floor((RamTotal - 0.3) / 0.1), 1)

+ 10 - 3
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -186,6 +186,9 @@
     <Compile Include="Controls\MyComboBox.vb" />
     <Compile Include="Controls\MyComboBoxItem.vb" />
     <Compile Include="Controls\MyCard.vb" />
+    <Compile Include="Controls\MyExtraTextButton.xaml.vb">
+      <DependentUpon>MyExtraTextButton.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Controls\MyImage.vb" />
     <Compile Include="Controls\MyPageRight.vb" />
     <Compile Include="Controls\MyPageLeft.vb" />
@@ -385,6 +388,10 @@
     <Compile Include="Pages\PageSpeedRight.xaml.vb">
       <DependentUpon>PageSpeedRight.xaml</DependentUpon>
     </Compile>
+    <Page Include="Controls\MyExtraTextButton.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Controls\MyExtraButton.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
@@ -479,10 +486,10 @@
       <DependentUpon>MyRadioButton.xaml</DependentUpon>
     </Compile>
     <Compile Include="FormMain.xaml.vb">
-
       <DependentUpon>FormMain.xaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
+
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
@@ -565,8 +572,8 @@
       <SubType>Designer</SubType>
     </Page>
     <Page Include="Pages\PageVersion\PageVersionExport.xaml">
-      <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
     </Page>
     <Page Include="Resources\Language\en_US.xaml">
       <SubType>Designer</SubType>
@@ -917,11 +924,11 @@
     <Resource Include="Images\Icons\Disabled.png" />
     <Resource Include="Images\Icons\Unavailable.png" />
   </ItemGroup>
-
   <ItemGroup>
     <Resource Include="Images\Heads\MCBBS.png" />
   </ItemGroup>
   <ItemGroup>
+
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Images\Blocks\NeoForge.png" />

+ 5 - 5
Plain Craft Launcher 2/Resources/Custom.xaml

@@ -87,22 +87,22 @@
     </StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="事件 3:打开文件" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+<local:MyCard Title="事件 3:执行命令" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
     <StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="将 EventType 设置为 打开文件,EventData 设置为文件路径,即可点击打开特定文件或启动程序。" />
+                    Text="将 EventType 设置为 执行命令(或 打开文件),EventData 设置为文件路径,即可点击打开特定文件或启动程序。" />
         <local:MyButton Margin="0,4,0,8" Width="200" Height="35"
-                    Text="打开记事本" EventType="打开文件" EventData="notepad.exe" />
+                    Text="打开记事本" EventType="执行命令" EventData="notepad.exe" />
         <TextBlock TextWrapping="Wrap" Margin="0,4,0,0"
                     Text="如果能在 PCL 文件夹中找到该文件,则会优先打开 PCL 文件夹中的文件。当然,你可以使用例如 C:\ 开头的绝对路径。" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="此外,PCL 会将文件中的 “花括号 path” 替换为 PCL 可执行文件所在文件夹({path}),可以用于执行特定程序或加载图片。" />
         <local:MyButton Margin="0,4,0,10" Width="200" Height="35"
-                    Text="打开 PCL 日志" EventType="打开文件" EventData="Log1.txt" ToolTip="PCL 的日志文件就在 PCL 文件夹内,且名为 Log1.txt" />
+                    Text="打开 PCL 日志" EventType="执行命令" EventData="Log1.txt" ToolTip="PCL 的日志文件就在 PCL 文件夹内,且名为 Log1.txt" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="就像为游戏指定服务器 IP 一样,你也可以在 EventData 的竖线后为程序添加启动参数。" />
         <local:MyButton Margin="0,4,0,0" Width="200" Height="35"
-                    Text="定位游戏启动脚本" EventType="打开文件" EventData="explorer.exe|/select,LatestLaunch.bat" ToolTip="PCL 会将上一次启动 MC 的脚本保存在 PCL 文件夹内,且名为 LatestLaunch.bat。&#xA;点击按钮会打开文件管理器,并且借助参数要求它选中这个文件。" />
+                    Text="定位游戏启动脚本" EventType="执行命令" EventData="explorer.exe|/select,LatestLaunch.bat" ToolTip="PCL 会将上一次启动 MC 的脚本保存在 PCL 文件夹内,且名为 LatestLaunch.bat。&#xA;点击按钮会打开文件管理器,并且借助参数要求它选中这个文件。" />
     </StackPanel>
 </local:MyCard>
 

BIN
Plain Craft Launcher 2/Resources/Help.zip


Fișier diff suprimat deoarece este prea mare
+ 118 - 118
Plain Craft Launcher 2/Resources/ModData.txt


+ 6 - 5
Plain Craft Launcher 2/app.manifest

@@ -51,11 +51,12 @@
   <!-- 更改原因见 #5337 -->
   <application xmlns="urn:schemas-microsoft-com:asm.v3">
   	<windowsSettings>
-  		<!-- The combination of below two tags have the following effect : 
-            1. Per-Monitor for >= Windows 10 Anniversary Update
-            2. System < Windows 10 Anniversary Update -->
-  		<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
-  		<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
+		<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> <!-- 长路径支持 -->
+		<!-- The combination of below two tags have the following effect : 
+		    1. Per-Monitor for >= Windows 10 Anniversary Update
+		    2. System < Windows 10 Anniversary Update -->
+		<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+		<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
   	</windowsSettings>
   </application>
 

BIN
最新正式版.zip


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff