龙腾猫跃 1 year ago
parent
commit
a41579b1da
71 changed files with 1148 additions and 1337 deletions
  1. 2 3
      Plain Craft Launcher 2/Application.xaml.vb
  2. 0 0
      Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb
  3. 0 0
      Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb
  4. 3 40
      Plain Craft Launcher 2/Controls/MyButton.xaml.vb
  5. 22 15
      Plain Craft Launcher 2/Controls/MyCard.vb
  6. 1 3
      Plain Craft Launcher 2/Controls/MyCheckBox.xaml.vb
  7. 12 9
      Plain Craft Launcher 2/Controls/MyComboBox.vb
  8. 32 29
      Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb
  9. 6 6
      Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.vb
  10. 1 31
      Plain Craft Launcher 2/Controls/MyHint.xaml.vb
  11. 3 36
      Plain Craft Launcher 2/Controls/MyIconButton.xaml.vb
  12. 1 34
      Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb
  13. 16 3
      Plain Craft Launcher 2/Controls/MyImage.vb
  14. 7 38
      Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
  15. 3 0
      Plain Craft Launcher 2/Controls/MyMenuItem.vb
  16. 1 0
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb
  17. 6 10
      Plain Craft Launcher 2/Controls/MyRadioButton.xaml.vb
  18. 3 0
      Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb
  19. 4 0
      Plain Craft Launcher 2/Controls/MyTextBox.vb
  20. 15 50
      Plain Craft Launcher 2/Controls/MyTextButton.vb
  21. 23 13
      Plain Craft Launcher 2/FormMain.xaml.vb
  22. 37 17
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  23. 21 19
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  24. 2 3
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  25. 32 66
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  26. 48 14
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  27. 12 12
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  28. 1 1
      Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
  29. 292 204
      Plain Craft Launcher 2/Modules/ModEvent.vb
  30. 86 35
      Plain Craft Launcher 2/Modules/ModMain.vb
  31. 12 12
      Plain Craft Launcher 2/Modules/ModMusic.vb
  32. 4 2
      Plain Craft Launcher 2/Modules/ModSecret.vb
  33. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  34. 7 7
      Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb
  35. 18 18
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  36. 11 11
      Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml
  37. 4 4
      Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb
  38. 9 9
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  39. 7 7
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  40. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuthSkin.xaml.vb
  41. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb
  42. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml
  43. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.vb
  44. 3 3
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb
  45. 0 19
      Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml
  46. 1 2
      Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml
  47. 1 1
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml
  48. 1 20
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
  49. 231 300
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  50. 2 2
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb
  51. 1 1
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelp.xaml.vb
  52. 4 2
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelpDetail.xaml.vb
  53. 13 13
      Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb
  54. 3 18
      Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb
  55. 23 8
      Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb
  56. 5 5
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml
  57. 7 7
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.vb
  58. 1 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb
  59. 2 2
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml.vb
  60. 2 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  61. 9 9
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  62. 1 1
      Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb
  63. 1 1
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml
  64. 7 6
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb
  65. 17 17
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb
  66. 11 11
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb
  67. 5 5
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml
  68. 1 1
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml.vb
  69. 2 2
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  70. 23 111
      Plain Craft Launcher 2/Resources/Custom.xaml
  71. BIN
      Plain Craft Launcher 2/Resources/Help.zip

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

@@ -78,7 +78,6 @@ Public Class Application
                 End If
             End Try
             Directory.CreateDirectory(PathTemp & "Cache")
-            Directory.CreateDirectory(PathTemp & "Download")
             Directory.CreateDirectory(PathAppdata)
             '检测单例
 #If Not DEBUG Then
@@ -124,9 +123,9 @@ WaitRetry:
             '添加日志
             Log($"[Start] 程序版本:{VersionDisplayName} ({VersionCode}{If(CommitHash = "", "", $",#{CommitHash}")})")
 #If RELEASE Then
-            Log($"[Start] 识别码:{UniqueAddress}{If(ThemeCheckOne(9), ",正式版", "")}")
+            Log($"[Start] 识别码:{Identify}{If(ThemeCheckOne(9), ",正式版", "")}")
 #Else
-            Log($"[Start] 识别码:{UniqueAddress}{If(ThemeCheckOne(9), ",已解锁反馈主题", "")}")
+            Log($"[Start] 识别码:{Identify}{If(ThemeCheckOne(9), ",已解锁反馈主题", "")}")
 #End If
             Log($"[Start] 程序路径:{PathWithName}")
             Log($"[Start] 系统编码:{Encoding.Default.HeaderName} ({Encoding.Default.CodePage}, GBK={IsGBKEncoding})")

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


+ 0 - 0
Plain Craft Launcher 2/Controls/Interfaces.vb → Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb


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

@@ -140,51 +140,14 @@ Public Class MyButton
         End Try
     End Sub
 
-    '实现自定义事件
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsMouseDown As Boolean = False
     Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
         If Not IsMouseDown Then Return
         Log("[Control] 按下按钮:" & Text)
         RaiseEvent Click(sender, e)
-        If Not String.IsNullOrEmpty(Tag) Then
-            If Tag.ToString.StartsWithF("链接-") OrElse Tag.ToString.StartsWithF("启动-") Then
-                Hint("主页自定义按钮语法已更新,且不再兼容老版本语法,请查看新的自定义示例!")
-            End If
-        End If
-        ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
+        RaiseCustomEvent() '自定义事件
     End Sub
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyButton), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyButton), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
-
-    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
-    Private IsMouseDown As Boolean = False
     Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
         IsMouseDown = True
         Focus()

+ 22 - 15
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -132,7 +132,9 @@
         For Each Data As Object In Stack.Tag
             Select Case Type
                 Case 0
-                    Stack.Children.Add(PageSelectRight.McVersionListItem(Data))
+                    Dim VersionItem As MyListItem = CType(Data, McVersion).ToListItem
+                    VersionItem.ContentHandler = AddressOf PageSelectRight.McVersionListContent
+                    Stack.Children.Add(VersionItem)
                 Case 2
                     Stack.Children.Add(McDownloadListItem(Data, AddressOf McDownloadMenuSave, True))
                 Case 3
@@ -309,9 +311,6 @@
         End Set
     End Property
     Private _IsSwapped As Boolean = False
-    ''' <summary>
-    ''' 是否已被折叠。(已过时,请使用 IsSwapped)
-    ''' </summary>
     <Obsolete("IsSwaped 存在拼写错误,请换用 IsSwapped 属性。")>
     Public Property IsSwaped As Boolean
         Get
@@ -325,37 +324,45 @@
     Sub(sender, e) If sender IsNot Nothing AndAlso TypeOf sender Is MyCard Then CType(sender, MyCard).IsSwapped = CType(e.NewValue, Boolean)))
 
     Public Property SwapLogoRight As Boolean = False
-    Private IsMouseDown As Boolean = False
+    Private IsSwapMouseDown As Boolean = False '用于触发卡片展开/折叠的 MouseDown
+    Private IsCustomMouseDown As Boolean = False '用于触发自定义事件的 MouseDown
     Public Event PreviewSwap(sender As Object, e As RouteEventArgs)
     Public Event Swap(sender As Object, e As RouteEventArgs)
     Public Const SwapedHeight As Integer = 40
     Private Sub MyCard_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
         Dim Pos As Double = Mouse.GetPosition(Me).Y
+        If Not IsSwapped AndAlso
+            (Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return
+        IsCustomMouseDown = True
         If Not IsSwapped AndAlso
             (SwapControl Is Nothing OrElse Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return '检测点击位置;或已经不在可视树上的误判
-        IsMouseDown = True
+        IsSwapMouseDown = True
     End Sub
-    Private Sub MyCard_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
-        If Not IsMouseDown Then Return
-        IsMouseDown = False
+    Private Sub MyCard_MouseLeftButtonUp() Handles Me.MouseLeftButtonUp
+        If Not IsCustomMouseDown Then Return
+        IsCustomMouseDown = False
+        RaiseCustomEvent() '触发自定义事件
+
+        If Not IsSwapMouseDown Then Return
+        IsSwapMouseDown = False
 
         Dim Pos As Double = Mouse.GetPosition(Me).Y
         If Not IsSwapped AndAlso
             (SwapControl Is Nothing OrElse Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return '检测点击位置;或已经不在可视树上的误判
 
-        Dim ee = New RouteEventArgs(True)
-        RaiseEvent PreviewSwap(Me, ee)
-        If ee.Handled Then
-            IsMouseDown = False
+        Dim e = New RouteEventArgs(True)
+        RaiseEvent PreviewSwap(Me, e)
+        If e.Handled Then
+            IsSwapMouseDown = False
             Return
         End If
 
         IsSwapped = Not IsSwapped
         Log("[Control] " & If(IsSwapped, "折叠卡片", "展开卡片") & If(Title Is Nothing, "", ":" & Title))
-        RaiseEvent Swap(Me, ee)
+        RaiseEvent Swap(Me, e)
     End Sub
     Private Sub MyCard_MouseLeave_Swap(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave
-        IsMouseDown = False
+        IsSwapMouseDown = False
     End Sub
 
 #End Region

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

@@ -12,9 +12,6 @@ Public Class MyCheckBox
     ''' <param name="user">是否为用户手动改变的勾选状态。</param>
     Public Event Change(sender As Object, user As Boolean)
     Public Event PreviewChange(sender As Object, e As RouteEventArgs)
-    Public Sub RaiseChange()
-        RaiseEvent Change(Me, False)
-    End Sub '使外部程序引发本控件的 Change 事件
 
     '自定义属性
     Public Property Checked As Boolean
@@ -59,6 +56,7 @@ Public Class MyCheckBox
 
             '更改动画
             SyncUI()
+            RaiseCustomEvent()
         Catch ex As Exception
             Log(ex, "设置 Checked 失败")
         End Try

+ 12 - 9
Plain Craft Launcher 2/Controls/MyComboBox.vb

@@ -117,15 +117,14 @@
     Private IsTextChanging As Boolean = False
     Private Sub MyComboBox_TextChanged(sender As Object, e As TextChangedEventArgs) Handles Me.TextChanged
         If IsTextChanging OrElse Not IsEditable Then Return
-        If SelectedItem IsNot Nothing AndAlso Text <> SelectedItem.ToString Then
-            Dim RawText As String = Text
-            Dim RawSelectionStart As Integer = TextBox.SelectionStart
-            IsTextChanging = True
-            SelectedItem = Nothing
-            Text = RawText
-            TextBox.SelectionStart = RawSelectionStart
-            IsTextChanging = False
-        End If
+        If SelectedItem Is Nothing OrElse Text = SelectedItem.ToString Then Return
+        Dim RawText As String = Text
+        Dim RawSelectionStart As Integer = TextBox.SelectionStart
+        IsTextChanging = True
+        SelectedItem = Nothing
+        Text = RawText
+        TextBox.SelectionStart = RawSelectionStart
+        IsTextChanging = False
     End Sub
 
     Public ReadOnly Property ContentPresenter As ContentPresenter
@@ -134,4 +133,8 @@
         End Get
     End Property
 
+    Private Sub MyComboBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles Me.SelectionChanged
+        If IsLoaded AndAlso AniControlEnabled = 0 Then RaiseCustomEvent()
+    End Sub
+
 End Class

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

@@ -53,25 +53,26 @@
         Set(value As Boolean)
             If _Show = value Then Return
             _Show = value
-            RunInUi(Sub()
-                        If value Then
-                            '有了
-                            Visibility = Visibility.Visible
-                            AniStart({
-                                AaScaleTransform(Me, 0.3 - CType(RenderTransform, ScaleTransform).ScaleX, 500, 60, New AniEaseOutFluent(AniEasePower.Weak)),
-                                AaScaleTransform(Me, 0.7, 500, 60, New AniEaseOutBack(AniEasePower.Weak)),
-                                AaHeight(Me, 50 - Height, 200,, New AniEaseOutFluent(AniEasePower.Weak))
-                            }, "MyExtraButton MainScale " & Uuid)
-                        Else
-                            '没了
-                            AniStart({
-                                AaScaleTransform(Me, -CType(RenderTransform, ScaleTransform).ScaleX, 100,, New AniEaseInFluent(AniEasePower.Weak)),
-                                AaHeight(Me, -Height, 400, 100, New AniEaseOutFluent()),
-                                AaCode(Sub() Visibility = Visibility.Collapsed,, True)
-                            }, "MyExtraButton MainScale " & Uuid)
-                        End If
-                        IsHitTestVisible = value '防止缩放动画中依然可以点进去
-                    End Sub)
+            RunInUi(
+            Sub()
+                If value Then
+                    '有了
+                    Visibility = Visibility.Visible
+                    AniStart({
+                        AaScaleTransform(Me, 0.3 - CType(RenderTransform, ScaleTransform).ScaleX, 500, 60, New AniEaseOutFluent(AniEasePower.Weak)),
+                        AaScaleTransform(Me, 0.7, 500, 60, New AniEaseOutBack(AniEasePower.Weak)),
+                        AaHeight(Me, 50 - Height, 200,, New AniEaseOutFluent(AniEasePower.Weak))
+                    }, "MyExtraButton MainScale " & Uuid)
+                Else
+                    '没了
+                    AniStart({
+                        AaScaleTransform(Me, -CType(RenderTransform, ScaleTransform).ScaleX, 100,, New AniEaseInFluent(AniEasePower.Weak)),
+                        AaHeight(Me, -Height, 400, 100, New AniEaseOutFluent()),
+                        AaCode(Sub() Visibility = Visibility.Collapsed,, True)
+                    }, "MyExtraButton MainScale " & Uuid)
+                End If
+                IsHitTestVisible = value '防止缩放动画中依然可以点进去
+            End Sub)
         End Set
     End Property
     Public Delegate Function ShowCheckDelegate() As Boolean
@@ -137,6 +138,7 @@
                 AaScaleTransform(PanScale, 1 - CType(PanScale.RenderTransform, ScaleTransform).ScaleX, 300,, New AniEaseOutBack)
             }, "MyExtraButton Scale " & Uuid)
         End If
+        If IsLeftMouseHeld Then RaiseCustomEvent()
         IsLeftMouseHeld = False
         RefreshColor() '直接刷新颜色以判断是否已触发 MouseLeave
     End Sub
@@ -199,16 +201,17 @@
     ''' 发出一圈波浪效果提示。
     ''' </summary>
     Public Sub Ribble()
-        RunInUi(Sub()
-                    Dim Shape As New Border With {.CornerRadius = New CornerRadius(1000), .BorderThickness = New Thickness(0.001), .Opacity = 0.5, .RenderTransformOrigin = New Point(0.5, 0.5), .RenderTransform = New ScaleTransform()}
-                    Shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5")
-                    PanScale.Children.Insert(0, Shape)
-                    AniStart({
-                       AaScaleTransform(Shape, 13, 1000, Ease:=New AniEaseInoutFluent(AniEasePower.Strong, 0.3)),
-                       AaOpacity(Shape, -Shape.Opacity, 1000),
-                       AaCode(Sub() PanScale.Children.Remove(Shape), After:=True)
-                    }, "ExtraButton Ribble " & GetUuid())
-                End Sub)
+        RunInUi(
+        Sub()
+            Dim Shape As New Border With {.CornerRadius = New CornerRadius(1000), .BorderThickness = New Thickness(0.001), .Opacity = 0.5, .RenderTransformOrigin = New Point(0.5, 0.5), .RenderTransform = New ScaleTransform()}
+            Shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5")
+            PanScale.Children.Insert(0, Shape)
+            AniStart({
+                AaScaleTransform(Shape, 13, 1000, Ease:=New AniEaseInoutFluent(AniEasePower.Strong, 0.3)),
+                AaOpacity(Shape, -Shape.Opacity, 1000),
+                AaCode(Sub() PanScale.Children.Remove(Shape), After:=True)
+            }, "ExtraButton Ribble " & GetUuid())
+        End Sub)
     End Sub
 
 End Class

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

@@ -81,12 +81,12 @@ Public Class MyExtraTextButton
 
     '触发点击事件
     Private Sub Button_LeftMouseUp(sender As Object, e As MouseButtonEventArgs) Handles PanClick.MouseLeftButtonUp
-        If IsLeftMouseHeld Then
-            Log("[Control] 按下附加图标按钮:" & Text)
-            RaiseEvent Click(sender, e)
-            e.Handled = True
-            Button_LeftMouseUp()
-        End If
+        If Not IsLeftMouseHeld Then Return
+        Log("[Control] 按下附加图标按钮:" & Text)
+        RaiseEvent Click(sender, e)
+        e.Handled = True
+        RaiseCustomEvent()
+        Button_LeftMouseUp()
     End Sub
 
     '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)

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

@@ -113,7 +113,7 @@ Public Class MyHint
         IsMouseDown = False
         Log("[Control] 按下提示条" & If(String.IsNullOrEmpty(Name), "", ":" & Name))
         e.Handled = True
-        ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
+        RaiseCustomEvent() '自定义事件
     End Sub
     Private Sub MyHint_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
         IsMouseDown = True
@@ -121,36 +121,6 @@ Public Class MyHint
     Private Sub MyHint_MouseLeave() Handles Me.MouseLeave
         IsMouseDown = False
     End Sub
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyHint), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyHint), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
 
 End Class
 Partial Public Module ModAnimation

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

@@ -48,48 +48,16 @@
         End Set
     End Property
 
-    '触发点击事件
+    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+    Private IsMouseDown As Boolean = False
     Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp
         If Not IsMouseDown Then Return
         Log("[Control] 按下图标按钮" & If(String.IsNullOrEmpty(Name), "", ":" & Name))
         RaiseEvent Click(sender, e)
         e.Handled = True
         Button_MouseUp()
-        ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
+        RaiseCustomEvent() '自定义事件
     End Sub
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyIconButton), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyIconButton), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
-
-    '鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
-    Private IsMouseDown As Boolean = False
     Private Sub Button_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
         IsMouseDown = True
         Focus()
@@ -114,7 +82,6 @@
         RefreshAnim() '直接刷新颜色以判断是否已触发 MouseLeave
     End Sub
 
-    '自定义事件
     '务必放在 IsMouseDown 更新之后
     Private Const AnimationColorIn As Integer = 120
     Private Const AnimationColorOut As Integer = 150

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

@@ -8,9 +8,6 @@ Public Class MyIconTextButton
     Public Uuid As Integer = GetUuid()
     Public Event Check(sender As Object, raiseByMouse As Boolean)
     Public Event Change(sender As Object, raiseByMouse As Boolean)
-    Public Sub RaiseChange()
-        RaiseEvent Change(Me, False)
-    End Sub '使外部程序可以引发本控件的 Change 事件
 
     '自定义属性
 
@@ -76,7 +73,7 @@ Public Class MyIconTextButton
         Log("[Control] 按下带图标按钮:" & Text)
         IsMouseDown = False
         RaiseEvent Click(Me, New RouteEventArgs(True))
-        ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
+        RaiseCustomEvent() '自定义事件
         RefreshColor()
     End Sub
     Private Sub MyIconTextButton_MouseDown() Handles Me.MouseLeftButtonDown
@@ -87,36 +84,6 @@ Public Class MyIconTextButton
         IsMouseDown = False
         RefreshColor()
     End Sub
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
 
     '动画
 

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

@@ -46,7 +46,7 @@
     Sub(sender, e) If sender IsNot Nothing Then CType(sender, MyImage).Source = e.NewValue.ToString())))
 
     ''' <summary>
-    ''' 若 Source 是一个网络图片,该地址将作为第二下载源。
+    ''' 若 Source 是一个网络图片,该地址将作为第二图片源。
     ''' </summary>
     Public Property FallbackSource As String
         Get
@@ -120,11 +120,12 @@
         End If
         RunInNewThread(
         Sub()
+            Dim IsLocalFallback As Boolean = FallbackSource IsNot Nothing AndAlso Not FallbackSource.StartsWithF("http")
             Try
                 '下载
                 ActualSource = LoadingSource '显示加载中的占位图片
                 NetDownloadByLoader(
-                    If(String.IsNullOrEmpty(FallbackSource), {Source}, {Source, FallbackSource}),
+                    If(String.IsNullOrEmpty(FallbackSource) OrElse IsLocalFallback, {Source}, {Source, FallbackSource}),
                     TempPath, SimulateBrowserHeaders:=True)
                 If EnableCache Then
                     '保存缓存并显示
@@ -139,7 +140,19 @@
                     If TempPath IsNot Nothing Then File.Delete(TempPath)
                 Catch
                 End Try
-                Log(ex, $"下载图片失败", LogLevel.Hint)
+                '加载本地备用图片
+                If IsLocalFallback Then
+                    Try
+                        RunInUiWait(Sub() ActualSource = FallbackSource)
+                        Log(ex, $"下载图片失败,使用本地备用图片({Source})")
+                        Return
+                    Catch exx As Exception
+                        Log(ex, $"下载图片失败({Source})", LogLevel.Hint)
+                        Log(exx, $"加载备用图片失败({FallbackSource})", LogLevel.Hint)
+                    End Try
+                Else
+                    Log(ex, $"下载图片失败({Source},备用:{FallbackSource})", LogLevel.Hint)
+                End If
             End Try
         End Sub, "MyImage PicLoader " & GetUuid() & "#", ThreadPriority.BelowNormal)
     End Sub

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

@@ -506,8 +506,8 @@ Public Class MyListItem
         RaiseEvent Click(sender, e)
         If e.Handled Then Return
         '触发自定义事件
-        If Not String.IsNullOrEmpty(EventType) Then
-            ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
+        If CustomEventService.GetEventType(sender) <> CustomEvent.EventType.None Then
+            RaiseCustomEvent()
             e.Handled = True
         End If
         If e.Handled Then Return
@@ -537,38 +537,6 @@ Public Class MyListItem
         If ButtonStack IsNot Nothing Then ButtonStack.IsHitTestVisible = True
     End Sub
 
-    '实现自定义事件
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyListItem), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyListItem), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
-
 #End Region
 
     Private StateLast As String
@@ -669,13 +637,14 @@ Public Class MyListItem
             SetResourceReference(ForegroundProperty, "ColorBrush1")
         End If
         ColumnPaddingRight.Width = New GridLength(MinPaddingRight)
-        If EventType = "打开帮助" AndAlso Not (Title <> "" AndAlso Info <> "") Then '#3266
+        If CustomEventService.GetEventType(Me) = CustomEvent.EventType.打开帮助 AndAlso Not (Title <> "" AndAlso Info <> "") Then '#3266
             Try
-                Dim Unused = New HelpEntry(GetEventAbsoluteUrls(EventData, EventType)(0)).SetToListItem(Me)
+                Dim Entry As New HelpEntry(CustomEvent.GetAbsoluteUrls(CustomEventService.GetEventData(Me), CustomEventService.GetEventType(Me))(0))
+                Entry.SetToListItem(Me)
             Catch ex As Exception
                 Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
-                EventType = Nothing
-                EventData = Nothing
+                CustomEventService.SetEventType(Me, CustomEvent.EventType.None)
+                CustomEventService.SetEventData(Me, "")
             End Try
         End If
     End Sub

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

@@ -54,4 +54,7 @@
         End If
     End Sub
 
+    Private Sub MyMenuItem_Click(sender As Object, e As RoutedEventArgs) Handles Me.Click
+        RaiseCustomEvent()
+    End Sub
 End Class

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

@@ -93,6 +93,7 @@ Public Class MyRadioBox
             If IsChanged Then
                 If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
                 RaiseEvent Changed(Me, New RouteEventArgs(user))
+                RaiseCustomEvent()
             End If
 
             '更改动画

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

@@ -7,10 +7,6 @@ Public Class MyRadioButton
 
     Public Uuid As Integer = GetUuid()
     Public Event Check(sender As Object, raiseByMouse As Boolean)
-    Public Event Change(sender As Object, raiseByMouse As Boolean)
-    Public Sub RaiseChange()
-        RaiseEvent Change(Me, False)
-    End Sub '使外部程序可以引发本控件的 Change 事件
 
     '自定义属性
 
@@ -46,23 +42,22 @@ Public Class MyRadioButton
     ''' 手动设置 Checked 属性。
     ''' </summary>
     ''' <param name="value">新的 Checked 属性。</param>
-    ''' <param name="user">是否由用户引发。</param>
+    ''' <param name="raiseByMouse">是否由用户引发。</param>
     ''' <param name="anime">是否执行动画。</param>
-    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+    Public Sub SetChecked(value As Boolean, raiseByMouse As Boolean, anime As Boolean)
         Try
 
             '自定义属性基础
 
             Dim IsChanged As Boolean = False
-            If IsLoaded AndAlso Not value = _Checked Then RaiseEvent Change(Me, user)
-            If Not value = _Checked Then
+            If _Checked <> value Then
                 _Checked = value
                 IsChanged = True
             End If
 
             '保证只有一个单选框选中
 
-            If IsNothing(Parent) Then Return
+            If Parent Is Nothing Then Return
             Dim RadioboxList As New List(Of MyRadioButton)
             Dim CheckedCount As Integer = 0
             '收集控件列表与选中个数
@@ -105,7 +100,8 @@ Public Class MyRadioButton
             RefreshColor(Nothing, anime)
 
             '触发事件
-            If Checked Then RaiseEvent Check(Me, user)
+            If Checked Then RaiseEvent Check(Me, raiseByMouse)
+            RaiseCustomEvent()
 
         Catch ex As Exception
             Log(ex, "单选按钮勾选改变错误", LogLevel.Hint)

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

@@ -39,4 +39,7 @@
         TextBox.Focus()
     End Sub
 
+    Private Sub MySearchBox_KeyUp(sender As Object, e As KeyEventArgs) Handles Me.KeyUp
+        If e.Key = Key.Enter Then RaiseCustomEvent()
+    End Sub
 End Class

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

@@ -291,4 +291,8 @@
         End If
     End Sub
 
+    '在按下回车时触发自定义事件
+    Private Sub MyTextBox_KeyUp(sender As Object, e As KeyEventArgs) Handles Me.KeyUp
+        If e.Key = Key.Enter Then RaiseCustomEvent()
+    End Sub
 End Class

+ 15 - 50
Plain Craft Launcher 2/Controls/MyTextButton.vb

@@ -22,15 +22,15 @@
         End Set
     End Property
     Public Shared ReadOnly TextProperty As DependencyProperty =
-        DependencyProperty.Register("Text", GetType(String), GetType(MyTextButton), New PropertyMetadata("", Sub(sender As MyTextButton, e As DependencyPropertyChangedEventArgs)
-                                                                                                                 If Not e.OldValue = e.NewValue Then
-                                                                                                                     AniStart({
-                                                                                                                              AaOpacity(sender, -sender.Opacity, 50),
-                                                                                                                              AaCode(Sub() sender.Content = e.NewValue,, True),
-                                                                                                                              AaOpacity(sender, 1, 170)
-                                                                                                                        }, "MyTextButton Text " & sender.Uuid)
-                                                                                                                 End If
-                                                                                                             End Sub))
+        DependencyProperty.Register("Text", GetType(String), GetType(MyTextButton), New PropertyMetadata("",
+        Sub(sender As MyTextButton, e As DependencyPropertyChangedEventArgs)
+            If e.OldValue = e.NewValue Then Return
+            AniStart({
+                AaOpacity(sender, -sender.Opacity, 50),
+                AaCode(Sub() sender.Content = e.NewValue,, True),
+                AaOpacity(sender, 1, 170)
+            }, "MyTextButton Text " & sender.Uuid)
+        End Sub))
 
     '鼠标事件
 
@@ -43,13 +43,12 @@
         IsMouseDown = False
     End Sub
     Private Sub MyTextButton_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles Me.PreviewMouseLeftButtonUp
-        If IsMouseDown Then
-            IsMouseDown = False
-            Log("[Control] 按下文本按钮:" & Text)
-            RaiseEvent Click(Me, Nothing)
-            ModEvent.ProcessCustomEvents(EventType, EventData, _CustomEvents)
-            e.Handled = True
-        End If
+        If Not IsMouseDown Then Return
+        IsMouseDown = False
+        Log("[Control] 按下文本按钮:" & Text)
+        RaiseEvent Click(Me, Nothing)
+        RaiseCustomEvent()
+        e.Handled = True
     End Sub
 
     '指向动画
@@ -85,38 +84,4 @@
         End If
     End Sub
 
-    '实现自定义事件
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register(
-        "EventType", GetType(String), GetType(MyTextButton), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register(
-        "EventData", GetType(String), GetType(MyTextButton), New PropertyMetadata(Nothing))
-    Public Property CustomEvents As CustomEventCollection
-        Get
-            If _CustomEvents Is Nothing Then
-                _CustomEvents = New CustomEventCollection
-            End If
-            Return _CustomEvents
-        End Get
-        Set(value As CustomEventCollection)
-            _CustomEvents = value
-        End Set
-    End Property
-    Private _CustomEvents As CustomEventCollection = Nothing
-
 End Class

+ 23 - 13
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -104,6 +104,15 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '1:BUG-
+        If LastVersion < 371 Then 'Snapshot 2.10.9
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
+            FeatureCount += 33
+            BugCount += 11
+        End If
         If LastVersion < 370 Then 'Snapshot 2.10.8
             If LastVersion >= 368 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
             FeatureCount += 3
@@ -318,7 +327,7 @@ Public Class FormMain
         FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay
         '模式提醒
 #If DEBUG Then
-        Hint("[开发者模式] PCL 正以开发者模式运行,这可能会造成严重的性能下降,请务必立即向开发者反馈此问题!", HintType.Critical)
+        Hint("[开发者模式] PCL 正以开发者模式运行,这可能会造成严重的性能下降,请务必立即向开发者反馈此问题!", HintType.Red)
 #End If
         If ModeDebug Then Hint("[调试模式] PCL 正以调试模式运行,这可能会导致性能下降,若无必要请不要开启!")
         '尽早执行的加载池
@@ -687,9 +696,9 @@ Public Class FormMain
         If e.Key = Key.F12 Then
             PageSetupUI.HiddenForceShow = Not PageSetupUI.HiddenForceShow
             If PageSetupUI.HiddenForceShow Then
-                Hint("功能隐藏设置已暂时关闭!", HintType.Finish)
+                Hint("功能隐藏设置已暂时关闭!", HintType.Green)
             Else
-                Hint("功能隐藏设置已重新开启!", HintType.Finish)
+                Hint("功能隐藏设置已重新开启!", HintType.Green)
             End If
             PageSetupUI.HiddenRefresh()
             Return
@@ -785,12 +794,12 @@ Public Class FormMain
                         Dim AuthlibServer As String = Net.WebUtility.UrlDecode(Str.Substring("authlib-injector:yggdrasil-server:".Length))
                         Log("[System] Authlib 拖拽:" & AuthlibServer)
                         If Not String.IsNullOrEmpty(New ValidateHttp().Validate(AuthlibServer)) Then
-                            Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", HintType.Critical)
+                            Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", HintType.Red)
                             Return
                         End If
                         Dim TargetVersion = If(PageCurrent = PageType.VersionSetup, PageVersionLeft.Version, McVersionCurrent)
                         If TargetVersion Is Nothing Then
-                            Hint("请先下载游戏,再设置第三方登录!", HintType.Critical)
+                            Hint("请先下载游戏,再设置第三方登录!", HintType.Red)
                             Return
                         End If
                         If AuthlibServer = "https://littleskin.cn/api/yggdrasil" Then
@@ -837,7 +846,7 @@ Public Class FormMain
                 '获取文件并检查
                 Dim FilePathRaw = e.Data.GetData(DataFormats.FileDrop)
                 If FilePathRaw Is Nothing Then '#2690
-                    Hint("请将文件解压后再拖入!", HintType.Critical)
+                    Hint("请将文件解压后再拖入!", HintType.Red)
                     Return
                 End If
                 e.Handled = True
@@ -855,10 +864,10 @@ Public Class FormMain
             Log("[System] 接受文件拖拽:" & FilePath & If(FilePathList.Any, $" 等 {FilePathList.Count} 个文件", ""), LogLevel.Developer)
             '基础检查
             If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
-                Hint("请拖入一个文件,而非文件夹!", HintType.Critical)
+                Hint("请拖入一个文件,而非文件夹!", HintType.Red)
                 Return
             ElseIf Not File.Exists(FilePathList.First) Then
-                Hint("拖入的文件不存在:" & FilePathList.First, HintType.Critical)
+                Hint("拖入的文件不存在:" & FilePathList.First, HintType.Red)
                 Return
             End If
             '多文件拖拽
@@ -866,7 +875,7 @@ Public Class FormMain
                 '必须要求全部为 jar 文件
                 For Each File In FilePathList
                     If Not {"jar", "litemod", "disabled", "old"}.Contains(File.AfterLast(".").ToLower) Then
-                        Hint("一次请只拖入一个文件!", HintType.Critical)
+                        Hint("一次请只拖入一个文件!", HintType.Red)
                         Return
                     End If
                 Next
@@ -885,7 +894,7 @@ Public Class FormMain
                 Sub()
                     Setup.Set("UiCustomType", 1)
                     FrmLaunchRight.ForceRefresh()
-                    Hint("已加载主页自定义文件!", HintType.Finish)
+                    Hint("已加载主页自定义文件!", HintType.Green)
                 End Sub)
                 Return
             End If
@@ -995,9 +1004,9 @@ Public Class FormMain
 #Region "切换页面"
 
     '页面种类与属性
-    '注意,这一枚举在 “切换页面” EventType 中调用,应视作公开 API 的一部分
     ''' <summary>
     ''' 页面种类。
+    ''' 该枚举在自定义事件中使用,是公开 API 的一部分。
     ''' </summary>
     Public Enum PageType
         ''' <summary>
@@ -1043,6 +1052,7 @@ Public Class FormMain
     End Enum
     ''' <summary>
     ''' 次要页面种类。其数值必须与 StackPanel 中的下标一致。
+    ''' 该枚举在自定义事件中使用,是公开 API 的一部分。
     ''' </summary>
     Public Enum PageSubType
         [Default] = 0
@@ -1485,7 +1495,7 @@ Public Class FormMain
     '投降
     Public Sub AprilGiveup() Handles BtnExtraApril.Click
         If IsAprilEnabled AndAlso Not IsAprilGiveup Then
-            Hint("=D", HintType.Finish)
+            Hint("=D", HintType.Green)
             IsAprilGiveup = True
             FrmLaunchLeft.AprilScaleTrans.ScaleX = 1
             FrmLaunchLeft.AprilScaleTrans.ScaleY = 1
@@ -1503,7 +1513,7 @@ Public Class FormMain
             For Each Watcher In McWatcherList
                 Watcher.Kill()
             Next
-            Hint("已关闭运行中的 Minecraft!", HintType.Finish)
+            Hint("已关闭运行中的 Minecraft!", HintType.Green)
         Catch ex As Exception
             Log(ex, "强制关闭所有 Minecraft 失败", LogLevel.Feedback)
         End Try

+ 37 - 17
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -13,13 +13,13 @@ Public Module ModBase
 #Region "声明"
 
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.10.8" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.10.8." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.10.9" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.10.9." & VersionBranchCode '标准格式的四段式版本号
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
 #If BETA Then
-    Public Const VersionCode As Integer = 367 'Release
+    Public Const VersionCode As Integer = 369 'Release
 #Else
-    Public Const VersionCode As Integer = 370 'Snapshot
+    Public Const VersionCode As Integer = 371 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -69,7 +69,7 @@ Public Module ModBase
     ''' <summary>
     ''' 识别码。
     ''' </summary>
-    Public UniqueAddress As String = SecretGetUniqueAddress()
+    Public Identify As String = SecretGetIdentify()
     ''' <summary>
     ''' 程序是否已结束。
     ''' </summary>
@@ -761,7 +761,6 @@ Public Module ModBase
         If FilePath.Contains("\") Then FilePath = FilePath.Substring(FilePath.LastIndexOfF("\") + 1)
         Dim length As Integer = FilePath.Length
         If length = 0 Then Throw New Exception("不包含文件名:" & FilePath)
-        If length > 250 Then Throw New PathTooLongException("文件名过长:" & FilePath)
         Return FilePath
     End Function
     ''' <summary>
@@ -1400,12 +1399,13 @@ RetryDir:
 
     ''' <summary>
     ''' 若路径长度大于指定值,则将长路径转换为短路径。
+    ''' 如果路径不存在,则返回原始路径。
     ''' </summary>
     Public Function ShortenPath(LongPath As String) As String
         If LongPath.Length <= 200 OrElse LongPath.StartsWithF("http", True) Then Return LongPath
         If Not Directory.Exists(LongPath) AndAlso Not File.Exists(LongPath) Then Return LongPath
         Dim ShortPath As New StringBuilder(260)
-        GetShortPathName(LongPath, ShortPath, 260)
+        GetShortPathName(LongPath.Replace("/", "\"), ShortPath, 260) '第一个参数也会在调用后被修改,使用 Replace 后不会影响原始字符串
         ShortenPath = ShortPath.ToString
         If String.IsNullOrEmpty(ShortenPath) Then Return LongPath
     End Function
@@ -1511,6 +1511,19 @@ RetryDir:
         Return [Enum].GetName(EnumData.GetType, EnumData)
     End Function
     ''' <summary>
+    ''' 将字符串转换为枚举类型,支持数字或原文两种格式,若为空字符串则返回 0。
+    ''' 若转换失败会抛出异常。
+    ''' </summary>
+    <Extension> Public Function ParseToEnum(Of T)(Str As String) As T
+        If String.IsNullOrEmpty(Str) Then
+            Return CType(0, Object)
+        ElseIf IsNumeric(Str) Then
+            Return CType(CInt(Str), Object)
+        Else
+            Return [Enum].Parse(GetType(T), Str, True)
+        End If
+    End Function
+    ''' <summary>
     ''' 将文件大小转化为适合的文本形式,如“1.28 M”。
     ''' </summary>
     ''' <param name="FileSize">以字节为单位的大小表示。</param>
@@ -2579,7 +2592,7 @@ Retry:
                     Log(ex, "可能由于剪贴板被其他程序占用,文本复制失败", LogLevel.Hint)
                 End If
             End Try
-            If ShowSuccessHint Then Hint("已成功复制!", HintType.Finish)
+            If ShowSuccessHint Then Hint("已成功复制!", HintType.Green)
         End Sub)
     End Sub
 
@@ -2716,6 +2729,11 @@ Retry:
     ''' 将 XML 转换为对应 UI 对象。
     ''' </summary>
     Public Function GetObjectFromXML(Str As String) As Object
+        Str = Str. '兼容旧版自定义事件写法
+            Replace("EventType=""", "local:CustomEventService.EventType=""").
+            Replace("EventData=""", "local:CustomEventService.EventData=""").
+            Replace("Property=""EventType""", "Property=""local:CustomEventService.EventType""").
+            Replace("Property=""EventData""", "Property=""local:CustomEventService.EventData""")
         Using Stream As New MemoryStream(Encoding.UTF8.GetBytes(Str))
             '类型检查
             Using Reader As New XamlXmlReader(Stream)
@@ -2831,7 +2849,7 @@ Retry:
                 File.Create(Path & "PCL\Log1.txt").Dispose()
             Catch ex As IOException
                 IsInitSuccess = False
-                Hint("可能同时开启了多个 PCL,程序可能会出现未知问题!", HintType.Critical)
+                Hint("可能同时开启了多个 PCL,程序可能会出现未知问题!", HintType.Red)
                 Log(ex, "日志初始化失败(疑似文件占用问题)")
             Catch ex As Exception
                 IsInitSuccess = False
@@ -2884,6 +2902,7 @@ Retry:
 
         '输出日志
         Dim AppendText As String = $"[{GetTimeNow()}] <{If(Thread.CurrentThread.Name = "", "主线程", Thread.CurrentThread.Name)}> {Text}{vbCrLf}" '减轻同步锁占用
+        AppendText = FilterUserName(FilterAccessToken(AppendText, "*"), "*")
         If ModeDebug Then
             SyncLock LogListLock
                 LogList.Append(AppendText)
@@ -2903,16 +2922,16 @@ Retry:
         Select Case Level
 #If DEBUG Then
             Case LogLevel.Developer
-                Hint("[开发者模式] " & Text, HintType.Info, False)
+                Hint("[开发者模式] " & Text, HintType.Blue, False)
             Case LogLevel.Debug
-                Hint("[调试模式] " & Text, HintType.Info, False)
+                Hint("[调试模式] " & Text, HintType.Blue, False)
 #Else
             Case LogLevel.Developer
             Case LogLevel.Debug
-                If ModeDebug Then Hint("[调试模式] " & Text, HintType.Info, False)
+                If ModeDebug Then Hint("[调试模式] " & Text, HintType.Blue, False)
 #End If
             Case LogLevel.Hint
-                Hint(Text, HintType.Critical, False)
+                Hint(Text, HintType.Red, False)
             Case LogLevel.Msgbox
                 MyMsgBox(Text, Title, IsWarn:=True)
             Case LogLevel.Feedback
@@ -2955,6 +2974,7 @@ Retry:
 
         '输出日志
         Dim AppendText As String = $"[{GetTimeNow()}] <{If(Thread.CurrentThread.Name = "", "主线程", Thread.CurrentThread.Name)}> {Desc}:{Ex.GetDetail(True)}{vbCrLf}" '减轻同步锁占用
+        AppendText = FilterUserName(FilterAccessToken(AppendText, "*"), "*")
         If ModeDebug Then
             SyncLock LogListLock
                 LogList.Append(AppendText)
@@ -2973,19 +2993,19 @@ Retry:
 #If DEBUG Then
             Case LogLevel.Developer
                 Dim ExLine As String = Desc & ":" & Ex.GetBrief()
-                Hint("[开发者模式] " & ExLine, HintType.Info, False)
+                Hint("[开发者模式] " & ExLine, HintType.Blue, False)
             Case LogLevel.Debug
                 Dim ExLine As String = Desc & ":" & Ex.GetBrief()
-                Hint("[调试模式] " & ExLine, HintType.Info, False)
+                Hint("[调试模式] " & ExLine, HintType.Blue, False)
 #Else
             Case LogLevel.Developer
             Case LogLevel.Debug
                 Dim ExLine As String = Desc & ":" & Ex.GetBrief()
-                If ModeDebug Then Hint("[调试模式] " & ExLine, HintType.Info, False)
+                If ModeDebug Then Hint("[调试模式] " & ExLine, HintType.Blue, False)
 #End If
             Case LogLevel.Hint
                 Dim ExLine As String = Desc & ":" & Ex.GetBrief()
-                Hint(ExLine, HintType.Critical, False)
+                Hint(ExLine, HintType.Red, False)
             Case LogLevel.Msgbox
                 MyMsgBox(ExFull, Title, IsWarn:=True)
             Case LogLevel.Feedback

+ 21 - 19
Plain Craft Launcher 2/Modules/Base/ModNet.vb

@@ -998,20 +998,18 @@ StartThread:
                 If ModeDebug AndAlso Response.RequestMessage.RequestUri.ToString <> Th.Source.Url Then Log($"[Download] {LocalName}:重定向至 {Response.RequestMessage.RequestUri}")
                 '文件大小校验
                 ContentLength = Response.Content.Headers.ContentLength.GetValueOrDefault(-1)
-                If ContentLength = -1 Then
+                If ContentLength < 0 Then
                     If FileSize > 1 Then
                         If Th.DownloadStart = 0 Then
                             Log($"[Download] {LocalName}:文件大小未知,但已从其他下载源获取,不作处理")
                         Else
-                            Log($"[Download] {LocalName}:ContentLength 返回了 -1,无法确定是否支持分段下载,视作不支持")
+                            Log($"[Download] {LocalName}:ContentLength 返回了 {ContentLength},无法确定是否支持分段下载,视作不支持")
                             GoTo NotSupportRange
                         End If
                     Else
                         FileSize = -1 : IsUnknownSize = True
                         Log($"[Download] {LocalName}:文件大小未知")
                     End If
-                ElseIf ContentLength < 0 Then
-                    Throw New Exception("获取片大小失败,结果为 " & ContentLength & "。")
                 ElseIf Th.IsFirstThread Then
                     If Check IsNot Nothing Then
                         If ContentLength < Check.MinSize AndAlso Check.MinSize > 0 Then
@@ -1040,15 +1038,11 @@ StartThread:
                 ElseIf Th.DownloadStart > 0 AndAlso ContentLength = FileSize Then
 NotSupportRange:
                     SyncLock LockSource
-                        If SourcesOnce.Contains(Th.Source) Then
-                            GoTo SourceBreak
-                        Else
-                            SourcesOnce.Add(Th.Source)
-                        End If
+                        If SourcesOnce.Contains(Th.Source) Then GoTo SourceBreak
                     End SyncLock
-                    Throw New WebException($"该下载源不支持分段下载:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
+                    Throw New RangeNotSupportedException($"该下载源不支持分段下载:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
                 ElseIf Not FileSize - Th.DownloadStart = ContentLength Then
-                    Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
+                    Throw New RangeNotSupportedException($"获取到的分段大小不一致:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
                 End If
                 'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}")
                 Th.State = NetState.Get
@@ -1061,6 +1055,7 @@ NotSupportRange:
                     Cache = New MemoryStream
                     ResultStream = Cache
                 Else
+                    Directory.CreateDirectory(PathTemp & "Download")
                     Th.Temp = $"{PathTemp}Download\{Uuid}_{Th.Uuid}_{RandomInteger(0, 999999)}.tmp"
                     ResultStream = New FileStream(Th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
                 End If
@@ -1165,9 +1160,10 @@ SourceBreak:
             Th.State = NetState.Error
             Th.Source.Ex = ex
             '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
-            If IsMergeFailure OrElse
-               ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("未能解析") OrElse ex.Message.Contains("(404)") OrElse
-               ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse ex.Message.Contains("获取到的分段大小不一致") OrElse
+            Dim IsRangeNotSupported As Boolean = TypeOf ex Is RangeNotSupportedException OrElse ex.Message.Contains("(416)")
+            If IsMergeFailure OrElse IsRangeNotSupported OrElse
+               ex.Message.Contains("(502)") OrElse ex.Message.Contains("(404)") OrElse
+               ex.Message.Contains("未能解析") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
                ((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.ContainsF("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
                (Th.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
                 '当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理
@@ -1180,9 +1176,9 @@ SourceBreak:
                 End SyncLock
                 '……后续处理
                 If IsThisFail Then
-                    Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id}):{Th.Source.Url}")
+                    Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id},Range 问题:{IsRangeNotSupported}):{Th.Source.Url}")
                     Log(ex, $"{If(SourcesOnce.FirstOrDefault?.SingleThread Is Nothing, "", "单线程")}下载源 {Th.Source.Id} 已被禁用",
-                        If(ex.Message.Contains("不支持分段下载") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(416)"), LogLevel.Developer, LogLevel.Debug))
+                        If(IsRangeNotSupported OrElse ex.Message.Contains("(404)"), LogLevel.Developer, LogLevel.Debug))
                     SyncLock LockSource
                         SourcesOnce.Remove(Th.Source)
                     End SyncLock
@@ -1191,9 +1187,10 @@ SourceBreak:
                         Fail(ex)
                     ElseIf HasAvailableSource() AndAlso Not IsMergeFailure Then
                         '当前源失败,但还有下载源:正常地继续执行
-                    ElseIf Not Retried AndAlso Sources.Count > 1 Then
+                    ElseIf Not Retried Then
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
-                        Hint($"{LocalName}:文件下载失败,正在自动重试……", If(ModeDebug, HintType.Critical, HintType.Info)) '在调试模式下给出红色提示
+                        '若所有源均不支持 Range,也会走到这里重试
+                        If Not IsRangeNotSupported Then Hint($"{LocalName}:文件下载失败,正在自动重试……", If(ModeDebug, HintType.Red, HintType.Blue)) '在调试模式下给出红色提示
                         Retried = True
                         SyncLock LockSource
                             SourcesOnce.Clear()
@@ -1388,6 +1385,12 @@ Retry:
         End Sub
 
     End Class
+    Private Class RangeNotSupportedException
+        Inherits WebException
+        Public Sub New(Message As String)
+            MyBase.New(Message)
+        End Sub
+    End Class
     ''' <summary>
     ''' 下载一系列文件的加载器。
     ''' </summary>
@@ -1890,7 +1893,6 @@ Retry:
                     Catch ex As Exception
                         Log(ex, "清理下载缓存失败")
                     End Try
-                    Directory.CreateDirectory(PathTemp & "Download")
                 End If
                 IsDownloadCacheCleared = True
             End SyncLock

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

@@ -908,8 +908,7 @@ NextStack:
                         If File.Exists(OutputFile) Then
                             If FileEncoding Is Nothing Then FileEncoding = GetEncoding(ReadFileBytes(OutputFile))
                             Dim FileContent As String = ReadFile(OutputFile, FileEncoding)
-                            FileContent = FilterAccessToken(FileContent, If(FileName = "启动脚本.bat", "F", "*"))
-                            FileContent = FilterUserName(FileContent, "*")
+                            FileContent = FilterUserName(FilterAccessToken(FileContent, If(FileName = "启动脚本.bat", "F", "*")), "*")
                             WriteFile(TempFolder & "Report\" & FileName, FileContent, Encoding:=FileEncoding)
                             Log($"[Crash] 导出文件:{FileName},编码:{FileEncoding.HeaderName}")
                         End If
@@ -917,7 +916,7 @@ NextStack:
                     '导出报告
                     Compression.ZipFile.CreateFromDirectory(TempFolder & "Report\", FileAddress)
                     DeleteDirectory(TempFolder & "Report\")
-                    Hint("错误报告已导出!", HintType.Finish)
+                    Hint("错误报告已导出!", HintType.Green)
                 Catch ex As Exception
                     Log(ex, "导出错误报告失败", LogLevel.Feedback)
                     Return

+ 32 - 66
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -35,7 +35,7 @@ Public Module ModLaunch
         '预检查
         If Not RunInUi() Then Throw New Exception("McLaunchStart 必须在 UI 线程调用!")
         If McLaunchLoader.State = LoadState.Loading Then
-            Hint("已有游戏正在启动中!", HintType.Critical)
+            Hint("已有游戏正在启动中!", HintType.Red)
             Return False
         End If
         '强制切换需要启动的版本
@@ -44,7 +44,7 @@ Public Module ModLaunch
             '检查版本
             CurrentLaunchOptions.Version.Load()
             If CurrentLaunchOptions.Version.State = McVersionState.Error Then
-                Hint("无法启动 Minecraft:" & CurrentLaunchOptions.Version.Info, HintType.Critical)
+                Hint("无法启动 Minecraft:" & CurrentLaunchOptions.Version.Info, HintType.Red)
                 Return False
             End If
             '切换版本
@@ -98,7 +98,7 @@ Public Module ModLaunch
             McLaunchPrecheck()
             McLaunchLog("预检测已通过")
         Catch ex As Exception
-            If Not ex.Message.StartsWithF("$$") Then Hint(ex.Message, HintType.Critical)
+            If Not ex.Message.StartsWithF("$$") Then Hint(ex.Message, HintType.Red)
             Throw
         End Try
         Dim IsSavingBatch As Boolean = CurrentLaunchOptions?.SaveBatch IsNot Nothing
@@ -146,7 +146,7 @@ Public Module ModLaunch
             '成功与失败处理
             Select Case LaunchLoader.State
                 Case LoadState.Finished
-                    Hint(McVersionCurrent.Name & " 启动成功!", HintType.Finish)
+                    Hint(McVersionCurrent.Name & " 启动成功!", HintType.Green)
                     '上报
                     If Not IsSavingBatch Then
                         Telemetry("Minecraft 启动成功",
@@ -155,9 +155,9 @@ Public Module ModLaunch
                     End If
                 Case LoadState.Aborted
                     If AbortHint Is Nothing Then
-                        Hint(If(IsSavingBatch, "已取消导出启动脚本!", "已取消启动!"), HintType.Info)
+                        Hint(If(IsSavingBatch, "已取消导出启动脚本!", "已取消启动!"), HintType.Blue)
                     Else
-                        Hint(AbortHint, HintType.Finish)
+                        Hint(AbortHint, HintType.Green)
                     End If
                 Case LoadState.Failed
                     Throw LaunchLoader.Error
@@ -270,7 +270,7 @@ NextInner:
                 Select Case MyMsgBox("你必须先登录正版账号,才能进行离线登录!", "正版验证", "购买正版", "试玩", "返回",
                     Button1Action:=Sub() OpenWebsite("https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"))
                     Case 2
-                        Hint("游戏将以试玩模式启动!", HintType.Critical)
+                        Hint("游戏将以试玩模式启动!", HintType.Red)
                         CurrentLaunchOptions.ExtraArgs.Add("--demo")
                     Case 3
                         Throw New Exception("$$")
@@ -755,10 +755,10 @@ LoginFinish:
                 ContentType:="application/json; charset=utf-8", RequireJson:=True))
             '检查登录结果
             If LoginJson("availableProfiles").Count = 0 Then
-                If Data.Input.ForceReselectProfile Then Hint("你还没有创建角色,无法更换!", HintType.Critical)
+                If Data.Input.ForceReselectProfile Then Hint("你还没有创建角色,无法更换!", HintType.Red)
                 Throw New Exception("$你还没有创建角色,请在创建角色后再试!")
             ElseIf Data.Input.ForceReselectProfile AndAlso LoginJson("availableProfiles").Count = 1 Then
-                Hint("你的账户中只有一个角色,无法更换!", HintType.Critical)
+                Hint("你的账户中只有一个角色,无法更换!", HintType.Red)
             End If
             Dim SelectedName As String = Nothing
             Dim SelectedId As String = Nothing
@@ -1304,7 +1304,7 @@ Retry:
             If McLaunchJavaSelected IsNot Nothing Then
                 McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
             Else
-                Hint("没有可用的 Java,已取消启动!", HintType.Critical)
+                Hint("没有可用的 Java,已取消启动!", HintType.Red)
                 Throw New Exception("$$")
             End If
 
@@ -1395,7 +1395,7 @@ Retry:
         Next
         '自定义
         Dim ArgumentGame As String = Setup.Get("VersionAdvanceGame", Version:=McVersionCurrent)
-        Arguments += " " & If(ArgumentGame = "", Setup.Get("LaunchAdvanceGame"), ArgumentGame)
+        Arguments += " " & ArgumentReplace(If(ArgumentGame = "", Setup.Get("LaunchAdvanceGame"), ArgumentGame))
         '进行参数替换
         Dim ReplaceArguments = McLaunchArgumentsReplace(McVersionCurrent, Loader)
         If String.IsNullOrWhiteSpace(ReplaceArguments("${version_type}")) Then
@@ -1412,6 +1412,9 @@ Retry:
             FinalArguments += Argument & " "
         Next
         FinalArguments = FinalArguments.TrimEnd
+        '替换 Module Path 为短路径(Forge 会在版本 JSON 硬编码这些路径)
+        FinalArguments = FinalArguments.RegexReplaceEach("(?<= (-p|--module-path) "")[^""]+(?="" )",
+            Function(ModulePath) ModulePath.Value.RegexReplaceEach("[A-Z]{1}:\\[^;]+", Function(m) ShortenPath(m.Value)))
         '进服
         Dim Server As String = If(String.IsNullOrEmpty(CurrentLaunchOptions.ServerIp), Setup.Get("VersionServerEnter", McVersionCurrent), CurrentLaunchOptions.ServerIp)
         If Server.Length > 0 Then
@@ -1427,7 +1430,7 @@ Retry:
                     '不包含端口号
                     FinalArguments += $" --server {Server} --port 25565"
                 End If
-                If McVersionCurrent.Version.HasOptiFine Then Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Critical)
+                If McVersionCurrent.Version.HasOptiFine Then Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Red)
             End If
         End If
         '输出
@@ -1447,7 +1450,7 @@ Retry:
         If ArgumentJvm = "" Then ArgumentJvm = Setup.Get("LaunchAdvanceJvm")
         If Not ArgumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true") Then ArgumentJvm += " -Dlog4j2.formatMsgNoLookups=true"
         ArgumentJvm = ArgumentJvm.Replace(" -XX:MaxDirectMemorySize=256M", "") '#3511 的清理
-        DataList.Insert(0, ArgumentJvm) '可变 JVM 参数
+        DataList.Insert(0, ArgumentReplace(ArgumentJvm)) '可变 JVM 参数
         DataList.Add("-Xmn" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024 * 0.15) & "m")
         DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024) & "m")
         DataList.Add("""-Djava.library.path=" & GetNativesFolder() & """")
@@ -1571,7 +1574,7 @@ NextVersion:
 
         '去重
         DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M") '#3511 的清理
-        Dim Result As String = Join(DeDuplicateDataList.Distinct.ToList, " ")
+        Dim Result As String = DeDuplicateDataList.Distinct.Join(" ")
 
         '添加 MainClass
         If Version.JsonObject("mainClass") Is Nothing Then
@@ -1592,7 +1595,7 @@ NextVersion:
         If Not BasicString.Contains("--height") Then BasicString += " --height ${resolution_height} --width ${resolution_width}"
         DataList.Add(BasicString)
 
-        Dim Result As String = Join(DataList, " ")
+        Dim Result As String = DataList.Join(" ")
 
         '特别改变 OptiFineTweaker
         If (Version.Version.HasForge OrElse Version.Version.HasLiteLoader) AndAlso Version.Version.HasOptiFine Then
@@ -1697,7 +1700,7 @@ NextVersion:
         GameArguments.Add("${launcher_version}", VersionCode)
         GameArguments.Add("${version_name}", Version.Name)
         Dim ArgumentInfo As String = Setup.Get("VersionArgumentInfo", Version:=McVersionCurrent)
-        GameArguments.Add("${version_type}", If(ArgumentInfo = "", Setup.Get("LaunchArgumentInfo"), ArgumentInfo))
+        GameArguments.Add("${version_type}", ArgumentReplace(If(ArgumentInfo = "", Setup.Get("LaunchArgumentInfo"), ArgumentInfo)))
         GameArguments.Add("${game_directory}", ShortenPath(Left(McVersionCurrent.PathIndie, McVersionCurrent.PathIndie.Count - 1)))
         GameArguments.Add("${assets_root}", ShortenPath(PathMcFolder & "assets"))
         GameArguments.Add("${user_properties}", "{}")
@@ -1707,7 +1710,7 @@ NextVersion:
         GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${user_type}", "msa") '#1221
-        GameArguments.Add("${primary_jar}", Version.Path & Version.Name & ".jar") '#6942
+        GameArguments.Add("${primary_jar}", ShortenPath(Version.Path & Version.Name & ".jar")) '#6942
 
         '窗口尺寸参数
         Dim GameSize As Size
@@ -1827,10 +1830,14 @@ NextVersion:
     ''' </summary>
     Private Function GetNativesFolder() As String
         Dim Result As String = McVersionCurrent.Path & McVersionCurrent.Name & "-natives"
-        If IsGBKEncoding OrElse Result.IsASCII() Then Return ShortenPath(Result)
-        Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives"
-        If Result.IsASCII() Then Return ShortenPath(Result)
-        Return OsDrive & "ProgramData\PCL\natives"
+        If Not (IsGBKEncoding OrElse Result.IsASCII()) Then
+            Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives"
+            If Not Result.IsASCII() Then
+                Result = OsDrive & "ProgramData\PCL\natives"
+            End If
+        End If
+        Directory.CreateDirectory(Result) '提前创建,这样 ShortenPath 才有结果(否则在长路径下首次启动 Forge 会崩溃)
+        Return ShortenPath(Result)
     End Function
 
 #End Region
@@ -1981,7 +1988,7 @@ NextVersion:
                McLoginLoader.Input.Type = McLoginType.Legacy AndAlso '离线登录
                (Setup.Get("LaunchSkinType") = 2 OrElse '强制 Alex
                (Setup.Get("LaunchSkinType") = 4 AndAlso Setup.Get("LaunchSkinSlim"))) Then '或选用 Alex 皮肤
-            Hint("此 Minecraft 版本尚不支持 Alex 皮肤,你的皮肤可能会显示为 Steve!", HintType.Critical)
+            Hint("此 Minecraft 版本尚不支持 Alex 皮肤,你的皮肤可能会显示为 Steve!", HintType.Red)
         End If
 
         '离线皮肤资源包
@@ -2125,9 +2132,9 @@ IgnoreCustomSkin:
 
         '获取自定义命令
         Dim CustomCommandGlobal As String = Setup.Get("LaunchAdvanceRun")
-        If CustomCommandGlobal <> "" Then CustomCommandGlobal = ArgumentReplace(CustomCommandGlobal, True)
+        If CustomCommandGlobal <> "" Then CustomCommandGlobal = ArgumentReplace(CustomCommandGlobal)
         Dim CustomCommandVersion As String = Setup.Get("VersionAdvanceRun", Version:=McVersionCurrent)
-        If CustomCommandVersion <> "" Then CustomCommandVersion = ArgumentReplace(CustomCommandVersion, True)
+        If CustomCommandVersion <> "" Then CustomCommandVersion = ArgumentReplace(CustomCommandVersion)
 
         '输出 bat
         Try
@@ -2283,7 +2290,7 @@ IgnoreCustomSkin:
         '获取窗口标题
         Dim WindowTitle As String = Setup.Get("VersionArgumentTitle", Version:=McVersionCurrent)
         If WindowTitle = "" Then WindowTitle = Setup.Get("LaunchArgumentTitle")
-        WindowTitle = ArgumentReplace(WindowTitle, False)
+        WindowTitle = ArgumentReplace(WindowTitle, ReplaceTime:=False)
 
         '初始化等待
         Dim Watcher As New Watcher(Loader, McVersionCurrent, WindowTitle)
@@ -2332,47 +2339,6 @@ IgnoreCustomSkin:
 
     End Sub
 
-    ''' <summary>
-    ''' 在启动结束时,对 PCL 约定的替换标记进行处理。
-    ''' </summary>
-    Private Function ArgumentReplace(Raw As String, ReplaceTimeAndDate As Boolean) As String
-        If Raw Is Nothing Then Return Nothing
-        '路径替换
-        Raw = Raw.Replace("{minecraft}", PathMcFolder)
-        Raw = Raw.Replace("{verpath}", McVersionCurrent.Path)
-        Raw = Raw.Replace("{verindie}", McVersionCurrent.PathIndie)
-        Raw = Raw.Replace("{java}", McLaunchJavaSelected.PathFolder)
-        '普通替换
-        Raw = Raw.Replace("{user}", McLoginLoader.Output.Name)
-        Raw = Raw.Replace("{uuid}", McLoginLoader.Output.Uuid)
-        If ReplaceTimeAndDate Then '设置窗口标题时需要动态替换日期和时间
-            Raw = Raw.Replace("{date}", Date.Now.ToString("yyyy/M/d"))
-            Raw = Raw.Replace("{time}", Date.Now.ToString("HH:mm:ss"))
-        End If
-        Select Case McLoginLoader.Input.Type
-            Case McLoginType.Legacy
-                If PageLinkHiper.HiperState = LoadState.Finished Then
-                    Raw = Raw.Replace("{login}", "联机离线")
-                Else
-                    Raw = Raw.Replace("{login}", "离线")
-                End If
-            Case McLoginType.Ms
-                Raw = Raw.Replace("{login}", "正版")
-            Case McLoginType.Nide
-                Raw = Raw.Replace("{login}", "统一通行证")
-            Case McLoginType.Auth
-                Raw = Raw.Replace("{login}", "Authlib-Injector")
-        End Select
-        Raw = Raw.Replace("{name}", McVersionCurrent.Name)
-        If {"unknown", "old", "pending"}.Contains(McVersionCurrent.Version.McName.ToLower) Then
-            Raw = Raw.Replace("{version}", McVersionCurrent.Name)
-        Else
-            Raw = Raw.Replace("{version}", McVersionCurrent.Version.McName)
-        End If
-        Raw = Raw.Replace("{path}", Path)
-        Return Raw
-    End Function
-
 #End Region
 
 End Module

+ 48 - 14
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -68,7 +68,7 @@ Public Module ModMinecraft
             For Each Folder As String In Setup.Get("LaunchFolders").Split("|")
                 If Folder = "" Then Continue For
                 If Not Folder.Contains(">") OrElse Not Folder.EndsWithF("\") Then
-                    Hint("无效的 Minecraft 文件夹:" & Folder, HintType.Critical)
+                    Hint("无效的 Minecraft 文件夹:" & Folder, HintType.Red)
                     Continue For
                 End If
                 Dim Name As String = Folder.Split(">")(0)
@@ -261,6 +261,12 @@ Public Module ModMinecraft
         ''' 显示的描述文本。
         ''' </summary>
         Public Info As String = "该版本未被加载,请向作者反馈此问题"
+        Public ReadOnly Property HasCustomInfo As Boolean
+            Get
+                Return ReadIni(Path & "PCL\Setup.ini", "CustomInfo") <> ""
+            End Get
+        End Property
+
         ''' <summary>
         ''' 该版本的列表检查原始结果,不受自定义影响。
         ''' </summary>
@@ -784,7 +790,17 @@ ExitDataLoad:
                 End If
                 '确定版本描述
                 Dim CustomInfo As String = ReadIni(Path & "PCL\Setup.ini", "CustomInfo")
-                Info = If(CustomInfo <> "", CustomInfo, GetDefaultDescription())
+                If CustomInfo <> "" Then
+                    Info = CustomInfo
+                Else
+                    Info = GetVersionDescription()
+                    If State = McVersionState.Fool Then
+                        Info = GetMcFoolName(Version.McName)
+                    ElseIf State <> McVersionState.Error Then
+                        If Setup.Get("VersionServerLogin", Version:=Me) = 3 Then Info += ", 统一通行证验证"
+                        If Setup.Get("VersionServerLogin", Version:=Me) = 4 Then Info += ", Authlib 验证"
+                    End If
+                End If
                 '确定版本收藏状态
                 IsStar = ReadIni(Path & "PCL\Setup.ini", "IsStar", False)
                 '确定版本显示种类
@@ -817,11 +833,13 @@ ExitDataLoad:
             End Try
             Return Me
         End Function
+        Public IsLoaded As Boolean = False
         ''' <summary>
-        ''' 获取版本的默认描述。
+        ''' 获取对该版本的简短描述。
+        ''' 例如 “快照 16w01a”、“原版 1.12.2”、“愚人节版本 2.0”。
         ''' </summary>
-        Public Function GetDefaultDescription() As String
-            Dim Info As String = ""
+        Public Function GetVersionDescription() As String
+            Dim Info As String
             Select Case State
                 Case McVersionState.Snapshot
                     If Version.McName.ContainsF("pre", True) Then
@@ -838,21 +856,38 @@ ExitDataLoad:
                 Case McVersionState.Original, McVersionState.Forge, McVersionState.NeoForge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
                     Info = Version.ToString
                 Case McVersionState.Fool
-                    Info = GetMcFoolName(Version.McName)
+                    Info = "愚人节版本 " & Version.McName
                 Case McVersionState.Error
                     Return Me.Info '已有错误信息
                 Case Else
                     Info = "发生了未知错误,请向作者反馈此问题"
             End Select
-            If Not State = McVersionState.Error Then
-                If Setup.Get("VersionServerLogin", Version:=Me) = 3 Then Info += ", 统一通行证验证"
-                If Setup.Get("VersionServerLogin", Version:=Me) = 4 Then Info += ", Authlib 验证"
-            End If
             Return Info
         End Function
 
-        Public IsLoaded As Boolean = False
+        Public Function ToListItem() As MyListItem
+            Dim NewItem As New MyListItem With {.Info = Info, .Height = 42, .Tag = Me, .SnapsToDevicePixels = True, .Type = MyListItem.CheckType.Clickable}
+            '标题
+            NewItem.Inlines.Clear()
+            NewItem.Inlines.Add(New Run(Name))
+            If HasCustomInfo Then '如果版本设置了自定义描述,在标题后面以淡灰色显示其版本号
+                NewItem.Inlines.Add(New Run("  |  " & GetVersionDescription()) With {.Foreground = New MyColor(215, 215, 215), .FontSize = 12})
+            End If
+            'Logo
+            Try
+                If Logo.EndsWith("PCL\Logo.png") Then
+                    NewItem.Logo = Path & "PCL\Logo.png" '修复老版本中,存储的自定义 Logo 使用完整路径,导致移动后无法加载的 Bug
+                Else
+                    NewItem.Logo = Logo
+                End If
+            Catch ex As Exception
+                Log(ex, "加载版本图标失败", LogLevel.Hint)
+                NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
+            End Try
+            Return NewItem
+        End Function
 
+        '运算符支持
         Public Overrides Function Equals(obj As Object) As Boolean
             Dim version = TryCast(obj, McVersion)
             Return version IsNot Nothing AndAlso Path = version.Path
@@ -865,7 +900,6 @@ ExitDataLoad:
         Public Shared Operator <>(a As McVersion, b As McVersion) As Boolean
             Return Not (a = b)
         End Operator
-
     End Class
     Public Enum McVersionState
         [Error]
@@ -1567,12 +1601,12 @@ OnLoaded:
         Try
             Dim Image As New MyBitmap(FileName)
             If Image.Pic.Width <> 64 OrElse Not (Image.Pic.Height = 32 OrElse Image.Pic.Height = 64) Then
-                Hint("皮肤图片大小应为 64x32 像素或 64x64 像素!", HintType.Critical)
+                Hint("皮肤图片大小应为 64x32 像素或 64x64 像素!", HintType.Red)
                 Return New McSkinInfo With {.IsVaild = False}
             End If
             Dim FileInfo As New FileInfo(FileName)
             If FileInfo.Length > 24 * 1024 Then
-                Hint("皮肤文件大小需小于 24 KB,而所选文件大小为 " & Math.Round(FileInfo.Length / 1024, 2) & " KB", HintType.Critical)
+                Hint("皮肤文件大小需小于 24 KB,而所选文件大小为 " & Math.Round(FileInfo.Length / 1024, 2) & " KB", HintType.Red)
                 Return New McSkinInfo With {.IsVaild = False}
             End If
         Catch ex As Exception

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

@@ -31,7 +31,7 @@ Public Module ModModpack
         Try
             '字符校验
             Dim TargetFolder As String = $"{PathMcFolder}versions\{VersionName}\"
-            If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("游戏路径中不能含有感叹号或分号:" & TargetFolder, HintType.Critical) : Throw New CancelledException
+            If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("游戏路径中不能含有感叹号或分号:" & TargetFolder, HintType.Red) : Throw New CancelledException
             '获取整合包种类与关键 Json
             Dim PackType As Integer = -1
             Try
@@ -344,7 +344,7 @@ Retry:
         '重复任务检查
         Dim LoaderName As String = "CurseForge 整合包安装:" & VersionName & " "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
-            Hint("该整合包正在安装中!", HintType.Critical)
+            Hint("该整合包正在安装中!", HintType.Red)
             Throw New CancelledException
         End If
 
@@ -485,7 +485,7 @@ Retry:
         '重复任务检查
         Dim LoaderName As String = $"Modrinth 整合包安装:{VersionName} "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
-            Hint("该整合包正在安装中!", HintType.Critical)
+            Hint("该整合包正在安装中!", HintType.Red)
             Throw New CancelledException
         End If
 
@@ -540,7 +540,7 @@ Retry:
         '重复任务检查
         Dim LoaderName As String = "HMCL 整合包安装:" & VersionName & " "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
-            Hint("该整合包正在安装中!", HintType.Critical)
+            Hint("该整合包正在安装中!", HintType.Red)
             Throw New CancelledException
         End If
         '启动
@@ -639,8 +639,8 @@ Retry:
         '构造版本安装请求
         If PackJson("components") Is Nothing Then Throw New Exception("该 MMC 整合包未提供游戏版本信息,无法安装!")
         Dim Request As New McInstallRequest With {.TargetVersionName = VersionName, .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\"}
-        For Each Component In PackJson("components")
-            If Not Component.Contains("uid") Then Continue For
+        For Each Component As JObject In PackJson("components")
+            If Not Component.ContainsKey("uid") Then Continue For
             Select Case Component("uid").ToString
                 Case "org.lwjgl"
                     Log("[ModPack] 已跳过 LWJGL 项")
@@ -667,7 +667,7 @@ Retry:
         '重复任务检查
         Dim LoaderName As String = "MMC 整合包安装:" & VersionName & " "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
-            Hint("该整合包正在安装中!", HintType.Critical)
+            Hint("该整合包正在安装中!", HintType.Red)
             Throw New CancelledException
         End If
 
@@ -725,7 +725,7 @@ Retry:
         Next
         If Not Addons.ContainsKey("game") Then Throw New Exception("该 MCBBS 整合包未提供游戏版本信息,无法安装!")
         If Addons.ContainsKey("quilt") Then
-            Hint("PCL 暂不支持安装需要 Quilt 的整合包!", HintType.Critical)
+            Hint("PCL 暂不支持安装需要 Quilt 的整合包!", HintType.Red)
             Throw New CancelledException
         End If
         Dim Request As New McInstallRequest With {
@@ -746,7 +746,7 @@ Retry:
         '重复任务检查
         Dim LoaderName As String = "MCBBS 整合包安装:" & VersionName & " "
         If LoaderTaskbar.Any(Function(l) l.Name = LoaderName) Then
-            Hint("该整合包正在安装中!", HintType.Critical)
+            Hint("该整合包正在安装中!", HintType.Red)
             Throw New CancelledException
         End If
 
@@ -766,7 +766,7 @@ Retry:
         MyMsgBox("接下来请选择一个空文件夹,它会被安装到这个文件夹里。", "安装", "继续", ForceWait:=True)
         Dim TargetFolder As String = SelectFolder("选择安装目标(必须是一个空文件夹)")
         If String.IsNullOrEmpty(TargetFolder) Then Throw New CancelledException
-        If Directory.GetFileSystemEntries(TargetFolder).Length > 0 Then Hint("请选择一个空文件夹作为安装目标!", HintType.Critical) : Throw New CancelledException
+        If Directory.GetFileSystemEntries(TargetFolder).Length > 0 Then Hint("请选择一个空文件夹作为安装目标!", HintType.Red) : Throw New CancelledException
         '解压
         Dim Loader As New LoaderCombo(Of String)("解压压缩包", {
             New LoaderTask(Of String, Integer)("解压压缩包",
@@ -843,8 +843,8 @@ Retry:
         MyMsgBox("接下来请选择一个空文件夹,它会被安装到这个文件夹里。", "安装", "继续", ForceWait:=True)
         Dim TargetFolder As String = SelectFolder("选择安装目标(必须是一个空文件夹)")
         If String.IsNullOrEmpty(TargetFolder) Then Throw New CancelledException
-        If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Critical) : Throw New CancelledException
-        If Directory.GetFileSystemEntries(TargetFolder).Length > 0 Then Hint("请选择一个空文件夹作为安装目标!", HintType.Critical) : Throw New CancelledException
+        If TargetFolder.Contains("!") OrElse TargetFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Red) : Throw New CancelledException
+        If Directory.GetFileSystemEntries(TargetFolder).Length > 0 Then Hint("请选择一个空文件夹作为安装目标!", HintType.Red) : Throw New CancelledException
         '解压
         Dim Loader As New LoaderCombo(Of String)("解压压缩包", {
             New LoaderTask(Of String, Integer)("解压压缩包",

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

@@ -349,7 +349,7 @@
             State = MinecraftState.Crashed
             '崩溃分析
             WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
-            Hint("检测到 Minecraft 出现错误,错误分析已开始……", HintType.Critical)
+            Hint("检测到 Minecraft 出现错误,错误分析已开始……", HintType.Red)
             FeedbackInfo()
             RunInNewThread(
             Sub()

+ 292 - 204
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -1,181 +1,291 @@
-Imports System.Collections.ObjectModel
+
+#Region "附加属性"
+
+''' <summary>
+''' 用于在 XAML 中初始化列表对象。
+''' 附加属性无法在 XAML 中为每个对象初始化独立的列表对象,因此需要一个包装类,然后在 XAML 中显式初始化。
+''' </summary>
+<Markup.ContentProperty("Events")>
+Public Class CustomEventCollection
+    Implements IEnumerable(Of CustomEvent)
+    Dim _Events As New List(Of CustomEvent)
+    Public ReadOnly Property Events As List(Of CustomEvent)
+        Get
+            Return _Events
+        End Get
+    End Property
+    Public Function GetEnumerator() As IEnumerator(Of CustomEvent) Implements IEnumerable(Of CustomEvent).GetEnumerator
+        Return DirectCast(Events, IEnumerable(Of CustomEvent)).GetEnumerator()
+    End Function
+    Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
+        Return DirectCast(Events, IEnumerable).GetEnumerator()
+    End Function
+End Class
+
+''' <summary>
+''' 提供自定义事件的附加属性。
+''' </summary>
+Public Class CustomEventService
+
+    'Events
+    Public Shared ReadOnly EventsProperty As DependencyProperty =
+            DependencyProperty.RegisterAttached("Events", GetType(CustomEventCollection), GetType(CustomEventService), New PropertyMetadata(Nothing))
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Sub SetEvents(d As DependencyObject, value As CustomEventCollection)
+        d.SetValue(EventsProperty, value)
+    End Sub
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Function GetEvents(d As DependencyObject) As CustomEventCollection
+        If d.GetValue(EventsProperty) Is Nothing Then d.SetValue(EventsProperty, New CustomEventCollection)
+        Return d.GetValue(EventsProperty)
+    End Function
+
+    'EventType
+    Public Shared ReadOnly EventTypeProperty As DependencyProperty =
+            DependencyProperty.RegisterAttached("EventType", GetType(CustomEvent.EventType), GetType(CustomEventService), New PropertyMetadata(Nothing))
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Sub SetEventType(d As DependencyObject, value As CustomEvent.EventType)
+        d.SetValue(EventTypeProperty, value)
+    End Sub
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Function GetEventType(d As DependencyObject) As CustomEvent.EventType
+        Return d.GetValue(EventTypeProperty)
+    End Function
+
+    'EventData
+    Public Shared ReadOnly EventDataProperty As DependencyProperty =
+            DependencyProperty.RegisterAttached("EventData", GetType(String), GetType(CustomEventService), New PropertyMetadata(Nothing))
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Sub SetEventData(d As DependencyObject, value As String)
+        d.SetValue(EventDataProperty, value)
+    End Sub
+    <AttachedPropertyBrowsableForType(GetType(DependencyObject))>
+    Public Shared Function GetEventData(d As DependencyObject) As String
+        Return d.GetValue(EventDataProperty)
+    End Function
+
+End Class
+
+Partial Public Module ModMain
 
-Public Module ModEvent
-#Region "事件执行方法"
     ''' <summary>
-    ''' 供外部调用的自定义事件执行接口,会在新建工作线程中运行逻辑。
+    ''' 触发该控件上的自定义事件。
+    ''' 事件会在新线程中执行。
     ''' </summary>
-    ''' <param name="SingleEventType">xaml中的EventType</param>
-    ''' <param name="SingleEventData">xaml中的EventData</param>
-    ''' <param name="EventCollection">xaml中的CustomEvents</param>
-    Public Sub ProcessCustomEvents(SingleEventType As String, SingleEventData As String, EventCollection As CustomEventCollection)
+    <Runtime.CompilerServices.Extension>
+    Public Sub RaiseCustomEvent(Control As DependencyObject)
+        '收集事件列表
+        Dim Events = CustomEventService.GetEvents(Control).ToList
+        Dim EventType = CustomEventService.GetEventType(Control)
+        If EventType <> CustomEvent.EventType.None Then Events.Add(New CustomEvent(EventType, CustomEventService.GetEventData(Control)))
+        '执行事件
+        If Not Events.Any Then Return
         RunInNewThread(
-            Sub()
-                Try
-                    '先执行单个事件
-                    StartCustomEvent(SingleEventType, SingleEventData)
-                    '再挨个执行事件集合中的事件
-                    If EventCollection IsNot Nothing Then
-                        For Each AnEvent In RunInUiWait(Function() EventCollection.DeepCloneToStructureList())
-                            StartCustomEvent(AnEvent.EventType, AnEvent.EventData)
-                        Next
-                    End If
-                Catch ex As Exception
-                    Log(ex, "自定义事件执行器发生异常", LogLevel.Feedback)
-                End Try
-            End Sub, "CustomEventProcessor " & GetUuid())
+        Sub()
+            For Each e In Events
+                e.Raise()
+            Next
+        End Sub, "执行自定义事件 " & GetUuid())
     End Sub
-    ''' <summary>
-    ''' 对当前线程阻塞地调用自定义事件处理器,接受来自 xaml 原始输入作为参数。
-    ''' </summary>
-    Private Sub StartCustomEvent(Type As String, DataRaw As String)
-        If String.IsNullOrEmpty(Type) Then Exit Sub
-        Dim Data = If(DataRaw Is Nothing, {""}, DataRaw.Split("|"))
-        Log("[Control] 执行自定义事件:" & Type & ", " & Join(Data, ", "))
-        '检测是否存在对应事件
-        If Not CustomEventProcessorsDict.ContainsKey(Type) Then
-            MyMsgBox($"未知的事件类型:'{Type}'{vbCrLf}请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败", ForceWait:=True)
-            Exit Sub
-        End If
-        '调用事件处理器
-        Try
-            CustomEventProcessorsDict(Type)(Data)
-        Catch ex As Exception
-            Log(ex, $"自定义事件执行失败:{Type}", LogLevel.Msgbox, "自定义主页事件执行失败")
-        End Try
+
+End Module
+
+#End Region
+
+''' <summary>
+''' 自定义事件。
+''' </summary>
+Public Class CustomEvent
+
+#Region "属性与触发"
+
+    Public Property Type As EventType = EventType.None
+    Public Property Data As String = Nothing
+    Public Sub New()
+    End Sub
+    Public Sub New(Type As EventType, Data As String)
+        Me.Type = Type
+        Me.Data = Data
     End Sub
 
     ''' <summary>
-    ''' 存储各个自定义事件处理器的字典,以 EventType 为键,执行自定义事件时在工作线程中调用 Action 并传入分割后的 EventData 。
-    ''' Action 应当在事件逻辑结束后,即可以开始下一个事件时再结束;以便于挨个调用事件不打架。
+    ''' 在当前线程中触发该自定义事件。
     ''' </summary>
-    Private ReadOnly CustomEventProcessorsDict As New Dictionary(Of String, Action(Of String())) From {
-        {"打开网页",
-        Sub(Data As String())
-            Data(0) = Data(0).Replace("\", "/")
-            If Not Data(0).Contains("://") OrElse Data(0).StartsWithF("file", True) Then '为了支持更多协议(#2200)
-                MyMsgBox("打开网页事件中,EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
-                Exit Sub
-            End If
-            Hint("网页正在开启中,请稍候……")
-            OpenWebsite(Data(0))
-        End Sub},
-        {"打开文件",
-        Sub(Data As String())
-            Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "打开文件")
-            Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
-            Log($"[Control] 打开文件:路径:{Location},工作目录:{WorkingDir}")
-            If Not CommandRunUserConfirm(If(Data.Length < 2, Location, $"{Location} {Data(1)}")) Then Exit Sub
-            Dim Info As New ProcessStartInfo With {
-                .Arguments = If(Data.Length < 2, "", Data(1)),
-                .FileName = Location,
-                .WorkingDirectory = ShortenPath(WorkingDir)
-            }
-            Process.Start(Info)
-        End Sub},
-        {"打开帮助",
-        Sub(Data As String())
-            Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "打开帮助")
-            Dim Location = ActualPaths(0)
-            Log($"[Control] 打开帮助:{Location}")
-            PageOtherHelp.EnterHelpPage(Location)
-        End Sub},
-        {"执行命令",
-        Sub(Data As String())
-            Dim ActualPaths = GetEventAbsoluteUrls(Data(0), "执行命令")
-            Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
-            Log($"[Control] 执行命令:路径:{Location},工作目录:{WorkingDir}")
-            If Not CommandRunUserConfirm(If(Data.Length < 2, Location, $"{Location} {Data(1)}")) Then Exit Sub
-            Dim Info As New ProcessStartInfo With {
-                .Arguments = If(Data.Length < 2, "", Data(1)),
-                .FileName = Location,
-                .WorkingDirectory = ShortenPath(WorkingDir)
-            }
-            Process.Start(Info)
-        End Sub},
-        {"启动游戏",
-        Sub(Data As String())
-            If Data(0) = "\current" Then
-                If McVersionCurrent Is Nothing Then
-                    Hint("请先选择一个 Minecraft 版本!", HintType.Critical)
-                    Exit Sub
-                Else
-                    Data(0) = McVersionCurrent.Name
-                End If
-            End If
-            RunInUiWait(
-            Sub()
-                If McLaunchStart(New McLaunchOptions With {.Version = New McVersion(Data(0)), .ServerIp = If(Data.Length >= 2, Data(1), Nothing)}) Then
-                    Hint("正在启动 " & Data(0) & "……")
-                End If
-            End Sub)
-        End Sub},
-        {"复制文本", Sub(Data As String()) ClipboardSet(Join(Data, "|"))},
-        {"刷新主页",
-        Sub(Data As String())
-            RunInUiWait(Sub() FrmLaunchRight.ForceRefresh())
-            If Data(0) = "" Then Hint("已刷新主页!", HintType.Finish)
-        End Sub},
-        {"刷新帮助", Sub() RunInUi(Sub() PageOtherLeft.RefreshHelp())},
-        {"今日人品", Sub() PageOtherTest.Jrrp()},
-        {"内存优化",
-        Sub(Data As String())
-            If Data.Length >= 1 AndAlso Data(0) = "前台运行" Then
-                PageOtherTest.MemoryOptimize(Data.Length >= 2 AndAlso Data(1) = "显示提示")
-            Else
-                RunInNewThread(Sub() PageOtherTest.MemoryOptimize(True))
-            End If
-        End Sub},
-        {"弹出窗口", Sub(Data As String()) MyMsgBox(Data(1).Replace("\n", vbCrLf), Data(0).Replace("\n", vbCrLf), ForceWait:=True)},
-        {"切换页面", Sub(Data As String()) RunInUiWait(Sub() FrmMain.PageChange(Val(Data(0)), Val(Data(1))))},
-        {"安装整合包", Sub() RunInUiWait(Sub() ModpackInstall())},
-        {"导入整合包", Sub() RunInUiWait(Sub() ModpackInstall())},
-        {"下载文件",
-        Sub(Data As String())
-            Data(0) = Data(0).Replace("\", "/")
-            If Not (Data(0).StartsWithF("http://", True) OrElse Data(0).StartsWithF("https://", True)) Then
-                MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的下载协议。", "事件执行失败")
-                Exit Sub
-            End If
-            Try
-                Select Case Data.Length
-                    Case 1
-                        PageOtherTest.StartCustomDownload(Data(0), GetFileNameFromPath(Data(0)))
-                    Case 2
-                        PageOtherTest.StartCustomDownload(Data(0), Data(1))
-                    Case Else
-                        PageOtherTest.StartCustomDownload(Data(0), Data(1), Data(2))
-                End Select
-            Catch
-                PageOtherTest.StartCustomDownload(Data(0), "未知")
-            End Try
-        End Sub}
-    }
+    Public Sub Raise()
+        Raise(Type, Data)
+    End Sub
 
 #End Region
-#Region "工具方法"
+
+    Public Enum EventType
+        None = 0
+        打开网页
+        打开文件
+        打开帮助
+        执行命令
+        启动游戏
+        复制文本
+        刷新主页
+        刷新页面
+        刷新帮助
+        今日人品
+        内存优化
+        清理垃圾
+        弹出窗口
+        弹出提示
+        切换页面
+        导入整合包
+        安装整合包
+        下载文件
+        修改设置
+        写入设置
+        修改变量
+        写入变量
+    End Enum
+
     ''' <summary>
-    ''' 弹窗询问用户是否同意继续执行打开类事件。
+    ''' 在当前线程中触发一个自定义事件。
     ''' </summary>
-    Private Function CommandRunUserConfirm(TextShowToUser As String) As Boolean
-        If Setup.Get("HintCustomCommand") Then Return True
-        Select Case MyMsgBox("即将执行:" & TextShowToUser & vbCrLf & "请在确认该操作没有安全隐患后继续。", "执行确认", "继续", "继续且今后不再要求确认", "取消")
-            Case 1
-                Return True
-            Case 2
-                Setup.Set("HintCustomCommand", True)
-                Return True
-            Case Else '3
-                Log("[Control] 用户取消了打开类事件")
-                Return False
-        End Select
-    End Function
+    Public Shared Sub Raise(Type As EventType, Arg As String)
+        If Type = EventType.None Then Return
+        Log($"[Control] 执行自定义事件:{Type}, {Arg}")
+        Try
+            Dim Args As String() = If(Arg?.Split("|"), {""})
+            Select Case Type
+
+                Case EventType.打开网页
+                    Arg = Arg.Replace("\", "/")
+                    If Not Arg.Contains("://") OrElse Arg.StartsWithF("file", True) Then '为了支持更多协议(#2200)
+                        MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
+                        Return
+                    End If
+                    Hint("正在开启中,请稍候:" & Arg)
+                    RunInThread(Sub() OpenWebsite(Arg))
+
+                Case EventType.打开文件, EventType.打开帮助, EventType.执行命令
+                    RunInThread(
+                    Sub()
+                        Try
+                            '确认实际路径
+                            Dim ActualPaths = GetAbsoluteUrls(Args(0), Type)
+                            Dim Location = ActualPaths(0), WorkingDir = ActualPaths(1)
+                            Log($"[Control] 打开类自定义事件实际路径:{Location},工作目录:{WorkingDir}")
+                            '执行
+                            If Type = EventType.打开帮助 Then
+                                PageOtherHelp.EnterHelpPage(Location)
+                            Else
+                                If Not EventSafetyConfirm("即将执行:" & Location & If(Args.Length >= 2, " " & Args(1), "")) Then Return
+                                Dim Info As New ProcessStartInfo With {
+                                    .Arguments = If(Args.Length >= 2, Args(1), ""),
+                                    .FileName = Location,
+                                    .WorkingDirectory = WorkingDir
+                                }
+                                StartProcess(Info)
+                            End If
+                        Catch ex As Exception
+                            Log(ex, "执行打开类自定义事件失败", LogLevel.Msgbox)
+                        End Try
+                    End Sub)
+
+                Case EventType.启动游戏
+                    If Args(0) = "\current" Then
+                        If McVersionCurrent Is Nothing Then
+                            Hint("请先选择一个 Minecraft 版本!", HintType.Red)
+                            Return
+                        Else
+                            Args(0) = McVersionCurrent.Name
+                        End If
+                    End If
+                    RunInUi(
+                    Sub()
+                        If McLaunchStart(New McLaunchOptions With
+                                {.ServerIp = If(Args.Length >= 2, Args(1), Nothing), .Version = New McVersion(Args(0))}) Then
+                            Hint($"正在启动 {Args(0)}……")
+                        End If
+                    End Sub)
+
+                Case EventType.复制文本
+                    ClipboardSet(Arg)
+
+                Case EventType.刷新主页, EventType.刷新页面
+                    If TypeOf FrmMain.PageRight Is IRefreshable Then
+                        RunInUiWait(Sub() CType(FrmMain.PageRight, IRefreshable).Refresh())
+                        If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
+                    Else
+                        Hint("当前页面不支持刷新操作!", HintType.Red)
+                    End If
+
+                Case EventType.刷新帮助
+                    RunInUiWait(Sub() PageOtherLeft.RefreshHelp())
+                    If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
+
+                Case EventType.今日人品
+                    PageOtherTest.Jrrp()
+
+                Case EventType.内存优化
+                    RunInThread(Sub() PageOtherTest.MemoryOptimize(True))
+
+                Case EventType.清理垃圾
+                    RunInThread(Sub() PageOtherTest.RubbishClear())
+
+                Case EventType.弹出窗口
+                    If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 弹窗标题|弹窗内容")
+                    MyMsgBox(Args(1).Replace("\n", vbCrLf), Args(0).Replace("\n", vbCrLf), If(Args.Length > 2, Args(2), "确定"))
+
+                Case EventType.弹出提示
+                    Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ParseToEnum(Of HintType)))
+
+                Case EventType.切换页面
+                    RunInUi(Sub() FrmMain.PageChange(
+                                Args(0).ParseToEnum(Of FormMain.PageType),
+                                If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ParseToEnum(Of FormMain.PageSubType))))
+
+                Case EventType.导入整合包, EventType.安装整合包
+                    RunInUi(Sub() ModpackInstall())
+
+                Case EventType.下载文件
+                    Args(0) = Args(0).Replace("\", "/")
+                    If Not (Args(0).StartsWithF("http://", True) OrElse Args(0).StartsWithF("https://", True)) Then
+                        MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的下载协议。", "事件执行失败")
+                        Return
+                    End If
+                    If Not EventSafetyConfirm("即将从该网址下载文件:" & vbCrLf & Args(0)) Then Return
+                    Try
+                        Select Case Args.Length
+                            Case 1
+                                PageOtherTest.StartCustomDownload(Args(0), GetFileNameFromPath(Args(0)))
+                            Case 2
+                                PageOtherTest.StartCustomDownload(Args(0), Args(1))
+                            Case Else
+                                PageOtherTest.StartCustomDownload(Args(0), Args(1), Args(2))
+                        End Select
+                    Catch
+                        PageOtherTest.StartCustomDownload(Args(0), "未知")
+                    End Try
+
+                Case EventType.修改设置, EventType.写入设置
+                    If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")
+                    Setup.SetSafe(Args(0), Args(1), Version:=McVersionCurrent)
+                    If Args.Length = 2 Then Hint($"已写入设置:{Args(0)} → {Args(1)}", HintType.Green)
+
+                Case EventType.修改变量, EventType.写入变量
+                    If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 VariableName|SomeValue")
+                    WriteReg("CustomEvent" & Args(0), Args(1))
+                    If Args.Length = 2 Then Hint($"已写入变量:{Args(0)} → {Args(1)}", HintType.Green)
+
+                Case Else
+                    MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败")
+            End Select
+        Catch ex As Exception
+            Log(ex, $"事件执行失败({Type}, {Arg})", LogLevel.Msgbox)
+        End Try
+    End Sub
 
     ''' <summary>
     ''' 返回自定义事件的绝对 Url。实际返回 {绝对 Url, WorkingDir}。
     ''' 失败会抛出异常。
     ''' </summary>
-    Public Function GetEventAbsoluteUrls(RelativeUrl As String, EventType As String) As String()
+    Public Shared Function GetAbsoluteUrls(RelativeUrl As String, Type As EventType) As String()
 
         '网页确认
         If RelativeUrl.StartsWithF("http", True) Then
@@ -216,25 +326,25 @@ Public Module ModEvent
         If RelativeUrl.Contains(":\") Then
             '绝对路径
             Location = RelativeUrl
-            Log("[Control] 自定义事件中由绝对路径" & EventType & ":" & Location)
+            Log("[Control] 自定义事件中由绝对路径" & Type & ":" & Location)
         ElseIf File.Exists(Path & "PCL\" & RelativeUrl) Then
             '相对 PCL 文件夹的路径
             Location = Path & "PCL\" & RelativeUrl
-            Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & EventType & ":" & Location)
+            Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & Type & ":" & Location)
         ElseIf File.Exists(Path & "PCL\Help\" & RelativeUrl) Then
             '相对 PCL 本地帮助文件夹的路径
             Location = Path & "PCL\Help\" & RelativeUrl
             WorkingDir = Path & "PCL\Help\"
-            Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & EventType & ":" & Location)
-        ElseIf EventType = "打开帮助" AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
+            Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & Type & ":" & Location)
+        ElseIf Type = EventType.打开帮助 AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
             '相对 PCL 自带帮助文件夹的路径
             Location = PathTemp & "Help\" & RelativeUrl
             WorkingDir = PathTemp & "Help\"
-            Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" & EventType & ":" & Location)
-        ElseIf EventType = "打开文件" OrElse EventType = "执行命令" Then
+            Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" & Type & ":" & Location)
+        ElseIf Type = EventType.打开文件 OrElse Type = EventType.执行命令 Then
             '直接使用原有路径启动程序
             Location = RelativeUrl
-            Log("[Control] 自定义事件中直接" & EventType & ":" & Location)
+            Log("[Control] 自定义事件中直接" & Type & ":" & Location)
         Else
             '打开帮助,但是格式不对劲
             Throw New FileNotFoundException("未找到 EventData 指向的本地 xaml 文件:" & RelativeUrl, RelativeUrl)
@@ -243,42 +353,20 @@ Public Module ModEvent
         Return {Location, WorkingDir}
     End Function
 
-#End Region
-End Module
-#Region "数据结构类声明"
-Public Class CustomEventCollection
-    Inherits ObservableCollection(Of CustomEvent)
-    Public Function DeepCloneToStructureList() As IList(Of CustomEventStructure)
-        DeepCloneToStructureList = New List(Of CustomEventStructure)
-        For Each AnEvent In Me
-            DeepCloneToStructureList.Add(New CustomEventStructure With {.EventType = AnEvent.EventType, .EventData = AnEvent.EventData})
-        Next
+    ''' <summary>
+    ''' 弹出安全确认弹窗。返回是否继续执行。
+    ''' </summary>
+    Private Shared Function EventSafetyConfirm(Message As String) As Boolean
+        If Setup.Get("HintCustomCommand") Then Return True
+        Select Case MyMsgBox(Message & vbCrLf & "请在确认没有安全隐患后再继续。", "执行确认", "继续", "继续且今后不再要求确认", "取消")
+            Case 1
+                Return True
+            Case 2
+                Setup.Set("HintCustomCommand", True)
+                Return True
+            Case Else
+                Return False
+        End Select
     End Function
-End Class
-Public Class CustomEvent
-    Inherits DependencyObject
-    Public Property EventType As String
-        Get
-            Return GetValue(EventTypeProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventTypeProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(CustomEvent), New PropertyMetadata(Nothing))
-    Public Property EventData As String
-        Get
-            Return GetValue(EventDataProperty)
-        End Get
-        Set(value As String)
-            SetValue(EventDataProperty, value)
-        End Set
-    End Property
-    Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(CustomEvent), New PropertyMetadata(Nothing))
-End Class
-Public Structure CustomEventStructure
-    Public EventType As String
-    Public EventData As String
-End Structure
 
-#End Region
+End Class

+ 86 - 35
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -7,23 +7,12 @@ Public Module ModMain
 
     ''' <summary>
     ''' 提示信息的种类。
+    ''' 该枚举在自定义事件中使用,是公开 API 的一部分。
     ''' </summary>
     Public Enum HintType
-        ''' <summary>
-        ''' 信息,通常是蓝色的“i”。
-        ''' </summary>
-        ''' <remarks></remarks>
-        Info
-        ''' <summary>
-        ''' 已完成,通常是绿色的“√”。
-        ''' </summary>
-        ''' <remarks></remarks>
-        Finish
-        ''' <summary>
-        ''' 错误,通常是红色的“×”。
-        ''' </summary>
-        ''' <remarks></remarks>
-        Critical
+        Blue
+        Green
+        Red
     End Enum
     Private Structure HintMessage
         Public Text As String
@@ -38,7 +27,7 @@ Public Module ModMain
     ''' <summary>
     ''' 在窗口左下角弹出提示文本。
     ''' </summary>
-    Public Sub Hint(Text As String, Optional Type As HintType = HintType.Info, Optional Log As Boolean = True)
+    Public Sub Hint(Text As String, Optional Type As HintType = HintType.Blue, Optional Log As Boolean = True)
         If HintWaiting Is Nothing Then HintWaiting = New SafeList(Of HintMessage)
         HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
     End Sub
@@ -68,13 +57,13 @@ Public Module ModMain
                 Dim TargetColor0, TargetColor1 As MyColor
                 Dim Percent As Double = 0.3
                 Select Case CurrentHint.Type
-                    Case HintType.Info
+                    Case HintType.Blue
                         TargetColor0 = New MyColor(215, 37, 155, 252)
                         TargetColor1 = New MyColor(215, 10, 142, 252)
-                    Case HintType.Finish
+                    Case HintType.Green
                         TargetColor0 = New MyColor(215, 33, 177, 33)
                         TargetColor1 = New MyColor(215, 29, 160, 29)
-                    Case Else 'HintType.Critical
+                    Case Else 'HintType.Red
                         TargetColor0 = New MyColor(215, 255, 53, 11)
                         TargetColor1 = New MyColor(215, 255, 43, 0)
                 End Select
@@ -490,7 +479,7 @@ EndHint:
         ''' 是否为 “执行事件”。
         ''' </summary>
         Public IsEvent As Boolean
-        Public EventType As String
+        Public EventType As CustomEvent.EventType
         Public EventData As String
         ''' <summary>
         ''' 若非执行事件,其对应的 .xaml 本地文件内容。
@@ -504,7 +493,7 @@ EndHint:
         ''' </summary>
         Public Sub New(FilePath As String)
             RawPath = FilePath
-            Dim JsonData As JObject = GetJson(HelpArgumentReplace(ReadFile(FilePath)))
+            Dim JsonData As JObject = GetJson(ArgumentReplace(ReadFile(FilePath), AddressOf EscapeXML))
             If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
             '加载常规信息
             If JsonData("Title") IsNot Nothing Then
@@ -524,8 +513,7 @@ EndHint:
             Next
             '加载事件信息
             If If(JsonData("IsEvent"), False) Then
-                EventType = JsonData("EventType")
-                If EventType Is Nothing Then Throw New ArgumentException("未找到 EventType 项")
+                EventType = [Enum].Parse(GetType(CustomEvent.EventType), JsonData("EventType").ToString)
                 EventData = If(JsonData("EventData"), "")
                 IsEvent = True
             Else
@@ -550,7 +538,7 @@ EndHint:
         Public Function SetToListItem(Item As MyListItem) As MyListItem
             Dim Logo As String
             If IsEvent Then
-                If EventType = "弹出窗口" Then
+                If EventType = CustomEvent.EventType.弹出窗口 Then
                     Logo = PathImage & "Blocks/GrassPath.png"
                 Else
                     Logo = PathImage & "Blocks/CommandBlock.png"
@@ -567,9 +555,9 @@ EndHint:
                 .Height = 42
                 .Type = MyListItem.CheckType.Clickable
                 .Tag = Me
-                .EventType = Nothing
-                .EventData = Nothing
             End With
+            CustomEventService.SetEventType(Item, CustomEvent.EventType.None) '清空自定义事件属性,它们会被下面的点击事件处理
+            CustomEventService.SetEventData(Item, Nothing)
             '项目的点击事件
             AddHandler Item.Click, Sub(sender, e) PageOtherHelp.OnItemClick(sender.Tag)
             Return Item
@@ -669,15 +657,6 @@ NextFile:
             Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), LogLevel.Debug)
         End If
     End Sub
-    ''' <summary>
-    ''' 对帮助文件约定的替换标记进行处理,如果遇到需要转义的字符会进行转义。
-    ''' </summary>
-    Public Function HelpArgumentReplace(Xaml As String) As String
-        Dim Result = Xaml.Replace("{path}", EscapeXML(Path))
-        Result = Result.RegexReplaceEach("\{hint\}", Function() EscapeXML(PageOtherTest.GetRandomHint()))
-        Result = Result.RegexReplaceEach("\{cave\}", Function() EscapeXML(PageOtherTest.GetRandomCave()))
-        Return Result
-    End Function
 
 #End Region
 
@@ -836,6 +815,78 @@ NextFile:
         End Using
     End Sub
 
+    ''' <summary>
+    ''' 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
+    ''' </summary>
+    Public Function ArgumentReplace(Text As String, Optional EscapeHandler As Func(Of String, String) = Nothing, Optional ReplaceTime As Boolean = True) As String
+        '预处理
+        If Text Is Nothing Then Return Nothing
+        Dim Replacer =
+        Function(s As String) As String
+            If s Is Nothing Then Return ""
+            If EscapeHandler Is Nothing Then Return s
+            If s.Contains(":\") Then s = ShortenPath(s)
+            Return EscapeHandler(s)
+        End Function
+        '基础
+        Text = Text.Replace("{pcl_version}", Replacer(VersionBaseName))
+        Text = Text.Replace("{pcl_version_code}", Replacer(VersionCode))
+        Text = Text.Replace("{pcl_version_branch}", Replacer(VersionBranchName))
+        Text = Text.Replace("{pcl_branch}", Replacer(VersionBranchMain))
+        Text = Text.Replace("{identify}", Replacer(Identify))
+        Text = Text.Replace("{path}", Replacer(Path))
+        Text = Text.Replace("{path_with_name}", Replacer(PathWithName))
+        Text = Text.Replace("{path_temp}", Replacer(PathTemp))
+        '时间
+        If ReplaceTime Then '在窗口标题中,时间会被后续动态替换,所以此时不应该替换
+            Text = Text.Replace("{date}", Replacer(Date.Now.ToString("yyyy/M/d")))
+            Text = Text.Replace("{time}", Replacer(Date.Now.ToString("HH:mm:ss")))
+        End If
+        'Minecraft
+        Text = Text.Replace("{java}", Replacer(McLaunchJavaSelected?.PathFolder))
+        Text = Text.Replace("{minecraft}", Replacer(PathMcFolder))
+        If McVersionCurrent IsNot Nothing Then
+            Text = Text.Replace("{version_path}", Replacer(McVersionCurrent.Path)) : Text = Text.Replace("{verpath}", Replacer(McVersionCurrent.Path))
+            Text = Text.Replace("{version_indie}", Replacer(McVersionCurrent.PathIndie)) : Text = Text.Replace("{verindie}", Replacer(McVersionCurrent.PathIndie))
+            Text = Text.Replace("{name}", Replacer(McVersionCurrent.Name))
+            If {"unknown", "old", "pending"}.Contains(McVersionCurrent.Version.McName.ToLower) Then
+                Text = Text.Replace("{version}", Replacer(McVersionCurrent.Name))
+            Else
+                Text = Text.Replace("{version}", Replacer(McVersionCurrent.Version.McName))
+            End If
+        Else
+            Text = Text.Replace("{version_path}", Replacer(Nothing)) : Text = Text.Replace("{verpath}", Replacer(Nothing))
+            Text = Text.Replace("{version_indie}", Replacer(Nothing)) : Text = Text.Replace("{verindie}", Replacer(Nothing))
+            Text = Text.Replace("{name}", Replacer(Nothing))
+            Text = Text.Replace("{version}", Replacer(Nothing))
+        End If
+        '登录信息
+        If McLoginLoader.State = LoadState.Finished Then
+            Text = Text.Replace("{user}", Replacer(McLoginLoader.Output.Name))
+            Text = Text.Replace("{uuid}", Replacer(McLoginLoader.Output.Uuid.ToLower))
+            Select Case McLoginLoader.Input.Type
+                Case McLoginType.Legacy
+                    Text = Text.Replace("{login}", Replacer("离线"))
+                Case McLoginType.Ms
+                    Text = Text.Replace("{login}", Replacer("正版"))
+                Case McLoginType.Nide
+                    Text = Text.Replace("{login}", Replacer("统一通行证"))
+                Case McLoginType.Auth
+                    Text = Text.Replace("{login}", Replacer("Authlib-Injector"))
+            End Select
+        Else
+            Text = Text.Replace("{user}", Replacer(Nothing))
+            Text = Text.Replace("{uuid}", Replacer(Nothing))
+            Text = Text.Replace("{login}", Replacer(Nothing))
+        End If
+        '高级
+        Text = Text.RegexReplaceEach("\{hint\}", Function() Replacer(PageOtherTest.GetRandomHint()))
+        Text = Text.RegexReplaceEach("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
+        Text = Text.RegexReplaceEach("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Setup.GetSafe(m.Groups(1).Value, McVersionCurrent)))
+        Text = Text.RegexReplaceEach("\{varible:([^\}]+)\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, Nothing)))
+        Return Text
+    End Function
+
 #End Region
 
 #Region "任务缓存"

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

@@ -111,7 +111,7 @@
     ''' </summary>
     Public Sub MusicControlPause()
         If MusicNAudio Is Nothing Then
-            Hint("音乐播放尚未开始!", HintType.Critical)
+            Hint("音乐播放尚未开始!", HintType.Red)
         Else
             Select Case MusicState
                 Case MusicStates.Pause
@@ -131,14 +131,14 @@
     Public Sub MusicControlNext()
         If MusicAllList.Count = 1 Then
             MusicStartPlay(MusicCurrent)
-            Hint("重新播放:" & GetFileNameFromPath(MusicCurrent), HintType.Finish)
+            Hint("重新播放:" & GetFileNameFromPath(MusicCurrent), HintType.Green)
         Else
             Dim Address As String = DequeueNextMusicAddress()
             If Address Is Nothing Then
-                Hint("没有可以播放的音乐!", HintType.Critical)
+                Hint("没有可以播放的音乐!", HintType.Red)
             Else
                 MusicStartPlay(Address)
-                Hint("正在播放:" & GetFileNameFromPath(Address), HintType.Finish)
+                Hint("正在播放:" & GetFileNameFromPath(Address), HintType.Green)
             End If
         End If
         MusicRefreshUI()
@@ -182,19 +182,19 @@
             MusicListInit(True)
             If Not MusicAllList.Any() Then
                 If MusicNAudio Is Nothing Then
-                    If ShowHint Then Hint("未检测到可用的背景音乐!", HintType.Critical)
+                    If ShowHint Then Hint("未检测到可用的背景音乐!", HintType.Red)
                 Else
                     MusicNAudio = Nothing
-                    If ShowHint Then Hint("背景音乐已清除!", HintType.Finish)
+                    If ShowHint Then Hint("背景音乐已清除!", HintType.Green)
                 End If
             Else
                 Dim Address As String = DequeueNextMusicAddress()
                 If Address Is Nothing Then
-                    If ShowHint Then Hint("没有可以播放的音乐!", HintType.Critical)
+                    If ShowHint Then Hint("没有可以播放的音乐!", HintType.Red)
                 Else
                     Try
                         MusicStartPlay(Address, IsFirstLoad)
-                        If ShowHint Then Hint("背景音乐已刷新:" & GetFileNameFromPath(Address), HintType.Finish, False)
+                        If ShowHint Then Hint("背景音乐已刷新:" & GetFileNameFromPath(Address), HintType.Green, False)
                     Catch
                     End Try
                 End If
@@ -303,18 +303,18 @@
         Catch ex As Exception
             Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", LogLevel.Developer)
             If TypeOf ex Is NAudio.MmException AndAlso ex.Message.Contains("AlreadyAllocated") Then
-                Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Critical)
+                Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Red)
                 Thread.Sleep(1000000000)
             End If
             If TypeOf ex Is NAudio.MmException AndAlso (ex.Message.Contains("NoDriver") OrElse ex.Message.Contains("BadDeviceId")) Then
-                Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Critical)
+                Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Red)
                 Thread.Sleep(1000000000)
             End If
             If ex.Message.Contains("Got a frame at sample rate") OrElse ex.Message.Contains("does not support changes to") Then
-                Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 不支持播放音频属性在中途发生变化的音乐", HintType.Critical)
+                Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 不支持播放音频属性在中途发生变化的音乐", HintType.Red)
             ElseIf Not (MusicCurrent.EndsWithF(".wav", True) OrElse MusicCurrent.EndsWithF(".mp3", True) OrElse MusicCurrent.EndsWithF(".flac", True)) OrElse
                 ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
-                Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Critical)
+                Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
             Else
                 Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", LogLevel.Hint)
             End If

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

@@ -6,6 +6,8 @@ Friend Module ModSecret
 
 #Region "杂项"
 
+    '标注 PCL 的不同分支,仅用于替换标记
+    Public Const VersionBranchMain As String = "OpenSource"
     '在开源版的注册表与常规版的注册表隔离,以防数据冲突
     Public Const RegFolder As String = "PCLDebug"
     '用于微软登录的 ClientId
@@ -56,13 +58,13 @@ Friend Module ModSecret
     ''' <summary>
     ''' 获取设备标识码。
     ''' </summary>
-    Friend Function SecretGetUniqueAddress() As String
+    Friend Function SecretGetIdentify() As String
         Return "0000-0000-0000-0000"
     End Function
 
     Friend Sub SecretLaunchJvmArgs(ByRef DataList As List(Of String))
         Dim DataJvmCustom As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
-        DataList.Insert(0, If(DataJvmCustom = "", Setup.Get("LaunchAdvanceJvm"), DataJvmCustom)) '可变 JVM 参数
+        DataList.Insert(0, ArgumentReplace(If(DataJvmCustom = "", Setup.Get("LaunchAdvanceJvm"), DataJvmCustom))) '可变 JVM 参数
         McLaunchLog("当前剩余内存:" & Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024 * 10) / 10 & "G")
         DataList.Add("-Xmn" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent) * 1024 * 0.15) & "m")
         DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent) * 1024) & "m")

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

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

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

@@ -290,9 +290,9 @@
             Sub(MyLoader)
                 Select Case MyLoader.State
                     Case LoadState.Failed
-                        Hint(MyLoader.Name & "失败:" & MyLoader.Error.GetBrief(), HintType.Critical)
+                        Hint(MyLoader.Name & "失败:" & MyLoader.Error.GetBrief(), HintType.Red)
                     Case LoadState.Aborted
-                        Hint(MyLoader.Name & "已取消!", HintType.Info)
+                        Hint(MyLoader.Name & "已取消!", HintType.Blue)
                     Case LoadState.Loading
                         Return '不重新加载版本列表
                 End Select
@@ -326,7 +326,7 @@
                 Dim DefaultFolder As String = Nothing
                 If File.Type <> CompType.ModPack Then
                     Dim SubFolder As String = Nothing
-                    Select Case Project.Type
+                    Select Case File.Type
                         Case CompType.Mod : SubFolder = "mods\"
                         Case CompType.ResourcePack : SubFolder = "resourcepacks\"
                         Case CompType.Shader : SubFolder = "shaderpacks\"
@@ -346,7 +346,7 @@
                     Function(Version)
                         If Not Version.IsLoaded Then Version.Load()
                         '只对 Mod 和数据包进行版本检测
-                        If Project.Type = CompType.Mod OrElse Project.Type = CompType.DataPack Then
+                        If File.Type = CompType.Mod OrElse File.Type = CompType.DataPack Then
                             If File.GameVersions.Any(Function(v) v.Contains(".")) AndAlso
                                Not File.GameVersions.Any(Function(v) v.Contains(".") AndAlso v = Version.Version.McName) Then Return False
                         End If
@@ -359,8 +359,8 @@
                         Return False
                     End Function
                     '获取常规资源默认下载位置
-                    If CachedFolder.ContainsKey(Project.Type) Then
-                        DefaultFolder = CachedFolder(Project.Type)
+                    If CachedFolder.ContainsKey(File.Type) Then
+                        DefaultFolder = CachedFolder(File.Type)
                         Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}")
                     ElseIf McVersionCurrent IsNot Nothing AndAlso IsVersionSuitable(McVersionCurrent) Then
                         DefaultFolder = $"{McVersionCurrent.PathIndie}{SubFolder}"
@@ -425,7 +425,7 @@
                     If Not Target.Contains("\") Then Return
                     '构造步骤加载器
                     Dim LoaderName As String = Desc & "下载:" & IO.Path.GetFileNameWithoutExtension(Target) & " "
-                    If Target <> DefaultFolder Then CachedFolder(Project.Type) = GetPathFromFullPath(Target)
+                    If Target <> DefaultFolder Then CachedFolder(File.Type) = GetPathFromFullPath(Target)
                     Dim Loaders As New List(Of LoaderBase)
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})
                     '启动

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

@@ -32,7 +32,7 @@ Public Module ModDownloadLib
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"Minecraft {Id} 下载" Then Continue For
                 If Behaviour = NetPreDownloadBehaviour.ExitWhileExistsOrDownloading Then Return OngoingLoader
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return OngoingLoader
             Next
 
@@ -223,7 +223,7 @@ Public Module ModDownloadLib
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"Minecraft {Id} 服务端下载" Then Continue For
-                Hint("该服务端正在下载中!", HintType.Critical)
+                Hint("该服务端正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -241,7 +241,7 @@ Public Module ModDownloadLib
                         File.Delete(VersionFolder & Id & ".json")
                         If Not New DirectoryInfo(VersionFolder).GetFileSystemInfos.Any() Then Directory.Delete(VersionFolder)
                         Task.Output = New List(Of NetFile)
-                        Hint($"Mojang 没有给 Minecraft {Id} 提供官方服务端下载,没法下,撤退!", HintType.Critical)
+                        Hint($"Mojang 没有给 Minecraft {Id} 提供官方服务端下载,没法下,撤退!", HintType.Red)
                         Thread.Sleep(2000) '等玩家把上一个提示看完
                         Task.Abort()
                         Return
@@ -300,7 +300,7 @@ pause"
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"Minecraft {Id} 下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -401,7 +401,7 @@ pause"
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"OptiFine {DownloadInfo.NameDisplay} 下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -832,7 +832,7 @@ Retry:
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"LiteLoader {Id} 下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -866,7 +866,7 @@ Retry:
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"LiteLoader {Id} 下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -1048,7 +1048,7 @@ Retry:
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"{DisplayName} 下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -1803,7 +1803,7 @@ Retry:
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar.ToList()
                 If OngoingLoader.Name <> $"Fabric {Version} 安装器下载" Then Continue For
-                Hint("该版本正在下载中!", HintType.Critical)
+                Hint("该版本正在下载中!", HintType.Red)
                 Return
             Next
 
@@ -1986,28 +1986,28 @@ Retry:
     ''' 在加载器状态改变后显示一条提示。
     ''' 不会进行任何其他操作。
     ''' </summary>
-    Public Sub LoaderStateChangedHintOnly(Loader)
+    Public Sub LoaderStateChangedHintOnly(Loader As LoaderBase)
         Select Case Loader.State
             Case LoadState.Finished
-                Hint(Loader.Name & "成功!", HintType.Finish)
+                Hint(Loader.Name & "成功!", HintType.Green)
             Case LoadState.Failed
-                Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Critical)
+                Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Red)
             Case LoadState.Aborted
-                Hint(Loader.Name & "已取消!", HintType.Info)
+                Hint(Loader.Name & "已取消!", HintType.Blue)
         End Select
     End Sub
     ''' <summary>
     ''' 安装加载器状态改变后进行提示和重载文件夹列表的方法。
     ''' </summary>
-    Public Sub McInstallState(Loader)
+    Public Sub McInstallState(Loader As LoaderBase)
         Select Case Loader.State
             Case LoadState.Finished
                 WriteIni(PathMcFolder & "PCL.ini", "VersionCache", "") '清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存)
-                Hint(Loader.Name & "成功!", HintType.Finish)
+                Hint(Loader.Name & "成功!", HintType.Green)
             Case LoadState.Failed
-                Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Critical)
+                Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Red)
             Case LoadState.Aborted
-                Hint(Loader.Name & "已取消!", HintType.Info)
+                Hint(Loader.Name & "已取消!", HintType.Blue)
             Case LoadState.Loading
                 Return '不重新加载版本列表
         End Select
@@ -2108,7 +2108,7 @@ Retry:
 
         '重复版本检查
         If File.Exists($"{VersionFolder}{Request.TargetVersionName}.json") Then
-            Hint("版本 " & Request.TargetVersionName & " 已经存在!", HintType.Critical)
+            Hint("版本 " & Request.TargetVersionName & " 已经存在!", HintType.Red)
             Throw New CancelledException
         End If
 

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

@@ -1,8 +1,8 @@
 <Grid x:Class="MyMsgLogin"
-             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:my="clr-namespace:PCL"
-             RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
+      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+      xmlns:local="clr-namespace:PCL"
+      RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
     <Grid.RenderTransform>
         <TransformGroup>
             <RotateTransform x:Name="TransformRotate" Angle="-4" />
@@ -23,17 +23,17 @@
             </Grid.RowDefinitions>
             <TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}" HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" Text="测试标题文本" VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" />
             <Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
-            <my:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17" Padding="7,0,15,0"
+            <local:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17" Padding="7,0,15,0"
                                VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" DeltaMult="0.7">
                 <TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabCaption" FontSize="15" LineHeight="18"
                            Foreground="#FF5C5C5C" FontWeight="Normal" Padding="1" />
-            </my:MyScrollViewer>
+            </local:MyScrollViewer>
             <StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="150,0,8,0" Orientation="Horizontal">
-                <my:MyButton ColorType="Normal" x:FieldModifier="public" Text="重新打开网页" x:Name="Btn1" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
-                             EventType="打开网页" />
-                <my:MyButton ColorType="Normal" x:FieldModifier="public" Text="复制代码" x:Name="Btn2" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
-                             EventType="复制文本" />
-                <my:MyButton ColorType="Normal" x:FieldModifier="public" Text="取消" x:Name="Btn3" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
+                <local:MyButton ColorType="Normal" x:FieldModifier="public" Text="重新打开网页" x:Name="Btn1" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
+                             local:CustomEventService.EventType="打开网页" />
+                <local:MyButton ColorType="Normal" x:FieldModifier="public" Text="复制代码" x:Name="Btn2" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
+                             local:CustomEventService.EventType="复制文本" />
+                <local:MyButton ColorType="Normal" x:FieldModifier="public" Text="取消" x:Name="Btn3" Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False" />
             </StackPanel>
         </Grid>
     </Border>

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

@@ -89,14 +89,14 @@
             $"登录网页将自动开启,请在网页中输入 {UserCode}(已自动复制)。" & vbCrLf & vbCrLf &
             $"如果网络环境不佳,网页可能一直加载不出来,届时请使用加速器或 VPN 改善网络环境。" & vbCrLf &
             $"你也可以用其他设备打开 {Website} 并输入上述代码。"
-        Btn1.EventData = Website
-        Btn2.EventData = UserCode
+        CustomEventService.SetEventData(Btn1, Website)
+        CustomEventService.SetEventData(Btn2, UserCode)
         '启动工作线程
         RunInNewThread(AddressOf WorkThread, "MyMsgLogin")
     End Sub
 
     Private Sub WorkThread()
-        Thread.Sleep(3000)
+        Thread.Sleep(2000)
         If MyConverter.IsExited Then Return
         OpenWebsite(Website)
         ClipboardSet(UserCode)
@@ -118,7 +118,7 @@
                 '获取结果
                 Dim ResultJson As JObject = GetJson(Result)
                 McLaunchLog($"令牌过期时间:{ResultJson("expires_in")} 秒")
-                Hint("网页登录成功!", HintType.Finish)
+                Hint("网页登录成功!", HintType.Green)
                 Finished({ResultJson("access_token").ToString, ResultJson("refresh_token").ToString})
                 Return
             Catch ex As Exception

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

@@ -47,7 +47,7 @@
     Public Shared Sub Save(Loader As LoaderTask(Of EqualableList(Of String), String))
         Dim Address = Loader.Output
         If Not Loader.State = LoadState.Finished Then
-            Hint("皮肤正在获取中,请稍候!", HintType.Critical)
+            Hint("皮肤正在获取中,请稍候!", HintType.Red)
             If Not Loader.State = LoadState.Loading Then Loader.Start()
             Return
         End If
@@ -61,7 +61,7 @@
                 Else
                     CopyFile(Address, FileAddress)
                 End If
-                Hint("皮肤保存成功!", HintType.Finish)
+                Hint("皮肤保存成功!", HintType.Green)
             End If
         Catch ex As Exception
             Log(ex, "保存皮肤失败", LogLevel.Hint)
@@ -143,7 +143,7 @@
         Next
         If FrmLaunchLeft IsNot Nothing AndAlso HasLoaderRunning Then
             '由于 Abort 不是实时的,暂时不会释放文件,会导致删除报错,故只能取消执行
-            Hint("有正在获取中的皮肤,请稍后再试!", HintType.Info)
+            Hint("有正在获取中的皮肤,请稍后再试!", HintType.Blue)
         Else
             RunInThread(
             Sub()
@@ -161,7 +161,7 @@
                     For Each SkinLoader In If(sender IsNot Nothing, {sender}, {PageLaunchLeft.SkinLegacy, PageLaunchLeft.SkinMs})
                         SkinLoader.WaitForExit(IsForceRestart:=True)
                     Next
-                    Hint("已刷新头像!", HintType.Finish)
+                    Hint("已刷新头像!", HintType.Green)
                 Catch ex As Exception
                     Log(ex, "刷新皮肤缓存失败", LogLevel.Msgbox)
                 End Try
@@ -184,7 +184,7 @@
                     SkinLoader.WaitForExit(IsForceRestart:=True)
                 Next
                 '完成提示
-                Hint("更改皮肤成功!", HintType.Finish)
+                Hint("更改皮肤成功!", HintType.Green)
             Catch ex As Exception
                 Log(ex, "更改正版皮肤后刷新皮肤失败", LogLevel.Feedback)
             End Try
@@ -212,7 +212,7 @@
             Return
         End If
         If McLoginMsLoader.State = LoadState.Failed Then
-            Hint("登录失败,无法更改披风!", HintType.Critical)
+            Hint("登录失败,无法更改披风!", HintType.Red)
             Return
         End If
         Hint("正在获取披风列表,请稍候……")
@@ -225,7 +225,7 @@ Retry:
                 '获取登录信息
                 If McLoginMsLoader.State <> LoadState.Finished Then McLoginMsLoader.WaitForExit(PageLoginMsSkin.GetLoginData())
                 If McLoginMsLoader.State <> LoadState.Finished Then
-                    Hint("登录失败,无法更改披风!", HintType.Critical)
+                    Hint("登录失败,无法更改披风!", HintType.Red)
                     Return
                 End If
                 Dim AccessToken As String = McLoginMsLoader.Output.AccessToken
@@ -265,10 +265,10 @@ Retry:
                     ContentType:="application/json",
                     Headers:={{"Authorization", "Bearer " & AccessToken}})
                 If Result.Contains("""errorMessage""") Then
-                    Hint("更改披风失败:" & GetJson(Result)("errorMessage").ToString, HintType.Critical)
+                    Hint("更改披风失败:" & GetJson(Result)("errorMessage").ToString, HintType.Red)
                     Return
                 Else
-                    Hint("更改披风成功!", HintType.Finish)
+                    Hint("更改披风成功!", HintType.Green)
                 End If
             Catch ex As Exception
                 Log(ex, "更改披风失败", LogLevel.Hint)

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

@@ -134,6 +134,8 @@ Public Class PageLaunchRight
     Private Sub OnlineLoaderSub(Task As LoaderTask(Of String, Integer))
         Dim Address As String = Task.Input '#3721 中连续触发两次导致内容变化
         Try
+            '替换自定义变量与设置
+            Address = ArgumentReplace(Address, AddressOf WebUtility.HtmlEncode)
             '获取版本校验地址
             Dim VersionAddress As String
             If Address.Contains(".xaml") Then
@@ -146,29 +148,27 @@ Public Class PageLaunchRight
             End If
             '校验版本
             Dim Version As String = ""
-            Dim NeedDownload As Boolean = True
             Try
                 Version = NetRequestByClientRetry(VersionAddress)
                 If Version.Length > 1000 Then Throw New Exception($"获取的主页版本过长({Version.Length} 字符)")
                 Dim CurrentVersion As String = Setup.Get("CacheSavedPageVersion")
                 If Version <> "" AndAlso CurrentVersion <> "" AndAlso Version = CurrentVersion Then
                     Log($"[Page] 当前缓存的主页已为最新,当前版本:{Version},检查源:{VersionAddress}")
-                    NeedDownload = False
-                Else
-                    Log($"[Page] 需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}")
+                    Return
                 End If
+                Log($"[Page] 需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}")
             Catch exx As Exception
                 Log(exx, $"联网获取主页版本失败", LogLevel.Developer)
                 Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{VersionAddress}")
             End Try
             '实际下载
-            If Not NeedDownload Then Return
             Dim FileContent As String = NetRequestByClientRetry(Address)
             Log($"[Page] 已联网下载主页,内容长度:{FileContent.Length},来源:{Address}")
             Setup.Set("CacheSavedPageUrl", Address)
             Setup.Set("CacheSavedPageVersion", Version)
             WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
-            Refresh() '要求刷新
+            '若内容变更则要求刷新
+            If LoadedContentHash <> FileContent.GetHashCode() Then Refresh()
         Catch ex As Exception
             Log(ex, $"下载主页失败({Address})", If(ModeDebug, LogLevel.Msgbox, LogLevel.Hint))
         End Try
@@ -220,7 +220,7 @@ Public Class PageLaunchRight
                 End If
                 Dim LoadStartTime As Date = Date.Now
                 '修改时应同时修改 PageOtherHelpDetail.Init
-                Content = HelpArgumentReplace(Content)
+                Content = ArgumentReplace(Content, AddressOf EscapeXML)
                 Do While Content.Contains("xmlns")
                     Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
                 Loop

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

@@ -50,7 +50,7 @@
             If CType(McLoginLoader.Input, McLoginServer).ForceReselectProfile Then
                 Hint("正在尝试更换,请稍候!")
             Else
-                Hint("正在登录中,请稍后再更换角色!", HintType.Critical)
+                Hint("正在登录中,请稍后再更换角色!", HintType.Red)
             End If
             Return
         End If

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

@@ -68,7 +68,7 @@ Public Class PageLoginLegacy
     End Sub
     Private Sub Skin_Click() Handles Skin.Click
         If (Setup.Get("UiHiddenPageSetup") OrElse Setup.Get("UiHiddenSetupLaunch")) AndAlso Not PageSetupUI.HiddenForceShow Then
-            Hint("启动设置已被禁用!", HintType.Critical)
+            Hint("启动设置已被禁用!", HintType.Red)
         Else
             FrmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupLaunch) '切换到皮肤设置页面
         End If

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

@@ -27,9 +27,9 @@
                 <ColumnDefinition Width="30" />
                 <ColumnDefinition Width="Auto" />
             </Grid.ColumnDefinitions>
-            <local:MyTextButton Text="»  购买正版" EventType="打开网页" EventData="https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"
+            <local:MyTextButton Text="»  购买正版" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"
                             Grid.Column="0" />
-            <local:MyTextButton Text="»  前往官网" EventType="打开网页" EventData="https://www.minecraft.net/zh-hans"
+            <local:MyTextButton Text="»  前往官网" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.minecraft.net/zh-hans"
                             Grid.Column="2" />
         </Grid>
     </Grid>

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

@@ -90,7 +90,7 @@
             Catch ex As Exception
                 If ex.Message = "$$" Then
                 ElseIf ex.Message.StartsWith("$") Then
-                    Hint(ex.Message.TrimStart("$"), HintType.Critical)
+                    Hint(ex.Message.TrimStart("$"), HintType.Red)
                 ElseIf TypeOf ex Is Security.Authentication.AuthenticationException AndAlso ex.Message.ContainsF("SSL/TLS") Then
                     Log(ex, "正版登录验证失败,请考虑在 [设置 → 其他] 中关闭 [在正版登录时验证 SSL 证书],然后再试。" & vbCrLf & vbCrLf & "原始错误信息:", LogLevel.Msgbox)
                 Else

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

@@ -77,7 +77,7 @@
             Return
         End If
         If McLoginLoader.State = LoadState.Failed Then
-            Hint("登录失败,无法更改皮肤!", HintType.Critical)
+            Hint("登录失败,无法更改皮肤!", HintType.Red)
             Return
         End If
         Dim SkinInfo As McSkinInfo = McSkinSelect()
@@ -106,7 +106,7 @@ Retry:
                     McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
                     GoTo Retry
                 ElseIf Result.Contains("""error""") Then
-                    Hint("更改皮肤失败:" & GetJson(Result)("error").ToString, HintType.Critical)
+                    Hint("更改皮肤失败:" & GetJson(Result)("error").ToString, HintType.Red)
                     Return
                 End If
                 '获取新皮肤地址
@@ -122,7 +122,7 @@ Retry:
                 Throw New Exception("未知错误(" & Result & ")")
             Catch ex As Exception
                 If TypeOf ex Is OperationCanceledException Then
-                    Hint("更改皮肤失败:连接皮肤服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Critical)
+                    Hint("更改皮肤失败:连接皮肤服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red)
                 Else
                     Log(ex, "更改皮肤失败", LogLevel.Hint)
                 End If

+ 0 - 19
Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml

@@ -8,29 +8,10 @@
         <StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
             <local:MyCard Title="关于" Margin="0,0,0,15">
                 <StackPanel Margin="20,40,18,18">
-                    <TextBlock TextWrapping="Wrap" Text="多人联机服务由速聚授权提供,PCL 仅为速聚提供多人联机服务入口,使用中遇到的任何问题由速聚负责处理。你在使用多人联机服务过程中所遇到的任何问题与纠纷(包括其付费业务)均与 PCL 无关,应与速聚协商解决。" 
-                           Margin="0,0,0,3" />
-                    <TextBlock TextWrapping="Wrap" Text="PCL 作者并不会从速聚的付费业务中获得任何分成、奖金等报酬,请不要瞎说。" 
-                           Margin="0,0,0,6" />
-                    <local:MyListItem Title="速聚官网" Info="打开速聚的官方网站"
-                              Type="Clickable" EventType="打开网页" EventData="https://www.the.bb/zh-CN.html"
-                              Logo="pack://application:,,,/images/Blocks/Grass.png" />
-                    <local:MyListItem Title="授权协议书" Info="查看速聚与 PCL 间的授权协议书"
-                              Type="Clickable" EventType="打开网页" EventData="https://shimo.im/docs/0l3NVXYzeXhb903R"
-                              Logo="pack://application:,,,/images/Blocks/Grass.png" />
-                    <local:MyListItem Title="用户协议与隐私政策" Info="查看速聚的用户协议与隐私政策文档"
-                              Type="Clickable" EventType="打开网页" EventData="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzkxMTMyODk3Mg==&amp;action=getalbum&amp;album_id=2585385685407514625&amp;scene=173&amp;from_msgid=2247483720&amp;from_itemidx=1&amp;count=3&amp;nolastread=1#wechat_redirect"
-                              Logo="pack://application:,,,/images/Blocks/Grass.png" />
                 </StackPanel>
             </local:MyCard>
             <local:MyCard Title="发送反馈" Margin="0,0,0,15">
                 <StackPanel Margin="20,40,18,18">
-                    <local:MyListItem Title="联机反馈与交流" Info="加入联机模块官方 QQ 群反馈联机异常,或询问联机功能的使用方式"
-                              Type="Clickable" Logo="pack://application:,,,/images/Blocks/RedstoneBlock.png"
-                              EventType="打开网页" EventData="https://shimo.im/docs/KrkEVXz1K0frowAJ#anchor-7qQG" />
-                    <local:MyListItem Title="违法违规举报" Info="投诉、举报联机使用者的各类违法违规行为"
-                              Type="Clickable" EventType="打开网页" EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW#anchor-7x6P"
-                              Logo="pack://application:,,,/images/Blocks/RedstoneBlock.png" />
                 </StackPanel>
             </local:MyCard>
         </StackPanel>

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

@@ -83,7 +83,7 @@
                                 <local:ValidateRegex Regex="^[^\u4e00-\u9fa5]{6,}$" />
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
-                        <local:MyButton Grid.Column="4" Grid.Row="4" Margin="0,0,15,0" Text="获取索引码" Padding="13,8" EventType="打开网页" EventData="https://shimo.im/docs/KrkEVXz1K0frowAJ" />
+                        <local:MyButton Grid.Column="4" Grid.Row="4" Margin="0,0,15,0" Text="获取索引码" Padding="13,8" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://shimo.im/docs/KrkEVXz1K0frowAJ" />
                         <local:MyButton x:Name="BtnCertDone" Grid.Column="5" Grid.Row="4" Text="确定" ColorType="Highlight" Padding="13,8" IsEnabled="False" />
                     </Grid>
                 </local:MyCard>
@@ -229,7 +229,6 @@
             </Grid>
         </Grid>
         <Grid x:Name="PanAlways" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="15" Opacity="0.5">
-            <local:MyTextButton Text="联机服务由 速聚 授权提供" EventType="打开网页" EventData="https://www.the.bb/zh-CN.html" />
         </Grid>
     </Grid>
 </local:MyPageRight>

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

@@ -44,7 +44,7 @@
             <local:MyLoading Text="正在加载联机模块" Margin="20,20,20,17" x:Name="Load" ShowProgress="True" HorizontalAlignment="Center" VerticalAlignment="Center" />
         </local:MyCard>
         <Grid x:Name="PanAlways" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="15" Opacity="0.5">
-            <local:MyTextButton Text="联机服务由 速聚 授权提供" EventType="打开网页" EventData="https://www.the.bb/zh-CN.html" />
+            <local:MyTextButton Text="联机服务由 速聚 授权提供" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.the.bb/zh-CN.html" />
         </Grid>
     </Grid>
 </local:MyPageRight>

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

@@ -121,26 +121,7 @@ Public Class PageLinkIoi
     ''' 获取当前的玩家名。
     ''' </summary>
     Public Shared Function GetPlayerName() As String
-        '自动生成玩家名
-        If AutogenPlayerName Is Nothing Then
-            If IsPlayerNameValid(McLoginName) Then
-                AutogenPlayerName = McLoginName()
-            Else
-                AutogenPlayerName = "玩家 " & CType(GetHash(If(UniqueAddress, "")) Mod 1048576, Integer).ToString("x5").ToUpper
-            End If
-        End If
-        '获取玩家自定义的名称
-        Dim CustomName As String = Setup.Get("LinkName").ToString.Trim()
-        If CustomName <> "" Then
-            If IsPlayerNameValid(CustomName) Then
-                Return CustomName.Trim
-            Else
-                Hint("你所设置的玩家名存在异常,已被重置!", HintType.Critical)
-                Setup.Set("LinkName", "")
-            End If
-        End If
-        '使用自动生成的玩家名
-        Return AutogenPlayerName
+        Return ""
     End Function
     Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
     ''' <summary>

+ 231 - 300
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -29,8 +29,10 @@
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                         </Image.Clip>
                     </Image>
-                    <local:MyButton Text="赞助 PCL !" Grid.Column="2" Grid.Row="0" Height="35" EventType="打开网页" EventData="https://afdian.com/a/LTCat" ColorType="Highlight" />
-                    <local:MyButton Text="查看源代码" Grid.Column="2" Grid.Row="1" Height="35" EventType="打开网页" EventData="https://github.com/Hex-Dragon/PCL2" />
+                    <local:MyButton Text="赞助 PCL !" Grid.Column="2" Grid.Row="0" Height="35" ColorType="Highlight"
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://afdian.com/a/LTCat" />
+                    <local:MyButton Text="查看源代码" Grid.Column="2" Grid.Row="1" Height="35" 
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Hex-Dragon/PCL2" />
                 </Grid>
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="特别鸣谢">
@@ -51,11 +53,11 @@
                         <ColumnDefinition Width="150" />
                     </Grid.ColumnDefinitions>
                     <local:MyListItem Title="bangbang93" Margin="-5,0,15,0" IsHitTestVisible="False" Info="提供 BMCLAPI 镜像源和 Forge 安装工具,详见 https://bmclapi.bangbang93.com" Grid.Row="0" Grid.Column="1" />
-                    <local:MyButton Text="赞助镜像源" Grid.Row="0" Grid.Column="2" Height="35" EventType="打开网页" EventData="https://afdian.com/a/bangbang93" />
+                    <local:MyButton Text="赞助镜像源" Grid.Row="0" Grid.Column="2" Height="35" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://afdian.com/a/bangbang93" />
                     <local:MyListItem Title="MC 百科" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了 Mod 名称的中文翻译和更多相关信息!" Grid.Row="1" Grid.Column="1" />
-                    <local:MyButton Text="打开百科" Grid.Row="1" Grid.Column="2" Height="35" EventType="打开网页" EventData="https://www.mcmod.cn" />
+                    <local:MyButton Text="打开百科" Grid.Row="1" Grid.Column="2" Height="35" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.mcmod.cn" />
                     <local:MyListItem Title="z0z0r4" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了 MCIM 社区资源镜像源和帮助库图床!" Grid.Row="2" Grid.Column="1" />
-                    <local:MyButton Text="MCIM 主页" Grid.Row="2" Grid.Column="2" Height="35" EventType="打开网页" EventData="https://github.com/mcmod-info-mirror" />
+                    <local:MyButton Text="MCIM 主页" Grid.Row="2" Grid.Column="2" Height="35" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/mcmod-info-mirror" />
                     <local:MyListItem Title="00ll00" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了 Java Launch Wrapper 和一些重要服务支持!" Grid.Row="3" Grid.Column="1" />
                     <local:MyListItem Title="Patrick" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="设计并制作了 PCL 图标,让我从做图标的水深火热中得到了解脱……" Grid.Row="4" Grid.Column="1" />
                     <local:MyListItem Title="Hao_Tian" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="在内测中找出了一大堆没人想得到的诡异 Bug,有非同寻常的 Bug 体质" Grid.Row="5" Grid.Column="1" />
@@ -105,288 +107,217 @@
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="赞助者">
                 <StackPanel Margin="25,40,10,20">
-                    <TextBlock HorizontalAlignment="Left" Text="以下是 2025 年 8 月的高级赞助者们!感谢各位对 PCL 和伟大的咕咕咕事业的支持!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
-                    <WrapPanel Margin="0,0,0,10">
                        <TextBlock Width="120" Margin="0,1.5" Text="粒子鹰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="暮色黄昏" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="未殃" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="咸鱼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Claud_9527" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Qqsnd" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="☆Kiana☆" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="晓枫-橘味C" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ferderica" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="过路人" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="寒溪" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小咪vv" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="容小狸" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="南宫流溪" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="懒包子" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="王家有政" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="??????" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="念念" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="启233" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="爱小黎" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="北笙T-T" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="listpmater" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="朝朝超级の好运" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="低调" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="老公" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="雍仕" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="AAboss" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="闲来无事" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="omgddcsl" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="轻舟" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="浮生Yui" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="YangFK" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="一曲勺" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="时白Herhilo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="寒冬落雨" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="柯基0v0" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="月兔" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="qkowo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="沐白" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="每雨天" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="AiNo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="木叶飞舞之处" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="wilbert_laogao" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="悠芸" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="黑白盗贼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="PotCatawa" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小王" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="花还没开" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="PixelFrontier像素边界" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="坊之岬" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="HuTu" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="爱言叶ray" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="lenrin" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="." />
-                        <TextBlock Width="120" Margin="0,1.5" Text="这是一头灯笼花" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="肖然星辰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="QMX" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="山外诗" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="刘" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="萧风溪水" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="风落" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="星星绒绒" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ciallo~(∠?ω" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="九十九" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Guuzen" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="极光" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="9331" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="钱程" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="杏坂和人" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="jinlijsbjaj" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="海豹" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="洛玉衡kale" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="柠乔" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="N0NiL" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="阿卡零" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="ADAA" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="岚瑬" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="爱料理的猫" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="01B8" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="MCDJ喜羊羊" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="hwch55" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="棉花糖十五" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鱼头" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="拉普兰德天下第一" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="KGIKUY" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="初洛" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="z .e.r.o" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="独孤真菜" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="阿囊" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="rrrwcsza" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="守卫军的杂鱼兵" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="念安" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="叶" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="rreorr" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="星辰大海" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="随便" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="烤着吃的鱼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="大概是" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="闹山麻雀" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="我是(⊙o⊙)啥?笔" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="北纬44" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="呼呼呼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="嘎嘎不咕咕" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="kronos" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="有个好梦,同志" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Amojun" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="bat-744" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="那谁" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="AYOSO" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="抽抽" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="柚子皮皮皮" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="白玉京" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="ALPSL" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="我爱喝兽奶" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="wwq557" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="爱莉希雅" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="上善若水" />
+                    <TextBlock HorizontalAlignment="Left" Text="以下是 2025 年 9 月的高级赞助者们!感谢各位对 PCL 和伟大的咕咕咕事业的支持!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
+                    <WrapPanel Margin="0,0,0,10">
                        <TextBlock Width="120" Margin="0,1.5" Text="lecoe" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="QIUQI" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Aquadtr" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="stb" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="mco" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="楠" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="砂糖蜂蜜锡红茶" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小宸老弟" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Le Play" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="烨神" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="Kamazi_Yuka5201" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="滚动的拉布拉多" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="王牟" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Lazarus" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="幻觉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="凌天" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="lIIIllIIllIIl" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Zsol" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="榴莲" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Night Voyager" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="JOHNJIANG" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="柒月" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="不温柔先森" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Freewind" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="纯萧子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="扇" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Amturester" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="maigm" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="JO23" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="JCNB" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="星尘" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="姜桐桐233" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="AkaneRyoba" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="南山必胜客" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="57" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="咸菜萝卜煲" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="shawnlong" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="DDNGSZX1" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="KaguraZaka" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="谶纛" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="666" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="betwangzai" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="卑微小林" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="fjvg" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="TH-x" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Q" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="清鸢" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="P·咕·D·咕咕·F" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="顾木" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LAZY" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="沙发上的沙发" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="有熊子来" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="月下誓约,予爱以心" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="超级白给马" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="fwlqpalzmd" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="安执妄dying" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="黄开心" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="于是" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="荆槐序" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="三千游魂" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="^ ^" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="zz" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="爱炫鱼的猫" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="玄月" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="BRfake" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Lucien_Liu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="煎bingo子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="对方正在输入..." />
+                        <TextBlock Width="120" Margin="0,1.5" Text="hakureireimu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="wwxa2019" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="TAIGA" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="墨玉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Jane" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雪下丨残影" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="未命名3742" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="烧冻鸡翅" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="嘟嘟可" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="利姆露" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ATH9" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="教主" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="落雪有声" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="苏子霄" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="一握砂" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="马春明" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="远力高" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="玉米片" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="sun次方" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="憨憨橙子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="染年" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="准江月" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="蓝叶" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ELUCIDATE" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="兔饼干哈哈" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Styx誓约" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="V" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="月芸柔" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="灯火鬼魂" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="A" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="项刀关志" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="剑鱼776" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="叶明心" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Ashlxy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小时" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="fisher426" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="老罗" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="百忧解" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="杨三海" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="锂化氩" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="饺子川" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="流年遥想" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="基围虾" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="山有木兮325" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="37号村民" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白承宣" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="某蓝毛借钱不还的贝斯手" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白狼" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="工蜂" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="封不觉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="南樵鱼子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="寂-御神幻狐" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="好" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="NeonSoda" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="UNICORN" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="红石beaker" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="EliasLSK" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="思考人生" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="L" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="起床了" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="服群" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="云符爱睡觉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="幽幽秋" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="FAMS" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="会变身的栖仔" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="baka王" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="深海带鱼" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="四戒梨子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="无限" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Xc_AcE" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="@6#" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="molFish" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="雪见" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="AAALLLAAANNN" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="华氏451" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="纸不好" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="博衍" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="志摩凛" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="八云蓝LIN" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LittleBlue" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="老梦" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="动漫男主" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Red_橙子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Junmucheng" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="无印" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="窗前露" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="1111111" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="什雨" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="七日 SEVENDAYS" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Gǔdù" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白吃白喝" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="路明非" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="子不语" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="王璟" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="阿星" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="小狗很担心你有没有好好喝水" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="hjhmeibao" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="网络白字话事人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="想要点好运" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Timmy1950" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="栩" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="玛卡巴卡" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="楞可" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="顶级牛板筋收藏家" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="HansOffice" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="466某某某" />
                         <TextBlock Width="120" Margin="0,1.5" Text="zztzzy" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="SHiao_Yui" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="一只花生" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="黑凤梨熊?" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="sdad" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="BLONG" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="787878" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="myry668" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="f三文鱼f" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="apqow" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="懒梦棉花糖" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="HaiMing" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Evernight" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CieloNeet" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="不青盆" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Purelimiter" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="丨白丨" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="喵" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="雨过山秋" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="急了" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="歌斐木" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="公子天邪" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="爱发癫" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="洪某某" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="漫卡洛樱" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="云与海" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="子曰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CyrilZ" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="笑颜如花兮" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="咸鱼超人FF酱!" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="达达利鸭" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="TFofN" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Kumo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="江俞白" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="聆风忘年" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="五符临门" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="亭书浅墨" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="翀归滉" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Anmuer" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="青天揽明月" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鲫鱼崽" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="PPTNPC" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="vIccvI" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="GOOD" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="归零之诗" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="ppp" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ethanhmm1234" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="库里斯库拉" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="被伪安全软件坑害的普通人" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="无元亓" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Raven" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="看看你的jjp" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="日月皆如水" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="gzwmmm" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="早上鸥" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="CJT" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="幽永森" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="东海帝皇" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="三岁" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="shui12dkhigl" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="豪猪" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="再相遇" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="?白樱Hakura" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="IssacPyromaniac" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="纯真丁一郎" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="岩之龙" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="星之金白" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="laocao" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="DLC...是什么" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="我家老米" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="TINY" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="只会coding的农民工" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="明月一行" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="nanako" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="辛霏Aiwx" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="奇妙的白给之旅" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="你咋不上天呢^_^" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="冰糕" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="sadas" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小叮当搞快点" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="leu" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="休伯利安本来的舰长" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="天诺一字号仓鼠" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="真爱粉只因龙" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="绝望的温度" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="busituteng" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="SiC" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="契特卡拉概念机" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="我还能再在死线挣扎一会" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="02_G1nTonic" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="渊" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="1" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小楠" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="阿冰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="EliasG" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="jhx" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="zyccmyk" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="luxin" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="我是我" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="13462738215" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="minecraft java" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="那是小林吖" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="GITATIG" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Musketeer" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="AuraSpeedJerry" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="luckymore" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="hcrmhj" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="我好焦灼啊" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小王同学" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="MoDust2131" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="啊超" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ander·Les" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Niko" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="五个麦卷" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="b0Ard" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="龙猫的专属粉丝" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="雾念~" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="烬灵梦" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="蝶落染花香" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="照井黎Terui_Rei" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="???狼??王?" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="辉光离子管" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="联开开" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="XIAOSHUAI" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="KMYLS" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="聽音" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="oPzPo" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="dream" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="123" />
                         <TextBlock Width="120" Margin="0,1.5" Text="Zik yeung" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Coco" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Ricardo" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="2969307002" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="鬼卿" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="wihz" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="九平" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="liu" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="PickStars" />
+                        <TextBlock Width="240" Margin="0,1.5" Text="我真的事仁济啊(恼)" />
                         <TextBlock Width="120" Margin="0,1.5" Text="李白" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="霜木翔凤" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="岽龙" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="joksqp" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="what are you 说啥嘞?" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="木得爱_没人疼" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="小黑子?" />
-                        <TextBlock Width="240" Margin="0,1.5" Text="删除线Strikethrough" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="REMOTE" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="H" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Parapha" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="南吕十三" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="米帝" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="zdxff" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="蔚空" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="cleverboy1066" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="烤老鼠" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="TS彦祖" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="狮某人" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="lmst_myh" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="耐·若何" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="qvq" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="刀客塔" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="mika" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="金尘" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="喜之狼与灰太羊" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="天才小宋" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="MdsR" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="诗和雨夜" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Samueldong" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="BBK" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="雪枫Sople" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="一坨灰" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="RI_CK" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="ximeng22" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="天天122_" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="李小浩&amp;mod" />
-                        <TextBlock Width="120" Margin="0,1.5" Text="Southsheng" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="HurgerLiPy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Crishen70400" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="孤独" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="夏末彡微凉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="山本樱桃不是df" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="陶牧馆长" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="一二三四" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="17382073935" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="不知道" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="重庆你周哥" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="熳ai" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Rainbow Stars" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="无名岩" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="快乐小子" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="沙西凯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="一只小孽QAQ" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="五度左右" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="咖喱鱼" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="千叶GG" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="泽木_Zemu" />
                     </WrapPanel>
                     <Grid Height="35">
                         <Grid.ColumnDefinitions>
@@ -395,7 +326,7 @@
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                         </Grid.ColumnDefinitions>
                         <local:MyButton x:Name="BtnDonateOutput" Grid.Column="0" MinWidth="140" Text="生成解锁码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" Visibility="Collapsed" />
-                        <local:MyButton x:Name="BtnDonateDonate" Grid.Column="0" MinWidth="140" Text="赞助作者" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" EventType="打开网页" EventData="https://afdian.com/a/LTCat" />
+                        <local:MyButton x:Name="BtnDonateDonate" Grid.Column="0" MinWidth="140" Text="赞助作者" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://afdian.com/a/LTCat" />
                         <local:MyButton x:Name="BtnDonateCopy" Grid.Column="1" MinWidth="140" Text="复制识别码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
                         <local:MyButton x:Name="BtnDonateInput" Grid.Column="2" MinWidth="140" Text="输入解锁码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
                     </Grid>
@@ -410,8 +341,8 @@
                     <TextBlock Text="其他信息" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
                     <TextBlock Margin="0,0,0,2" LineHeight="17" Text="Copyright © 龙腾猫跃 2016. All Rights Reserved.&#13;计算机软件著作权登记号:2020SR0875133&#13;违法违规行为举报投诉邮箱:[email protected]&#13;非 MINECRAFT 官方产品。未经 MOJANG 或 MICROSOFT 批准,也不与 MOJANG 或 MICROSOFT 关联。" TextWrapping="Wrap" />
                     <StackPanel Orientation="Horizontal" Height="35" HorizontalAlignment="Left" Margin="0,12,0,0">
-                        <local:MyButton Text="用户协议与免责声明" Width="170" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" EventType="打开网页" EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW" />
-                        <local:MyButton Text="开源代码" Width="140" Padding="13,0" EventType="打开网页" EventData="https://github.com/Hex-Dragon/PCL2" />
+                        <local:MyButton Text="用户协议与免责声明" Width="170" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW" />
+                        <local:MyButton Text="开源代码" Width="140" Padding="13,0" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Hex-Dragon/PCL2" />
                     </StackPanel>
                 </StackPanel>
             </local:MyCard>
@@ -443,49 +374,49 @@
                     <TextBlock Grid.Row="0" Grid.Column="0" Text="Java Launch Wrapper" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2022 00ll00&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="1" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/00ll00/java_launch_wrapper" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/00ll00/java_launch_wrapper" />
                     <local:MyButton Grid.Row="1" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/00ll00/java_launch_wrapper?tab=MIT-1-ov-file#readme" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/00ll00/java_launch_wrapper?tab=MIT-1-ov-file#readme" />
 
                     <TextBlock Grid.Row="2" Grid.Column="0" Text="Newtonsoft.Json" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2007 James Newton-King&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="3" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://www.newtonsoft.com/json" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.newtonsoft.com/json" />
                     <local:MyButton Grid.Row="3" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://opensource.org/licenses/MIT" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://opensource.org/licenses/MIT" />
                     
                     <TextBlock Grid.Row="4" Grid.Column="0" Text="NAudio" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2015 Mark Heath ([email protected])&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="5" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/naudio/NAudio" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio" />
                     <local:MyButton Grid.Row="5" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
-                                    EventType="打开网页" EventData="https://github.com/naudio/NAudio/blob/master/license.txt" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio/blob/master/license.txt" />
                     
                     <TextBlock Grid.Row="6" Grid.Column="0" Text="Ookii.Dialogs" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2009 Sven Groot (Ookii.org)" TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://www.ookii.org/Software/Dialogs/" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.ookii.org/Software/Dialogs/" />
                     <local:MyButton Grid.Row="7" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/caioproiete/ookii-dialogs-wpf/blob/master/LICENSE" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/caioproiete/ookii-dialogs-wpf/blob/master/LICENSE" />
                     
                     <TextBlock Grid.Row="8" Grid.Column="0" Text="Iconmonstr" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2019 iconmonstr (Alexander Kahlkopf)" TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="9" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://iconmonstr.com/" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://iconmonstr.com/" />
                     <local:MyButton Grid.Row="9" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://iconmonstr.com/license/" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://iconmonstr.com/license/" />
                     
                     <TextBlock Grid.Row="10" Grid.Column="0" Text="libwebp wrapper for .NET" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="10" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2012 Imazen&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="11" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/imazen/libwebp-net" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/imazen/libwebp-net" />
                     <local:MyButton Grid.Row="11" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://github.com/imazen/libwebp-net/blob/master/LICENSE.txt" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/imazen/libwebp-net/blob/master/LICENSE.txt" />
 
                     <TextBlock Grid.Row="12" Grid.Column="0" Text="Iconfont" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="12" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © ALIMAMA MUX, powered by alimama THX." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="13" Grid.Column="2" MinWidth="140" Text="查看原网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    EventType="打开网页" EventData="https://www.iconfont.cn/" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.iconfont.cn/" />
 
                 </Grid>
             </local:MyCard>

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

@@ -18,8 +18,8 @@
 
     End Sub
 
-    Public Shared Sub CopyUniqueAddress() Handles BtnDonateCopy.Click
-        ClipboardSet(UniqueAddress)
+    Public Shared Sub CopyIdentify() Handles BtnDonateCopy.Click
+        ClipboardSet(Identify)
     End Sub
     Private Sub BtnDonateCodeInput_Click() Handles BtnDonateInput.Click
         DonateCodeInput()

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

@@ -71,7 +71,7 @@
     Public Shared Sub OnItemClick(Entry As HelpEntry)
         Try
             If Entry.IsEvent Then
-                ModEvent.ProcessCustomEvents(Entry.EventType, Entry.EventData, Nothing)
+                CustomEvent.Raise(Entry.EventType, Entry.EventData)
             Else
                 EnterHelpPage(Entry)
             End If

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

@@ -18,8 +18,10 @@
         If Content = "" Then Throw New Exception("帮助 xaml 文件为空")
         Try
             '修改时应同时修改 PageLaunchRight.LoadContent
-            Content = HelpArgumentReplace(Content)
-            If Content.Contains("xmlns") Then Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
+            Content = ArgumentReplace(Content, AddressOf EscapeXML)
+            Do While Content.Contains("xmlns")
+                Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
+            Loop
             Content = "<StackPanel xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sys=""clr-namespace:System;assembly=mscorlib"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" xmlns:local=""clr-namespace:PCL;assembly=Plain Craft Launcher 2"">" & Content & "</StackPanel>"
             Me.Entry = Entry
             PanCustom.Children.Clear()

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

@@ -150,14 +150,14 @@
         Dim NewFolder As String = ""
         '检查是否有下载任务
         If HasDownloadingTask() Then
-            Hint("在下载任务进行时,无法添加游戏文件夹!", HintType.Critical)
+            Hint("在下载任务进行时,无法添加游戏文件夹!", HintType.Red)
             Return
         End If
         Try
             '获取输入
             NewFolder = SelectFolder()
             If NewFolder = "" Then Return
-            If NewFolder.Contains("!") OrElse NewFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Critical) : Return
+            If NewFolder.Contains("!") OrElse NewFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Red) : Return
             '要求输入显示名称
             Dim SplitedNames As String() = NewFolder.TrimEnd("\").Split("\")
             Dim DefaultName As String = If(SplitedNames.Last = ".minecraft", If(SplitedNames.Count >= 3, SplitedNames(SplitedNames.Count - 2), ""), SplitedNames.Last)
@@ -182,7 +182,7 @@
                 '检查文件夹权限
                 If Not CheckPermission(FolderPath) Then
                     If ShowHint Then
-                        Hint("添加文件夹失败:PCL 没有访问该文件夹的权限!", HintType.Critical)
+                        Hint("添加文件夹失败:PCL 没有访问该文件夹的权限!", HintType.Red)
                         Return
                     Else
                         Throw New Exception("PCL 没有访问文件夹的权限:" & FolderPath)
@@ -207,12 +207,12 @@
                     If Folder.Split(">")(1) = FolderPath Then
                         IsAdded = True
                         If Folder.Split(">")(0) = DisplayName Then
-                            If ShowHint Then Hint("此文件夹已在列表中!", HintType.Info)
+                            If ShowHint Then Hint("此文件夹已在列表中!", HintType.Blue)
                             Return
                         Else
                             Folders(i) = DisplayName & ">" & FolderPath
                             IsReplace = True
-                            If ShowHint Then Hint("文件夹名称已更新为 " & DisplayName & " !", HintType.Finish)
+                            If ShowHint Then Hint("文件夹名称已更新为 " & DisplayName & " !", HintType.Green)
                         End If
                         Exit For
                     End If
@@ -226,7 +226,7 @@
                 McFolderListLoader.Start(IsForceRestart:=True)
                 '提示
                 If IsReplace Then Return
-                If ShowHint Then Hint("文件夹 " & DisplayName & " 已添加!", HintType.Finish)
+                If ShowHint Then Hint("文件夹 " & DisplayName & " 已添加!", HintType.Green)
                 '检查是否为根目录整合包,自动关闭版本隔离
                 '1. 根目录中存在数个 Mod
                 Dim ModFolder As New DirectoryInfo(FolderPath & "mods\")
@@ -257,7 +257,7 @@
     Public Sub Create_Click()
         '检查是否有下载任务
         If HasDownloadingTask() Then
-            Hint("在下载任务进行时,无法创建游戏文件夹!", HintType.Critical)
+            Hint("在下载任务进行时,无法创建游戏文件夹!", HintType.Red)
             Return
         End If
         If Not Directory.Exists(Path & ".minecraft\") Then
@@ -265,7 +265,7 @@
             Directory.CreateDirectory(Path & ".minecraft\versions\")
             Setup.Set("LaunchFolderSelect", "$.minecraft\")
             McFolderLauncherProfilesJsonCreate(Path & ".minecraft\")
-            Hint("新建 .minecraft 文件夹成功!", HintType.Finish)
+            Hint("新建 .minecraft 文件夹成功!", HintType.Green)
         End If
         McFolderListLoader.Start(IsForceRestart:=True)
     End Sub
@@ -307,7 +307,7 @@
             Next
             '保存
             Setup.Set("LaunchFolders", If(Not Folders.Any(), "", Join(Folders.ToArray, "|")))
-            Hint(If(Folder.Type = McFolderType.Custom, "文件夹 " & Name & " 已从列表中移除!", "文件夹名称已复原!"), HintType.Finish)
+            Hint(If(Folder.Type = McFolderType.Custom, "文件夹 " & Name & " 已从列表中移除!", "文件夹名称已复原!"), HintType.Green)
             McFolderListLoader.Start(IsForceRestart:=True)
 
         Catch ex As Exception
@@ -336,10 +336,10 @@
         Sub()
             '删除文件夹
             Try
-                Hint("正在" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Info)
+                Hint("正在" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Blue)
                 DeleteDirectory(Folder.Path)
                 If DeleteText = "清空" Then Directory.CreateDirectory(Folder.Path)
-                Hint("已" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Finish)
+                Hint("已" & DeleteText & "文件夹 " & Folder.Name & "!", HintType.Green)
             Catch ex As Exception
                 Log(ex, DeleteText & "文件夹 " & Folder.Name & " 失败", LogLevel.Hint)
             Finally
@@ -389,7 +389,7 @@
             Next
             '如果没有添加过,则添加进去(因为修改了默认项的名称)
             If Not IsAdded Then Folders.Add(NewName & ">" & Folder.Path)
-            Hint("文件夹名称已更新为 " & NewName & " !", HintType.Finish)
+            Hint("文件夹名称已更新为 " & NewName & " !", HintType.Green)
             '保存
             Setup.Set("LaunchFolders", Join(Folders.ToArray, "|"))
             McFolderListLoader.Start(IsForceRestart:=True)
@@ -403,7 +403,7 @@
         If Not e.RaiseByMouse OrElse Not sender.Checked Then Return
         '检查是否有下载任务
         If HasDownloadingTask(True) Then
-            Hint("在下载任务进行时,无法切换游戏文件夹!", HintType.Critical)
+            Hint("在下载任务进行时,无法切换游戏文件夹!", HintType.Red)
             e.Handled = True
             Return
         End If

+ 3 - 18
Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb

@@ -114,22 +114,7 @@
             Log(ex, "将版本列表转换显示时失败", LogLevel.Feedback)
         End Try
     End Sub
-    Public Shared Function McVersionListItem(Version As McVersion) As MyListItem
-        Dim NewItem As New MyListItem With {.Title = Version.Name, .Info = Version.Info, .Height = 42, .Tag = Version, .SnapsToDevicePixels = True, .Type = MyListItem.CheckType.Clickable}
-        Try
-            If Version.Logo.EndsWith("PCL\Logo.png") Then
-                NewItem.Logo = Version.Path & "PCL\Logo.png" '修复老版本中,存储的自定义 Logo 使用完整路径,导致移动后无法加载的 Bug
-            Else
-                NewItem.Logo = Version.Logo
-            End If
-        Catch ex As Exception
-            Log(ex, "加载版本图标失败", LogLevel.Hint)
-            NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
-        End Try
-        NewItem.ContentHandler = AddressOf McVersionListContent
-        Return NewItem
-    End Function
-    Private Shared Sub McVersionListContent(sender As MyListItem, e As EventArgs)
+    Public Shared Sub McVersionListContent(sender As MyListItem, e As EventArgs)
         Dim Version As McVersion = sender.Tag
         '注册点击事件
         AddHandler sender.Click, AddressOf Item_Click
@@ -225,10 +210,10 @@
                     IniClearCache(Version.Path & "PCL\Setup.ini")
                     If IsShiftPressed Then
                         DeleteDirectory(Version.Path)
-                        Hint("版本 " & Version.Name & " 已永久删除!", HintType.Finish)
+                        Hint("版本 " & Version.Name & " 已永久删除!", HintType.Green)
                     Else
                         FileIO.FileSystem.DeleteDirectory(Version.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin)
-                        Hint("版本 " & Version.Name & " 已删除到回收站!", HintType.Finish)
+                        Hint("版本 " & Version.Name & " 已删除!", HintType.Green)
                     End If
                 Case 2
                     Return

+ 23 - 8
Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb

@@ -227,7 +227,7 @@
                 Me.Value = Value
                 Me.Source = Source
                 Me.Type = If(Value, New Object).GetType
-                Me.DefaultValueEncoded = If(Encoded, SecretEncrypt(Value, "PCL" & UniqueAddress), Value)
+                Me.DefaultValueEncoded = If(Encoded, SecretEncrypt(Value, "PCL" & Identify), Value)
             Catch ex As Exception
                 Log(ex, "初始化 SetupEntry 失败", LogLevel.Feedback) '#5095 的 fallback
             End Try
@@ -241,6 +241,14 @@
     Public Sub [Set](Key As String, Value As Object, Optional ForceReload As Boolean = False, Optional Version As McVersion = Nothing)
         [Set](Key, Value, SetupDict(Key), ForceReload, Version)
     End Sub
+    ''' <summary>
+    ''' 写入某个未经加密的设置项。
+    ''' 若该设置项经过了加密,则会抛出异常。
+    ''' </summary>
+    Public Sub SetSafe(Key As String, Value As Object, Optional ForceReload As Boolean = False, Optional Version As McVersion = Nothing)
+        If SetupDict(Key).Encoded Then Throw New InvalidOperationException("禁止写入加密设置项:" & Key)
+        [Set](Key, Version, ForceReload, Version)
+    End Sub
     Private Sub [Set](Key As String, Value As Object, E As SetupEntry, ForceReload As Boolean, Version As McVersion)
         Try
 
@@ -258,7 +266,7 @@
             If E.Encoded Then
                 Try
                     If Value Is Nothing Then Value = ""
-                    Value = SecretEncrypt(Value, "PCL" & UniqueAddress)
+                    Value = SecretEncrypt(Value, "PCL" & Identify)
                 Catch ex As Exception
                     Log(ex, "加密设置失败:" & Key, LogLevel.Developer)
                 End Try
@@ -303,9 +311,17 @@
     ''' 获取某个设置项的值。
     ''' </summary>
     Public Function [Get](Key As String, Optional Version As McVersion = Nothing)
-        If Not SetupDict.ContainsKey(Key) Then Throw New KeyNotFoundException("未找到设置项:" & Key) With {.Source = Key}
+        If Not SetupDict.ContainsKey(Key) Then Throw New KeyNotFoundException("未找到设置项:" & Key)
         Return [Get](Key, SetupDict(Key), Version)
     End Function
+    ''' <summary>
+    ''' 获取某个未经加密的设置项的值。
+    ''' 若该设置项经过了加密,则会抛出异常。
+    ''' </summary>
+    Public Function GetSafe(Key As String, Optional Version As McVersion = Nothing)
+        If SetupDict(Key).Encoded Then Throw New InvalidOperationException("禁止读取加密设置项:" & Key)
+        Return [Get](Key, Version)
+    End Function
     Private Function [Get](Key As String, E As SetupEntry, Version As McVersion)
         '获取强制值
         Dim Force As String = ForceValue(Key)
@@ -383,7 +399,7 @@
                     SourceValue = E.DefaultValue
                 Else
                     Try
-                        SourceValue = SecretDecrypt(SourceValue, "PCL" & UniqueAddress)
+                        SourceValue = SecretDecrypt(SourceValue, "PCL" & Identify)
                     Catch ex As Exception
                         Log(ex, "解密设置失败:" & Key, LogLevel.Developer)
                         SourceValue = E.DefaultValue
@@ -601,8 +617,7 @@
                 FrmSetupUI.HintCustom.Visibility = Visibility.Visible
                 FrmSetupUI.HintCustomWarn.Visibility = If(Setup.Get("HintCustomWarn"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupUI.HintCustom.Text = $"从 PCL 文件夹下的 Custom.xaml 读取主页内容。{vbCrLf}你可以手动编辑该文件,向主页添加文本、图片、常用网站、快捷启动等功能。"
-                FrmSetupUI.HintCustom.EventType = ""
-                FrmSetupUI.HintCustom.EventData = ""
+                CustomEventService.SetEventType(FrmSetupUI.HintCustom, CustomEvent.EventType.None)
             Case 2 '联网
                 FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed
                 FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed
@@ -610,8 +625,8 @@
                 FrmSetupUI.HintCustom.Visibility = Visibility.Visible
                 FrmSetupUI.HintCustomWarn.Visibility = If(Setup.Get("HintCustomWarn"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupUI.HintCustom.Text = $"从指定网址联网获取主页内容。服主也可以用于动态更新服务器公告。{vbCrLf}如果你制作了稳定运行的联网主页,可以点击这条提示投稿,若合格即可加入预设!"
-                FrmSetupUI.HintCustom.EventType = "打开网页"
-                FrmSetupUI.HintCustom.EventData = "https://github.com/Meloong-Git/PCL/discussions/2528"
+                CustomEventService.SetEventType(FrmSetupUI.HintCustom, CustomEvent.EventType.打开网页)
+                CustomEventService.SetEventData(FrmSetupUI.HintCustom, "https://github.com/Meloong-Git/PCL/discussions/2528")
             Case 3 '预设
                 FrmSetupUI.PanCustomPreset.Visibility = Visibility.Visible
                 FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed

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

@@ -41,9 +41,9 @@
                             <local:MyComboBoxItem Content="隔离所有版本" ToolTip="不同版本之间的存档、Mod、资源包等均不互通。&#xa;这会导致不同原版版本间的存档不能共用,所以一般不推荐。" />
                         </local:MyComboBox>
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏窗口标题" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则不进行更改。&#xa;支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号" HintText="默认" MaxLength="100" />
+                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则不进行更改。&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" HintText="默认" MaxLength="100" />
                         <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" Tag="LaunchArgumentInfo" Text="PCL" Grid.Column="1" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角">
+                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" Tag="LaunchArgumentInfo" Text="PCL" Grid.Column="1" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角。&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。">
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
@@ -240,13 +240,13 @@
                                         ToolTip="还原" ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
                                         Logo="M530 0c287 0 521 229 521 511s-233 511-521 511c-233 0-436-151-500-368a63 63 0 0 1 44-79 65 65 0 0 1 80 43c48 162 200 276 375 276 215 0 390-171 390-383s-174-383-390-383c-103 0-199 39-270 106l21-5a63 63 0 0 1 33 123l-157 42a65 65 0 0 1-90-42l-49-183a65 65 0 1 1 126-33l6 26A524 524 0 0 1 530 0z" LogoScale="0.9" />
                     </StackPanel>
-                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.ColumnSpan="2" Tag="LaunchAdvanceJvm" Grid.Column="1" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,除非有确定把握,否则请不要修改。&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。" MaxLength="4000"
+                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.ColumnSpan="2" Tag="LaunchAdvanceJvm" Grid.Column="1" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,除非有确定把握,否则请不要修改。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" MaxLength="4000"
                                  Height="100" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" VerticalScrollBarVisibility="Auto" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏参数" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。" />
+                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" />
                     <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动前执行命令" Margin="0,0,25,0" />
                     <local:MyTextBox x:Name="TextAdvanceRun" Grid.Row="4" Grid.ColumnSpan="2" Tag="LaunchAdvanceRun" Grid.Column="1" HintText=""
-                                 ToolTip="在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;可以使用以下替换标记实现相对路径(路径均以 \ 结尾):&#xa; · {path}:PCL 的 exe 文件所在的文件夹&#xa; · {minecraft}:.minecraft 文件夹&#xa; · {verpath}:版本文件夹(.minecraft\versions\版本名\)&#xa; · {verindie}:开启版本隔离时等同版本文件夹,未开启时等同 .minecraft 文件夹&#xa; · {java}:游戏运行时的 Java 文件夹&#xa;&#xa;除此之外,也支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {uuid}:玩家的 UUID&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号&#xa;&#xa;例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
+                                 ToolTip="在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;你可以用替换标记来动态改变其内容或实现相对路径,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
                     <local:MyCheckBox Grid.Row="5" Grid.Column="1" x:Name="CheckAdvanceRunWait" Tag="LaunchAdvanceRunWait" Visibility="Collapsed"
                                   Text="等待命令执行完成后再继续启动" Margin="0,8,0,7" />
                     <StackPanel Margin="0,12,0,4" Grid.Row="6" Grid.ColumnSpan="2" HorizontalAlignment="Left">

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

@@ -93,7 +93,7 @@
             JavaSearchLoader.Start(IsForceRestart:=True)
 
             Log("[Setup] 已初始化启动设置")
-            Hint("已初始化启动设置!", HintType.Finish, False)
+            Hint("已初始化启动设置!", HintType.Green, False)
         Catch ex As Exception
             Log(ex, "初始化启动设置失败", LogLevel.Msgbox)
         End Try
@@ -170,7 +170,7 @@
         Try
             File.Delete(PathAppdata & "CustomSkin.png")
             RadioSkinType0.SetChecked(True, True)
-            Hint("离线皮肤已清空!", HintType.Finish)
+            Hint("离线皮肤已清空!", HintType.Green)
         Catch ex As Exception
             Log(ex, "清空离线皮肤失败", LogLevel.Msgbox)
         End Try
@@ -468,7 +468,7 @@ PreFin:
     '手动选择
     Private Sub BtnArgumentJavaSelect_Click(sender As Object, e As EventArgs) Handles BtnArgumentJavaSelect.Click
         If JavaSearchLoader.State = LoadState.Loading Then
-            Hint("正在搜索 Java,请稍候!", HintType.Critical)
+            Hint("正在搜索 Java,请稍候!", HintType.Red)
             Return
         End If
         '选择 Java
@@ -489,7 +489,7 @@ PreFin:
             Setup.Set("LaunchArgumentJavaAll", JavaNewList.ToString(Newtonsoft.Json.Formatting.None))
             '重新加载列表
             JavaSearchLoader.Start(IsForceRestart:=True)
-            Hint("已将该 Java 加入 Java 列表!", HintType.Finish)
+            Hint("已将该 Java 加入 Java 列表!", HintType.Green)
         Catch ex As Exception
             Log(ex, "该 Java 存在异常,无法使用", LogLevel.Msgbox, "异常的 Java")
             Return
@@ -498,7 +498,7 @@ PreFin:
     '自动查找
     Private Sub BtnArgumentJavaSearch_Click(sender As Object, e As EventArgs) Handles BtnArgumentJavaSearch.Click
         If JavaSearchLoader.State = LoadState.Loading Then
-            Hint("正在搜索 Java,请稍候!", HintType.Critical)
+            Hint("正在搜索 Java,请稍候!", HintType.Red)
             Return
         End If
         RunInThread(
@@ -506,9 +506,9 @@ PreFin:
             Hint("正在搜索 Java!")
             JavaSearchLoader.WaitForExit(IsForceRestart:=True)
             If Not JavaList.Any() Then
-                Hint("未找到可用的 Java!", HintType.Critical)
+                Hint("未找到可用的 Java!", HintType.Red)
             Else
-                Hint("已找到 " & JavaList.Count & " 个 Java,请检查下拉框查看列表!", HintType.Finish)
+                Hint("已找到 " & JavaList.Count & " 个 Java,请检查下拉框查看列表!", HintType.Green)
             End If
         End Sub)
     End Sub

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

@@ -28,7 +28,7 @@
             Setup.Reset("LinkHiperCertWarn")
 
             Log("[Setup] 已初始化联机页设置")
-            Hint("已初始化联机页设置!", HintType.Finish, False)
+            Hint("已初始化联机页设置!", HintType.Green, False)
         Catch ex As Exception
             Log(ex, "初始化联机页设置失败", LogLevel.Msgbox)
         End Try

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

@@ -85,7 +85,7 @@
             Setup.Reset("SystemSystemTelemetry")
 
             Log("[Setup] 已初始化启动器页设置")
-            Hint("已初始化启动器页设置!", HintType.Finish, False)
+            Hint("已初始化启动器页设置!", HintType.Green, False)
         Catch ex As Exception
             Log(ex, "初始化启动器页设置失败", LogLevel.Msgbox)
         End Try
@@ -136,7 +136,7 @@
 
     '识别码/解锁码替代入口
     Private Sub BtnSystemIdentify_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnSystemIdentify.Click
-        PageOtherAbout.CopyUniqueAddress()
+        PageOtherAbout.CopyIdentify()
     End Sub
     Private Sub BtnSystemUnlock_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnSystemUnlock.Click
         DonateCodeInput()

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

@@ -225,7 +225,8 @@
                             <ColumnDefinition />
                         </Grid.ColumnDefinitions>
                         <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="下载地址" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextCustomNet" Tag="UiCustomNet" Grid.Column="1" MaxHeight="28">
+                        <local:MyTextBox x:Name="TextCustomNet" Tag="UiCustomNet" Grid.Column="1" MaxHeight="28"
+                                         ToolTip="你可以用替换标记动态改变网址,以传入参数或变量。&#xa;在 更多 → 帮助 中搜索 “替换标记” 以了解用法。">
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateNullable />
                                 <local:ValidateHttp />

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

@@ -162,7 +162,7 @@
             Setup.Reset("UiHiddenOtherTest")
 
             Log("[Setup] 已初始化个性化设置!")
-            Hint("已初始化个性化设置", HintType.Finish, False)
+            Hint("已初始化个性化设置", HintType.Green, False)
         Catch ex As Exception
             Log(ex, "初始化个性化设置失败", LogLevel.Msgbox)
         End Try
@@ -215,7 +215,7 @@
         If MyMsgBox("即将删除背景图片文件夹中的所有文件。" & vbCrLf & "此操作不可撤销,是否确定?", "警告",, "取消", IsWarn:=True) = 1 Then
             DeleteDirectory(Path & "PCL\Pictures")
             BackgroundRefresh(False, True)
-            Hint("背景图片已清空!", HintType.Finish)
+            Hint("背景图片已清空!", HintType.Green)
         End If
     End Sub
     ''' <summary>
@@ -238,10 +238,10 @@
             If Not Pic.Any() Then
                 If Refresh Then
                     If FrmMain.ImgBack.Visibility = Visibility.Collapsed Then
-                        If IsHint Then Hint("未检测到可用背景图片!", HintType.Critical)
+                        If IsHint Then Hint("未检测到可用背景图片!", HintType.Red)
                     Else
                         FrmMain.ImgBack.Visibility = Visibility.Collapsed
-                        If IsHint Then Hint("背景图片已清除!", HintType.Finish)
+                        If IsHint Then Hint("背景图片已清除!", HintType.Green)
                     End If
                 End If
                 If Not IsNothing(FrmSetupUI) Then FrmSetupUI.BackgroundRefreshUI(False, 0)
@@ -253,7 +253,7 @@
                         FrmMain.ImgBack.Background = New MyBitmap(Address)
                         Setup.Load("UiBackgroundSuit", True)
                         FrmMain.ImgBack.Visibility = Visibility.Visible
-                        If IsHint Then Hint("背景图片已刷新:" & GetFileNameFromPath(Address), HintType.Finish, False)
+                        If IsHint Then Hint("背景图片已刷新:" & GetFileNameFromPath(Address), HintType.Green, False)
                     Catch ex As Exception
                         If ex.Message.Contains("参数无效") Then
                             Log("刷新背景图片失败,该图片文件可能并非标准格式。" & vbCrLf &
@@ -338,7 +338,7 @@ Refresh:
         Try
             File.Delete(Path & "PCL\Logo.png")
             RadioLogoType1.SetChecked(True, True)
-            Hint("标题栏图片已清空!", HintType.Finish)
+            Hint("标题栏图片已清空!", HintType.Green)
         Catch ex As Exception
             Log(ex, "清空标题栏图片失败", LogLevel.Msgbox)
         End Try
@@ -381,7 +381,7 @@ Refresh:
                 '删除文件
                 Try
                     DeleteDirectory(Path & "PCL\Musics")
-                    Hint("背景音乐已删除!", HintType.Finish)
+                    Hint("背景音乐已删除!", HintType.Green)
                 Catch ex As Exception
                     Log(ex, "删除背景音乐失败", LogLevel.Msgbox)
                 End Try
@@ -410,7 +410,7 @@ Refresh:
                 If MyMsgBox("当前已存在布局文件,继续生成教学文件将会覆盖现有布局文件!", "覆盖确认", "继续", "取消", IsWarn:=True) = 2 Then Return
             End If
             WriteFile(Path & "PCL\Custom.xaml", GetResources("Custom"))
-            Hint("教学文件已生成!", HintType.Finish)
+            Hint("教学文件已生成!", HintType.Green)
             OpenExplorer(Path & "PCL\Custom.xaml")
         Catch ex As Exception
             Log(ex, "生成教学文件失败", LogLevel.Feedback)
@@ -418,7 +418,7 @@ Refresh:
     End Sub
     Private Sub BtnCustomRefresh_Click() Handles BtnCustomRefresh.Click
         FrmLaunchRight.ForceRefresh()
-        Hint("已刷新主页!", HintType.Finish)
+        Hint("已刷新主页!", HintType.Green)
     End Sub
     Private Sub BtnCustomTutorial_Click(sender As Object, e As EventArgs) Handles BtnCustomTutorial.Click
         MyMsgBox("1. 点击 生成教学文件 按钮,这会在 PCL 文件夹下生成 Custom.xaml 布局文件。" & vbCrLf &

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

@@ -96,7 +96,7 @@ Public Class PageSpeedLeft
                             AddHandler Tb.MouseLeftButtonDown,
                             Sub(sender As TextBlock, e As EventArgs)
                                 ClipboardSet(sender.Text, False)
-                                Hint("已复制错误详情!", HintType.Finish)
+                                Hint("已复制错误详情!", HintType.Green)
                             End Sub
                             Card.Children.Add(Tb)
 #End Region

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

@@ -262,7 +262,7 @@
                             <local:MyButton Grid.Column="0" x:Name="BtnAdvancedImport" MinWidth="140" Text="读取配置" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" />
                             <local:MyButton Grid.Column="1" x:Name="BtnAdvancedExport" MinWidth="140" Text="保存配置" Padding="13,0" Margin="0,0,20,0" />
                             <local:MyButton Grid.Column="2" MinWidth="140" Text="整合包制作指南" Padding="13,0" Margin="0,0,20,0" x:Name="BtnAdvancedHelp"
-                                            EventType="打开帮助" />
+                                            local:CustomEventService.EventType="打开帮助" />
                         </Grid>
                     </StackPanel>
                 </local:MyCard>

+ 7 - 6
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb

@@ -22,7 +22,8 @@ Public Class PageVersionExport
     Private Sub PageVersionExport_Loaded() Handles Me.Loaded
         AniControlEnabled += 1
         If CurrentVersion <> PageVersionLeft.Version.Path Then RefreshAll() '切换到了另一个版本,重置页面
-        BtnAdvancedHelp.EventData = If(VersionBranchName = "Release", "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json")
+        CustomEventService.SetEventData(BtnAdvancedHelp,
+            If(VersionBranchName = "Release", "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
         AniControlEnabled -= 1
     End Sub
     Public Sub RefreshAll() Implements IRefreshable.Refresh
@@ -35,7 +36,7 @@ Public Class PageVersionExport
         TextExportVersion.HintText = "1.0.0"
         CheckAdvancedInclude.Checked = False
         CheckAdvancedModrinth.Checked = False
-        GetExportOption(CheckOptionsBasic).Description = PageVersionLeft.Version.GetDefaultDescription()
+        GetExportOption(CheckOptionsBasic).Description = PageVersionLeft.Version.GetVersionDescription()
         ResetConfigOverrides()
         ReloadAllSubOptions()
         RefreshAllOptionsUI()
@@ -341,7 +342,7 @@ Public Class PageVersionExport
             ConfigLines.AddRange(GetExtraFileLines())
             '结束
             WriteFile(ConfigPath, ConfigLines.Join(vbCrLf))
-            Hint("已保存配置文件:" & ConfigPath, HintType.Finish)
+            Hint("已保存配置文件:" & ConfigPath, HintType.Green)
             OpenExplorer(ConfigPath)
         Catch ex As Exception
             Log(ex, "保存配置失败", LogLevel.Msgbox)
@@ -378,7 +379,7 @@ Public Class PageVersionExport
                 ExtraFiles = Nothing
             End If
             '结束
-            Hint("已读取配置文件:" & ConfigPath, HintType.Finish)
+            Hint("已读取配置文件:" & ConfigPath, HintType.Green)
         Catch ex As Exception
             Log(ex, "读取配置失败", LogLevel.Msgbox)
         End Try
@@ -517,13 +518,13 @@ Public Class PageVersionExport
                     If Directory.Exists(Line) Then
                         CopyDirectory(Line, BaseFolder & GetFolderNameFromPath(Line) & "\")
                     Else
-                        Hint($"未找到配置文件中指定的文件夹:{Line}", HintType.Critical)
+                        Hint($"未找到配置文件中指定的文件夹:{Line}", HintType.Red)
                     End If
                 Else
                     If File.Exists(Line) Then
                         CopyFile(Line, BaseFolder & GetFileNameFromPath(Line))
                     Else
-                        Hint($"未找到配置文件中指定的单个文件:{Line}", HintType.Critical)
+                        Hint($"未找到配置文件中指定的单个文件:{Line}", HintType.Red)
                     End If
                 End If
             Next

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

@@ -306,7 +306,7 @@
             If Result.Any Then
                 MyMsgBox(Join(Result, vbCrLf & vbCrLf), "Mod 检查结果")
             Else
-                Hint("Mod 检查完成,未发现任何问题!", HintType.Finish)
+                Hint("Mod 检查完成,未发现任何问题!", HintType.Green)
             End If
         Catch ex As Exception
             Log(ex, "进行 Mod 检查时出错", LogLevel.Feedback)
@@ -340,7 +340,7 @@
         Log("[System] 文件为 jar/litemod 格式,尝试作为 Mod 安装")
         '检查回收站:回收站中的文件有错误的文件名
         If FilePathList.First.Contains(":\$RECYCLE.BIN\") Then
-            Hint("请先将文件从回收站还原,再尝试安装!", HintType.Critical)
+            Hint("请先将文件从回收站还原,再尝试安装!", HintType.Red)
             Return True
         End If
         '获取并检查目标版本
@@ -362,9 +362,9 @@ Install:
                     CopyFile(ModFile, TargetVersion.PathIndie & "mods\" & NewFileName)
                 Next
                 If FilePathList.Count = 1 Then
-                    Hint($"已安装 {GetFileNameFromPath(FilePathList.First).Replace(".disabled", "").Replace(".old", "")}!", HintType.Finish)
+                    Hint($"已安装 {GetFileNameFromPath(FilePathList.First).Replace(".disabled", "").Replace(".old", "")}!", HintType.Green)
                 Else
-                    Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Finish)
+                    Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Green)
                 End If
                 '刷新列表
                 If FrmMain.PageCurrent = FormMain.PageType.VersionSetup AndAlso FrmMain.PageCurrentSub = FormMain.PageSubType.VersionMod Then
@@ -570,7 +570,7 @@ Install:
         If IsSuccessful Then
             RefreshBars()
         Else
-            Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
+            Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
             ReloadModList(True)
         End If
         LoaderRun(LoaderFolderRunType.UpdateOnly)
@@ -643,14 +643,14 @@ Install:
                 Try
                     For Each Entry As McMod In ModList
                         If File.Exists(Entry.Path) Then
-                            My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                            My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                         Else
                             Log($"[Mod] 未找到更新前的 Mod 文件,跳过对它的删除:{Entry.Path}", LogLevel.Debug)
                         End If
                     Next
                     For Each Entry As KeyValuePair(Of String, String) In FileCopyList
                         If File.Exists(Entry.Value) Then
-                            My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                            My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                             Log($"[Mod] 更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}", LogLevel.Debug)
                         End If
                         If Directory.Exists(GetPathFromFullPath(Entry.Value)) Then
@@ -676,14 +676,14 @@ Install:
                             Case 0 '一般是由于 Mod 文件被占用,然后玩家主动取消
                                 Log($"[Mod] 没有 Mod 被成功更新")
                             Case 1
-                                Hint($"已成功更新 {FinishedFileNames.Single}!", HintType.Finish)
+                                Hint($"已成功更新 {FinishedFileNames.Single}!", HintType.Green)
                             Case Else
-                                Hint($"已成功更新 {FinishedFileNames.Count} 个 Mod!", HintType.Finish)
+                                Hint($"已成功更新 {FinishedFileNames.Count} 个 Mod!", HintType.Green)
                         End Select
                     Case LoadState.Failed
-                        Hint("Mod 更新失败:" & Loader.Error.GetBrief(), HintType.Critical)
+                        Hint("Mod 更新失败:" & Loader.Error.GetBrief(), HintType.Red)
                     Case LoadState.Aborted
-                        Hint("Mod 更新已中止!", HintType.Info)
+                        Hint("Mod 更新已中止!", HintType.Blue)
                     Case Else
                         Return
                 End Select
@@ -739,7 +739,7 @@ Install:
                     If IsShiftPressed Then
                         File.Delete(ModEntity.Path)
                     Else
-                        My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                        My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                     End If
                 Catch ex As OperationCanceledException
                     Log(ex, "删除 Mod 被主动取消")
@@ -760,7 +760,7 @@ Install:
             Next
             RefreshBars()
             If Not IsSuccessful Then
-                Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical)
+                Hint("由于文件被占用,Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Red)
                 ReloadModList(True)
             ElseIf PanList.Children.Count = 0 Then
                 ReloadModList(True) '删除了全部文件
@@ -771,15 +771,15 @@ Install:
             If Not IsSuccessful Then Return
             If IsShiftPressed Then
                 If ModList.Count = 1 Then
-                    Hint($"已彻底删除 {ModList.Single.FileName}!", HintType.Finish)
+                    Hint($"已彻底删除 {ModList.Single.FileName}!", HintType.Green)
                 Else
-                    Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Finish)
+                    Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Green)
                 End If
             Else
                 If ModList.Count = 1 Then
-                    Hint($"已将 {ModList.Single.FileName} 删除到回收站!", HintType.Finish)
+                    Hint($"已将 {ModList.Single.FileName} 删除到回收站!", HintType.Green)
                 Else
-                    Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Finish)
+                    Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Green)
                 End If
             End If
         Catch ex As OperationCanceledException

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

@@ -30,7 +30,7 @@
         BtnFolderMods.Visibility = If(PageVersionLeft.Version.Modable, Visibility.Visible, Visibility.Collapsed)
         '刷新版本显示
         PanDisplayItem.Children.Clear()
-        ItemVersion = PageSelectRight.McVersionListItem(PageVersionLeft.Version)
+        ItemVersion = PageVersionLeft.Version.ToListItem
         ItemVersion.IsHitTestVisible = False
         PanDisplayItem.Children.Add(ItemVersion)
         FrmMain.PageNameRefresh()
@@ -169,7 +169,7 @@
                 Log(ex, "重命名版本 json 失败")
             End Try
             '刷新与提示
-            Hint("重命名成功!", HintType.Finish)
+            Hint("重命名成功!", HintType.Green)
             PageVersionLeft.Version = New McVersion(NewName).Load()
             If Not IsNothing(McVersionCurrent) AndAlso McVersionCurrent.Equals(PageVersionLeft.Version) Then WriteIni(PathMcFolder & "PCL.ini", "Version", NewName)
             Reload()
@@ -263,7 +263,7 @@
             If SavePath = "" Then Return
             '检查中断(等玩家选完弹窗指不定任务就结束了呢……)
             If McLaunchLoader.State = LoadState.Loading Then
-                Hint("请在当前启动任务结束后再试!", HintType.Critical)
+                Hint("请在当前启动任务结束后再试!", HintType.Red)
                 Return
             End If
             '生成脚本
@@ -284,13 +284,13 @@
         Try
             '忽略文件检查提示
             If ShouldIgnoreFileCheck(PageVersionLeft.Version) Then
-                Hint("请先关闭 [版本设置 → 设置 → 高级启动选项 → 关闭文件校验],然后再尝试补全文件!", HintType.Info)
+                Hint("请先关闭 [版本设置 → 设置 → 高级启动选项 → 关闭文件校验],然后再尝试补全文件!", HintType.Blue)
                 Return
             End If
             '重复任务检查
             For Each OngoingLoader In LoaderTaskbar
                 If OngoingLoader.Name <> PageVersionLeft.Version.Name & " 文件补全" Then Continue For
-                Hint("正在处理中,请稍候!", HintType.Critical)
+                Hint("正在处理中,请稍候!", HintType.Red)
                 Return
             Next
             '启动
@@ -299,11 +299,11 @@
             Sub()
                 Select Case Loader.State
                     Case LoadState.Finished
-                        Hint(Loader.Name & "成功!", HintType.Finish)
+                        Hint(Loader.Name & "成功!", HintType.Green)
                     Case LoadState.Failed
-                        Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Critical)
+                        Hint(Loader.Name & "失败:" & Loader.Error.GetBrief(), HintType.Red)
                     Case LoadState.Aborted
-                        Hint(Loader.Name & "已取消!", HintType.Info)
+                        Hint(Loader.Name & "已取消!", HintType.Blue)
                 End Select
             End Sub
             Loader.Start(PageVersionLeft.Version.Name)
@@ -329,10 +329,10 @@
                     IniClearCache(PageVersionLeft.Version.Path & "PCL\Setup.ini")
                     If IsShiftPressed Then
                         DeleteDirectory(PageVersionLeft.Version.Path)
-                        Hint("版本 " & PageVersionLeft.Version.Name & " 已永久删除!", HintType.Finish)
+                        Hint("版本 " & PageVersionLeft.Version.Name & " 已永久删除!", HintType.Green)
                     Else
-                        FileIO.FileSystem.DeleteDirectory(PageVersionLeft.Version.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
-                        Hint("版本 " & PageVersionLeft.Version.Name & " 已删除到回收站!", HintType.Finish)
+                        FileIO.FileSystem.DeleteDirectory(PageVersionLeft.Version.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                        Hint("版本 " & PageVersionLeft.Version.Name & " 已删除到回收站!", HintType.Green)
                     End If
                 Case 2
                     Return

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

@@ -34,9 +34,9 @@
                                                   ToolTip="与其余关闭隔离的版本共享存档、Mod 等文件。&#xa;若存在多个安装了 Mod 的版本,可能会由于 Mod 冲突而导致崩溃。" ToolTipService.HorizontalOffset="100"/>
                         </local:MyComboBox>
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏窗口标题" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则跟随全局设置的值。&#xa;支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号" HintText="跟随全局设置" MaxLength="100" />
+                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则跟随全局设置的值。&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" HintText="跟随全局设置" MaxLength="100" />
                         <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" Tag="VersionArgumentInfo" Grid.Column="1" HintText="跟随全局设置" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角。&#xa;若留空,则跟随全局设置的值。">
+                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" Tag="VersionArgumentInfo" Grid.Column="1" HintText="跟随全局设置" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角。&#xa;若留空,则跟随全局设置的值。&#xa;&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。">
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
@@ -209,13 +209,13 @@
                         <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="Java 虚拟机参数" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" Tag="VersionAdvanceJvm" Grid.Column="1" MaxLength="4000" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置"
+                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" Tag="VersionAdvanceJvm" Grid.Column="1" MaxLength="4000" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置"
                                      Height="100" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" VerticalScrollBarVisibility="Auto" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏参数" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置" />
+                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置" />
                     <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动前执行命令" Margin="0,0,25,3" />
                     <local:MyTextBox x:Name="TextAdvanceRun" Grid.Row="4" Grid.ColumnSpan="2" Tag="VersionAdvanceRun" Grid.Column="1" HintText="" Margin="0,0,0,3"
-                                     ToolTip="该项不会覆盖全局设置:启动时会先执行全局设置的命令,再执行版本设置的命令。&#xa;&#xa;在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;可以使用以下替换标记实现相对路径(路径均以 \ 结尾):&#xa; · {path}:PCL 的 exe 文件所在的文件夹&#xa; · {minecraft}:.minecraft 文件夹&#xa; · {verpath}:版本文件夹(.minecraft\versions\版本名\)&#xa; · {verindie}:开启版本隔离时等同版本文件夹,未开启时等同 .minecraft 文件夹&#xa; · {java}:游戏运行时的 Java 文件夹&#xa;&#xa;除此之外,也支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {uuid}:玩家的 UUID&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号&#xa;&#xa;例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
+                                     ToolTip="该项不会覆盖全局设置:启动时会先执行全局设置的命令,再执行版本设置的命令。&#xa;&#xa;在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;你可以用替换标记来动态改变其内容或实现相对路径,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
                     <local:MyCheckBox Grid.Row="5" Grid.Column="1" x:Name="CheckAdvanceRunWait" Tag="VersionAdvanceRunWait" Visibility="Collapsed"
                                       Text="等待命令执行完成后再继续启动" Margin="0,5,0,0" />
                     <StackPanel Margin="0,12,0,4" Grid.Row="6" Grid.ColumnSpan="2" HorizontalAlignment="Left">

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

@@ -98,7 +98,7 @@
             JavaSearchLoader.Start(IsForceRestart:=True)
 
             Log("[Setup] 已初始化版本独立设置")
-            Hint("已初始化版本独立设置!", HintType.Finish, False)
+            Hint("已初始化版本独立设置!", HintType.Green, False)
         Catch ex As Exception
             Log(ex, "初始化版本独立设置失败", LogLevel.Msgbox)
         End Try

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

@@ -206,8 +206,8 @@
       <SubType>Designer</SubType>
     </ApplicationDefinition>
     <Compile Include="Controls\Behaviors\ClipboardInterceptor.vb" />
-    <Compile Include="Controls\IMyRadio.vb" />
-    <Compile Include="Controls\Interfaces.vb" />
+    <Compile Include="Controls\Behaviors\IMyRadio.vb" />
+    <Compile Include="Controls\Behaviors\Interfaces.vb" />
     <Compile Include="Controls\MyComboBox.vb" />
     <Compile Include="Controls\MyComboBoxItem.vb" />
     <Compile Include="Controls\MyCard.vb" />

+ 23 - 111
Plain Craft Launcher 2/Resources/Custom.xaml

@@ -53,114 +53,37 @@
     </StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="事件 1:打开网页、弹出窗口" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
+<local:MyCard Title="自定义事件" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
+	<StackPanel Margin="25,40,23,15">
+		<TextBlock Margin="0,0,0,10"
                     Text="将按钮的 EventType 属性设为 打开网页,然后在 EventData 属性中写入网址,即可通过点击按钮打开网页。" />
-        <local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
+		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0"
                     Text="打开 Minecraft Wiki" EventType="打开网页" EventData="https://zh.minecraft.wiki/" />
-        <TextBlock TextWrapping="Wrap" Margin="0,10,0,10"
+		<TextBlock 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"
+		<TextBlock Margin="0,10,0,0"
                     Text="local:MyTextButton 同样可以设置 EventType 与 EventData 让它具有特定功能。" />
-        <local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
-                    Text="点击打开知乎" EventType="打开网页" EventData="https://www.zhihu.com/" />
-    </StackPanel>
-</local:MyCard>
-
-<local:MyCard Title="事件 2:启动游戏" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <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"
-                    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"
-                    Text="启动 1.20.1 并进入 Hypixel" EventType="启动游戏" EventData="1.20.1|mc.hypixel.net" ToolTip="服务器 IP:mc.hypixel.net" />
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="在 EventData 中填写 \current,表示启动当前被选中的游戏版本,同样可以在后面填写服务器 IP。" />
-        <local:MyButton Margin="0,4,0,10" Width="250" Height="35"
-                    Text="启动当前选中版本并进入 Hypixel" EventType="启动游戏" EventData="\current|mc.hypixel.net" ToolTip="启动当前版本" />
-    </StackPanel>
+		<local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
+                    Text="点击打开哔哩哔哩" EventType="打开网页" EventData="https://www.bilibili.com/" />
+		<TextBlock Margin="0,30,0,0"
+                    Text="你也可以让一个控件触发多个事件。自定义事件的详细用法请参考下方的 Wiki。" />
+		<local:MyListItem Margin="-5,8,-5,5"
+            Title="自定义事件 - PCL Wiki" Info="点击查看自定义事件的完整列表与说明"
+            Logo="pack://application:,,,/images/Blocks/CommandBlock.png" Type="Clickable"
+		    EventType="打开网页" EventData="https://github.com/Meloong-Git/PCL/wiki/自定义事件" />
+	</StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="事件 3:执行命令" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <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"
-                    Text="打开记事本" EventType="执行命令" EventData="notepad.exe" />
-        <TextBlock TextWrapping="Wrap" Margin="0,4,0,0"
-                    Text="如果能在 PCL 文件夹中找到该文件,则会优先打开 PCL 文件夹中的文件。当然,你可以使用例如 C:\ 开头的绝对路径。" />
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="此外,PCL 会将文件中的 “花括号 path” 替换为 PCL 可执行文件所在文件夹({path}),可以用于执行特定程序或加载图片。" />
-        <local:MyButton Margin="0,4,0,10" Width="200" Height="35"
-                    Text="打开 PCL 日志" EventType="执行命令" EventData="Log1.txt" ToolTip="PCL 的日志文件就在 PCL 文件夹内,且名为 Log1.txt" />
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="就像为游戏指定服务器 IP 一样,你也可以在 EventData 的竖线后为程序添加启动参数。" />
-        <local:MyButton Margin="0,4,0,0" Width="200" Height="35"
-                    Text="定位游戏启动脚本" EventType="执行命令" EventData="explorer.exe|/select,LatestLaunch.bat" ToolTip="PCL 会将上一次启动 MC 的脚本保存在 PCL 文件夹内,且名为 LatestLaunch.bat。&#xA;点击按钮会打开文件管理器,并且借助参数要求它选中这个文件。" />
-    </StackPanel>
-</local:MyCard>
-
-<local:MyCard Title="事件 4:其他事件类型" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock Margin="0,4,0,6" FontWeight="Bold"
-                    Text="EventType 共有以下种类:" />
-        <TextBlock Margin="0,0,0,4"
-                    Text="· 打开网页、启动游戏、打开文件:已在上方的卡片中介绍" />
-        <local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="查看 PCL 源代码" EventType="打开网页" EventData="https://github.com/Meloong-Git/PCL/blob/main/Plain%20Craft%20Launcher%202/FormMain.xaml.vb"/>
-        <TextBlock Margin="0,0,0,4"
-                    Text="· 今日人品、清理垃圾、内存优化、安装整合包:触发对应功能" />
-        <TextBlock Margin="0,0,0,4"
-                    Text="· 复制文本:将 EventData 中的内容复制到剪贴板" />
-        <TextBlock Margin="0,0,0,4"
-                    Text="· 刷新主页:点击重新加载主页。如果主页文件为联网更新,这会重新下载主页内容" />
-        <TextBlock Margin="0,0,0,4"
-                    Text="· 下载文件:在点击时要求选择文件保存位置,并且下载 EventData 的链接中的文件&#xa;       EventData 中也能可选地指定文件名或目标文件夹,格式为:链接|文件名|文件夹" />
-        <local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="下载百度的 Logo" EventType="下载文件" EventData="https://www.baidu.com/img/flexible/logo/pc/result.png" />
-        <TextBlock Margin="0,4,0,4"
-                    Text="· 打开帮助:跳转到 EventData 对应的帮助文件(输入相对 Help 文件夹的路径)"/>
-        <local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="打开自定义帮助页面" EventType="打开帮助" EventData="帮助/自定义帮助.json"/>
-        <TextBlock Margin="0,4,0,4"
-                    Text="· 切换页面:强制切换到 PCL 的某一页面(尝试切换到不太对头的页面会出现不可预知的 Bug)&#xa;       EventData 为 页面种类|次要页面种类(PageType|PageSubType,例如 1|1)&#xa;       点击上方的 查看 PCL 源代码 按钮,在 1000 行左右的位置即可查找到对应数值"/>
-        <local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="切换到下载页面" EventType="切换页面" EventData="1|1"/>
-        <local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="切换到启动器设置" EventType="切换页面" EventData="3|2"/>
-		<TextBlock Margin="0,4,0,4"
-                    Text="· 下载版本:切换到 EventData 对应的 Minecraft 下载页面"/>
-		<local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="下载 Minecraft 1.12.2" EventType="下载版本" EventData="1.12.2"/>
-		<TextBlock Margin="0,4,0,4"
-                    Text="· 修改设置:将 PCL 的特定设置修改为特定值"/>
-		<local:MyButton Margin="0,5,0,6" Width="250" Height="35"
-                    Text="将窗口改为半透明" EventType="修改设置" EventData="UiLauncherTransparent|400"/>
-    </StackPanel>
-</local:MyCard>
-
-<local:MyCard Title="在单个按钮中执行多个事件" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock Margin="0,4,0,6"
-                    Text="通过按钮的 CustomEvents 属性来指定多个事件,它们将挨个运行" />
-        <local:MyButton Margin="0,5,0,6" Width="400" Height="35"
-                    Text="打开 Minecraft Wiki,显示一个弹窗,然后打开知乎">
-            <local:MyButton.CustomEvents>
-                <local:CustomEvent EventType="打开网页" EventData="https://zh.minecraft.wiki/" />
-                <local:CustomEvent EventType="弹出窗口" EventData="刚刚打开了 Wiki|该弹窗关闭后将打开知乎" />
-                <local:CustomEvent EventType="打开网页" EventData="https://www.zhihu.com/" />
-            </local:MyButton.CustomEvents>
-        </local:MyButton>
-    </StackPanel>
+<local:MyCard Title="替换标记" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
+	<StackPanel Margin="25,40,23,10">
+		<local:MyListItem Margin="-5,0,-5,8"
+		                  EventType="打开帮助" EventData="个性化/替换标记.json" />
+	</StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="StackPanel 横向布局" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
+<local:MyCard Title="横向布局" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
     <StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
                     Text="你需要使用 StackPanel 在一行里塞下多个按钮。你可以仅在这个教学卡片的基础上稍作调整,来实现自己的按钮布局。" />
@@ -198,7 +121,7 @@
                     Logo="pack://application:,,,/images/Blocks/Grass.png"
                     EventType="打开帮助" EventData="帮助/自定义帮助.json" Type="Clickable" />
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="EventType、EventData 与按钮几乎一致,唯一不同的是,列表项不支持联网加载帮助文件。" />
+                    Text="EventType、EventData 与按钮几乎一致。唯一不同的是,列表项不支持联网加载帮助文件。" />
     </StackPanel>
 </local:MyCard>
 
@@ -261,17 +184,6 @@
     </StackPanel>
 </local:MyCard>
 
-<local:MyCard Title="替换标记" Margin="0,0,0,15" CanSwap="True" IsSwapped="True">
-    <StackPanel Margin="25,40,23,15">
-        <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
-                    Text="在花括号 {} 中写入特定内容,PCL 会在加载时对其进行替换。&#xa;例如,path 会被替换为 PCL 可执行文件所在文件夹({path}),用于在 EventData 中指定特定文件,或是加载图片。" />
-        <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" IsSwapped="True">
     <StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"

BIN
Plain Craft Launcher 2/Resources/Help.zip