龙腾猫跃 3 سال پیش
والد
کامیت
3b8de89309
28فایلهای تغییر یافته به همراه613 افزوده شده و 449 حذف شده
  1. 2 2
      Plain Craft Launcher 2/Application.xaml
  2. 8 0
      Plain Craft Launcher 2/Application.xaml.vb
  3. 0 4
      Plain Craft Launcher 2/Controls/MyCard.vb
  4. 2 2
      Plain Craft Launcher 2/Controls/MyExtraButton.xaml
  5. 2 2
      Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb
  6. 27 21
      Plain Craft Launcher 2/Controls/MyPageLeft.vb
  7. 5 1
      Plain Craft Launcher 2/Controls/MyScrollViewer.vb
  8. 24 0
      Plain Craft Launcher 2/FormMain.xaml.vb
  9. 20 24
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  10. 9 14
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  11. 11 29
      Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
  12. 9 4
      Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
  13. 152 99
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  14. 18 12
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  15. 18 14
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  16. 2 2
      Plain Craft Launcher 2/Modules/ModMusic.vb
  17. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  18. 108 74
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  19. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  20. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  21. 1 8
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  22. 1 1
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
  23. 1 1
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelpDetail.xaml.vb
  24. 1 0
      Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb
  25. 1 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml
  26. 0 1
      Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb
  27. 185 127
      Plain Craft Launcher 2/Resources/Custom.xaml
  28. BIN
      Plain Craft Launcher 2/Resources/Help.zip

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

@@ -133,7 +133,7 @@
                 <Setter Property="Template">
                     <Setter.Value>
                         <ControlTemplate>
-                            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1.5" CornerRadius="4" MaxWidth="500" Background="#E5FFFFFF" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                            <Border Loaded="TooltipLoaded" Unloaded="TooltipUnloaded" BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1.5" CornerRadius="4" MaxWidth="500" Background="#E5FFFFFF" Margin="0,0,4,4" SnapsToDevicePixels="True">
                                 <Border.Effect>
                                     <DropShadowEffect Opacity="0.3" BlurRadius="4" ShadowDepth="2" Color="{DynamicResource ColorObject1}" />
                                 </Border.Effect>
@@ -147,7 +147,7 @@
                 <Setter Property="Template">
                     <Setter.Value>
                         <ControlTemplate>
-                            <Border BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1" CornerRadius="3" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
+                            <Border Loaded="TooltipLoaded" Unloaded="TooltipUnloaded" BorderBrush="{DynamicResource ColorBrush1}" BorderThickness="1" CornerRadius="3" Background="White" Margin="0,0,4,4" SnapsToDevicePixels="True">
                                 <TextBlock Text="{TemplateBinding ContentControl.Content}" TextWrapping="Wrap" Foreground="{DynamicResource ColorBrush1}" Margin="7,5" FontSize="12"/>
                             </Border>
                         </ControlTemplate>

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

@@ -247,4 +247,12 @@ Public Class Application
         End Select
     End Sub
 
+    Public Shared ShowingTooltips As New List(Of Border)
+    Private Sub TooltipLoaded(sender As Border, e As EventArgs)
+        ShowingTooltips.Add(sender)
+    End Sub
+    Private Sub TooltipUnloaded(sender As Border, e As RoutedEventArgs)
+        ShowingTooltips.Remove(sender)
+    End Sub
+
 End Class

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

@@ -41,10 +41,6 @@
     End Sub
     Private IsLoad As Boolean = False
     Private Sub MyCard_Loaded() Handles Me.Loaded
-        '卡片的进入时动画已被页面通用切换动画替代
-        'If LoadAnimation AndAlso IsLoad Then
-        '    StartHeightAnimation(Math.Min(50, ActualHeight), ActualHeight - Math.Min(50, ActualHeight), True)
-        'End If
         If IsLoad Then Exit Sub
         '初次加载限定
         If Not Title = "" AndAlso IsNothing(MainTextBlock) Then

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

@@ -16,9 +16,9 @@
                 <DropShadowEffect ShadowDepth="0" BlurRadius="10" Opacity="0.2" />
             </Border.Effect>
         </Border>
-        <Border x:Name="PanProgress" CornerRadius="1000" BorderThickness="0.001" Background="#2AFFFFFF" ClipToBounds="True" Visibility="Collapsed">
+        <Border x:Name="PanProgress" CornerRadius="1000" BorderThickness="0.001" Background="#2FFFFFFF" ClipToBounds="True" Visibility="Collapsed">
             <Border.Clip>
-                <RectangleGeometry x:Name="RectProgress" Rect="0,0,40,25" />
+                <RectangleGeometry x:Name="RectProgress" Rect="0,5,40,35" />
             </Border.Clip>
         </Border>
         <Path x:Name="Path" Stretch="Uniform" Margin="12" Fill="{DynamicResource ColorBrush8}" RenderTransformOrigin="0.5,0.5">

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

@@ -17,7 +17,7 @@
                 PanProgress.Visibility = Visibility.Collapsed
             Else
                 PanProgress.Visibility = Visibility.Visible
-                RectProgress.Rect = New Rect(0, 0, 40, 40 * (1 - value))
+                RectProgress.Rect = New Rect(0, 40 * (1 - value), 40, 40 * value)
             End If
         End Set
     End Property
@@ -179,7 +179,7 @@
 
                 AniStop("MyExtraButton Color " & Uuid)
                 If IsMouseOver Then
-                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush2")
+                    PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4")
                 Else
                     PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3")
                 End If

+ 27 - 21
Plain Craft Launcher 2/Controls/MyPageLeft.vb

@@ -29,21 +29,28 @@
             '逐个进入动画
             Dim AniList As New List(Of AniData)
             Dim Id As Integer = 0, Delay As Integer = 0
-            Dim Controls = GetAllAnimControls()
-            For Each Element As FrameworkElement In Controls
-                Element.Opacity = 0
-                Element.RenderTransform = New TranslateTransform(-25, 0)
-                AniList.Add(AaOpacity(Element, If(TypeOf Element Is TextBlock, 0.6, 1), 200, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
-                AniList.Add(AaTranslateX(Element, 5, 200, Delay, New AniEaseOutFluent))
-                AniList.Add(AaTranslateX(Element, 20, 300, Delay, New AniEaseOutBack(AniEasePower.Weak)))
-                If TypeOf Element Is MyListItem Then
-                    AniList.Add(AaCode(Sub()
-                                           CType(Element, MyListItem).IsMouseOverAnimationEnabled = True
-                                           CType(Element, MyListItem).RefreshColor(Me, New EventArgs)
-                                       End Sub, Delay + 280))
+            For Each Element As FrameworkElement In GetAllAnimControls(True)
+                If Element.Visibility = Visibility.Collapsed Then
+                    '还原之前的隐藏动画可能导致的改变(#2436)
+                    Element.Opacity = 1
+                    Element.RenderTransform = New TranslateTransform(0, 0)
+                    If TypeOf Element Is MyListItem Then CType(Element, MyListItem).IsMouseOverAnimationEnabled = True
+                Else
+                    Element.Opacity = 0
+                    Element.RenderTransform = New TranslateTransform(-25, 0)
+                    If TypeOf Element Is MyListItem Then CType(Element, MyListItem).IsMouseOverAnimationEnabled = False
+                    AniList.Add(AaOpacity(Element, If(TypeOf Element Is TextBlock, 0.6, 1), 200, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
+                    AniList.Add(AaTranslateX(Element, 5, 200, Delay, New AniEaseOutFluent))
+                    AniList.Add(AaTranslateX(Element, 20, 300, Delay, New AniEaseOutBack(AniEasePower.Weak)))
+                    If TypeOf Element Is MyListItem Then
+                        AniList.Add(AaCode(Sub()
+                                               CType(Element, MyListItem).IsMouseOverAnimationEnabled = True
+                                               CType(Element, MyListItem).RefreshColor(Me, New EventArgs)
+                                           End Sub, Delay + 280))
+                    End If
+                    Delay += Math.Max(8, 20 - Id) * 2.5
+                    Id += 1
                 End If
-                Delay += Math.Max(8, 20 - Id) * 2.5
-                Id += 1
             Next
             AniStart(AniList, "PageLeft PageChange " & Uuid)
         End If
@@ -74,23 +81,22 @@
     End Sub
 
     '遍历获取所有需要生成动画的控件
-    Private Function GetAllAnimControls() As List(Of FrameworkElement)
+    Private Function GetAllAnimControls(Optional IgnoreInvisibility As Boolean = False) As List(Of FrameworkElement)
         Dim AllControls As New List(Of FrameworkElement)
-        GetAllAnimControls(AnimatedControl, AllControls)
+        GetAllAnimControls(AnimatedControl, AllControls, IgnoreInvisibility)
         Return AllControls
     End Function
-    Private Sub GetAllAnimControls(Element As FrameworkElement, ByRef AllControls As List(Of FrameworkElement))
-        If Element.Visibility = Visibility.Collapsed Then Exit Sub
+    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 MyTextButton Then
             AllControls.Add(Element)
         ElseIf TypeOf Element Is MyListItem Then
-            CType(Element, MyListItem).IsMouseOverAnimationEnabled = False
             AllControls.Add(Element)
         ElseIf TypeOf Element Is ContentControl Then
-            GetAllAnimControls(CType(Element, ContentControl).Content, AllControls)
+            GetAllAnimControls(CType(Element, ContentControl).Content, AllControls, IgnoreInvisibility)
         ElseIf TypeOf Element Is Panel Then
             For Each Element2 As FrameworkElement In CType(Element, Panel).Children
-                GetAllAnimControls(Element2, AllControls)
+                GetAllAnimControls(Element2, AllControls, IgnoreInvisibility)
             Next
         Else
             AllControls.Add(Element)

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

@@ -15,8 +15,12 @@
             '如果当前是在对有滚动条的下拉框或文本框执行,则不接管操作
             Exit Sub
         End If
-        PerformVerticalOffsetDelta(-e.Delta)
         e.Handled = True
+        PerformVerticalOffsetDelta(-e.Delta)
+        '关闭 Tooltip (#2552)
+        For Each TooltipBorder In Application.ShowingTooltips
+            AniStart(AaOpacity(TooltipBorder, -1, 100), $"Hide Tooltip {GetUuid()}")
+        Next
     End Sub
     Public Sub PerformVerticalOffsetDelta(Delta As Double)
         AniStart(AaDouble(Sub(AnimDelta As Double)

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

@@ -10,6 +10,11 @@ Public Class FormMain
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
 #If BETA Then
+        If LastVersion < 304 Then 'Release 2.6.8
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复开始或结束游戏时可能报错的 Bug"))
+            FeatureCount += 20
+            BugCount += 25
+        End If
         If LastVersion < 302 Then 'Release 2.6.7
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "再次修复网络条件差时可能无法下载 MC 的 Bug"))
             BugCount += 1
@@ -128,6 +133,11 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '1:BUG-
+        If LastVersion < 305 Then 'Snapshot 2.6.8
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复开始或结束游戏时可能报错的 Bug"))
+            FeatureCount += 20
+            BugCount += 25
+        End If
         If LastVersion < 303 Then 'Snapshot 2.6.7
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "再次修复网络条件差时可能无法下载 MC 的 Bug"))
             BugCount += 1
@@ -331,6 +341,7 @@ Public Class FormMain
     End Sub
 
     '窗口加载
+    Private Declare Auto Function ChangeWindowMessageFilterEx Lib "user32.dll" (ByVal hwnd As IntPtr, ByVal message As UInt32, ByVal action As UInt32, ByVal changeInfo As UInt32) As Boolean
     Private IsWindowLoadFinished As Boolean = False
     ''' <summary>
     ''' 是否为联机提权后自动重启。
@@ -442,6 +453,7 @@ Public Class FormMain
         TimerMainStartRun()
         '加载池
         RunInNewThread(Sub()
+                           'EULA 提示
                            If Not Setup.Get("SystemEula") Then
                                Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
                                    Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
@@ -451,6 +463,7 @@ Public Class FormMain
                                        EndProgram(False)
                                End Select
                            End If
+                           '启动加载器池
                            Try
                                JavaListInit() '延后到同意协议后再执行,避免在初次启动时进行进程操作
                                Thread.Sleep(200)
@@ -460,11 +473,22 @@ Public Class FormMain
                            Catch ex As Exception
                                Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
                            End Try
+                           '清理自动更新文件
                            Try
                                If File.Exists(Path & "PCL\Plain Craft Launcher 2.exe") Then File.Delete(Path & "PCL\Plain Craft Launcher 2.exe")
                            Catch ex As Exception
                                Log(ex, "清理自动更新文件失败")
                            End Try
+                           '开启管理员权限下的文件拖拽(#2531)
+                           Try
+                               If IsAdmin() Then
+                                   Log("[System] PCL 正以管理员权限运行")
+                                   ChangeWindowMessageFilterEx(Handle, &H233, 1, Nothing)
+                                   ChangeWindowMessageFilterEx(Handle, &H49, 1, Nothing)
+                               End If
+                           Catch ex As Exception
+                               Log(ex, "开启管理员权限下的文件拖拽失败")
+                           End Try
                        End Sub, "Start Loader", ThreadPriority.Lowest)
 
         Log("[Start] 第三阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")

+ 20 - 24
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -11,12 +11,12 @@ Public Module ModBase
 #Region "声明"
 
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.6.7" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.6.7." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.6.8" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.6.8." & VersionBranchCode '标准格式的四段式版本号
 #If BETA Then
-    Public Const VersionCode As Integer = 302 'Release
+    Public Const VersionCode As Integer = 304 'Release
 #Else
-    Public Const VersionCode As Integer = 303 'Snapshot
+    Public Const VersionCode As Integer = 305 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -1242,23 +1242,7 @@ Re:
     Public Function ExtractFile(CompressFilePath As String, DestDirectory As String, Optional Encode As Encoding = Nothing) As Boolean
         Try
             Directory.CreateDirectory(DestDirectory)
-            If CompressFilePath.EndsWith(".zip") OrElse CompressFilePath.EndsWith(".jar") OrElse CompressFilePath.EndsWith(".mrpack") Then
-                '以 zip 方式解压
-                Using stream As New FileStream(CompressFilePath, FileMode.Open)
-                    Using archive As New ZipArchive(stream, ZipArchiveMode.Read, False, If(Encode, Encoding.GetEncoding("GB18030")))
-                        For Each entry As ZipArchiveEntry In archive.Entries
-                            Dim destinationPath As String = IO.Path.Combine(DestDirectory, entry.FullName)
-                            If destinationPath.EndsWith("\") OrElse destinationPath.EndsWith("/") Then
-                                Directory.CreateDirectory(destinationPath)
-                            Else
-                                If File.Exists(destinationPath) Then File.Delete(destinationPath)
-                                entry.ExtractToFile(destinationPath, True)
-                            End If
-                        Next
-                    End Using
-                End Using
-                Return True
-            ElseIf CompressFilePath.EndsWith(".gz") Then
+            If CompressFilePath.EndsWith(".gz") Then
                 '以 gz 方式解压
                 Dim stream As New GZipStream(New FileStream(CompressFilePath, FileMode.Open, FileAccess.ReadWrite), CompressionMode.Decompress)
                 Dim decompressedFile As New FileStream(DestDirectory & GetFileNameFromPath(CompressFilePath).ToLower.Replace(".tar", "").Replace(".gz", ""), FileMode.OpenOrCreate, FileAccess.Write)
@@ -1271,7 +1255,19 @@ Re:
                 stream.Close()
                 Return True
             Else
-                Return False
+                '以 zip 方式解压
+                Using Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read, If(Encode, Encoding.GetEncoding("GB18030")))
+                    For Each Entry As ZipArchiveEntry In Archive.Entries
+                        Dim DestinationPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
+                        If DestinationPath.EndsWith("\") OrElse DestinationPath.EndsWith("/") Then
+                            Continue For '不创建空文件夹
+                        Else
+                            Directory.CreateDirectory(GetPathFromFullPath(DestinationPath))
+                            Entry.ExtractToFile(DestinationPath, True)
+                        End If
+                    Next
+                End Using
+                Return True
             End If
         Catch ex As Exception
             Log(ex, "尝试解压文件失败")
@@ -1301,7 +1297,7 @@ Re:
         Next
         Temp = Directory.GetDirectories(Path)
         For Each str As String In Temp
-            DeleteDirectory(str)
+            DeleteDirectory(str, IgnoreIssue)
         Next
         Try
             Directory.Delete(Path, True)
@@ -1578,7 +1574,7 @@ Re:
     ''' 为字符串进行 XML 转义。
     ''' </summary>
     Public Function EscapeXML(Str As String) As String
-        Return Str.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("'", "&apos;").Replace("""", "&quot;")
+        Return Str.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("'", "&apos;").Replace("""", "&quot;").Replace(vbCrLf, "&#xa;")
     End Function
 
     '正则

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

@@ -437,10 +437,6 @@
                             For Each Loader In Loaders
                                 Loader.Abort()
                             Next
-                            '从列表删除
-                            SyncLock LoaderTaskbarLock
-                                If LoaderTaskbar.Contains(Me) Then LoaderTaskbar.Remove(Me)
-                            End SyncLock
                         End Sub)
         End Sub
 
@@ -571,6 +567,7 @@ Restart:
         If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRemove(Loader)
         SyncLock LoaderTaskbarLock
             LoaderTaskbar.Add(Loader)
+            Log($"[Taskbar] {Loader.Name} 已加入任务列表")
         End SyncLock
     End Sub
     Public Sub LoaderTaskbarProgressRefresh()
@@ -585,16 +582,13 @@ Restart:
                     If Loader.Show AndAlso Loader.State = LoadState.Loading Then IsAllDownloadTaskCompleted = False
                 Next
                 '若单个任务已中止或全部任务已完成,则刷新并移除
-                Dim RemovedTask As New List(Of LoaderBase)
-                For i = 0 To LoaderTaskbar.Count - 1
-                    If (IsAllDownloadTaskCompleted OrElse LoaderTaskbar(i).State = LoadState.Aborted) AndAlso LoaderTaskbar(i).Show Then
-                        If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRefresh(LoaderTaskbar(i))
-                        RemovedTask.Add(LoaderTaskbar(i))
+                For Each Task In LoaderTaskbar.ToList()
+                    If (IsAllDownloadTaskCompleted OrElse Task.State = LoadState.Aborted OrElse Task.State = LoadState.Waiting) AndAlso Task.Show Then
+                        If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRefresh(Task)
+                        LoaderTaskbar.Remove(Task)
+                        Log($"[Taskbar] {Task.Name} 已移出任务列表")
                     End If
                 Next
-                For Each Task In RemovedTask
-                    LoaderTaskbar.Remove(Task)
-                Next
             End SyncLock
             '更新平滑后的进度
             If NewProgress <= 0 OrElse NewProgress >= 1 OrElse LoaderTaskbarProgress > NewProgress Then
@@ -625,8 +619,9 @@ Restart:
         Try
             Dim Total As Double = 0, Count As Integer = 0
             SyncLock LoaderTaskbarLock
-                For i = 0 To LoaderTaskbar.Count - 1
-                    Total += LoaderTaskbar(i).Progress
+                For Each Task In LoaderTaskbar.ToList
+                    If Not Task.Show Then Continue For
+                    Total += Task.Progress
                     Count += 1 '避免多线程影响导致计数出错
                 Next
             End SyncLock

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

@@ -28,7 +28,7 @@ Public Class MyBitmap
     Public Shared Widening Operator CType(Image As MyBitmap) As ImageSource
         Dim Bitmap = Image.Pic
         Dim rect = New System.Drawing.Rectangle(0, 0, Bitmap.Width, Bitmap.Height)
-        Dim bitmapData = Bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
+        Dim bitmapData = Bitmap.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb)
         Try
             Dim size = rect.Width * rect.Height * 4
             Return BitmapSource.Create(Bitmap.Width, Bitmap.Height, Bitmap.HorizontalResolution, Bitmap.VerticalResolution, PixelFormats.Bgra32, Nothing, bitmapData.Scan0, size, bitmapData.Stride)
@@ -104,45 +104,27 @@ Public Class MyBitmap
         End Using
     End Sub
 
-    ''' <summary>
-    ''' 获取旋转的图片,这个方法不会导致原对象改变且会返回一个新的对象。图片长宽必须相等。
-    ''' </summary>
-    ''' <param name="angle">旋转角度(单位为角度)。</param>
-    Public Function Rotation(angle As Double) As System.Drawing.Bitmap
-        Dim img As System.Drawing.Image = Me.Pic
-        Dim bitSize As Single = img.Width
-        Dim bmp As New System.Drawing.Bitmap(CInt(bitSize), CInt(bitSize), img.PixelFormat)
-        bmp.SetResolution(img.HorizontalResolution, img.VerticalResolution)
-        Using g As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(bmp)
-            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor
-            g.TranslateTransform(bitSize / 2, bitSize / 2)
-            g.RotateTransform(angle)
-            g.TranslateTransform(-bitSize / 2, -bitSize / 2)
-            g.DrawImage(img, New System.Drawing.Rectangle(0, 0, img.Width, img.Width))
-        End Using
-        Return bmp
-    End Function
-
     ''' <summary>
     ''' 获取裁切的图片,这个方法不会导致原对象改变且会返回一个新的对象。
     ''' </summary>
-    Public Function Clip(rect As System.Drawing.Rectangle) As System.Drawing.Bitmap
-        Dim img As System.Drawing.Image = Pic
-        Dim bmp As New System.Drawing.Bitmap(rect.Width, rect.Height, img.PixelFormat)
-        bmp.SetResolution(img.HorizontalResolution, img.VerticalResolution)
+    Public Function Clip(X As Integer, Y As Integer, Width As Integer, Height As Integer) As MyBitmap
+        Dim bmp As New System.Drawing.Bitmap(Width, Height, Pic.PixelFormat)
+        bmp.SetResolution(Pic.HorizontalResolution, Pic.VerticalResolution)
         Using g As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(bmp)
             g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor
-            g.DrawImageUnscaled(img, rect.X, rect.Y)
+            g.TranslateTransform(-X, -Y)
+            g.DrawImage(Pic, New System.Drawing.Rectangle(0, 0, Pic.Width, Pic.Height))
         End Using
         Return bmp
     End Function
 
     ''' <summary>
-    ''' 获取左右翻转的图片,这个方法不会导致原对象改变。
+    ''' 获取旋转或翻转后的图片,这个方法不会导致原对象改变且会返回一个新的对象。
     ''' </summary>
-    Public Function LeftRightFilp() As System.Drawing.Bitmap
+    Public Function RotateFlip(Type As System.Drawing.RotateFlipType) As MyBitmap
         Dim bmp As New System.Drawing.Bitmap(Pic)
-        bmp.RotateFlip(System.Drawing.RotateFlipType.RotateNoneFlipX)
+        bmp.SetResolution(Pic.HorizontalResolution, Pic.VerticalResolution)
+        bmp.RotateFlip(Type)
         Return bmp
     End Function
 
@@ -152,7 +134,7 @@ Public Class MyBitmap
     Public Sub Save(FilePath As String)
         Dim encoder As BitmapEncoder = New PngBitmapEncoder()
         encoder.Frames.Add(BitmapFrame.Create(Me))
-        Using fileStream = New System.IO.FileStream(FilePath, FileMode.Create)
+        Using fileStream As New FileStream(FilePath, FileMode.Create)
             encoder.Save(fileStream)
         End Using
     End Sub

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

@@ -434,7 +434,7 @@
                 Dim NormalNameList = New List(Of String)
                 For Each Name In NameLists
                     Dim LowerName As String = Name.ToLower
-                    If Name.ToUpper = Name Then
+                    If Name.ToUpper = Name AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
                         '缩写
                         ExNameList.Add(Name)
                     ElseIf (LowerName.Contains("forge") OrElse LowerName.Contains("fabric") OrElse LowerName.Contains("quilt")) AndAlso
@@ -487,8 +487,10 @@ NoExName:
                     '将 “Forge” 等提示改为 “Forge 版”
                     If (Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")) AndAlso Not Ex.Contains("版") AndAlso
                         Ex.ToLower.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
-                        Ex = Ex.Replace("Edition", "").Trim.Capitalize & " 版"
+                        Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
                     End If
+                    '将 “forge” 等词语的首字母大写
+                    Ex = Ex.Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
                     ExName &= "  |  " & Ex.Trim
                 Next
                 If ExName = "" Then GoTo NoExName
@@ -933,7 +935,10 @@ Retry:
                             Next
                             '更新结果
                             SyncLock ResultsLock
-                                RawResults.AddRange(ProjectList)
+                                For Each Project In ProjectList
+                                    If Task.Input.Type = CompType.Mod AndAlso Not Project.ModLoaders.Any() Then Continue For '过滤插件(#2458)
+                                    RawResults.Add(Project)
+                                Next
                             End SyncLock
                             Storage.ModrinthOffset += ProjectList.Count
                             Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
@@ -988,7 +993,7 @@ Retry:
                                                   Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
         '加入列表
         RealResults.AddRange(RawResults)
-        Log($"[Comp] 去重后累计新增结果 {RealResults.Count} 个")
+        Log($"[Comp] 去重、筛选后累计新增结果 {RealResults.Count} 个")
 
 #End Region
 

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

@@ -127,7 +127,7 @@
         End Try
 
         '导入其中的日志文件
-        For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles
+        For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles.ToList()
             Try
                 If Not TargetFile.Exists OrElse TargetFile.Length = 0 Then Continue For
                 Dim Ext As String = TargetFile.Extension.ToLower
@@ -137,6 +137,8 @@
                     Else
                         AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TargetFile.FullName, ReadFile(TargetFile.FullName, Encoding.UTF8).Split(vbCrLf.ToCharArray)))
                     End If
+                Else
+                    File.Delete(TargetFile.FullName)
                 End If
             Catch ex As Exception
                 Log(ex, "导入单个日志文件失败")
@@ -235,6 +237,7 @@
                         End If
                     Case AnalyzeFileType.MinecraftLog
                         LogMc = ""
+                        LogMcDebug = ""
                         '创建文件名词典
                         Dim FileNameDict As New Dictionary(Of String, KeyValuePair(Of String, String()))
                         For Each SelectedFile In SelectedFiles
@@ -272,11 +275,21 @@
                                 Exit For
                             End If
                         Next
+                        '查找 Debug Log
+                        For Each FileName As String In {"debug.log", "debug log.txt"}
+                            If FileNameDict.ContainsKey(FileName) Then
+                                Dim CurrentLog = FileNameDict(FileName)
+                                LogMcDebug += GetHeadTailLines(CurrentLog.Value, 1000, 0)
+                                Log("[Crash] 导入分析:" & CurrentLog.Key & ",作为 Minecraft Debug 日志")
+                                Exit For
+                            End If
+                        Next
                         '检查错误
                         If LogMc = "" Then
                             LogMc = Nothing
                             Throw New Exception("无法找到匹配的 Minecraft Log")
                         End If
+                        If LogMcDebug = "" Then LogMcDebug = Nothing
                     Case AnalyzeFileType.ExtraLog
                         '全部丢过去
                         For Each SelectedFile In SelectedFiles
@@ -294,7 +307,7 @@
         If ResultCount = 0 Then
             Log("[Crash] 步骤 2:准备日志文本完成,没有任何可供分析的日志")
         Else
-            Log(("[Crash] 步骤 2:准备日志文本完成,找到" & If(LogMc Is Nothing, "", "游戏日志、") & If(LogHs Is Nothing, "", "虚拟机日志、") & If(LogCrash Is Nothing, "", "崩溃日志、")).TrimEnd("、") & "用作分析")
+            Log(("[Crash] 步骤 2:准备日志文本完成,找到" & If(LogMc Is Nothing, "", "游戏日志、") & If(LogMcDebug Is Nothing, "", "游戏 Debug 日志、") & If(LogHs Is Nothing, "", "虚拟机日志、") & If(LogCrash Is Nothing, "", "崩溃日志、")).TrimEnd("、") & "用作分析")
         End If
         Return ResultCount
 
@@ -315,7 +328,7 @@
     End Function
 
     '3:根据文本分析崩溃原因
-    Private LogMc As String = Nothing, LogHs As String = Nothing, LogCrash As String = Nothing
+    Private LogMc As String = Nothing, LogMcDebug As String = Nothing, LogHs As String = Nothing, LogCrash As String = Nothing
     Private LogAll As String
     '可能导致崩溃的原因与附加信息
     Private CrashReasons As New Dictionary(Of CrashReason, List(Of String))
@@ -324,6 +337,7 @@
     ''' </summary>
     Private Enum CrashReason
         Mod文件被解压
+        MixinBootstrap缺失
         内存不足
         使用JDK
         显卡不支持OpenGL
@@ -344,9 +358,8 @@
         ModMixin失败
         Mod加载器报错
         Mod初始化失败
-        崩溃日志堆栈分析发现关键字
-        崩溃日志堆栈分析发现Mod名称
-        MC日志堆栈分析发现关键字
+        堆栈分析发现关键字
+        堆栈分析发现Mod名称
         OptiFine导致无法加载世界 'https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world
         特定方块导致崩溃
         特定实体导致崩溃
@@ -372,7 +385,6 @@
     ''' </summary>
     Public Sub Analyze(Optional Version As McVersion = Nothing)
         Log("[Crash] 步骤 3:分析崩溃原因")
-        LogAll = (If(LogMc, "") & If(LogHs, "") & If(LogCrash, "")).ToLower
 
         '1. 精准日志匹配,中/高优先级
         AnalyzeCrit1()
@@ -381,37 +393,41 @@
         If CrashReasons.Count > 0 Then GoTo Done
 
         '2. 堆栈分析
-        If LogAll.Contains("forge") OrElse LogAll.Contains("fabric") OrElse LogAll.Contains("liteloader") Then
-            '来源崩溃日志的分析
+        LogAll = (If(LogMc, "") & If(LogMcDebug, "") & If(LogHs, "") & If(LogCrash, ""))
+        If LogAll.Contains("orge") OrElse LogAll.Contains("abric") OrElse LogAll.Contains("uilt") OrElse LogAll.Contains("iteloader") Then
+            Dim Keywords As New List(Of String)
+            '崩溃日志
             If LogCrash IsNot Nothing Then
                 Log("[Crash] 开始进行崩溃日志堆栈分析")
                 Dim StackLogs As String = LogCrash.Split("System Details").First
-                Dim Keywords = AnalyzeStackKeyword(StackLogs)
-                If Keywords.Count > 0 Then
-                    Dim Names = AnalyzeModName(Keywords)
-                    If Names Is Nothing Then
-                        AppendReason(CrashReason.崩溃日志堆栈分析发现关键字, Keywords)
-                    Else
-                        AppendReason(CrashReason.崩溃日志堆栈分析发现Mod名称, Names)
-                    End If
-                    GoTo Done
-                End If
+                Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
             End If
-            '来自 Minecraft 日志的分析
+            'Minecraft 日志
             If LogMc IsNot Nothing Then
                 Dim Fatals = RegexSearch(LogMc, "/FATAL] [\w\W]+?(?=[\n]+\[)")
                 Log("[Crash] 开始进行 Minecraft 日志堆栈分析,发现 " & Fatals.Count & " 个报错项")
                 If Fatals.Count > 0 Then
-                    Dim Keywords As New List(Of String)
                     For Each Fatal In Fatals
                         Keywords.AddRange(AnalyzeStackKeyword(Fatal))
                     Next
-                    If Keywords.Count > 0 Then
-                        AppendReason(CrashReason.MC日志堆栈分析发现关键字, Keywords.Distinct.ToList)
-                        GoTo Done
-                    End If
                 End If
             End If
+            '虚拟机日志
+            If LogHs IsNot Nothing Then
+                Log("[Crash] 开始进行虚拟机堆栈分析")
+                Dim StackLogs As String = LogHs.Split("Registers:").First.Split("T H R E A D").Last
+                Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
+            End If
+            'Mod 名称分析
+            If Keywords.Count > 0 Then
+                Dim Names = AnalyzeModName(Keywords)
+                If Names Is Nothing Then
+                    AppendReason(CrashReason.堆栈分析发现关键字, Keywords)
+                Else
+                    AppendReason(CrashReason.堆栈分析发现Mod名称, Names)
+                End If
+                GoTo Done
+            End If
         Else
             Log("[Crash] 可能并未安装 Mod,不进行堆栈分析")
         End If
@@ -563,6 +579,19 @@ Done:
     ''' </summary>
     Private Sub AnalyzeCrit2()
 
+        '游戏日志分析
+        If LogMc IsNot Nothing Then
+            '常规信息
+            If LogMc.Contains("An exception was thrown, the game will display an error screen and halt.") Then AppendReason(CrashReason.Forge报错, RegexSeek(LogMc, "(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")?.Trim(vbCrLf))
+            If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
+        End If
+
+    End Sub
+    ''' <summary>
+    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
+    ''' </summary>
+    Private Sub AnalyzeCrit3()
+
         '游戏日志分析
         If LogMc IsNot Nothing Then
             'Mixin 崩溃
@@ -572,28 +601,17 @@ Done:
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=in mixins.)[^./ ]+(?=.json.+failed injection check)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in mixins.)[^./ ]+(?=.json)")
+                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed mixins.)[^./ ]+(?=.json:)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[)[^./ ]+(?=.mixins.json\] FAILED during )")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[mixins.)[^./ ]+(?=.json\] FAILED during )")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=from mod )[^./ ]+(?=\] from)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=for mod )[^./ ]+(?= failed)")
                 '兜底名称判断
-                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json")
-                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json")
+                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json)")
+                If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json)")
                 AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray)))
             End If
-            '常规信息
-            If LogMc.Contains("An exception was thrown, the game will display an error screen and halt.") Then AppendReason(CrashReason.Forge报错, RegexSeek(LogMc, "(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")?.Trim(vbCrLf))
-            If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
-        End If
-
-    End Sub
-    ''' <summary>
-    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
-    ''' </summary>
-    Private Sub AnalyzeCrit3()
-
-        '游戏日志分析
-        If LogMc IsNot Nothing Then
+            '极短的程序输出
             If Not (LogMc.Contains("at net.") OrElse LogMc.Contains("INFO]")) AndAlso LogHs Is Nothing AndAlso LogCrash Is Nothing AndAlso LogMc.Length < 100 Then
                 AppendReason(CrashReason.极短的程序输出, LogMc)
             End If
@@ -667,7 +685,7 @@ NextStack:
 
     End Function
     ''' <summary>
-    ''' 根据 Mod 关键词与详细信息(崩溃报告第二部分)与,获取实际的 Mod 名称。
+    ''' 根据 Mod 关键词尝试获取实际的 Mod 名称。
     ''' 若失败则返回 Nothing。
     ''' </summary>
     Private Function AnalyzeModName(Keywords As List(Of String)) As List(Of String)
@@ -682,60 +700,95 @@ NextStack:
         Next
         Keywords = RealKeywords
 
-        '获取崩溃报告对应部分
-        If LogCrash Is Nothing Then Return Nothing
-        If Not LogCrash.Contains("A detailed walkthrough of the error") Then Return Nothing
-        Dim Details As String = LogCrash.Replace("A detailed walkthrough of the error", "¨")
-        Dim IsFabricDetail As Boolean = Details.Contains("Fabric Mods") '是否为 Fabric 信息格式
-        If IsFabricDetail Then
-            Details = Details.Replace("Fabric Mods", "¨")
-            Log("[Crash] 检测到 Fabric Mod 信息格式")
+        '从崩溃报告获取 Mod 信息
+        If LogCrash IsNot Nothing AndAlso LogCrash.Contains("A detailed walkthrough of the error") Then
+            Dim Details As String = LogCrash.Replace("A detailed walkthrough of the error", "¨")
+            Dim IsFabricDetail As Boolean = Details.Contains("Fabric Mods") '是否为 Fabric 信息格式
+            If IsFabricDetail Then
+                Details = Details.Replace("Fabric Mods", "¨")
+                Log("[Crash] 崩溃报告中检测到 Fabric Mod 信息格式")
+            End If
+            Details = Details.Split("¨").Last
+
+            '[Forge] 获取所有包含 .jar 的行
+            '[Fabric] 获取所有包含 Mod 信息的行
+            Dim ModNameLines As New List(Of String)
+            For Each Line In Details.Split(vbLf)
+                If Line.ToLower.Contains(".jar") OrElse
+                   (IsFabricDetail AndAlso Line.StartsWith(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
+            Next
+            Log("[Crash] 崩溃报告中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
+
+            '获取 Mod ID 与关键词的匹配行
+            Dim HintLines As New List(Of String)
+            For Each KeyWord As String In Keywords
+                For Each ModString As String In ModNameLines
+                    Dim RealModString As String = ModString.ToLower.Replace("_", "")
+                    If Not RealModString.Contains(KeyWord.ToLower.Replace("_", "")) Then Continue For
+                    If RealModString.Contains("minecraft.jar") OrElse RealModString.Contains(" forge-") OrElse RealModString.Contains(" mixin-") Then Continue For
+                    HintLines.Add(ModString.Trim(vbCrLf.ToCharArray))
+                    Exit For
+                Next
+            Next
+            HintLines = HintLines.Distinct.ToList
+            Log("[Crash] 崩溃报告中找到 " & HintLines.Count & " 个可能的崩溃 Mod 匹配行")
+            For Each ModLine As String In HintLines
+                Log("[Crash]  - " & ModLine)
+            Next
+
+            '从 Mod 匹配行中提取 .jar 文件的名称
+            For Each Line As String In HintLines
+                Dim Name As String
+                If IsFabricDetail Then
+                    Name = RegexSeek(Line, "(?<=: )[^\n]+(?= [^\n]+)")
+                Else
+                    Name = RegexSeek(Line, "(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar", RegularExpressions.RegexOptions.IgnoreCase)
+                End If
+                If Name IsNot Nothing Then ModFileNames.Add(Name)
+            Next
+
         End If
-        Details = Details.Split("¨").Last
-
-        '[Forge] 获取所有包含 .jar 的行
-        '[Fabric] 获取所有包含 Mod 信息的行
-        Dim ModNameLines As New List(Of String)
-        For Each Line In Details.Split(vbLf)
-            If Line.ToLower.Contains(".jar") OrElse
-               (IsFabricDetail AndAlso Line.StartsWith(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
-        Next
-        Log("[Crash] 找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
-
-        '获取 Mod ID 与关键词的匹配行
-        Dim HintLines As New List(Of String)
-        For Each KeyWord As String In Keywords
-            For Each ModString As String In ModNameLines
-                Dim RealModString As String = ModString.ToLower.Replace("_", "")
-                If Not RealModString.Contains(KeyWord.ToLower.Replace("_", "")) Then Continue For
-                If RealModString.Contains("minecraft.jar") OrElse RealModString.Contains(" forge-") OrElse RealModString.Contains(" mixin-") Then Continue For
-                HintLines.Add(ModString.Trim(vbCrLf.ToCharArray))
-                Exit For
+
+        '从 debug.log 获取 Mod 信息
+        If LogMcDebug IsNot Nothing Then
+
+            'Forge: Found valid mod file YungsBetterStrongholds-1.20-Forge-4.0.1.jar with {betterstrongholds} mods - versions {1.20-Forge-4.0.1}
+            Dim ModNameLines As List(Of String) = RegexSearch(LogMcDebug, "(?<=valid mod file ).*", RegularExpressions.RegexOptions.Multiline)
+            Log("[Crash] Debug 信息中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
+
+            '获取 Mod ID 与关键词的匹配行
+            Dim HintLines As New List(Of String)
+            For Each KeyWord As String In Keywords
+                For Each ModString As String In ModNameLines
+                    If ModString.Contains($"{{{KeyWord}}}") Then HintLines.Add(ModString)
+                Next
+            Next
+            HintLines = HintLines.Distinct.ToList
+            Log("[Crash] Debug 信息中找到 " & HintLines.Count & " 个可能的崩溃 Mod 匹配行")
+            For Each ModLine As String In HintLines
+                Log("[Crash]  - " & ModLine)
             Next
-        Next
-        HintLines = HintLines.Distinct.ToList
-        Log("[Crash] 找到 " & HintLines.Count & " 个可能的崩溃 Mod 匹配行")
-        For Each ModName As String In HintLines
-            Log("[Crash]  - " & ModName)
-        Next
 
-        '从 Mod 匹配行中提取 .jar 文件的名称
-        For Each Line As String In HintLines
-            Dim Name As String
-            If IsFabricDetail Then
-                Name = RegexSeek(Line, "(?<=: )[^\n]+(?= [^\n]+)")
-            Else
-                Name = RegexSeek(Line, "(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar", RegularExpressions.RegexOptions.IgnoreCase)
-            End If
-            If Name IsNot Nothing Then ModFileNames.Add(Name)
-        Next
-        ModFileNames = ModFileNames.Distinct.ToList
-        Log("[Crash] 找到 " & ModFileNames.Count & " 个可能的崩溃 Mod 文件名")
-        For Each ModFileName As String In ModFileNames
-            Log("[Crash]  - " & ModFileName)
-        Next
+            '从 Mod 匹配行中提取 .jar 文件的名称
+            For Each Line As String In HintLines
+                Dim Name As String
+                Name = RegexSeek(Line, ".*(?= with)")
+                If Name IsNot Nothing Then ModFileNames.Add(Name)
+            Next
 
-        Return If(ModFileNames.Count = 0, Nothing, ModFileNames)
+        End If
+
+        '输出
+        ModFileNames = ModFileNames.Distinct.ToList
+        If ModFileNames.Count = 0 Then
+            Return Nothing
+        Else
+            Log("[Crash] 找到 " & ModFileNames.Count & " 个可能的崩溃 Mod 文件名")
+            For Each ModFileName As String In ModFileNames
+                Log("[Crash]  - " & ModFileName)
+            Next
+            Return ModFileNames
+        End If
     End Function
     ''' <summary>
     ''' 尝试获取 Mod 名称,若失败则返回原关键字。
@@ -841,26 +894,26 @@ NextStack:
                 Case CrashReason.Mod名称包含特殊字符
                     Results.Add("由于有 Mod 的名称包含特殊字符,导致游戏崩溃。\n请尝试修改 Mod 文件名,让它只包含英文字母、数字、减号(-)、下划线(_)和小数点,然后再启动游戏。")
                 Case CrashReason.MixinBootstrap缺失
-                    Results.Add("由于缺失 MixinBootstrap,导致游戏崩溃。\n请尝试安装 MixinBootstrap 解决。若安装后崩溃,尝试在该文件名前添加英文“!”后再试")
+                    Results.Add("由于缺失 MixinBootstrap,导致游戏崩溃。\n请尝试安装 MixinBootstrap。若安装后依然崩溃,可以尝试在文件名前添加英文感叹号。")
                 Case CrashReason.使用32位Java导致JVM无法分配足够多的内存
                     If Environment.Is64BitOperatingSystem Then
                         Results.Add("你似乎正在使用 32 位 Java,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n请在启动设置的 Java 选择一项中改用 64 位的 Java 再启动游戏,然后再启动游戏。\n如果你没有安装 64 位的 Java,你可以从网络中下载、安装一个。")
                     Else
                         Results.Add("你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h")
                     End If
-                Case CrashReason.崩溃日志堆栈分析发现关键字, CrashReason.MC日志堆栈分析发现关键字
-                    If Additional.Count = 1 Then
-                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了一个可疑的关键词:" & Additional.First & "。\n\n如果你知道它对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
-                    Else
-                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
-                    End If
                 Case CrashReason.Mod缺少前置或MC版本错误
                     If Additional.Count > 0 Then
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n未满足的依赖项:\n - " & Join(Additional, "\n - ") & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。")
                     Else
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h")
                     End If
-                Case CrashReason.崩溃日志堆栈分析发现Mod名称
+                Case CrashReason.堆栈分析发现关键字
+                    If Additional.Count = 1 Then
+                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了一个可疑的关键词:" & Additional.First & "。\n\n如果你知道它对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
+                    Else
+                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
+                    End If
+                Case CrashReason.堆栈分析发现Mod名称
                     If Additional.Count = 1 Then
                         Results.Add("名为 " & Additional.First & " 的 Mod 可能导致了游戏出错。\n\e\h")
                     Else

+ 18 - 12
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -1141,14 +1141,11 @@ SystemBrowser:
                 '1.15:Java 8 - 15
                 MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 15, 999, 999)
             ElseIf VersionSortBoolean(McVersionCurrent.Version.ForgeVersion, "34.0.0") AndAlso VersionSortBoolean("36.2.25", McVersionCurrent.Version.ForgeVersion) Then
-                '1.16.X,Forge 34.X ~ 36.2.25:最高 Java 8u320
+                '1.16,Forge 34.X ~ 36.2.25:最高 Java 8u320
                 MaxVer = New Version(1, 8, 0, 320)
-            ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 99 AndAlso McVersionCurrent.Version.HasOptiFine Then '#305
-                '1.18+:若安装了 OptiFine,最高 Java 18
+            ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 19 AndAlso McVersionCurrent.Version.HasOptiFine Then '#305
+                '1.18:若安装了 OptiFine,最高 Java 18
                 MaxVer = New Version(1, 18, 999, 999)
-            ElseIf McVersionCurrent.Version.McCodeMain >= 19 AndAlso McVersionCurrent.Version.McCodeMain < 99 Then
-                '1.19+:最高 Java 19
-                MaxVer = New Version(1, 19, 999, 999)
             End If
         End If
 
@@ -1753,6 +1750,15 @@ NextVersion:
 
         '更新 options.txt
         Dim SetupFileAddress As String = McVersionCurrent.PathIndie & "options.txt"
+        If Not File.Exists(SetupFileAddress) Then
+            'Yosbr Mod 兼容(#2385):https://www.curseforge.com/minecraft/mc-mods/yosbr
+            Dim YosbrFileAddress As String = McVersionCurrent.PathIndie & "config\yosbr\options.txt"
+            If File.Exists(YosbrFileAddress) Then
+                McLaunchLog("将修改 Yosbr Mod 中的 options.txt")
+                SetupFileAddress = YosbrFileAddress
+                WriteIni(SetupFileAddress, "lang", "none") '忽略默认语言
+            End If
+        End If
         Try
             '语言
             '1.0-     :没有语言选项
@@ -1773,11 +1779,11 @@ NextVersion:
                 WriteIni(SetupFileAddress, "lang", RequiredLang)
                 McLaunchLog($"已将语言从 {CurrentLang} 修改为 {RequiredLang}")
             End If
-            '如果是初次设置,一并修改 forceUnicodeFont
-            If Setup.Get("ToolHelpChinese") AndAlso (CurrentLang = "none" OrElse Not Directory.Exists(McVersionCurrent.PathIndie & "saves")) Then
-                WriteIni(SetupFileAddress, "forceUnicodeFont", "true")
-                McLaunchLog("已开启 forceUnicodeFont")
-            End If
+            ''如果是初次设置,一并修改 forceUnicodeFont
+            'If Setup.Get("ToolHelpChinese") AndAlso (CurrentLang = "none" OrElse Not Directory.Exists(McVersionCurrent.PathIndie & "saves")) Then
+            '    WriteIni(SetupFileAddress, "forceUnicodeFont", "true")
+            '    McLaunchLog("已开启 forceUnicodeFont")
+            'End If
             '窗口
             Select Case Setup.Get("LaunchArgumentWindowType")
                 Case 0 '全屏
@@ -1861,7 +1867,7 @@ NextVersion:
                 Dim Skin As New MyBitmap(PathAppdata & "CustomSkin.png")
                 If (McVersionCurrent.Version.McCodeMain = 6 OrElse McVersionCurrent.Version.McCodeMain = 7) AndAlso Skin.Pic.Height = 64 Then
                     McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
-                    Skin = Skin.Clip(New System.Drawing.Rectangle(0, 0, 64, 32))
+                    Skin = Skin.Clip(0, 0, 64, 32)
                 End If
                 Skin.Save(Path & "PCL\CustomSkin_Cliped.png")
                 '构建压缩文件

+ 18 - 14
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -1,4 +1,6 @@
-Public Module ModModpack
+Imports System.IO.Compression
+
+Public Module ModModpack
 
     '触发整合包安装的外部接口
     ''' <summary>
@@ -109,7 +111,7 @@
     '整合包缓存清理
     Private IsInstallCacheCleared As Boolean = False
     Private IsInstallCacheClearing As Boolean = False
-    Private Sub UnpackFiles(InstallTemp As String, FileAddress As String)
+    Private Sub UnpackFiles(InstallTemp As String, FileAddress As String, Loader As LoaderBase)
         '清理缓存文件夹
         If Not IsInstallCacheCleared Then
             IsInstallCacheCleared = True
@@ -135,9 +137,8 @@
         Try
 Retry:
             '完全不知道为啥会出现文件正在被另一进程使用的问题,总之多试试
-            Directory.CreateDirectory(InstallTemp)
             DeleteDirectory(InstallTemp)
-            Compression.ZipFile.ExtractToDirectory(FileAddress, InstallTemp, Encode)
+            ExtractFile(FileAddress, InstallTemp, Encode)
         Catch ex As Exception
             Log(ex, "第 " & RetryCount & " 次解压尝试失败")
             If TypeOf ex Is ArgumentException Then
@@ -146,6 +147,7 @@ Retry:
             End If
             If RetryCount < 5 Then
                 Thread.Sleep(RetryCount * 2000)
+                If Loader IsNot Nothing AndAlso Loader.LoadingState <> MyLoading.MyLoadingState.Run Then Exit Sub
                 RetryCount += 1
                 GoTo Retry
             Else
@@ -215,13 +217,15 @@ Retry:
         If OverrideHome <> "" Then
             InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
             Sub(Task As LoaderTask(Of String, Integer))
-                UnpackFiles(InstallTemp, FileAddress)
+                UnpackFiles(InstallTemp, FileAddress, Task)
                 Task.Progress = 0.5
+                Dim OverridePath As String = InstallTemp & ArchiveBaseFolder & OverrideHome
                 '复制结果
-                If Directory.Exists(InstallTemp & ArchiveBaseFolder & OverrideHome) Then
-                    CopyDirectory(InstallTemp & ArchiveBaseFolder & OverrideHome, PathMcFolder & "versions\" & VersionName)
+                If Directory.Exists(OverridePath) Then
+                    CopyDirectory(OverridePath, PathMcFolder & "versions\" & VersionName)
+                    Log($"[ModPack] 整合包 override 复制:{OverridePath} -> {PathMcFolder & "versions\" & VersionName}")
                 Else
-                    Log("[ModPack] 整合包中未找到 override 目录,已跳过")
+                    Log($"[ModPack] 整合包中未找到 override 文件夹:{OverridePath}")
                 End If
                 Task.Progress = 0.9
                 '开启版本隔离
@@ -393,7 +397,7 @@ Retry:
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
             Sub(Task As LoaderTask(Of String, Integer))
-                UnpackFiles(InstallTemp, FileAddress)
+                UnpackFiles(InstallTemp, FileAddress, Task)
                 Task.Progress = 0.5
                 '复制 overrides 文件夹和 client-overrides 文件夹
                 If Directory.Exists(InstallTemp & ArchiveBaseFolder & "overrides") Then
@@ -425,7 +429,7 @@ Retry:
                 End Select
             End If
             '添加下载文件
-            FileList.Add(New NetFile(File("downloads").Select(Function(t) t.ToString).ToArray,
+            FileList.Add(New NetFile(File("downloads").Select(Function(t) t.ToString.Replace("://edge.forgecdn", "://media.forgecdn")).ToArray, '修复 #2390
                                      PathMcFolder & "versions\" & VersionName & "\" & File("path").ToString,
                                      New FileChecker(ActualSize:=File("fileSize").ToObject(Of Long), Hash:=File("hashes")("sha1").ToString)))
         Next
@@ -520,7 +524,7 @@ Retry:
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
-            UnpackFiles(InstallTemp, FileAddress)
+            UnpackFiles(InstallTemp, FileAddress, Task)
             Task.Progress = 0.5
             '复制结果
             If Directory.Exists(InstallTemp & ArchiveBaseFolder & "minecraft") Then
@@ -619,7 +623,7 @@ Retry:
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
-            UnpackFiles(InstallTemp, FileAddress)
+            UnpackFiles(InstallTemp, FileAddress, Task)
             Task.Progress = 0.5
             '复制结果
             If Directory.Exists(InstallTemp & ArchiveBaseFolder & ".minecraft") Then
@@ -714,7 +718,7 @@ Retry:
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
-            UnpackFiles(InstallTemp, FileAddress)
+            UnpackFiles(InstallTemp, FileAddress, Task)
             Task.Progress = 0.5
             '复制结果
             If Directory.Exists(InstallTemp & ArchiveBaseFolder & "overrides") Then
@@ -805,7 +809,7 @@ Retry:
         If String.IsNullOrWhiteSpace(NewName) Then Exit Sub
         '解压
         Hint("正在解压压缩包……")
-        UnpackFiles(TargetFolder, FileAddress)
+        UnpackFiles(TargetFolder, FileAddress, Nothing)
         '加入文件夹列表
         PageSelectLeft.AddFolder(TargetFolder, NewName, False)
         Hint("已加入游戏文件夹列表:" & NewName, HintType.Finish)

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

@@ -25,7 +25,7 @@
                 For Each File In My.Computer.FileSystem.GetFiles(Path & "PCL\Musics\", FileIO.SearchOption.SearchAllSubDirectories, "*.*")
                     '文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的
                     Dim Extend As String = File.Split(".").Last.ToLower
-                    If Not (Extend = "ini" OrElse Extend = "jpg" OrElse Extend = "txt" OrElse Extend = "cfg" OrElse Extend = "lrc" OrElse Extend = "png") Then
+                    If Not (Extend = "ini" OrElse Extend = "jpg" OrElse Extend = "txt" OrElse Extend = "cfg" OrElse Extend = "lrc" OrElse Extend = "db" OrElse Extend = "png") Then
                         MusicAllList.Add(File)
                     End If
                 Next
@@ -212,7 +212,7 @@
     ''' 继续音乐播放,返回是否成功切换了状态。
     ''' </summary>
     Public Function MusicResume() As Boolean
-        If MusicState = MusicStates.Play Then
+        If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
             Return False
         Else
             RunInThread(Sub()

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

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

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

@@ -196,7 +196,7 @@ Public Module ModDownloadLib
         Else
             NewItem.Info = Entry("lore").ToString
         End If
-        If Entry("url").ToString.Contains("pcl") Then NewItem.Info = "[PCL 特别提供] " & NewItem.Info
+        If Entry("url").ToString.Contains("pcl") Then NewItem.Info = "[PCL 特供下载] " & NewItem.Info
         AddHandler NewItem.Click, OnClick
         '建立菜单
         If IsSaveOnly Then
@@ -299,8 +299,8 @@ Public Module ModDownloadLib
             WikiName = "Java版Infdev_20100618"
         ElseIf Id = "c0.30_01c" OrElse Id = "c0.30_survival" OrElse Id.Contains("生存测试") Then
             WikiName = "Java版Classic_0.30(生存模式)"
-        ElseIf Id = "c0.31" Then
-            WikiName = "Java版Indev_0.31(2010年1月30日)"
+        ElseIf Id.StartsWith("c0.31") Then
+            WikiName = "Java版Indev_0.31_20100130"
         ElseIf Id.StartsWith("c") Then
             WikiName = "Java版" & Id.Replace("c", "Classic_")
         ElseIf Id.StartsWith("rd-") Then
@@ -382,7 +382,7 @@ Public Module ModDownloadLib
             Log(ex, "开始 OptiFine 下载失败", LogLevel.Feedback)
         End Try
     End Sub
-    Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean))
+    Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
         '选择 Java
         Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
         If Java Is Nothing Then
@@ -399,7 +399,12 @@ Public Module ModDownloadLib
             If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
         End If
         '添加 Java Wrapper 作为主 Jar
-        Dim Arguments = "-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """ -Duser.home=""" & BaseMcFolderHome & """ -cp """ & Target & """ -jar """ & ExtractJavaWrapper() & """ optifine.Installer"
+        Dim Arguments As String
+        If UseJavaWrapper Then
+            Arguments = $"-Doolloo.jlw.tmpdir=""{GetJavaWrapperDir()}"" -Duser.home=""{BaseMcFolderHome}"" -cp ""{Target}"" -jar ""{ExtractJavaWrapper()}"" optifine.Installer"
+        Else
+            Arguments = $"-Duser.home=""{BaseMcFolderHome}"" -cp ""{Target}"" optifine.Installer"
+        End If
         If Java.VersionCode >= 9 Then Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " & Arguments
         '开始启动
         SyncLock InstallSyncLock
@@ -504,67 +509,66 @@ Public Module ModDownloadLib
 
         '获取下载地址
         Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("获取 OptiFine 主文件下载地址",
-                                                            Sub(Task As LoaderTask(Of String, List(Of NetFile)))
-                                                                '启动依赖版本的下载
-                                                                If ClientDownloadLoader Is Nothing Then
-                                                                    If IsCustomFolder Then Throw New Exception("如果没有指定原版下载器,则不能指定 MC 安装文件夹")
-                                                                    ClientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, DownloadInfo.Inherit)
-                                                                End If
-                                                                Task.Progress = 0.1
-                                                                Dim Sources As New List(Of String)
-                                                                'BMCLAPI 源
-                                                                If DownloadInfo.IsPreview Then
-                                                                    Sources.Add("https://download.mcbbs.net/optifine/" & DownloadInfo.Inherit & "/HD_U_" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", "").Replace(" ", "/"))
-                                                                    Sources.Add("https://bmclapi2.bangbang93.com/optifine/" & DownloadInfo.Inherit & "/HD_U_" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", "").Replace(" ", "/"))
-                                                                Else
-                                                                    Sources.Add("https://download.mcbbs.net/optifine/" & DownloadInfo.Inherit & "/HD_U/" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", ""))
-                                                                    Sources.Add("https://bmclapi2.bangbang93.com/optifine/" & DownloadInfo.Inherit & "/HD_U/" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", ""))
-                                                                End If
-                                                                '官方源
-                                                                Dim PageData As String
-                                                                Try
-                                                                    PageData = NetGetCodeByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, New UTF8Encoding(False), 15000, "text/html")
-                                                                    Task.Progress = 0.8
-                                                                    Sources.Add("https://optifine.net/" & RegexSearch(PageData, "downloadx\?f=[^""']+")(0))
-                                                                    Log("[Download] OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址:" & Sources(0))
-                                                                Catch ex As Exception
-                                                                    Log(ex, "获取 OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址失败")
-                                                                End Try
-                                                                Task.Progress = 0.9
-                                                                'OptiFine 中文镜像源
-                                                                Sources.Add("https://optifine.cn/download/" & DownloadInfo.NameFile)
-                                                                '构造文件请求
-                                                                Task.Output = New List(Of NetFile) From {New NetFile(Sources.ToArray, Target, New FileChecker(MinSize:=64 * 1024))}
-                                                            End Sub) With {.ProgressWeight = 8})
+            Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+                '启动依赖版本的下载
+                If ClientDownloadLoader Is Nothing Then
+                    If IsCustomFolder Then Throw New Exception("如果没有指定原版下载器,则不能指定 MC 安装文件夹")
+                    ClientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, DownloadInfo.Inherit)
+                End If
+                Task.Progress = 0.1
+                Dim Sources As New List(Of String)
+                'BMCLAPI 源
+                If DownloadInfo.IsPreview Then
+                    Sources.Add("https://download.mcbbs.net/optifine/" & DownloadInfo.Inherit & "/HD_U_" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", "").Replace(" ", "/"))
+                    Sources.Add("https://bmclapi2.bangbang93.com/optifine/" & DownloadInfo.Inherit & "/HD_U_" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", "").Replace(" ", "/"))
+                Else
+                    Sources.Add("https://download.mcbbs.net/optifine/" & DownloadInfo.Inherit & "/HD_U/" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", ""))
+                    Sources.Add("https://bmclapi2.bangbang93.com/optifine/" & DownloadInfo.Inherit & "/HD_U/" & DownloadInfo.NameDisplay.Replace(DownloadInfo.Inherit & " ", ""))
+                End If
+                '官方源
+                Dim PageData As String
+                Try
+                    PageData = NetGetCodeByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, New UTF8Encoding(False), 15000, "text/html")
+                    Task.Progress = 0.8
+                    Sources.Add("https://optifine.net/" & RegexSearch(PageData, "downloadx\?f=[^""']+")(0))
+                    Log("[Download] OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址:" & Sources(0))
+                Catch ex As Exception
+                    Log(ex, "获取 OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址失败")
+                End Try
+                'OptiFine 中文镜像源
+                Sources.Add("https://optifine.cn/download/" & DownloadInfo.NameFile)
+                '构造文件请求
+                Task.Output = New List(Of NetFile) From {New NetFile(Sources.ToArray, Target, New FileChecker(MinSize:=64 * 1024))}
+            End Sub) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderDownload("下载 OptiFine 主文件", New List(Of NetFile)) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("等待原版下载",
-                                                                         Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
-                                                                             '等待原版文件下载完成
-                                                                             If ClientDownloadLoader Is Nothing Then Exit Sub
-                                                                             Dim TargetLoaders As List(Of LoaderBase) =
-                                                                                ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
-                                                                                Where(Function(l) l.State <> LoadState.Finished).ToList
-                                                                             If TargetLoaders.Count > 0 Then Log("[Download] OptiFine 安装正在等待原版文件下载完成")
-                                                                             Do While TargetLoaders.Count > 0 AndAlso Not Task.IsAborted
-                                                                                 TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
-                                                                                 Thread.Sleep(50)
-                                                                             Loop
-                                                                             If Task.IsAborted Then Exit Sub
-                                                                             '拷贝原版文件
-                                                                             If Not IsCustomFolder Then Exit Sub
-                                                                             SyncLock VanillaSyncLock
-                                                                                 Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
-                                                                                 Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
-                                                                                 If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
-                                                                                     CopyFile(ClientFolder & ClientName & ".json",
+            Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
+                '等待原版文件下载完成
+                If ClientDownloadLoader Is Nothing Then Exit Sub
+                Dim TargetLoaders As List(Of LoaderBase) =
+                   ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
+                   Where(Function(l) l.State <> LoadState.Finished).ToList
+                If TargetLoaders.Count > 0 Then Log("[Download] OptiFine 安装正在等待原版文件下载完成")
+                Do While TargetLoaders.Count > 0 AndAlso Not Task.IsAborted
+                    TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
+                    Thread.Sleep(50)
+                Loop
+                If Task.IsAborted Then Exit Sub
+                '拷贝原版文件
+                If Not IsCustomFolder Then Exit Sub
+                SyncLock VanillaSyncLock
+                    Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
+                    Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
+                    If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
+                        CopyFile(ClientFolder & ClientName & ".json",
                                                                                           McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json")
-                                                                                 End If
-                                                                                 If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") Then
-                                                                                     CopyFile(ClientFolder & ClientName & ".jar",
+                    End If
+                    If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") Then
+                        CopyFile(ClientFolder & ClientName & ".jar",
                                                                                               McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar")
-                                                                                 End If
-                                                                             End SyncLock
-                                                                         End Sub) With {.ProgressWeight = 0.1, .Show = False})
+                    End If
+                End SyncLock
+            End Sub) With {.ProgressWeight = 0.1, .Show = False})
 
         '安装(新旧方式均需要原版 Jar 和 Json)
         If IsNewVersion Then
@@ -586,7 +590,19 @@ Public Module ModDownloadLib
                               BaseMcFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar")
                     Task.Progress = 0.06
                     '进行安装
-                    McDownloadOptiFineInstall(BaseMcFolderHome, Target, Task)
+                    Dim UseJavaWrapper As Boolean = True
+Retry:
+                    Try
+                        McDownloadOptiFineInstall(BaseMcFolderHome, Target, Task, UseJavaWrapper)
+                    Catch ex As Exception
+                        If UseJavaWrapper Then
+                            Log(ex, "使用 JavaWrapper 安装 OptiFine 失败,将不使用 JavaWrapper 并重试")
+                            UseJavaWrapper = False
+                            GoTo Retry
+                        Else
+                            Throw New Exception("运行 OptiFine 安装器失败", ex)
+                        End If
+                    End Try
                     Task.Progress = 0.96
                     '复制文件
                     File.Delete(BaseMcFolder & "launcher_profiles.json")
@@ -596,7 +612,7 @@ Public Module ModDownloadLib
                     File.Delete(Target)
                     DeleteDirectory(BaseMcFolderHome)
                 Catch ex As Exception
-                    Throw New Exception("安装新版 OptiFine 版本失败", ex)
+                    Throw New Exception("安装 OptiFine(方式 A)失败", ex)
                 End Try
             End Sub) With {.ProgressWeight = 8})
         Else
@@ -645,7 +661,7 @@ Public Module ModDownloadLib
                     End If
                     WriteFile(VersionFolder & Id & ".json", Json)
                 Catch ex As Exception
-                    Throw New Exception("安装旧版 OptiFine 版本失败", ex)
+                    Throw New Exception("安装 OptiFine(方式 B)失败", ex)
                 End Try
             End Sub) With {.ProgressWeight = 1})
         End If
@@ -1110,7 +1126,7 @@ Public Module ModDownloadLib
         End Try
     End Sub
 
-    Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String)
+    Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String, UseJavaWrapper As Boolean)
         '选择 Java
         Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
         If Java Is Nothing Then
@@ -1127,7 +1143,12 @@ Public Module ModDownloadLib
             If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
         End If
         '添加 Java Wrapper 作为主 Jar
-        Dim Arguments = "-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """ -cp """ & PathTemp & "Cache\forge_installer.jar;" & Target & """ -jar """ & ExtractJavaWrapper() & """  com.bangbang93.ForgeInstaller """ & McFolder
+        Dim Arguments As String
+        If UseJavaWrapper Then
+            Arguments = $"-Doolloo.jlw.tmpdir=""{GetJavaWrapperDir()}"" -cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" -jar ""{ExtractJavaWrapper()}"" com.bangbang93.ForgeInstaller ""{McFolder}"
+        Else
+            Arguments = $"-cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" com.bangbang93.ForgeInstaller ""{McFolder}"
+        End If
         If Java.VersionCode >= 9 Then Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " & Arguments
         '开始启动
         SyncLock InstallSyncLock
@@ -1415,12 +1436,25 @@ Public Module ModDownloadLib
                                                                     '释放 launcher_installer.json
                                                                     McFolderLauncherProfilesJsonCreate(McFolder)
                                                                     Task.Progress = 0.05
-                                                                    '释放 Forge 注入器
-                                                                    WriteFile(PathTemp & "Cache\forge_installer.jar", GetResources("ForgeInstaller"))
-                                                                    Task.Progress = 0.06
-                                                                    '运行注入器
-                                                                    ForgeInjector(InstallerAddress, Task, McFolder)
-                                                                    Task.Progress = 0.97
+                                                                    '运行 Forge 安装器
+                                                                    Dim UseJavaWrapper As Boolean = True
+Retry:
+                                                                    Try
+                                                                        '释放 Forge 注入器
+                                                                        WriteFile(PathTemp & "Cache\forge_installer.jar", GetResources("ForgeInstaller"))
+                                                                        Task.Progress = 0.06
+                                                                        '运行注入器
+                                                                        ForgeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper)
+                                                                        Task.Progress = 0.97
+                                                                    Catch ex As Exception
+                                                                        If UseJavaWrapper Then
+                                                                            Log(ex, "使用 JavaWrapper 安装 Forge 失败,将不使用 JavaWrapper 并重试")
+                                                                            UseJavaWrapper = False
+                                                                            GoTo Retry
+                                                                        Else
+                                                                            Throw New Exception("运行 Forge 安装器失败", ex)
+                                                                        End If
+                                                                    End Try
                                                                     '拷贝新增的版本 Json
                                                                     Dim DeltaList = New DirectoryInfo(McFolder & "versions\").EnumerateDirectories.SkipWhile(Function(Info As DirectoryInfo) As Boolean
                                                                                                                                                                  Return OldList.Contains(Info.FullName)
@@ -1470,7 +1504,7 @@ Public Module ModDownloadLib
                                                                         Task.Progress = 0.6
                                                                         '解压支持库文件
                                                                         Installer.Dispose()
-                                                                        ZipFile.ExtractToDirectory(InstallerAddress, InstallerAddress & "_unrar\")
+                                                                        ExtractFile(InstallerAddress, InstallerAddress & "_unrar\")
                                                                         CopyDirectory(InstallerAddress & "_unrar\maven\", McFolder & "libraries\")
                                                                         DeleteDirectory(InstallerAddress & "_unrar\")
                                                                     Else

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

@@ -848,7 +848,7 @@
         If LoadFabric.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……"
         If LoadFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadFabric.State, Object).Error.Message
         For Each Version As JObject In DlFabricListLoader.Output.Value("game")
-            If Version("version").ToString = SelectedMinecraftId.Replace("∞", "infinite") Then
+            If Version("version").ToString = SelectedMinecraftId.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") Then
                 If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容"
                 'If SelectedOptiFine IsNot Nothing Then Return "与 OptiFine 不兼容"
                 Return Nothing
@@ -908,7 +908,7 @@
     Public Shared Function IsSuitableFabricApi(DisplayName As String, MinecraftVersion As String) As Boolean
         Try
             If DisplayName Is Nothing OrElse MinecraftVersion Is Nothing Then Return False
-            DisplayName = DisplayName.ToLower : MinecraftVersion = MinecraftVersion.ToLower
+            DisplayName = DisplayName.ToLower : MinecraftVersion = MinecraftVersion.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").ToLower
             If DisplayName.StartsWith("[" & MinecraftVersion & "]") Then Return True
             If Not DisplayName.Contains("/") OrElse Not DisplayName.Contains("]") Then Return False
             '直接的判断(例如 1.18.1/22w03a)

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

@@ -104,7 +104,7 @@
                    (Image.Pic.GetPixel(1, 1) <> Image.Pic.GetPixel(Scale * 41, Scale * 9) AndAlso '或是头部颜色和透明区均不一样
                     Image.Pic.GetPixel(Image.Pic.Width - 1, Image.Pic.Height - 1) <> Image.Pic.GetPixel(Scale * 41, Scale * 9) AndAlso
                     Image.Pic.GetPixel(Image.Pic.Width - 2, Image.Pic.Height / 2 - 2) <> Image.Pic.GetPixel(Scale * 41, Scale * 9)) Then
-                    ImgFore.Source = New CroppedBitmap(Image, New Int32Rect(Scale * 40, Scale * 8, Scale * 8, Scale * 8))
+                    ImgFore.Source = Image.Clip(Scale * 40, Scale * 8, Scale * 8, Scale * 8)
                 Else
                     ImgFore.Source = Nothing
                 End If
@@ -112,7 +112,7 @@
                 ImgFore.Source = Nothing
             End If
             '脸层
-            ImgBack.Source = New CroppedBitmap(Image, New Int32Rect(Scale * 8, Scale * 8, Scale * 8, Scale * 8))
+            ImgBack.Source = Image.Clip(Scale * 8, Scale * 8, Scale * 8, Scale * 8)
             Log("[Skin] 载入头像成功:" & Loader.Name)
         Catch ex As Exception
             Log(ex, "载入头像失败(" & If(Address, "null") & "," & Loader.Name & ")", LogLevel.Hint)

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

@@ -673,14 +673,7 @@ ExitRefresh:
         McLaunchProcess = Nothing
         McLaunchWatcher = Nothing
         '获取 “你知道吗” 提示
-        Dim Hint As String = PageOtherTest.GetRandomHint()
-        Try
-            Dim RawHints = ReadFile(Path & "PCL\hints.txt").Split(vbCrLf.ToCharArray).Where(Function(s) Not String.IsNullOrWhiteSpace(s)).ToList
-            If RawHints.Count > 0 Then Hint = RandomOne(RawHints)
-        Catch ex As Exception
-            Log(ex, "获取自定义 你知道吗 提示失败", LogLevel.Hint)
-        End Try
-        LabLaunchingHint.Text = Hint
+        LabLaunchingHint.Text = PageOtherTest.GetRandomHint()
     End Sub
     Private ShowProgress As Double = 0
     ''' <summary>

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

@@ -133,7 +133,7 @@ StartDownload:
         Log("[IOI] 解压联机模块以完成下载")
         If File.Exists(PathTemp & "ioi.exe") Then File.Delete(PathTemp & "ioi.exe")
         If File.Exists(PathIoi) Then File.Delete(PathIoi)
-        Compression.ZipFile.ExtractToDirectory(PathTemp & "联机模块.zip", PathTemp)
+        ExtractFile(PathTemp & "联机模块.zip", PathTemp)
         File.Delete(PathTemp & "联机模块.zip")
         CopyFile(PathTemp & "ioi.exe", PathIoi)
         File.Delete(PathTemp & "ioi.exe")

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

@@ -14,7 +14,7 @@
             If String.IsNullOrEmpty(Entry.XamlContent) Then Throw New Exception("帮助 xaml 文件为空")
             Me.Entry = Entry
             PanCustom.Children.Clear()
-            FileContent = FileContent.Replace("{path}", Path)
+            FileContent = HelpArgumentReplace(FileContent)
             PanCustom.Children.Add(GetObjectFromXML(FileContent))
             Return True
         Catch ex As Exception

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

@@ -219,6 +219,7 @@
                         If(IsHintIndie, vbCrLf & "由于该版本开启了版本隔离,删除版本时该版本对应的存档、资源包、Mod 等文件也将被一并删除!", ""),
                         "版本删除确认", , "取消",, True)
                 Case 1
+                    IniClearCache(Version.Path & "PCL\Setup.ini")
                     If IsShiftPressed Then
                         DeleteDirectory(Version.Path)
                         Hint("版本 " & Version.Name & " 已永久删除!", HintType.Finish)

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

@@ -74,7 +74,7 @@
                     <local:MyCheckBox Text="正式版更新提示" Grid.Column="1" x:Name="CheckUpdateRelease" Tag="ToolUpdateRelease" ToolTip="在 Minecraft 正式版更新时弹窗提示" />
                     <local:MyCheckBox Text="测试版更新提示" Grid.Column="2" Height="22" x:Name="CheckUpdateSnapshot" Tag="ToolUpdateSnapshot" ToolTip="在 Minecraft 测试版(快照、预发布版)更新时弹窗提示" />
                     <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="游戏语言" Margin="0,0,25,0" />
-                    <local:MyCheckBox Text="默认设置为中文" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="5" x:Name="CheckHelpChinese" Tag="ToolHelpChinese" />
+                    <local:MyCheckBox Text="自动设置为中文" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="5" x:Name="CheckHelpChinese" Tag="ToolHelpChinese" />
                 </Grid>
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="系统">

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

@@ -195,7 +195,6 @@
                                                  AniDispose(sender, False)
                                                  AniDispose(Card, True, Sub() If FrmSpeedRight.PanMain.Children.Count = 0 AndAlso FrmMain.PageCurrent = FormMain.PageType.DownloadManager Then FrmMain.PageBack())
                                                  RightCards.Remove(Loader.Name)
-                                                 LoaderTaskbar.Remove(Loader)
                                                  Log($"[Watcher] 关闭下载管理卡片:{Loader.Name}")
                                                  RunInThread(Sub() Loader.Abort())
                                              End Sub

+ 185 - 127
Plain Craft Launcher 2/Resources/Custom.xaml

@@ -4,102 +4,102 @@
      你也可以使用 Ctrl + F 快速查找。例如,若需要改变文本颜色,则搜索 “颜色” 即可。 -->
 
 <local:MyCard Title="纯文本" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="每个 local:MyCard 代表一张卡片,你可以添加、删除格式类似的 MyCard 来添加多个卡片。每个 TextBlock 代表一段文本,你可以在 Text 属性中书写任何你想写的内容,也可以自行添加更多的 TextBlock。" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4" FontSize="11"
                     Text="你可以通过添加、删除属性修改样式,例如上一行的 FontSize 就会将字号改为 11 号。" />
         <TextBlock TextWrapping="Wrap" Margin="20,5,20,0" Foreground="#8C7721"
                     Text="它还有许多可以调整的属性:上一行的 Margin 调整了边距,Foreground 则会让文字变色。" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="卡片、提示条" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
-        <local:MyHint Text="local:MyHint 代表提示条,它默认是警告样式。" />
-        <local:MyHint Margin="0,8,0,2" IsWarn="False"
+	<StackPanel Margin="25,40,23,15">
+		<local:MyHint Text="local:MyHint 代表提示条,它默认是警告样式。" />
+		<local:MyHint Margin="0,8,0,2" IsWarn="False"
                     Text="将提示条的 IsWarn 属性改为 False 即可改为提示样式。&#xA;使用左边的那堆字符可以在任意地方手动换行,如果需要使用等号、引号等特殊字符,可以自行百度 XAML 转义字符。" />
         <TextBlock TextWrapping="Wrap" Margin="0,6,0,0"
                     Text="卡片(local:MyCard)的 Title 属性决定了它的标题。&#xA;为卡片添加 CanSwap 属性让它可以被折叠,True 代表是,False 代表否。在此基础上,再使用 IsSwaped 属性调整它是否默认被折叠。" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="长宽属性、图片" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4" Width="320" HorizontalAlignment="Right"
                     Text="任意项目都可以添加 Width 与 Height 属性来控制宽高。HorizontalAlignment 属性可以控制对齐:Center 代表居中,Right 代表居右,例如这段文本就被居右了。" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,6"
                     Text="Image 代表图片,你需要在它的 Source 属性中填写一个网址或文件路径,它会从该处获取图片并显示。通常需要使用 Height 限制它的高度。" />
-        <Image Height="50" HorizontalAlignment="Center" Source="https://www.mcbbs.net/template/mcbbs/image/logo_sc.png" />
-    </StackPanel>
+		<Image Height="50" HorizontalAlignment="Center" Source="https://www.mcbbs.net/template/mcbbs/image/logo_sc.png" />
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="按钮" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="添加 local:MyButton 即可新建一个按钮。你需要限定它的尺寸与位置,并通过 Padding 属性进一步控制它的内边距。" />
-        <local:MyButton Margin="0,4,0,10" Height="35" HorizontalAlignment="Left" Padding="25,0,25,0"
+		<local:MyButton Margin="0,4,0,10" Height="35" HorizontalAlignment="Left" Padding="25,0,25,0"
                     Text="这是一个按钮!一个按钮!" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="你可以修改 ColorType 属性来调整按钮配色:Highlight 代表当前主题色,Red 代表红色。ToolTip 属性还可以让你在鼠标悬浮在上面的时候显示提示文本。" />
-        <local:MyButton Margin="0,4,0,10" Width="250" Height="35" ColorType="Highlight"
+		<local:MyButton Margin="0,4,0,10" Width="250" Height="35" ColorType="Highlight"
                     Text="把鼠标停在这里别动……" ToolTip="……就能看到这句话!" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="local:MyTextButton 是没有边框的 “纯文本按钮”,其作用与 local:MyButton 一致。" />
-        <local:MyTextButton Margin="0,8,0,10" HorizontalAlignment="Center"
+		<local:MyTextButton Margin="0,8,0,10" HorizontalAlignment="Center"
                     Text="一个精简版的文本按钮" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="按钮可以做到很多事,具体将在 “事件” 部分介绍。" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="事件 1:打开网页、弹出窗口" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
                     Text="将按钮的 EventType 属性设为 打开网页,然后在 EventData 属性中写入网址,即可通过点击按钮打开网页。" />
-        <local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+		<local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                     Text="打开 MCBBS" EventType="打开网页" EventData="https://www.mcbbs.net/" />
         <TextBlock TextWrapping="Wrap" Margin="0,10,0,10"
                     Text="或者将 EventType 属性改为 弹出窗口,然后在 EventData 属性中写入弹窗的标题与内容……" />
-        <local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+		<local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                     Text="显示一个弹窗" EventType="弹出窗口" EventData="这是标题|标题与内容以竖线间隔。\n你也可以直接在其中换行。" />
         <TextBlock TextWrapping="Wrap" Margin="0,10,0,0"
                     Text="local:MyTextButton 同样可以设置 EventType 与 EventData 让它具有特定功能。" />
-        <local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
+		<local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
                     Text="点击打开知乎" EventType="打开网页" EventData="https://www.zhihu.com/" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="事件 2:启动游戏" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="将 EventType 设置为 启动游戏,EventData 设置为具体的游戏版本,即可通过点击按钮启动游戏。如果当前游戏文件夹没有该版本,则无法启动。" />
-        <local:MyButton Margin="0,4,0,10" Width="250" Height="35"
+		<local:MyButton Margin="0,4,0,10" Width="250" Height="35"
                     Text="启动 Minecraft 1.12.2" EventType="启动游戏" EventData="1.12.2" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="在 EventData 后面添加一条竖线(|),竖线后填写服务器 IP,即可在启动该版本的同时自动进入服务器。" />
-        <local:MyButton Margin="0,4,0,0" Width="250" Height="35"
+		<local:MyButton Margin="0,4,0,0" Width="250" Height="35"
                     Text="启动 1.12.2 并进入 Hypixel" EventType="启动游戏" EventData="1.12.2|mc.hypixel.net" ToolTip="服务器 IP:mc.hypixel.net" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="事件 3:打开文件" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="将 EventType 设置为 打开文件,EventData 设置为文件路径,即可点击打开特定文件或启动程序。" />
-        <local:MyButton Margin="0,4,0,8" Width="200" Height="35"
+		<local:MyButton Margin="0,4,0,8" Width="200" Height="35"
                     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="此外,PCL2 会将文件中的花括号 path 替换为 PCL 可执行文件所在文件夹({path}),可以用于执行特定程序或加载图片。" />
-        <local:MyButton Margin="0,4,0,10" Width="200" Height="35"
-                    Text="打开 PCL 日志" EventType="打开文件" EventData="Log1.txt" ToolTip="PCL2 的日志文件就在 PCL 文件夹内,且名为 Log1.txt" />
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+		<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" />
+		<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="PCL2 会将上一次启动 MC 的脚本保存在 PCL 文件夹内,且名为 LatestLaunch.bat。&#xA;点击按钮会打开文件管理器,并且借助参数要求它选中这个文件。" />
-    </StackPanel>
+		<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;点击按钮会打开文件管理器,并且借助参数要求它选中这个文件。" />
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="事件 4:其他事件类型" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
@@ -133,128 +133,176 @@
 	</StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="横向布局 1:StackPanel" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
+<local:MyCard Title="StackPanel 横向布局" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+	<StackPanel Margin="25,40,23,15">
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
                     Text="你需要使用 StackPanel 在一行里塞下多个按钮。你可以仅在这个教学卡片的基础上稍作调整,来实现自己的按钮布局。" />
-        <!-- 这是第一行 -->
-        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0" ColorType="Highlight"
+		<!-- 这是第一行 -->
+		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0" ColorType="Highlight"
                         Text="打开 B 站" EventType="打开网页" EventData="https://www.bilibili.com/" />
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                         Text="进入 Hypixel" EventType="启动游戏" EventData="1.16.3|mc.hypixel.net" ToolTip="使用 Minecraft 1.16.3 进入 Hypixel" />
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                         Text="打开画图" EventType="打开文件" EventData="mspaint.exe" />
-        </StackPanel>
-        <!-- 这是第二行 -->
-        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0">
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0" ColorType="Red"
+		</StackPanel>
+		<!-- 这是第二行 -->
+		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0">
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0" ColorType="Red"
                         Text="复制召唤命令" EventType="复制文本" EventData="/execute as @a at @s run summon Creeper ~ ~2 ~" ToolTip="这条 Minecraft 命令会在每个玩家头顶召唤一只苦力怕!" />
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                         Text="内存优化" EventType="内存优化" />
-            <local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+			<local:MyButton Margin="0,0,10,0" Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                         Text="下载作者的头像" EventType="下载文件" EventData="https://www.mcbbs.net/uc_server/data/avatar/000/14/51/06_avatar_small.jpg?ts=1471402592" />
-        </StackPanel>
-    </StackPanel>
-</local:MyCard>
-
-<local:MyCard Title="横向布局 2:Grid" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="如果要实现更复杂的布局,则必须使用 Grid。Grid 可以让按钮们自动适应窗口宽度:随意拉伸 PCL 窗口,按钮大小会自动改变。" />
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
-                    Text="你可以上网查找 “WPF Grid” 的相关教程,这里仅给出一个例子,不作详细介绍。你可以仅在这个教学卡片的基础上稍作调整,来实现自己的按钮布局。" />
-        <Grid>
-            <Grid.ColumnDefinitions>
-                <ColumnDefinition Width="1*" />
-                <ColumnDefinition Width="1.6*" />
-                <!-- 这个按钮的宽度是其他按钮的 1.6 倍 -->
-                <ColumnDefinition Width="1*" />
-                <!-- 如果不打 *,则会占用固定的宽度 -->
-            </Grid.ColumnDefinitions>
-            <!-- 为按钮添加 Grid.Column 属性,删除 Width 与 HorizontalAlignment 属性 -->
-            <local:MyButton Grid.Column="0" Margin="0,0,10,0" Height="35" ColorType="Highlight"
-                        Text="打开 B 站" EventType="打开网页" EventData="https://www.bilibili.com/" />
-            <local:MyButton Grid.Column="1" Margin="0,0,10,0" Height="35" 
-                        Text="进入 Hypixel" EventType="启动游戏" EventData="1.16.3|mc.hypixel.net" ToolTip="使用 Minecraft 1.16.3 进入 Hypixel" />
-            <local:MyButton Grid.Column="2" Margin="0,0,10,0" Height="35" 
-                        Text="打开画图" EventType="打开文件" EventData="mspaint.exe" />
-        </Grid>
-    </StackPanel>
+		</StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="列表项 1:基础" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="你也可以使用列表项 “local:MyListItem” 来替代按钮,其使用方式与按钮类似。" />
-        <local:MyListItem  Margin="-5,2,-5,8"
-		            Logo="https://www.baidu.com/favicon.ico" Title="百度" Info="百度一下,你就知道!" 
+		<local:MyListItem  Margin="-5,2,-5,8"
+		            Logo="https://www.baidu.com/favicon.ico" Title="百度" Info="百度一下,你就知道!"
 		            EventType="打开网页" EventData="https://www.baidu.com/" Type="Clickable" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="Logo 属性为列表项的图标,指定的可以是链接也可以是文件路径;Title 是大标题,Info 是可选的小标题(详细信息);Type 如果省略,该列表项则单纯只展示信息,仅当 Type 为 Clickable 才能触发点击效果。"/>
-        <local:MyListItem Margin="-5,2,-5,8"
+		<local:MyListItem Margin="-5,2,-5,8"
                     Logo="pack://application:,,,/images/Blocks/Grass.png"
 		            EventType="打开帮助" EventData="帮助/自定义帮助.json" Type="Clickable" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="EventType、EventData 与按钮几乎一致,唯一不同的是,列表项不支持联网加载帮助文件。" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="列表项 2:内置图片" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="PCL 内置了一些 Minecraft 方块与物品图片,可以直接使用。"/>
-        <StackPanel Orientation="Horizontal" Margin="10,8,0,15" Height="30">
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/CommandBlock.png"/>
-            <!-- 命令方块 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Cobblestone.png"/>
-            <!-- 圆石 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/GoldBlock.png"/>
-            <!-- 金块 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Grass.png"/>
-            <!-- 草方块 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/GrassPath.png"/>
-            <!-- 草径 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Anvil.png"/>
-            <!-- 铁砧 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneBlock.png"/>
-            <!-- 红石块 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneLampOn.png"/>
-            <!-- 红石灯(亮) -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneLampOff.png"/>
-            <!-- 红石灯(灭) -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Egg.png"/>
-            <!-- 鸡蛋 -->
-            <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Fabric.png"/>
-            <!-- 布料(Fabric 图标) -->
-        </StackPanel>
+		<StackPanel Orientation="Horizontal" Margin="10,8,0,15" Height="30">
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/CommandBlock.png"/>
+			<!-- 命令方块 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Cobblestone.png"/>
+			<!-- 圆石 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/GoldBlock.png"/>
+			<!-- 金块 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Grass.png"/>
+			<!-- 草方块 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/GrassPath.png"/>
+			<!-- 草径 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Anvil.png"/>
+			<!-- 铁砧 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneBlock.png"/>
+			<!-- 红石块 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneLampOn.png"/>
+			<!-- 红石灯(亮) -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/RedstoneLampOff.png"/>
+			<!-- 红石灯(灭) -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Egg.png"/>
+			<!-- 鸡蛋 -->
+			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Fabric.png"/>
+			<!-- 布料(Fabric 图标) -->
+		</StackPanel>
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="使用时,将上方对应行的 Source 复制到 MyListItem 的 Logo 属性处即可。"/>
-        <local:MyListItem Margin="-5,0,-5,8"
+		<local:MyListItem Margin="-5,0,-5,8"
             Logo="pack://application:,,,/images/Blocks/CommandBlock.png"
             Title="清理垃圾" Info="使用内置的命令方块图片作为 Logo" EventType="清理垃圾" Type="Clickable" />
-    </StackPanel>
+	</StackPanel>
 </local:MyCard>
 
 <local:MyCard Title="主题色" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
-    <StackPanel Margin="25,40,23,15">
+	<StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,15"
                     Text="你可以用类似 {DynamicResource ColorBrush5} 的格式使用 PCL 当前的主题颜色,修改末尾的数字编号以改变颜色浓度。" />
-        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="8,4">
+		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="8,4">
             <TextBlock TextWrapping="Wrap" Text=" 浓度 1 " Margin="5,0" Background="{DynamicResource ColorBrush1}" Foreground="#FFFFFF" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 2 " Margin="5,0" Background="{DynamicResource ColorBrush2}" Foreground="#FFFFFF" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 3 " Margin="5,0" Background="{DynamicResource ColorBrush3}" Foreground="#FFFFFF" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 4 " Margin="5,0" Background="{DynamicResource ColorBrush4}" Foreground="#FFFFFF" />
-        </StackPanel>
-        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="8,4">
+		</StackPanel>
+		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="8,4">
             <TextBlock TextWrapping="Wrap" Text=" 浓度 5 " Margin="5,0" Background="{DynamicResource ColorBrush5}" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 6 " Margin="5,0" Background="{DynamicResource ColorBrush6}" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 7 " Margin="5,0" Background="{DynamicResource ColorBrush7}" />
             <TextBlock TextWrapping="Wrap" Text=" 浓度 8 " Margin="5,0" Background="{DynamicResource ColorBrush8}" />
-        </StackPanel>
-        <TextBlock TextWrapping="Wrap" Margin="0,15,0,8" Foreground="{DynamicResource ColorBrush2}" Background="{DynamicResource ColorBrush6}"
+		</StackPanel>
+		<TextBlock Margin="0,15,0,8" Foreground="{DynamicResource ColorBrush2}" Background="{DynamicResource ColorBrush6}"
                     Text="除了字体颜色,你也可以将主题色用于背景色、边框颜色等颜色参数。例如这行字的颜色为浓度 2,背景为浓度 6。" />
-    </StackPanel>
+	</StackPanel>
+</local:MyCard>
+
+<local:MyCard Title="替换标记" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+	<StackPanel Margin="25,40,23,15">
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="在花括号 {} 中写入特定内容,PCL 会在加载时对其进行替换。&#xa;例如,path 会被替换为 PCL 可执行文件所在文件夹({path}),用于在 EventData 中指定特定文件,或是加载图片。" />
+		<local:MyHint Margin="0,6,0,10" Text="以下特性于 PCL 2.6.5 (299) 加入,不适用于更早版本的 PCL。" IsWarn="False" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="你也可以使用替换标记来获取随机内容:cave 会被替换为一条随机的回声洞内容,hint 则是 “你知道吗” 的内容。" />
+		<TextBlock TextWrapping="Wrap" Margin="10,6,0,5" Foreground="{DynamicResource ColorBrush3}"
+                    Text="你知道吗:{hint}" />
+	</StackPanel>
+</local:MyCard>
+
+<local:MyCard Title="进阶:Grid 布局" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+	<StackPanel Margin="25,40,23,15">
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="如果要实现更复杂的布局,则必须使用 Grid。Grid 可以让按钮们自动适应窗口宽度:随意拉伸 PCL 窗口,按钮大小会自动改变。" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
+                    Text="你可以上网查找 “WPF Grid” 的相关教程,这里仅给出一个例子,不作详细介绍。你可以仅在这个教学卡片的基础上稍作调整,来实现自己的按钮布局。" />
+		<Grid>
+			<Grid.ColumnDefinitions>
+				<ColumnDefinition Width="1*" />
+				<ColumnDefinition Width="1.6*" MinWidth="200" />
+				<!-- 第二个按钮的宽度是第一个按钮的 1.6 倍,且至少为 200 -->
+				<ColumnDefinition Width="150" />
+				<!-- 如果不打 *,则会占用固定的宽度,也可以写 Auto 来自适应 -->
+			</Grid.ColumnDefinitions>
+			<!-- 为按钮添加 Grid.Column 属性指定它所在的列,不要添加 Width 与 HorizontalAlignment 属性 -->
+			<local:MyButton Grid.Column="0" Margin="0,0,10,0" Height="35" ColorType="Highlight"
+                        Text="打开 B 站" EventType="打开网页" EventData="https://www.bilibili.com/" />
+			<local:MyButton Grid.Column="1" Margin="0,0,10,0" Height="35"
+                        Text="进入 Hypixel" EventType="启动游戏" EventData="1.16.3|mc.hypixel.net" ToolTip="使用 Minecraft 1.16.3 进入 Hypixel" />
+			<local:MyButton Grid.Column="2" Margin="0,0,10,0" Height="35"
+                        Text="打开画图" EventType="打开文件" EventData="mspaint.exe" />
+		</Grid>
+	</StackPanel>
+</local:MyCard>
+
+<local:MyCard Title="进阶:图标按钮" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+	<StackPanel Margin="25,40,23,15">
+		<local:MyHint Margin="0,0,0,10" Text="以下特性于 PCL 2.6.5 (299) 加入,不适用于更早版本的 PCL。" IsWarn="False" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="MyIconTextButton 是一种带有自定义矢量图标的按钮变体,它同样支持 EventType 与 EventData 属性。&#xa;通过设置 Logo 属性来控制它的图标,设置 LogoScale 属性还可以调整图标的缩放比例。" />
+		<local:MyIconTextButton Margin="0,4,0,8" Height="35" HorizontalAlignment="Center"
+                    Text="查看 SVG Path 简介" EventType="打开网页" EventData="https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths"
+                    LogoScale="1.02" Logo="M1091 0H78C35 0 0 35 0 78v863c0 43 35 78 78 78H1091c43 0 78-35 78-78V78C1170 35 1134 0 1091 0z m-8 87v78H87v-78h994zM87 933V254h994v679H87v0zM724 340l-206 547a43 43 0 0 1-82-31l206-547a43 43 0 0 1 82 30zM369 545l-85 85 85 85a43 43 0 0 1-62 62l-116-116a43 43 0 0 1 0-62l116-116a43 43 0 0 1 62 61z m610-37a43 43 0 0 1 0 62l-116 116a43 43 0 0 1-62 0 43 43 0 0 1 0-62l85-85-85-85a43 43 0 0 1 62-62l116 116z" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="Logo 属性的内容是 SVG Path 的值,你需要使用 SVG 编辑器或者从图标资源网站(如 IconFont)查找它。不要自己写,写不懂的。&#xa;此外,它还有两种 ColorType:Black(默认)和 Highlight。" />
+		<local:MyIconTextButton Margin="0,4,0,8" Height="35" HorizontalAlignment="Center"
+                    ColorType="Highlight" Text="打开 IconFont" EventType="打开网页" EventData="https://www.iconfont.cn/"
+                    LogoScale="1" Logo="M510 959c-51-31-40-86-74-120-61-63-168-66-231-138C-5 457 196 3 593 70c231 39 407 363 250 592-67 98-198 106-277 213-23 23-11 81-55 83zM176 432c-9 176 240 167 240 9 0-80-72-145-166-111-66 23-71 49-74 101z m509 129c212 59 217-307 0-231-102 36-90 206 0 231z m-213 111c33 7 44-91 9-92-19 19-64 79-9 92z m83 0H575c23-37-8-87-46-92 3 37-11 92 27 92z" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="如果只想要图标,不要文字,还可以使用 MyIconButton。它不能设置 Text 属性,只有一个图标可以按。&#xa;它的 Theme 属性可以设置为 Color(默认)、White、Black、Red。" />
+		<local:MyIconButton Margin="0,4,0,0" Width="25" Height="25" HorizontalAlignment="Center"
+                    Theme="Black" EventType="弹出窗口" EventData="事件|MyIconButton 当然也支持 EventType 和 EventData——要不然介绍它干啥?"
+                    LogoScale="1" Logo="M149 873c47 47 101 83 162 109 63 26 130 40 199 40 69 0 136-13 199-40 61-25 115-62 162-109s83-101 109-162c26-63 40-130 40-199 0-69-13-136-40-199-25-61-62-115-109-162-46-46-101-83-162-109C648 13 580 0 511 0s-136 13-199 40c-61 25-115 62-162 109s-83 101-109 162C13 375 0 442 0 511s13 136 40 199c25 61 62 115 109 162zM97 511c0-228 185-414 414-414 228 0 414 185 414 414S740 926 511 926c-228 0-414-185-414-414z M539 244c-8-4-17-7-27-7-9 0-19 2-27 7-16 9-27 27-27 47 0 30 24 54 54 54 30 0 54-24 54-54 0-19-10-37-27-47zM566 732v-284c0-30-24-54-54-54-30 0-54 24-54 54v284c0 30 24 54 54 54 30 0 54-24 54-54z" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
+                    Text="如果只想显示一个图标,不想让它是能按的按钮,可以使用 WPF 自带的 Path 等绘图控件,感兴趣的话可以自行搜索。想偷懒也可以把 MyIconButton 的 IsHitTestVisible 属性改为 False……" />
+		<Path Margin="0,4,0,0" Width="20" Height="20" HorizontalAlignment="Center"
+                    Stretch="Uniform" Fill="{DynamicResource ColorBrush1}"
+                    Data="M149 873c47 47 101 83 162 109 63 26 130 40 199 40 69 0 136-13 199-40 61-25 115-62 162-109s83-101 109-162c26-63 40-130 40-199 0-69-13-136-40-199-25-61-62-115-109-162-46-46-101-83-162-109C648 13 580 0 511 0s-136 13-199 40c-61 25-115 62-162 109s-83 101-109 162C13 375 0 442 0 511s13 136 40 199c25 61 62 115 109 162zM97 511c0-228 185-414 414-414 228 0 414 185 414 414S740 926 511 926c-228 0-414-185-414-414z M539 244c-8-4-17-7-27-7-9 0-19 2-27 7-16 9-27 27-27 47 0 30 24 54 54 54 30 0 54-24 54-54 0-19-10-37-27-47zM566 732v-284c0-30-24-54-54-54-30 0-54 24-54 54v284c0 30 24 54 54 54 30 0 54-24 54-54z" />
+	</StackPanel>
+</local:MyCard>
+
+<local:MyCard Title="进阶:联网自定义主页" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+	<StackPanel Margin="25,40,23,15">
+		<local:MyHint Margin="0,0,0,10" Text="以下特性于 PCL 2.6.5 (299) 加入,不适用于更早版本的 PCL。" IsWarn="False" />
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
+                    Text="如果你在尝试制作联网更新的自定义主页,可以查看下方的 GitHub 讨论页。&#xa;你可以根据其中的介绍为主页添加版本号检查以节省流量,也可以通过检查 Referer 和 User Agent 来确定对方的 PCL 版本。" />
+		<local:MyButton Margin="0,0,0,8" Height="35" HorizontalAlignment="Center" Padding="20,0,20,0"
+                    Text="打开 GitHub 讨论页" EventType="打开网页" EventData="https://github.com/Hex-Dragon/PCL2/discussions/2528" />
+	</StackPanel>
 </local:MyCard>
 
 <!-- 
@@ -262,8 +310,10 @@
 全部通用的属性:
  - Width、Height:设置宽度与高度
  - ToolTip:设置鼠标指向时的提示文本
- - Background:调整背景颜色(十六进制字符串,例如 #FF010203 代表 ARGB 中的 255,1,2,3。前两位代表不透明度,FF 为全不透明,00 为全透明)
  - Margin:调整外边距(格式为 左,上,右,下。例如 Margin="0,0,0,2" 即为 2 单位的下边距)
+ - HorizontalAlignment:设置横向对齐方式,居左:Left、居中:Center、居右:Right、拉伸(默认):Stretch
+ - VerticalAlignment:设置纵向对齐方式,居上:Top、居中:Center、居下:Bottom、拉伸(默认):Stretch
+ - IsHitTestVisible:该控件是否响应交互(点击、指向动画等),True 为是(默认),False 为否
 
 TextBlock(纯文本):
  - Text:设置显示的文本(如果要使用引号等特殊字符,自行百度 XAML 转义)
@@ -271,37 +321,32 @@ TextBlock(纯文本):
  - FontSize:设置字号
  - FontWeight:设置为 Bold 时,文字加粗
  - Foreground:调整前景颜色(十六进制字符串,例如 #FF010203 代表 ARGB 中的 255,1,2,3。前两位代表不透明度,FF 为全不透明,00 为全透明)
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)
 
 Image(图片):
- - Source:要显示的图片的网址
- - Foreground:调整前景颜色(十六进制字符串,例如 #FF010203 代表 ARGB 中的 255,1,2,3。前两位代表不透明度,FF 为全不透明,00 为全透明)
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)
+ - Source:要显示的图片的网址,或 PCL 内置图片位置
 
 local:MyCard(卡片):
  - Title:设置显示的标题文本
- - CanSwap:这张卡片是否可以折叠,True 为是,False 为否
- - IsSwaped:这张卡片是否默认折叠,要求 CanSwap 必须为 True
- - Foreground:调整前景颜色(十六进制字符串,例如 #FF010203 代表 ARGB 中的 255,1,2,3。前两位代表不透明度,FF 为全不透明,00 为全透明)
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)(使用时,CanSwap 属性需为 False)
+ - CanSwap:卡片是否可以折叠,True 为是,False 为否
+ - IsSwaped:卡片是否默认折叠,要求 CanSwap 必须为 True
+ - HorizontalAlignment:若使用,要求 CanSwap 必须为 False
+ - UseAnimation:是否在展开等高度改变时触发动画,True 为是(默认),False 为否
+ - SwapLogoRight:卡片折叠时的箭头是朝下还是朝右,True 为朝右,False 为朝下(默认)
+ - HasMouseAnimation:是否在鼠标指向时改变卡片颜色,True 为是(默认),False 为否
 
 local:MyHint(提示条):
  - Text:设置显示的文本(&#xA; 代表换行)
  - IsWarn:设置为 True 代表警告样式,False 代表提示样式
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)
 
 local:MyButton(按钮):
  - Text:设置显示的文本
  - Padding:设置内边距,格式与 Margin 一致
  - ColorType:设置颜色主题(Highlight 为当前启动器的主题颜色,Red 为红色,默认为黑色)
  - EventType、EventData:触发特定事件
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)
 
 local:MyTextButton(文本按钮):
  - Text:设置显示的文本
  - EventType、EventData:触发特定事件
- - Foreground:调整前景颜色(十六进制字符串,例如 #FF010203 代表 ARGB 中的 255,1,2,3。前两位代表不透明度,FF 为全不透明,00 为全透明)
- - HorizontalAlignment:设置对齐方式(Left、Center、Right)
 
 local:MyListItem(列表项):
  - Logo:列表项的图标(所指定的可以是链接也可以是文件路径)
@@ -309,4 +354,17 @@ local:MyListItem(列表项):
  - Info:列表项的详细信息,它将会以灰色显示在大标题下方
  - Type:若留空仅单纯展示信息,设置为 Clickable 触发相应活动
 
--->
+local:MyIconTextButton(带图标的按钮):
+ - Text:设置显示的文本
+ - Logo:设置显示的图标
+ - LogoScale:设置图标的缩放,默认为 1
+ - ColorType:设置颜色主题(Highlight 为当前启动器的主题颜色,默认为黑色)
+ - EventType、EventData:触发特定事件
+
+local:MyIconButton(图标按钮):
+ - Logo:设置显示的图标
+ - LogoScale:设置图标的缩放,默认为 1
+ - Theme:设置颜色主题,Color(默认)为当前启动器的主题颜色,也可设置为 White、Black、Red
+ - EventType、EventData:触发特定事件
+
+-->

BIN
Plain Craft Launcher 2/Resources/Help.zip