龙腾猫跃 1 год назад
Родитель
Сommit
8d2eb85aec
46 измененных файлов с 1427 добавлено и 1459 удалено
  1. 2 1
      Plain Craft Launcher 2/Application.xaml.vb
  2. 0 4
      Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb
  3. 8 0
      Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb
  4. 2 2
      Plain Craft Launcher 2/Controls/MyImage.vb
  5. 23 24
      Plain Craft Launcher 2/Controls/MyLoading.xaml.vb
  6. 9 4
      Plain Craft Launcher 2/Controls/MyPageRight.vb
  7. 3 3
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb
  8. 44 27
      Plain Craft Launcher 2/FormMain.xaml.vb
  9. 42 40
      Plain Craft Launcher 2/Modules/Base/ModAnimation.vb
  10. 17 12
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  11. 34 21
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  12. 30 4
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  13. 2 2
      Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
  14. 1 3
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  15. 20 15
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  16. 23 13
      Plain Craft Launcher 2/Modules/ModEvent.vb
  17. 8 12
      Plain Craft Launcher 2/Modules/ModMain.vb
  18. 9 2
      Plain Craft Launcher 2/Modules/ModSecret.vb
  19. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  20. 1 2
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  21. 5 5
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  22. 1 1
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml
  23. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  24. 1 5
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  25. 8 7
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  26. 0 19
      Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml
  27. 0 2
      Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml.vb
  28. 0 117
      Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml
  29. 0 395
      Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb
  30. 0 50
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml
  31. 0 411
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
  32. 0 44
      Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml
  33. 0 112
      Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb
  34. 202 0
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml
  35. 844 0
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb
  36. 10 2
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  37. 6 9
      Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb
  38. 0 0
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml
  39. 13 11
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.vb
  40. 3 24
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml
  41. 4 9
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb
  42. 6 6
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  43. 2 2
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  44. 3 25
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  45. 38 9
      Plain Craft Launcher 2/Resources/Custom.xaml
  46. BIN
      Plain Craft Launcher 2/Resources/Help.zip

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

@@ -132,7 +132,8 @@ WaitRetry:
             Log($"[Start] 管理员权限:{IsAdmin()}")
             Log($"[Start] 管理员权限:{IsAdmin()}")
             '检测异常环境
             '检测异常环境
             If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
             If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
-                MyMsgBox("请将 PCL 从压缩包中解压之后再使用!" & vbCrLf & "在当前环境下运行可能会导致丢失游戏存档或设置,部分功能也可能无法使用!", "环境警告", "我知道了", IsWarn:=True)
+                MyMsgBox("请将 PCL 从压缩包中解压后再使用!" & vbCrLf & "如果不会解压,可以在网上寻找教程。", "尚未解压", "我知道了", IsWarn:=True)
+                FormMain.EndProgramForce(ProcessReturnValues.Cancel)
             End If
             End If
             If Is32BitSystem Then
             If Is32BitSystem Then
                 MyMsgBox("PCL 和新版 Minecraft 均不再支持 32 位系统,部分功能将无法使用。" & vbCrLf & "非常建议重装为 64 位系统后再进行游戏!", "环境警告", "我知道了", IsWarn:=True)
                 MyMsgBox("PCL 和新版 Minecraft 均不再支持 32 位系统,部分功能将无法使用。" & vbCrLf & "非常建议重装为 64 位系统后再进行游戏!", "环境警告", "我知道了", IsWarn:=True)

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

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

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

@@ -13,3 +13,11 @@ End Interface
 Public Interface IDispatcherUnhandledException
 Public Interface IDispatcherUnhandledException
     Sub DispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs)
     Sub DispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs)
 End Interface
 End Interface
+
+''' <summary>
+''' 指示该控件是可勾选的。
+''' </summary>
+Public Interface IMyRadio
+    Event Check(sender As Object, e As RouteEventArgs)
+    Event Changed(sender As Object, e As RouteEventArgs)
+End Interface

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

@@ -120,12 +120,12 @@
         End If
         End If
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
-            Dim IsLocalFallback As Boolean = FallbackSource IsNot Nothing AndAlso Not FallbackSource.StartsWithF("http")
+            Dim IsLocalFallback As Boolean = Not String.IsNullOrEmpty(FallbackSource) AndAlso Not FallbackSource.StartsWithF("http")
             Try
             Try
                 '下载
                 '下载
                 ActualSource = LoadingSource '显示加载中的占位图片
                 ActualSource = LoadingSource '显示加载中的占位图片
                 NetDownloadByLoader(
                 NetDownloadByLoader(
-                    If(String.IsNullOrEmpty(FallbackSource) OrElse IsLocalFallback, {Source}, {Source, FallbackSource}),
+                    If(FallbackSource?.StartsWithF("http"), {Source, FallbackSource}, {Source}),
                     TempPath, SimulateBrowserHeaders:=True)
                     TempPath, SimulateBrowserHeaders:=True)
                 If EnableCache Then
                 If EnableCache Then
                     '保存缓存并显示
                     '保存缓存并显示

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

@@ -183,30 +183,29 @@ Public Class MyLoading
         IsLooping = True
         IsLooping = True
         ErrorAnimationWaiting = True
         ErrorAnimationWaiting = True
         AniStart({
         AniStart({
-                    AaRotateTransform(PathPickaxe, -20 - CType(PathPickaxe.RenderTransform, RotateTransform).Angle, 350, 250, New AniEaseInBack(AniEasePower.Weak)),
-                    AaRotateTransform(PathPickaxe, 50, 900,, New AniEaseOutFluent, True),
-                    AaRotateTransform(PathPickaxe, 25, 900,, New AniEaseOutElastic(AniEasePower.Weak)),
-                    AaCode(Sub()
-                               PathLeft.Opacity = 1
-                               PathLeft.Margin = New Thickness(7, 41, 0, 0)
-                               PathRight.Opacity = 1
-                               PathRight.Margin = New Thickness(14, 41, 0, 0)
-                               ErrorAnimationWaiting = False
-                           End Sub),
-                    AaOpacity(PathLeft, -1, 100, 50),
-                    AaX(PathLeft, -5, 180,, New AniEaseOutFluent),
-                    AaY(PathLeft, -6, 180,, New AniEaseOutFluent),
-                    AaOpacity(PathRight, -1, 100, 50),
-                    AaX(PathRight, 5, 180,, New AniEaseOutFluent),
-                    AaY(PathRight, -6, 180,, New AniEaseOutFluent),
-                    AaCode(Sub()
-                               IsLooping = False
-                               AniLoop()
-                           End Sub,, True)
-            }, "MyLoader Loop " & Uuid & "/" & GetUuid())
-        If ShowProgress Then
-
-        End If
+            AaRotateTransform(PathPickaxe, -20 - CType(PathPickaxe.RenderTransform, RotateTransform).Angle, 350, 250, New AniEaseInBack(AniEasePower.Weak)),
+            AaRotateTransform(PathPickaxe, 50, 900,, New AniEaseOutFluent, True),
+            AaRotateTransform(PathPickaxe, 25, 900,, New AniEaseOutElastic(AniEasePower.Weak)),
+            AaCode(
+            Sub()
+                PathLeft.Opacity = 1
+                PathLeft.Margin = New Thickness(7, 41, 0, 0)
+                PathRight.Opacity = 1
+                PathRight.Margin = New Thickness(14, 41, 0, 0)
+                ErrorAnimationWaiting = False
+            End Sub),
+            AaOpacity(PathLeft, -1, 100, 50),
+            AaX(PathLeft, -5, 180,, New AniEaseOutFluent),
+            AaY(PathLeft, -6, 180,, New AniEaseOutFluent),
+            AaOpacity(PathRight, -1, 100, 50),
+            AaX(PathRight, 5, 180,, New AniEaseOutFluent),
+            AaY(PathRight, -6, 180,, New AniEaseOutFluent),
+            AaCode(
+            Sub()
+                IsLooping = False
+                AniLoop()
+            End Sub,, True)
+        }, "MyLoader Loop " & Uuid & "/" & GetUuid())
     End Sub
     End Sub
 
 
     ''' <summary>
     ''' <summary>

+ 9 - 4
Plain Craft Launcher 2/Controls/MyPageRight.vb

@@ -130,7 +130,7 @@
         Select Case PageState
         Select Case PageState
             Case PageStates.Empty
             Case PageStates.Empty
                 If PageLoader Is Nothing OrElse PageLoader.State = LoadState.Finished OrElse PageLoader.State = LoadState.Waiting OrElse PageLoader.State = LoadState.Aborted Then
                 If PageLoader Is Nothing OrElse PageLoader.State = LoadState.Finished OrElse PageLoader.State = LoadState.Waiting OrElse PageLoader.State = LoadState.Aborted Then
-                    '如果加载器在进入页面时不启动(例如 HiPer 联机),那么在此时就会有 State = Waiting
+                    '如果加载器在进入页面时不启动(例如联机),那么在此时就会有 State = Waiting
                     PageState = PageStates.ContentEnter
                     PageState = PageStates.ContentEnter
                     TriggerEnterAnimation(PanAlways, If(PanContent, Child))
                     TriggerEnterAnimation(PanAlways, If(PanContent, Child))
                 ElseIf PageLoader.State = LoadState.Loading Then
                 ElseIf PageLoader.State = LoadState.Loading Then
@@ -152,7 +152,8 @@
                     PageState = PageStates.LoaderEnter
                     PageState = PageStates.LoaderEnter
                     TriggerEnterAnimation(PanLoader)
                     TriggerEnterAnimation(PanLoader)
                 End If
                 End If
-            Case PageStates.ContentEnter '重复调用 PageOnEnter,直接忽略
+            Case PageStates.ContentEnter
+                '重复调用 PageOnEnter,直接忽略
             Case Else
             Case Else
                 Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnter 事件。")
                 Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnter 事件。")
         End Select
         End Select
@@ -207,8 +208,8 @@
     Public Sub PageOnContentExit()
     Public Sub PageOnContentExit()
         If ModeDebug Then Log("[UI] 已触发 PageOnContentExit")
         If ModeDebug Then Log("[UI] 已触发 PageOnContentExit")
         If PageLoader IsNot Nothing AndAlso PageLoader.State = LoadState.Loading Then
         If PageLoader IsNot Nothing AndAlso PageLoader.State = LoadState.Loading Then
-            Throw New Exception("在调用 PageOnContentExit 时,加载器不能为 Loading 状态")
             'Loading 的加载器可能触发进一步变化,难以预测会触发子页面的动画还是加载器完成的动画
             'Loading 的加载器可能触发进一步变化,难以预测会触发子页面的动画还是加载器完成的动画
+            Throw New Exception("在调用 PageOnContentExit 时,加载器不能为 Loading 状态")
         End If
         End If
         Select Case PageState
         Select Case PageState
             Case PageStates.ContentEnter, PageStates.ContentStay
             Case PageStates.ContentEnter, PageStates.ContentStay
@@ -219,8 +220,12 @@
             Case PageStates.LoaderEnter, PageStates.LoaderStayForce, PageStates.LoaderStay
             Case PageStates.LoaderEnter, PageStates.LoaderStayForce, PageStates.LoaderStay
                 PageState = PageStates.ContentExit
                 PageState = PageStates.ContentExit
                 TriggerExitAnimation(PanLoader)
                 TriggerExitAnimation(PanLoader)
-            Case PageStates.LoaderWait, PageStates.Empty
+            Case PageStates.LoaderWait
                 PageOnEnter()
                 PageOnEnter()
+            Case PageStates.Empty
+                '可能是在离开页面后,其他后台线程调用了本函数,此时不应触发 PageOnEnter()
+                '例如:在加入联机房间后,切换到其他页面,然后联机房间被解散
+                If IsLoaded Then PageOnEnter()
         End Select
         End Select
     End Sub
     End Sub
 
 

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

@@ -89,15 +89,15 @@ Public Class MyRadioBox
                     End If
                     End If
             End Select
             End Select
 
 
-            '触发事件
             If IsChanged Then
             If IsChanged Then
+                '触发事件
                 If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
                 If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
                 RaiseEvent Changed(Me, New RouteEventArgs(user))
                 RaiseEvent Changed(Me, New RouteEventArgs(user))
                 RaiseCustomEvent()
                 RaiseCustomEvent()
+                '更改动画
+                SyncUI()
             End If
             End If
 
 
-            '更改动画
-            SyncUI()
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, "单选框勾选改变错误", LogLevel.Hint)
             Log(ex, "单选框勾选改变错误", LogLevel.Hint)
         End Try
         End Try

+ 44 - 27
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -11,6 +11,15 @@ Public Class FormMain
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
         '统计更新日志条目
 #If BETA Then
 #If BETA Then
+        If LastVersion < 372 Then 'Release 2.10.9
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
+            FeatureCount += 33
+            BugCount += 11
+        End If
         If LastVersion < 369 Then 'Release 2.10.8
         If LastVersion < 369 Then 'Release 2.10.8
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
@@ -104,6 +113,11 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '2:BUG* IMP-
         '1:BUG-
         '1:BUG-
+        If LastVersion < 373 Then 'Snapshot 2.11.0
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!"))
+            FeatureCount += 7
+            BugCount += 10
+        End If
         If LastVersion < 371 Then 'Snapshot 2.10.9
         If LastVersion < 371 Then 'Snapshot 2.10.9
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
@@ -407,11 +421,16 @@ Public Class FormMain
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
             'EULA 提示
             'EULA 提示
-            If Not Setup.Get("SystemEula") Then
-                Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
+            Const EulaVersion As Integer = 2
+            If Setup.Get("SystemEulaVersion") < EulaVersion Then
+                Select Case MyMsgBox(
+                    If(Setup.Get("SystemEulaVersion") = 0,
+                        "在使用 PCL 前,请先阅读用户协议与免责声明。",
+                        $"PCL 的用户协议与免责声明已更新。{vbCrLf}请阅读更新后的用户协议与免责声明。"),
+                        "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
                         Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
                         Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
                     Case 1
                     Case 1
-                        Setup.Set("SystemEula", True)
+                        Setup.Set("SystemEulaVersion", EulaVersion)
                     Case 2
                     Case 2
                         EndProgram(False)
                         EndProgram(False)
                 End Select
                 End Select
@@ -476,6 +495,8 @@ Public Class FormMain
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
         End If
         End If
 #End If
 #End If
+        '迁移 EULA 版本
+        If Setup.Get("SystemEula") AndAlso Setup.Get("SystemEulaVersion") = 0 Then Setup.Set("SystemEulaVersion", 1)
         '被移除的窗口设置选项
         '被移除的窗口设置选项
         If Setup.Get("LaunchArgumentWindowType") = 5 Then Setup.Set("LaunchArgumentWindowType", 1)
         If Setup.Get("LaunchArgumentWindowType") = 5 Then Setup.Set("LaunchArgumentWindowType", 1)
         '修改主题设置项名称
         '修改主题设置项名称
@@ -554,20 +575,20 @@ Public Class FormMain
     ''' </summary>
     ''' </summary>
     ''' <param name="SendWarning">是否在还有下载任务未完成时发出警告。</param>
     ''' <param name="SendWarning">是否在还有下载任务未完成时发出警告。</param>
     Public Sub EndProgram(SendWarning As Boolean)
     Public Sub EndProgram(SendWarning As Boolean)
-        '发出警告
-        If SendWarning AndAlso HasDownloadingTask() Then
-            If MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 1 Then
-                '强行结束下载任务
-                RunInNewThread(
-                Sub()
-                    Log("[System] 正在强行停止任务")
-                    For Each Task As LoaderBase In LoaderTaskbar.ToList()
-                        Task.Abort()
-                    Next
-                End Sub, "强行停止下载任务")
-            Else
-                Return
-            End If
+        '强行结束下载任务?
+        If HasDownloadingTask() Then
+            If SendWarning AndAlso MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 2 Then Return
+            RunInNewThread(
+            Sub()
+                Log("[System] 正在强行停止任务")
+                For Each Task As LoaderBase In LoaderTaskbar.ToList()
+                    Task.Abort()
+                Next
+            End Sub, "强行停止下载任务")
+        End If
+        '关闭联机?
+        If FrmLinkMain IsNot Nothing Then
+            If FrmLinkMain.TryExit(SendWarning) Then Return
         End If
         End If
         '关闭
         '关闭
         RunInUiWait(
         RunInUiWait(
@@ -1063,14 +1084,10 @@ Public Class FormMain
         DownloadResourcePack = 5
         DownloadResourcePack = 5
         DownloadShader = 6
         DownloadShader = 6
         SetupLaunch = 0
         SetupLaunch = 0
-        SetupUI = 1
-        SetupSystem = 2
-        SetupLink = 3
-        LinkHiper = 1
-        LinkIoi = 2
-        LinkSetup = 4
-        LinkHelp = 5
-        LinkFeedback = 6
+        SetupLink = 1
+        SetupUI = 2
+        SetupSystem = 3
+        LinkMain = 0
         OtherHelp = 0
         OtherHelp = 0
         OtherAbout = 1
         OtherAbout = 1
         OtherTest = 2
         OtherTest = 2
@@ -1298,8 +1315,8 @@ Public Class FormMain
                     'PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
                     'PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
                     PageChangeAnim(FrmDownloadLeft, FrmDownloadLeft.PageGet(SubType))
                     PageChangeAnim(FrmDownloadLeft, FrmDownloadLeft.PageGet(SubType))
                 Case PageType.Link '联机
                 Case PageType.Link '联机
-                    If FrmLinkLeft Is Nothing Then FrmLinkLeft = New PageLinkLeft
-                    PageChangeAnim(FrmLinkLeft, FrmLinkLeft.PageGet(SubType))
+                    If FrmLinkMain Is Nothing Then FrmLinkMain = New PageLinkMain
+                    PageChangeAnim(New MyPageLeft, FrmLinkMain)
                 Case PageType.Setup '设置
                 Case PageType.Setup '设置
                     If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
                     If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
                     PageChangeAnim(FrmSetupLeft, FrmSetupLeft.PageGet(SubType))
                     PageChangeAnim(FrmSetupLeft, FrmSetupLeft.PageGet(SubType))

+ 42 - 40
Plain Craft Launcher 2/Modules/Base/ModAnimation.vb

@@ -467,7 +467,7 @@ Public Module ModAnimation
     ''' 按照 WPF 方式旋转控件的动画。
     ''' 按照 WPF 方式旋转控件的动画。
     ''' </summary>
     ''' </summary>
     ''' <param name="Obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
     ''' <param name="Obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
-    ''' <param name="Value">大小改变的百分比(如-0.6)。</param>
+    ''' <param name="Value">旋转角度(度)。</param>
     ''' <param name="Time">动画长度(毫秒)。</param>
     ''' <param name="Time">动画长度(毫秒)。</param>
     ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
     ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
     ''' <param name="Ease">插值器类型。</param>
     ''' <param name="Ease">插值器类型。</param>
@@ -792,44 +792,46 @@ Public Module ModAnimation
         AniFPSTimer = AniLastTick
         AniFPSTimer = AniLastTick
         AniRunning = True '标记动画执行开始
         AniRunning = True '标记动画执行开始
 
 
-        RunInNewThread(Sub()
-                           Try
-                               Log("[Animation] 动画线程开始")
-                               Do While True
-                                   '两帧之间的间隔时间
-                                   Dim DeltaTime As Long = MathClamp(GetTimeTick() - AniLastTick, 0, 100000)
-                                   If DeltaTime < 3 Then GoTo Sleeper
-                                   AniLastTick = GetTimeTick()
-                                   '记录 FPS
-                                   If ModeDebug Then
-                                       If MathClamp(AniLastTick - AniFPSTimer, 0, 100000) >= 500 Then
-                                           AniFPS = AniFPSCounter
-                                           AniFPSCounter = 0
-                                           AniFPSTimer = AniLastTick
-                                       End If
-                                       AniFPSCounter += 2
-                                   End If
-                                   '执行动画
-                                   RunInUiWait(Sub()
-                                                   AniCount = 0
-                                                   AniTimer(DeltaTime * AniSpeed)
-                                                   '#If DEBUG Then
-                                                   '    FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain
-                                                   '#Else
-                                                   '    If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain
-                                                   '#End If
-                                                   If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
-                                                       Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & GetString(NetManager.Speed) & "/s)")
-                                                   End If
-                                               End Sub)
+        RunInNewThread(
+        Sub()
+            Try
+                Log("[Animation] 动画线程开始")
+                Do While True
+                    '两帧之间的间隔时间
+                    Dim DeltaTime As Long = MathClamp(GetTimeTick() - AniLastTick, 0, 100000)
+                    If DeltaTime < 3 Then GoTo Sleeper
+                    AniLastTick = GetTimeTick()
+                    '记录 FPS
+                    If ModeDebug Then
+                        If MathClamp(AniLastTick - AniFPSTimer, 0, 100000) >= 500 Then
+                            AniFPS = AniFPSCounter
+                            AniFPSCounter = 0
+                            AniFPSTimer = AniLastTick
+                        End If
+                        AniFPSCounter += 2
+                    End If
+                    '执行动画
+                    RunInUiWait(
+                    Sub()
+                        AniCount = 0
+                        AniTimer(DeltaTime * AniSpeed)
+                        '#If DEBUG Then
+                        '    FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain
+                        '#Else
+                        '    If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain
+                        '#End If
+                        If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
+                            Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & GetString(NetManager.Speed) & "/s)")
+                        End If
+                    End Sub)
 Sleeper:
 Sleeper:
-                                   '控制 FPS
-                                   Thread.Sleep(1)
-                               Loop
-                           Catch ex As Exception
-                               Log(ex, "动画帧执行失败", LogLevel.Critical)
-                           End Try
-                       End Sub, "Animation", ThreadPriority.AboveNormal)
+                    '控制 FPS
+                    Thread.Sleep(1)
+                Loop
+            Catch ex As Exception
+                Log(ex, "动画帧执行失败", LogLevel.Critical)
+            End Try
+        End Sub, "Animation", ThreadPriority.AboveNormal)
     End Sub
     End Sub
 
 
     ''' <summary>
     ''' <summary>
@@ -996,7 +998,7 @@ NextAni:
                 Case AniType.ScaleTransform
                 Case AniType.ScaleTransform
                     Dim Obj As FrameworkElement = Ani.Obj
                     Dim Obj As FrameworkElement = Ani.Obj
                     If TypeOf Obj.RenderTransform IsNot ScaleTransform Then
                     If TypeOf Obj.RenderTransform IsNot ScaleTransform Then
-                        Obj.RenderTransformOrigin = New Point(0.5, 0.5)
+                        If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         Obj.RenderTransform = New ScaleTransform(1, 1)
                         Obj.RenderTransform = New ScaleTransform(1, 1)
                     End If
                     End If
                     Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
                     Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
@@ -1006,7 +1008,7 @@ NextAni:
                 Case AniType.RotateTransform
                 Case AniType.RotateTransform
                     Dim Obj As FrameworkElement = Ani.Obj
                     Dim Obj As FrameworkElement = Ani.Obj
                     If TypeOf Obj.RenderTransform IsNot RotateTransform Then
                     If TypeOf Obj.RenderTransform IsNot RotateTransform Then
-                        Obj.RenderTransformOrigin = New Point(0.5, 0.5)
+                        If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         Obj.RenderTransform = New RotateTransform(0)
                         Obj.RenderTransform = New RotateTransform(0)
                     End If
                     End If
                     Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
                     Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))

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

@@ -13,13 +13,13 @@ Public Module ModBase
 #Region "声明"
 #Region "声明"
 
 
     '下列版本信息由更新器自动修改
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.10.9" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.10.9." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.11.0" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.11.0." & VersionBranchCode '标准格式的四段式版本号
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
 #If BETA Then
 #If BETA Then
-    Public Const VersionCode As Integer = 369 'Release
+    Public Const VersionCode As Integer = 372 'Release
 #Else
 #Else
-    Public Const VersionCode As Integer = 371 'Snapshot
+    Public Const VersionCode As Integer = 373 'Snapshot
 #End If
 #End If
     '自动生成的版本信息
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -498,6 +498,7 @@ Public Module ModBase
             RealNum += Digit * Scale
             RealNum += Digit * Scale
             Scale *= FromRadix
             Scale *= FromRadix
         Next
         Next
+        If RealNum = 0 Then Return "0"
         '转换为指定进制
         '转换为指定进制
         Dim Result = ""
         Dim Result = ""
         While RealNum > 0
         While RealNum > 0
@@ -1300,10 +1301,12 @@ Re:
                 Dim TotalCount As Long = Archive.Entries.Count
                 Dim TotalCount As Long = Archive.Entries.Count
                 For Each Entry As ZipArchiveEntry In Archive.Entries
                 For Each Entry As ZipArchiveEntry In Archive.Entries
                     If ProgressIncrementHandler IsNot Nothing AndAlso TotalCount > 0 Then ProgressIncrementHandler(1 / TotalCount)
                     If ProgressIncrementHandler IsNot Nothing AndAlso TotalCount > 0 Then ProgressIncrementHandler(1 / TotalCount)
-                    Dim DestinationPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
-                    If DestinationPath.EndsWithF("\") OrElse DestinationPath.EndsWithF("/") Then Continue For '不创建空文件夹
-                    Directory.CreateDirectory(GetPathFromFullPath(DestinationPath))
-                    Entry.ExtractToFile(DestinationPath, True)
+                    Dim DestinationFullPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
+                    If Not IO.Path.GetFullPath(DestinationFullPath).StartsWithF(IO.Path.GetFullPath(DestDirectory), True) Then Continue For 'ZipSlip 漏洞修复
+                    If DestinationFullPath.EndsWithF("\") OrElse DestinationFullPath.EndsWithF("/") Then Continue For '不创建空文件夹
+                    Dim DestinationDirectory As String = GetPathFromFullPath(DestinationFullPath)
+                    Directory.CreateDirectory(DestinationDirectory)
+                    Entry.ExtractToFile(DestinationFullPath.Replace(DestinationDirectory, ShortenPath(DestinationDirectory)), True) '#7329
                 Next
                 Next
             End Using
             End Using
         End If
         End If
@@ -2368,7 +2371,7 @@ NextElement:
     ''' <param name="Arguments">运行参数。</param>
     ''' <param name="Arguments">运行参数。</param>
     ''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
     ''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
     Public Function StartProcessAndGetOutput(FileName As String, Optional Arguments As String = "", Optional Timeout As Integer = 1000000,
     Public Function StartProcessAndGetOutput(FileName As String, Optional Arguments As String = "", Optional Timeout As Integer = 1000000,
-                                      Optional WorkingDirectory As String = Nothing) As String
+        Optional WorkingDirectory As String = Nothing, Optional Encoding As Encoding = Nothing, Optional PrintLog As Boolean = True) As String
         Dim Info = New ProcessStartInfo With {
         Dim Info = New ProcessStartInfo With {
             .Arguments = Arguments,
             .Arguments = Arguments,
             .FileName = ShortenPath(FileName),
             .FileName = ShortenPath(FileName),
@@ -2376,7 +2379,9 @@ NextElement:
             .CreateNoWindow = True,
             .CreateNoWindow = True,
             .RedirectStandardError = True,
             .RedirectStandardError = True,
             .RedirectStandardOutput = True,
             .RedirectStandardOutput = True,
-            .WorkingDirectory = ShortenPath(If(WorkingDirectory, Path.TrimEnd("\"c)))
+            .WorkingDirectory = ShortenPath(If(WorkingDirectory, Path.TrimEnd("\"c))),
+            .StandardErrorEncoding = Encoding,
+            .StandardOutputEncoding = Encoding
         }
         }
         If WorkingDirectory IsNot Nothing Then
         If WorkingDirectory IsNot Nothing Then
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
@@ -2385,7 +2390,7 @@ NextElement:
                 Info.EnvironmentVariables.Add("appdata", WorkingDirectory)
                 Info.EnvironmentVariables.Add("appdata", WorkingDirectory)
             End If
             End If
         End If
         End If
-        Log("[System] 执行外部命令并等待返回结果:" & FileName & " " & Arguments)
+        If PrintLog Then Log("[System] 执行命令并等待返回结果:" & FileName & " " & Arguments)
         Using Program As New Process With {.StartInfo = Info}
         Using Program As New Process With {.StartInfo = Info}
             Program.Start()
             Program.Start()
             Dim Result As String = Program.StandardOutput.ReadToEnd & Program.StandardError.ReadToEnd
             Dim Result As String = Program.StandardOutput.ReadToEnd & Program.StandardError.ReadToEnd
@@ -3111,7 +3116,7 @@ Retry:
     End Function
     End Function
 
 
     ''' <summary>
     ''' <summary>
-    ''' 取随机整数。
+    ''' 取随机整数(包含)。
     ''' </summary>
     ''' </summary>
     Public Function RandomInteger(min As Integer, max As Integer) As Integer
     Public Function RandomInteger(min As Integer, max As Integer) As Integer
         Return Math.Floor((max - min + 1) * Random.NextDouble) + min
         Return Math.Floor((max - min + 1) * Random.NextDouble) + min

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

@@ -8,6 +8,7 @@
         Implements ILoadingTrigger
         Implements ILoadingTrigger
 
 
         Public ReadOnly Property IsLoader As Boolean = True Implements ILoadingTrigger.IsLoader
         Public ReadOnly Property IsLoader As Boolean = True Implements ILoadingTrigger.IsLoader
+        Protected Shared LogBlackList As String() = {"EasyTier CLI"}
 
 
         '基础属性
         '基础属性
         ''' <summary>
         ''' <summary>
@@ -89,7 +90,7 @@
                 Dim OldState = _State
                 Dim OldState = _State
                 If value = LoadState.Finished AndAlso Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
                 If value = LoadState.Finished AndAlso Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
                 _State = value
                 _State = value
-                Log("[Loader] 加载器 " & Name & " 状态改变:" & GetStringFromEnum(value))
+                If Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载器 {Name} 状态改变:{GetStringFromEnum(value)}")
                 '实现 ILoadingTrigger 接口与 OnStateChanged 回调
                 '实现 ILoadingTrigger 接口与 OnStateChanged 回调
                 RunInUi(
                 RunInUi(
                 Sub()
                 Sub()
@@ -170,6 +171,7 @@
         '状态变化
         '状态变化
         Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
         Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
         Public MustOverride Sub Abort()
         Public MustOverride Sub Abort()
+        Public MustOverride Sub Failed(Ex As Exception)
 
 
         '等待结束
         '等待结束
         Public Const WaitForExitTimeoutMessage As String = "等待加载器执行超时。"
         Public Const WaitForExitTimeoutMessage As String = "等待加载器执行超时。"
@@ -290,6 +292,7 @@
             '检验输入以确定情况
             '检验输入以确定情况
             If IsForceRestart Then Return True '强制要求重启
             If IsForceRestart Then Return True '强制要求重启
             If ((Input Is Nothing) <> (Me.Input Is Nothing)) OrElse (Input IsNot Nothing AndAlso Not Input.Equals(Me.Input)) Then Return True '输入不同
             If ((Input Is Nothing) <> (Me.Input Is Nothing)) OrElse (Input IsNot Nothing AndAlso Not Input.Equals(Me.Input)) Then Return True '输入不同
+
             If (State = LoadState.Loading OrElse State = LoadState.Finished) AndAlso '正在加载或已结束…
             If (State = LoadState.Loading OrElse State = LoadState.Finished) AndAlso '正在加载或已结束…
                (IgnoreReloadTimeout OrElse ReloadTimeout = -1 OrElse
                (IgnoreReloadTimeout OrElse ReloadTimeout = -1 OrElse
                LastFinishedTime = 0 OrElse GetTimeTick() - LastFinishedTime < ReloadTimeout) Then '…且没有超时…
                LastFinishedTime = 0 OrElse GetTimeTick() - LastFinishedTime < ReloadTimeout) Then '…且没有超时…
@@ -316,13 +319,13 @@
             Sub()
             Sub()
                 Try
                 Try
                     IsForceRestarting = IsForceRestart
                     IsForceRestarting = IsForceRestart
-                    If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
+                    If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
                     LoadDelegate(Me)
                     LoadDelegate(Me)
                     If IsAborted Then
                     If IsAborted Then
                         Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
                         Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
                         Return
                         Return
                     End If
                     End If
-                    If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
+                    If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
                     RaisePreviewFinish()
                     RaisePreviewFinish()
                     State = LoadState.Finished
                     State = LoadState.Finished
                     LastFinishedTime = GetTimeTick() '未中断,本次输出有效
                     LastFinishedTime = GetTimeTick() '未中断,本次输出有效
@@ -335,25 +338,33 @@
                     '如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
                     '如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
                     If Not IsAborted Then State = LoadState.Aborted
                     If Not IsAborted Then State = LoadState.Aborted
                 Catch ex As Exception
                 Catch ex As Exception
-                    If IsAborted Then Return
-                    Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
-                    [Error] = ex
-                    State = LoadState.Failed
+                    Failed(ex)
                 End Try
                 End Try
             End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority}
             End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority}
             LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
             LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
         End Sub
         End Sub
+        Public Overrides Sub Failed(ex As Exception)
+            SyncLock LockState
+                If IsAborted OrElse State >= LoadState.Finished Then Return
+                State = LoadState.Failed
+            End SyncLock
+            Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
+            [Error] = ex
+            TriggerThreadAbort()
+        End Sub
         Public Overrides Sub Abort()
         Public Overrides Sub Abort()
-            If State <> LoadState.Loading Then Return
             SyncLock LockState
             SyncLock LockState
+                If State <> LoadState.Loading Then Return
                 State = LoadState.Aborted
                 State = LoadState.Aborted
             End SyncLock
             End SyncLock
             TriggerThreadAbort()
             TriggerThreadAbort()
         End Sub
         End Sub
         Private Sub TriggerThreadAbort()
         Private Sub TriggerThreadAbort()
             If LastRunningThread Is Nothing Then Return
             If LastRunningThread Is Nothing Then Return
-            If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({LastRunningThread.ManagedThreadId}) 已中断")
-            If LastRunningThread.IsAlive Then LastRunningThread.Interrupt()
+            If LastRunningThread.IsAlive Then
+                LastRunningThread.Interrupt()
+                If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({LastRunningThread.ManagedThreadId}) 已中断")
+            End If
             LastRunningThread = Nothing
             LastRunningThread = Nothing
         End Sub
         End Sub
 
 
@@ -452,6 +463,17 @@
                 Next
                 Next
             End Sub)
             End Sub)
         End Sub
         End Sub
+        Public Overrides Sub Failed(Ex As Exception)
+            SyncLock LockState
+                If State >= LoadState.Finished Then Return
+                State = LoadState.Failed
+            End SyncLock
+            [Error] = Ex
+            For Each Loader In Loaders
+                Loader.Abort()
+            Next
+            FrmMain.BtnExtraDownload.ShowRefresh()
+        End Sub
 
 
         ''' <summary>
         ''' <summary>
         ''' 子任务状态变更。
         ''' 子任务状态变更。
@@ -469,18 +491,9 @@
                 Case LoadState.Aborted
                 Case LoadState.Aborted
                     '被中断,这个任务也中断
                     '被中断,这个任务也中断
                     Abort()
                     Abort()
-                Case Else
+                Case LoadState.Failed
                     '完蛋,出错了
                     '完蛋,出错了
-                    SyncLock LockState
-                        If State >= LoadState.Finished Then Return
-                        [Error] = New Exception(Loader.Name & "失败", Loader.Error)
-                        State = Loader.State
-                    End SyncLock
-                    For Each Loader In Loaders
-                        Loader.Abort()
-                    Next
-                    FrmMain.BtnExtraDownload.ShowRefresh()
-                    Return
+                    Failed(New Exception(Loader.Name & "失败", Loader.Error))
             End Select
             End Select
         End Sub
         End Sub
         ''' <summary>
         ''' <summary>

+ 30 - 4
Plain Craft Launcher 2/Modules/Base/ModNet.vb

@@ -1063,7 +1063,7 @@ NotSupportRange:
                 ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
                 ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
                 If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
                 If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
                 Dim ResponseBytes As Byte() = New Byte(16384) {}
                 Dim ResponseBytes As Byte() = New Byte(16384) {}
-                HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetAwaiter().GetResult()
+                HttpDataCount = ReadWithTimeout(ResponseStream, ResponseBytes, 0, 16384, Timeout)
                 While (IsUnknownSize OrElse Th.DownloadUndone > 0) AndAlso '判断是否下载完成
                 While (IsUnknownSize OrElse Th.DownloadUndone > 0) AndAlso '判断是否下载完成
                             HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Th.Source.IsFailed OrElse Th.Source.SingleThread = Th)
                             HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Th.Source.IsFailed OrElse Th.Source.SingleThread = Th)
                     '限速
                     '限速
@@ -1113,7 +1113,7 @@ NotSupportRange:
                         '无数据,且已超时
                         '无数据,且已超时
                         Throw New TimeoutException("操作超时,无数据。")
                         Throw New TimeoutException("操作超时,无数据。")
                     End If
                     End If
-                    HttpDataCount = ResponseStream.Read(ResponseBytes, 0, 16384)
+                    HttpDataCount = ReadWithTimeout(ResponseStream, ResponseBytes, 0, 16384, Timeout)
                 End While
                 End While
 SourceBreak:
 SourceBreak:
                 If State = NetState.Error OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
                 If State = NetState.Error OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
@@ -1190,7 +1190,7 @@ SourceBreak:
                     ElseIf Not Retried Then
                     ElseIf Not Retried Then
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
                         '若所有源均不支持 Range,也会走到这里重试
                         '若所有源均不支持 Range,也会走到这里重试
-                        If Not IsRangeNotSupported Then Hint($"{LocalName}:文件下载失败,正在自动重试……", If(ModeDebug, HintType.Red, HintType.Blue)) '在调试模式下给出红色提示
+                        If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", LogLevel.Debug)
                         Retried = True
                         Retried = True
                         SyncLock LockSource
                         SyncLock LockSource
                             SourcesOnce.Clear()
                             SourcesOnce.Clear()
@@ -1642,9 +1642,12 @@ Retry:
             Next
             Next
             OnFail(File.Ex)
             OnFail(File.Ex)
         End Sub
         End Sub
+        Public Overrides Sub Failed(Ex As Exception)
+            OnFail(New List(Of Exception) From {Ex})
+        End Sub
         Public Sub OnFail(ExList As List(Of Exception))
         Public Sub OnFail(ExList As List(Of Exception))
             SyncLock LockState
             SyncLock LockState
-                If State > LoadState.Loading Then Return
+                If State >= LoadState.Finished Then Return
                 If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
                 If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
                 '寻找有效的错误信息
                 '寻找有效的错误信息
                 Dim UsefulExs = ExList.Where(Function(e) TypeOf e IsNot OperationCanceledException AndAlso TypeOf e IsNot ThreadInterruptedException).ToList
                 Dim UsefulExs = ExList.Where(Function(e) TypeOf e IsNot OperationCanceledException AndAlso TypeOf e IsNot ThreadInterruptedException).ToList
@@ -1982,4 +1985,27 @@ Retry:
         }.Any(Function(k) Detail.ContainsF(k, True))
         }.Any(Function(k) Detail.ContainsF(k, True))
     End Function
     End Function
 
 
+    ''' <summary>
+    ''' 随机获取一个可用的端口。
+    ''' </summary>
+    Public Function GetAvailablePort() As Integer
+        Dim Listener As New TcpListener(IPAddress.Loopback, 0)
+        Listener.Start()
+        Dim port As Integer = CType(Listener.LocalEndpoint, IPEndPoint).Port
+        Listener.Stop()
+        Return port
+    End Function
+
+    ''' <summary>
+    ''' 带有 Timeout 的读取 Stream 方法。
+    ''' </summary>
+    Private Function ReadWithTimeout(TargetStream As Stream, Buffer() As Byte, Offset As Integer, Count As Integer, ReadTimeoutMs As Integer) As Integer
+        Dim ReadTask = TargetStream.ReadAsync(Buffer, Offset, Count)
+        Dim DelayTask = Task.Delay(ReadTimeoutMs)
+        If Task.WhenAny(ReadTask, DelayTask).GetAwaiter().GetResult() Is DelayTask Then
+            Throw New TimeoutException($"读取数据超时({ReadTimeoutMs} ms)")
+        End If
+        Return ReadTask.GetAwaiter().GetResult()
+    End Function
+
 End Module
 End Module

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

@@ -1211,11 +1211,11 @@ Retry:
         Function(Project As CompProject) As Double
         Function(Project As CompProject) As Double
             Select Case Request.Type
             Select Case Request.Type
                 Case CompType.Mod, CompType.ModPack
                 Case CompType.Mod, CompType.ModPack
-                    Return If(Project.FromCurseForge, 1, 7)
+                    Return If(Project.FromCurseForge, 1, 5)
                 Case CompType.DataPack
                 Case CompType.DataPack
                     Return If(Project.FromCurseForge, 10, 1)
                     Return If(Project.FromCurseForge, 10, 1)
                 Case CompType.ResourcePack, CompType.Shader
                 Case CompType.ResourcePack, CompType.Shader
-                    Return If(Project.FromCurseForge, 1, 5)
+                    Return If(Project.FromCurseForge, 1, 4)
                 Case Else
                 Case Else
                     Return 1
                     Return 1
             End Select
             End Select

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

@@ -1463,9 +1463,7 @@ Retry:
         'Authlib-Injector
         'Authlib-Injector
         If McLoginLoader.Output.Type = "Auth" Then
         If McLoginLoader.Output.Type = "Auth" Then
             If McLaunchJavaSelected.VersionCode >= 6 Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") '信任系统根证书(5252#)
             If McLaunchJavaSelected.VersionCode >= 6 Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") '信任系统根证书(5252#)
-            Dim Server As String = If(McLoginLoader.Input.Type = McLoginType.Legacy,
-                "http://hiperauth.tech/api/yggdrasil-hiper/", 'HiPer 登录
-                Setup.Get("VersionServerAuthServer", McVersionCurrent))
+            Dim Server As String = Setup.Get("VersionServerAuthServer", McVersionCurrent)
             Try
             Try
                 Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8)
                 Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8)
                 DataList.Insert(0, "-javaagent:""" & PathPure & "authlib-injector.jar""=" & Server &
                 DataList.Insert(0, "-javaagent:""" & PathPure & "authlib-injector.jar""=" & Server &

+ 20 - 15
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -1926,17 +1926,29 @@ OnLoaded:
                              .OriginalName = Library("name"),
                              .OriginalName = Library("name"),
                              .Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
                              .Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
                              .LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
                              .LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
-                                 McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
-                                 CustomMcFolder & "libraries\" & Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")),
+                                 McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
+                                    Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").
+                                    Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
+                                    $"{CustomMcFolder}libraries\{Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")}"),
                              .Size = Val(Library("downloads")("classifiers")("natives-windows")("size").ToString),
                              .Size = Val(Library("downloads")("classifiers")("natives-windows")("size").ToString),
                              .IsNatives = True,
                              .IsNatives = True,
                              .SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
                              .SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
                     Else
                     Else
-                        BasicArray.Add(New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")), .Size = 0, .IsNatives = True, .SHA1 = Nothing})
+                        BasicArray.Add(
+                        New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
+                            .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
+                                 Replace(".jar", $"-{Library("natives")("windows")}.jar").
+                                 Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
+                        })
                     End If
                     End If
                 Catch ex As Exception
                 Catch ex As Exception
                     Log(ex, "处理实际支持库列表失败(有 Natives," & If(Library("name"), "Nothing").ToString & ")")
                     Log(ex, "处理实际支持库列表失败(有 Natives," & If(Library("name"), "Nothing").ToString & ")")
-                    BasicArray.Add(New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")), .Size = 0, .IsNatives = True, .SHA1 = Nothing})
+                    BasicArray.Add(
+                        New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
+                             .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
+                                 Replace(".jar", $"-{Library("natives")("windows")}.jar").
+                                 Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
+                        })
                 End Try
                 End Try
             End If
             End If
 
 
@@ -2130,19 +2142,12 @@ OnLoaded:
     ''' <param name="WithHead">是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。</param>
     ''' <param name="WithHead">是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。</param>
     Public Function McLibGet(Original As String, Optional WithHead As Boolean = True, Optional IgnoreLiteLoader As Boolean = False, Optional CustomMcFolder As String = Nothing) As String
     Public Function McLibGet(Original As String, Optional WithHead As Boolean = True, Optional IgnoreLiteLoader As Boolean = False, Optional CustomMcFolder As String = Nothing) As String
         CustomMcFolder = If(CustomMcFolder, PathMcFolder)
         CustomMcFolder = If(CustomMcFolder, PathMcFolder)
+        '有时候原始的有四段,例如 net.minecraftforge:forge:1.21.4-54.0.34:client
+        '文件夹名应该忽略最后一段(client),但文件名需要保留
+        '参见 #7306(以及 #5376 也是因为这里没有处理好导致的)
         Dim Splited = Original.Split(":")
         Dim Splited = Original.Split(":")
         McLibGet = If(WithHead, CustomMcFolder & "libraries\", "") &
         McLibGet = If(WithHead, CustomMcFolder & "libraries\", "") &
-                   Splited(0).Replace(".", "\") & "\" & Splited(1) & "\" & Splited(2) & "\" & Splited(1) & "-" & Splited(2) & ".jar"
-        '判断 OptiFine 是否应该使用 installer
-        If McLibGet.Contains("optifine\OptiFine\1.") AndAlso Splited(2).Split(".").Count > 1 Then
-            Dim MajorVersion As Integer = Val(Splited(2).Split(".")(1).BeforeFirst("_"))
-            Dim MinorVersion As Integer = If(Splited(2).Split(".").Count > 2, Val(Splited(2).Split(".")(2).BeforeFirst("_")), 0)
-            If (MajorVersion = 12 OrElse (MajorVersion = 20 AndAlso MinorVersion >= 4) OrElse MajorVersion >= 21) AndAlso '仅在 1.12 (无法追溯) 和 1.20.4+ (#5376) 遇到此问题
-                File.Exists($"{CustomMcFolder}libraries\{Splited(0).Replace(".", "\")}\{Splited(1)}\{Splited(2)}\{Splited(1)}-{Splited(2)}-installer.jar") Then
-                McLaunchLog("已将 " & Original & " 替换为对应的 Installer 文件")
-                McLibGet = McLibGet.Replace(".jar", "-installer.jar")
-            End If
-        End If
+            $"{Splited(0).Replace(".", "\")}\{Splited(1)}\{Splited(2)}\{Splited.Skip(1).Join("-")}.jar"
     End Function
     End Function
 
 
     ''' <summary>
     ''' <summary>

+ 23 - 13
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -207,7 +207,14 @@ Public Class CustomEvent
                 Case EventType.复制文本
                 Case EventType.复制文本
                     ClipboardSet(Arg)
                     ClipboardSet(Arg)
 
 
-                Case EventType.刷新主页, EventType.刷新页面
+                Case EventType.刷新主页
+                    RunInUi(
+                    Sub()
+                        FrmLaunchRight.ForceRefresh()
+                        If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
+                    End Sub)
+
+                Case EventType.刷新页面
                     If TypeOf FrmMain.PageRight Is IRefreshable Then
                     If TypeOf FrmMain.PageRight Is IRefreshable Then
                         RunInUiWait(Sub() CType(FrmMain.PageRight, IRefreshable).Refresh())
                         RunInUiWait(Sub() CType(FrmMain.PageRight, IRefreshable).Refresh())
                         If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
                         If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
@@ -250,18 +257,21 @@ Public Class CustomEvent
                         Return
                         Return
                     End If
                     End If
                     If Not EventSafetyConfirm("即将从该网址下载文件:" & vbCrLf & Args(0)) Then Return
                     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
+                    RunInUi(
+                    Sub()
+                        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
+                    End Sub)
 
 
                 Case EventType.修改设置, EventType.写入设置
                 Case EventType.修改设置, EventType.写入设置
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")

+ 8 - 12
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -237,10 +237,6 @@ EndHint:
         '将弹窗列入队列
         '将弹窗列入队列
         Dim Converter As New MyMsgBoxConverter With {.Type = MyMsgBoxType.Text, .Button1 = Button1, .Button2 = Button2, .Button3 = Button3, .Text = Caption, .IsWarn = IsWarn, .Title = Title, .HighLight = HighLight, .ForceWait = True, .Button1Action = Button1Action, .Button2Action = Button2Action, .Button3Action = Button3Action}
         Dim Converter As New MyMsgBoxConverter With {.Type = MyMsgBoxType.Text, .Button1 = Button1, .Button2 = Button2, .Button3 = Button3, .Text = Caption, .IsWarn = IsWarn, .Title = Title, .HighLight = HighLight, .ForceWait = True, .Button1Action = Button1Action, .Button2Action = Button2Action, .Button3Action = Button3Action}
         WaitingMyMsgBox.Add(Converter)
         WaitingMyMsgBox.Add(Converter)
-        If RunInUi() Then
-            '若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗
-            MyMsgBoxTick()
-        End If
         If Button2.Length > 0 OrElse ForceWait Then
         If Button2.Length > 0 OrElse ForceWait Then
             '若有多个按钮则开始等待
             '若有多个按钮则开始等待
             If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing AndAlso RunInUi() Then
             If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing AndAlso RunInUi() Then
@@ -264,6 +260,7 @@ EndHint:
             Else
             Else
                 Try
                 Try
                     FrmMain.DragStop()
                     FrmMain.DragStop()
+                    If RunInUi() Then MyMsgBoxTick()
                     ComponentDispatcher.PushModal()
                     ComponentDispatcher.PushModal()
                     Dispatcher.PushFrame(Converter.WaitFrame)
                     Dispatcher.PushFrame(Converter.WaitFrame)
                 Finally
                 Finally
@@ -295,6 +292,7 @@ EndHint:
         '虽然我也不知道这是啥但是能用就成了 :)
         '虽然我也不知道这是啥但是能用就成了 :)
         Try
         Try
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            If RunInUi() Then MyMsgBoxTick()
             ComponentDispatcher.PushModal()
             ComponentDispatcher.PushModal()
             Dispatcher.PushFrame(Converter.WaitFrame)
             Dispatcher.PushFrame(Converter.WaitFrame)
         Finally
         Finally
@@ -317,6 +315,7 @@ EndHint:
         '虽然我也不知道这是啥但是能用就成了 :)
         '虽然我也不知道这是啥但是能用就成了 :)
         Try
         Try
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            If RunInUi() Then MyMsgBoxTick()
             ComponentDispatcher.PushModal()
             ComponentDispatcher.PushModal()
             Dispatcher.PushFrame(Converter.WaitFrame)
             Dispatcher.PushFrame(Converter.WaitFrame)
         Finally
         Finally
@@ -339,7 +338,7 @@ EndHint:
             ElseIf WaitingMyMsgBox.Any Then
             ElseIf WaitingMyMsgBox.Any Then
                 '没有弹窗,显示一个等待的弹窗
                 '没有弹窗,显示一个等待的弹窗
                 FrmMain.PanMsg.Visibility = Visibility.Visible
                 FrmMain.PanMsg.Visibility = Visibility.Visible
-                Select Case CType(WaitingMyMsgBox(0), MyMsgBoxConverter).Type
+                Select Case WaitingMyMsgBox(0).Type
                     Case MyMsgBoxType.Input
                     Case MyMsgBoxType.Input
                         FrmMain.PanMsg.Children.Add(New MyMsgInput(WaitingMyMsgBox(0)))
                         FrmMain.PanMsg.Children.Add(New MyMsgInput(WaitingMyMsgBox(0)))
                     Case MyMsgBoxType.Select
                     Case MyMsgBoxType.Select
@@ -352,7 +351,7 @@ EndHint:
                 WaitingMyMsgBox.RemoveAt(0)
                 WaitingMyMsgBox.RemoveAt(0)
             Else
             Else
                 '没有弹窗,没有等待的弹窗
                 '没有弹窗,没有等待的弹窗
-                If Not FrmMain.PanMsg.Visibility = Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
+                If FrmMain.PanMsg.Visibility <> Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
             Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
@@ -377,11 +376,7 @@ EndHint:
     Public FrmSpeedRight As PageSpeedRight
     Public FrmSpeedRight As PageSpeedRight
 
 
     '联机页面声明
     '联机页面声明
-    Public FrmLinkLeft As PageLinkLeft
-    Public FrmLinkIoi As PageLinkIoi
-    Public FrmLinkHiper As PageLinkHiper
-    Public FrmLinkHelp As PageOtherHelpDetail
-    Public FrmLinkFeedback As PageLinkFeedback
+    Public FrmLinkMain As PageLinkMain
 
 
     '下载页面声明
     '下载页面声明
     Public FrmDownloadLeft As PageDownloadLeft
     Public FrmDownloadLeft As PageDownloadLeft
@@ -863,7 +858,7 @@ NextFile:
         '登录信息
         '登录信息
         If McLoginLoader.State = LoadState.Finished Then
         If McLoginLoader.State = LoadState.Finished Then
             Text = Text.Replace("{user}", Replacer(McLoginLoader.Output.Name))
             Text = Text.Replace("{user}", Replacer(McLoginLoader.Output.Name))
-            Text = Text.Replace("{uuid}", Replacer(McLoginLoader.Output.Uuid.ToLower))
+            Text = Text.Replace("{uuid}", Replacer(McLoginLoader.Output.Uuid?.ToLower))
             Select Case McLoginLoader.Input.Type
             Select Case McLoginLoader.Input.Type
                 Case McLoginType.Legacy
                 Case McLoginType.Legacy
                     Text = Text.Replace("{login}", Replacer("离线"))
                     Text = Text.Replace("{login}", Replacer("离线"))
@@ -884,6 +879,7 @@ NextFile:
         Text = Text.RegexReplaceEach("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
         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("\{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, m.Groups(2).Value)))
         Text = Text.RegexReplaceEach("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
+        Text = Text.RegexReplaceEach("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
         Return Text
         Return Text
     End Function
     End Function
 
 

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

@@ -281,9 +281,16 @@ Friend Module ModSecret
 
 
 #End Region
 #End Region
 
 
-#Region "联网通知"
+#Region "联网配置"
 
 
-    Public ServerLoader As New LoaderTask(Of Integer, Integer)("PCL 服务", Sub() Log("[Server] 该版本中不包含更新通知功能……"), Priority:=ThreadPriority.BelowNormal)
+    ''' <summary>
+    ''' 联网获取的配置信息。
+    ''' 若获取失败或仍在获取中,可能为 Nothing。
+    ''' </summary>
+    Public ServerConfig As JObject
+
+    Public ServerLoader As New LoaderTask(Of Integer, Integer)("PCL 配置更新", Sub() Log("[Server] 该版本中不包含更新通知功能……"), Priority:=ThreadPriority.BelowNormal) With
+        {.ReloadTimeout = 1000 * 60 * 60} '超时 1 小时
 
 
 #End Region
 #End Region
 
 

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

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

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

@@ -1,5 +1,4 @@
 Imports System.IO.Compression
 Imports System.IO.Compression
-Imports System.Net.Http
 
 
 Public Module ModDownloadLib
 Public Module ModDownloadLib
 
 
@@ -1345,7 +1344,7 @@ Retry:
                         Dim RawJson As JObject = GetJson(NetRequestByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         Dim RawJson As JObject = GetJson(NetRequestByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg]
                         '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg]
                         Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@")
                         Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@")
-                        Dim Address = McLibGet(OriginalName).Replace(".jar", "-mappings." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
+                        Dim Address = McLibGet(OriginalName).Replace(".jar", "." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
                         Dim ClientMappings As JToken = RawJson("downloads")("client_mappings")
                         Dim ClientMappings As JToken = RawJson("downloads")("client_mappings")
                         Libs.Add(New McLibToken With {
                         Libs.Add(New McLibToken With {
                                  .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,
                                  .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,

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

@@ -613,16 +613,16 @@
             PanMinecraft.Children.Clear()
             PanMinecraft.Children.Clear()
             '添加最新版本
             '添加最新版本
             Dim CardInfo As New MyCard With {.Title = "最新版本", .Margin = New Thickness(0, 15, 0, 15), .SwapType = 2}
             Dim CardInfo As New MyCard With {.Title = "最新版本", .Margin = New Thickness(0, 15, 0, 15), .SwapType = 2}
-            Dim TopestVersions As New List(Of JObject)
+            Dim PinnedVersions As New List(Of JObject)
             Dim Release As JObject = Dict("正式版")(0).DeepClone()
             Dim Release As JObject = Dict("正式版")(0).DeepClone()
             Release("lore") = "最新正式版,发布于 " & Release("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
             Release("lore") = "最新正式版,发布于 " & Release("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
-            TopestVersions.Add(Release)
+            PinnedVersions.Add(Release)
             If Dict("正式版")(0)("releaseTime").Value(Of Date) < Dict("预览版")(0)("releaseTime").Value(Of Date) Then
             If Dict("正式版")(0)("releaseTime").Value(Of Date) < Dict("预览版")(0)("releaseTime").Value(Of Date) Then
                 Dim Snapshot As JObject = Dict("预览版")(0).DeepClone()
                 Dim Snapshot As JObject = Dict("预览版")(0).DeepClone()
                 Snapshot("lore") = "最新预览版,发布于 " & Snapshot("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
                 Snapshot("lore") = "最新预览版,发布于 " & Snapshot("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
-                TopestVersions.Add(Snapshot)
+                PinnedVersions.Add(Snapshot)
             End If
             End If
-            Dim PanInfo As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = TopestVersions}
+            Dim PanInfo As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = PinnedVersions}
             MyCard.StackInstall(PanInfo, 7)
             MyCard.StackInstall(PanInfo, 7)
             CardInfo.Children.Add(PanInfo)
             CardInfo.Children.Add(PanInfo)
             PanMinecraft.Children.Insert(0, CardInfo)
             PanMinecraft.Children.Insert(0, CardInfo)
@@ -630,7 +630,7 @@
             For Each Pair As KeyValuePair(Of String, List(Of JObject)) In Dict
             For Each Pair As KeyValuePair(Of String, List(Of JObject)) In Dict
                 If Not Pair.Value.Any() Then Continue For
                 If Not Pair.Value.Any() Then Continue For
                 '增加卡片
                 '增加卡片
-                Dim NewCard As New MyCard With {.Title = Pair.Key & " (" & Pair.Value.Count & ")", .Margin = New Thickness(0, 0, 0, 15), .SwapType = 7}
+                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15), .SwapType = 7}
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack

+ 1 - 1
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml

@@ -5,7 +5,7 @@
     <local:MyScrollViewer PanningMode="VerticalOnly" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Style="{StaticResource ScrollViewerFullMargin}">
     <local:MyScrollViewer PanningMode="VerticalOnly" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Style="{StaticResource ScrollViewerFullMargin}">
         <StackPanel Margin="0,12,0,0" Name="PanItem">
         <StackPanel Margin="0,12,0,0" Name="PanItem">
 
 
-            <local:MyListItem x:Name="ItemInstall" Checked="True" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="0" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Minecraft" 
+            <local:MyListItem x:Name="ItemInstall" Checked="True" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="0" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="原版游戏" 
                           Logo="m351.9,801.7c-8.6,0-17-3-23.7-8.4L18.9,614.7c-12.5-7.2-19.7-20.5-18.8-34.9V221c-.7-15,6.6-28.8,19.2-36L325.2,8.3c6.6-5.3,15.1-8.3,24-8.3s17.3,2.9,23.9,8.2l308.3,178c.2.1.5.3.8.5h0c12,6.7,19.4,19.8,19.4,34.1,0,13.9-7.1,26-18.7,32.5l-290.1,167.5v276l234-135.1v-168.9c0-20.8,17-37.8,37.8-37.8s37.8,17,37.8,37.8v191.7c0,16.4-10.9,31.2-26.5,36.1l-299.4,172.8c-6.7,5.4-15.1,8.3-23.7,8.3h-.9Zm-34.8-101.9v-276l-241.4-139.4v276l241.4,139.4Zm35.3-342.8l236.8-136.7-240-138.6L112.4,218.3l240,138.6Z">
                           Logo="m351.9,801.7c-8.6,0-17-3-23.7-8.4L18.9,614.7c-12.5-7.2-19.7-20.5-18.8-34.9V221c-.7-15,6.6-28.8,19.2-36L325.2,8.3c6.6-5.3,15.1-8.3,24-8.3s17.3,2.9,23.9,8.2l308.3,178c.2.1.5.3.8.5h0c12,6.7,19.4,19.8,19.4,34.1,0,13.9-7.1,26-18.7,32.5l-290.1,167.5v276l234-135.1v-168.9c0-20.8,17-37.8,37.8-37.8s37.8,17,37.8,37.8v191.7c0,16.4-10.9,31.2-26.5,36.1l-299.4,172.8c-6.7,5.4-15.1,8.3-23.7,8.3h-.9Zm-34.8-101.9v-276l-241.4-139.4v276l241.4,139.4Zm35.3-342.8l236.8-136.7-240-138.6L112.4,218.3l240,138.6Z">
                 <local:MyListItem.Buttons>
                 <local:MyListItem.Buttons>
                     <x:Array Type="{x:Type local:MyIconButton}">
                     <x:Array Type="{x:Type local:MyIconButton}">

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

@@ -243,7 +243,7 @@ Retry:
                             {"Minecon2013", "Minecon 2013 参与者披风"}, {"Minecon2015", "Minecon 2015 参与者披风"}, {"Minecon2016", "Minecon 2016 参与者披风"},
                             {"Minecon2013", "Minecon 2013 参与者披风"}, {"Minecon2015", "Minecon 2015 参与者披风"}, {"Minecon2016", "Minecon 2016 参与者披风"},
                             {"Cherry Blossom", "樱花披风"}, {"15th Anniversary", "15 周年纪念披风"}, {"Purple Heart", "紫色心形披风"},
                             {"Cherry Blossom", "樱花披风"}, {"15th Anniversary", "15 周年纪念披风"}, {"Purple Heart", "紫色心形披风"},
                             {"Follower's", "追随者披风"}, {"MCC 15th Year", "MCC 15 周年披风"}, {"Minecraft Experience", "村民救援披风"},
                             {"Follower's", "追随者披风"}, {"MCC 15th Year", "MCC 15 周年披风"}, {"Minecraft Experience", "村民救援披风"},
-                            {"Mojang Office", "Mojang 办公室披风"}, {"Home", "家园披风"}, {"Menace", "入侵披风"}, {"Yearn", "渴望披风"},
+                            {"Mojang Office", "Mojang 办公室披风"}, {"Waiting", "家园披风"}, {"Menace", "入侵披风"}, {"Yearn", "渴望披风"},
                             {"Common", "普通披风"}, {"Pan", "薄煎饼披风"}, {"Founder's", "创始人披风"}, {"Copper", "铜披风"}
                             {"Common", "普通披风"}, {"Pan", "薄煎饼披风"}, {"Founder's", "创始人披风"}, {"Copper", "铜披风"}
                         }
                         }
                         Dim SelectionControl As New List(Of IMyRadio) From {New MyRadioBox With {.Text = "无披风"}}
                         Dim SelectionControl As New List(Of IMyRadio) From {New MyRadioBox With {.Text = "无披风"}}

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

@@ -110,11 +110,7 @@
         '修改登陆方式
         '修改登陆方式
         Select Case Setup.Get("LoginType")
         Select Case Setup.Get("LoginType")
             Case McLoginType.Legacy
             Case McLoginType.Legacy
-                If PageLinkHiper.HiperState = LoadState.Finished Then
-                    LabLaunchingMethod.Text = "联机离线登录"
-                Else
-                    LabLaunchingMethod.Text = "离线登录"
-                End If
+                LabLaunchingMethod.Text = "离线登录"
             Case McLoginType.Ms
             Case McLoginType.Ms
                 LabLaunchingMethod.Text = "正版登录"
                 LabLaunchingMethod.Text = "正版登录"
             Case McLoginType.Nide
             Case McLoginType.Nide

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

@@ -113,14 +113,14 @@ Public Class PageLaunchRight
                 Log("[Page] 主页自定义数据来源:联网缓存文件")
                 Log("[Page] 主页自定义数据来源:联网缓存文件")
                 Content = ReadFile(PathTemp & "Cache\Custom.xaml")
                 Content = ReadFile(PathTemp & "Cache\Custom.xaml")
                 '后台更新缓存
                 '后台更新缓存
-                OnlineLoader.Start(Url)
+                OnlineLoader.Start(New Tuple(Of String, Boolean)(Url, False))
             Else
             Else
                 '缓存不可用
                 '缓存不可用
                 Log("[Page] 主页自定义数据来源:联网全新下载")
                 Log("[Page] 主页自定义数据来源:联网全新下载")
                 Hint("正在加载主页……")
                 Hint("正在加载主页……")
                 RunInUiWait(Sub() LoadContent(Nothing)) '在加载结束前清空页面
                 RunInUiWait(Sub() LoadContent(Nothing)) '在加载结束前清空页面
                 Setup.Set("CacheSavedPageVersion", "")
                 Setup.Set("CacheSavedPageVersion", "")
-                OnlineLoader.Start(Url) '下载完成后将会再次触发更新
+                OnlineLoader.Start(New Tuple(Of String, Boolean)(Url, True)) '下载完成后将会再次触发更新
                 Return
                 Return
             End If
             End If
         End If
         End If
@@ -130,9 +130,10 @@ Public Class PageLaunchRight
     Private RefreshLock As New Object
     Private RefreshLock As New Object
 
 
     '联网获取主页文件
     '联网获取主页文件
-    Private OnlineLoader As New LoaderTask(Of String, Integer)("下载主页", AddressOf OnlineLoaderSub) With {.ReloadTimeout = 10 * 60 * 1000}
-    Private Sub OnlineLoaderSub(Task As LoaderTask(Of String, Integer))
-        Dim Address As String = Task.Input '#3721 中连续触发两次导致内容变化
+    Private OnlineLoader As New LoaderTask(Of Tuple(Of String, Boolean), Integer)("下载主页", AddressOf OnlineLoaderSub) With {.ReloadTimeout = 10 * 60 * 1000}
+    Private Sub OnlineLoaderSub(Task As LoaderTask(Of Tuple(Of String, Boolean), Integer))
+        Dim Address As String = Task.Input.Item1 '#3721 中连续触发两次导致内容变化
+        Dim ShouldRefresh As Boolean = Task.Input.Item2
         Try
         Try
             '替换自定义变量与设置
             '替换自定义变量与设置
             Address = ArgumentReplace(Address, AddressOf WebUtility.HtmlEncode)
             Address = ArgumentReplace(Address, AddressOf WebUtility.HtmlEncode)
@@ -168,7 +169,7 @@ Public Class PageLaunchRight
             Setup.Set("CacheSavedPageVersion", Version)
             Setup.Set("CacheSavedPageVersion", Version)
             WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
             WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
             '若内容变更则要求刷新
             '若内容变更则要求刷新
-            If LoadedContentHash <> FileContent.GetHashCode() Then Refresh()
+            If LoadedContentHash <> FileContent.GetHashCode() AndAlso ShouldRefresh Then Refresh()
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, $"下载主页失败({Address})", If(ModeDebug, LogLevel.Msgbox, LogLevel.Hint))
             Log(ex, $"下载主页失败({Address})", If(ModeDebug, LogLevel.Msgbox, LogLevel.Hint))
         End Try
         End Try
@@ -195,7 +196,7 @@ Public Class PageLaunchRight
     ''' </summary>
     ''' </summary>
     Private Sub ClearCache()
     Private Sub ClearCache()
         LoadedContentHash = -1
         LoadedContentHash = -1
-        OnlineLoader.Input = ""
+        OnlineLoader.Input = New Tuple(Of String, Boolean)("", True)
         Setup.Set("CacheSavedPageUrl", "")
         Setup.Set("CacheSavedPageUrl", "")
         Setup.Set("CacheSavedPageVersion", "")
         Setup.Set("CacheSavedPageVersion", "")
         Log("[Page] 已清空主页缓存")
         Log("[Page] 已清空主页缓存")

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

@@ -1,19 +0,0 @@
-<local:MyPageRight
-    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-    mc:Ignorable="d" x:Class="PageLinkFeedback"
-    PanScroll="PanBack">
-    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
-        <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">
-                </StackPanel>
-            </local:MyCard>
-            <local:MyCard Title="发送反馈" Margin="0,0,0,15">
-                <StackPanel Margin="20,40,18,18">
-                </StackPanel>
-            </local:MyCard>
-        </StackPanel>
-    </local:MyScrollViewer>
-</local:MyPageRight>

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

@@ -1,2 +0,0 @@
-Public Class PageLinkFeedback
-End Class

Разница между файлами не показана из-за своего большого размера
+ 0 - 117
Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml


+ 0 - 395
Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb

@@ -1,395 +0,0 @@
-Public Class PageLinkHiper
-    Public Const RequestVersion As Char = "2"
-
-    '记录的启动情况
-    Public Shared IsServerSide As Boolean
-    Private Shared HostIp As String
-    Private Shared HostPort As Integer
-
-#Region "初始化"
-
-    '加载器初始化
-    Private Sub LoaderInit() Handles Me.Initialized
-        PageLoaderInit(Load, PanLoad, PanContent, PanAlways, InitLoader, AutoRun:=False)
-        '注册自定义的 OnStateChanged
-        AddHandler InitLoader.OnStateChangedUi, AddressOf OnLoadStateChanged
-    End Sub
-
-    Private IsLoad As Boolean = False
-    Private Sub OnLoaded() Handles Me.Loaded
-        FormMain.EndProgramForce(ProcessReturnValues.Aborted)
-        If IsLoad Then Return
-        IsLoad = True
-        '启动监视线程
-        If Not IsWatcherStarted Then RunInNewThread(AddressOf WatcherThread, "Hiper Watcher")
-        '读取索引码
-        Try
-            Dim Time As String = Setup.Get("LinkHiperCertTime")
-            If Time = "" Then
-                Log("[HiPer] 没有缓存凭证")
-            ElseIf Date.Parse(Time) > Date.Now Then
-                TextCert.Text = Setup.Get("LinkHiperCertLast")
-                Log("[HiPer] 缓存凭证尚未过期:" & Time)
-                CurrentSubpage = Subpages.PanSelect
-            Else
-                Log("[HiPer] 缓存凭证已过期:" & Time)
-                LabCertTitle.Text = "输入索引码"
-                LabCertDesc.Text = "你的 HiPer 索引码已经过期,请输入新的索引码。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
-            End If
-        Catch ex As Exception
-            Log(ex, "读取缓存凭证失败")
-            Setup.Set("LinkHiperCertTime", "")
-        End Try
-    End Sub
-
-#End Region
-
-#Region "加载步骤"
-
-    Public Shared PathHiper As String = PathAppdata & "联机模块\"
-    Public Shared WithEvents InitLoader As New LoaderCombo(Of Integer)("HiPer 初始化", {
-        New LoaderTask(Of Integer, Integer)("网络环境:连通检测", AddressOf InitPingCheck) With {.Block = False, .ProgressWeight = 0.5},
-        New LoaderTask(Of Integer, Integer)("网络环境:IP 检测", AddressOf InitIpCheck) With {.Block = False, .ProgressWeight = 1},
-        New LoaderTask(Of Integer, Integer)("检查网络环境", AddressOf InitCheck) With {.ProgressWeight = 0.5},
-        New LoaderTask(Of Integer, List(Of NetFile))("获取所需文件", AddressOf InitGetFile) With {.ProgressWeight = 4},
-        New LoaderDownload("下载所需文件", New List(Of NetFile)) With {.ProgressWeight = 4},
-        New LoaderTask(Of Integer, Integer)("启动联机模块", AddressOf InitLaunch) With {.ProgressWeight = 7}
-    })
-
-    '检查网络状态
-    Private Shared Sub InitPingCheck(Task As LoaderTask(Of Integer, Integer))
-    End Sub
-    Private Shared Sub InitIpCheck()
-    End Sub
-    Private Shared PingTime As Integer, IpCheckStatus As LoadState = LoadState.Loading, IpIsInChina As Boolean
-    Private Shared Sub InitCheck(Task As LoaderTask(Of Integer, Integer))
-    End Sub
-
-    '获取所需文件
-    Private Shared Sub InitGetFile(Task As LoaderTask(Of Integer, List(Of NetFile)))
-    End Sub
-    Public Class CertOutdatedException
-        Inherits Exception
-    End Class
-    '启动联机模块
-    Private Shared Sub InitLaunch(Task As LoaderTask(Of Integer, Integer))
-    End Sub
-    Private Shared PingNodes As Integer, AllNodes As List(Of String)
-
-#End Region
-
-#Region "进程管理"
-
-    Private Shared _HiperState As LoadState = LoadState.Waiting
-    Public Shared Property HiperState As LoadState
-        Get
-            Return _HiperState
-        End Get
-        Set(value As LoadState)
-            _HiperState = value
-            RunInUi(Sub() If FrmLinkLeft IsNot Nothing Then CType(FrmLinkLeft.ItemHiper.Buttons(0), MyIconButton).Visibility = If(HiperState = LoadState.Finished OrElse HiperState = LoadState.Loading, Visibility.Visible, Visibility.Collapsed))
-        End Set
-    End Property
-    Public Shared Sub ModuleStopManually() '关闭联机模块按钮
-        HiperExit(False)
-    End Sub
-
-    Private Shared HiperIp As String = Nothing
-    Private Shared HiperProcessId As Integer = -1, McbProcessId As Integer = -1
-    Private Shared HiperCertTime As Date = Date.Now
-
-    ''' <summary>
-    ''' 若程序正在运行,则结束程序进程,同时初始化状态数据。返回是否关闭了相关进程。
-    ''' </summary>
-    Public Shared Function HiperStop(SleepWhenKilled As Boolean) As Boolean
-        Return False
-    End Function
-    ''' <summary>
-    ''' 启动程序,并等待初始化完成后退出运行,同时更新 HiperIp。
-    ''' 若启动失败,则会直接抛出异常。
-    ''' 若程序正在运行,则会先停止其运行。
-    ''' </summary>
-    Public Shared Sub HiperStart(Task As LoaderTask(Of Integer, Integer))
-    End Sub
-
-    'Hiper 日志
-    Private Shared Sub HiperLogLine(Content As String, Task As LoaderTask(Of Integer, Integer))
-    End Sub
-    Private Shared PossibleFailReason As String = Nothing
-
-#End Region
-
-#Region "监视线程"
-
-    '主 Timer 线程
-    Private IsWatcherStarted As Boolean = False
-    Private Sub WatcherThread()
-        Dim Sec15 As Integer = 0
-        Do While True
-            Try
-                For i = 1 To 5
-                    Thread.Sleep(200)
-                    If InitLoader.State = LoadState.Loading Then
-                        RunInUi(AddressOf UpdateProgress)
-                    End If
-                Next
-                Thread.Sleep(1000)
-                Sec15 += 1
-                WatcherTimer1()
-                If Sec15 = 15 Then
-                    Sec15 = 0
-                    WatcherTimer15()
-                End If
-            Catch ex As Exception
-                Log(ex, "联机模块主时钟出错", LogLevel.Feedback)
-                Thread.Sleep(20000)
-            End Try
-        Loop
-    End Sub
-
-    '每 1 秒执行的 Timer
-    Private Sub WatcherTimer1()
-        If HiperState <> LoadState.Finished Then Return
-        RunInUi(Sub()
-                    '索引码剩余时间
-                    Dim Span As TimeSpan = HiperCertTime - Date.Now
-                    If Span.TotalDays >= 30 Then
-                        LabFinishTime.Text = "> 30 天"
-                    ElseIf Span.TotalDays >= 4 Then
-                        LabFinishTime.Text = Span.Days & " 天"
-                    ElseIf Span.TotalDays >= 1 Then
-                        LabFinishTime.Text = Span.Days & " 天" & If(Span.Hours > 0, " " & Span.Hours & " 小时", "")
-                    ElseIf Span.TotalMinutes >= 10 Then
-                        LabFinishTime.Text = Span.Hours & ":" & Span.Minutes.ToString.PadLeft(2, "0") & "'"
-                    Else
-                        LabFinishTime.Text = Span.Minutes & "'" & Span.Seconds.ToString.PadLeft(2, "0") & """"
-                    End If
-                    '提示索引码即将到期
-                    If Span.TotalSeconds <= 5 * 60 AndAlso Span.TotalSeconds > 5 * 60 - 1 AndAlso Setup.Get("LinkHiperCertWarn") Then
-                        MyMsgBox("你的索引码还有不到 5 分钟就要过期了!" & vbCrLf & "你可以在设置中关闭这个提示……", "索引码即将过期", "我知道了……")
-                        ShowWindowToTop(Handle)
-                        Beep()
-                    End If
-                    '检查索引码到期
-                    If Span.TotalSeconds < 2 Then
-                        LabCertTitle.Text = "索引码已过期"
-                        LabCertDesc.Text = "你的 HiPer 索引码已经过期,请输入新的索引码。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
-                        TextCert.Text = ""
-                        HiperExit(True)
-                        ShowWindowToTop(Handle)
-                        Beep()
-                        Return
-                    End If
-                    '网络质量
-                    Dim QualityScore As Integer = If(IpIsInChina, 0, -2)
-                    QualityScore -= Math.Ceiling((Math.Min(PingTime, 600) + Math.Min(PingNodes, 600)) / 80)
-                    Select Case QualityScore
-                        Case Is >= -1
-                            LabFinishQuality.Text = "优秀"
-                        Case Is >= -2
-                            LabFinishQuality.Text = "优良"
-                        Case Is >= -3
-                            LabFinishQuality.Text = "良好"
-                        Case Is >= -5
-                            LabFinishQuality.Text = "一般"
-                        Case Is >= -7
-                            LabFinishQuality.Text = "较差"
-                        Case Else
-                            LabFinishQuality.Text = "很差"
-                    End Select
-                    'Ping
-                    If HostPing <> -1 Then
-                        If FrmLinkHiper IsNot Nothing AndAlso FrmLinkHiper.LabFinishPing.IsLoaded Then
-                            FrmLinkHiper.LabFinishPing.Text = HostPing & "ms"
-                        End If
-                    End If
-                End Sub)
-    End Sub
-    '每 15 秒执行的 Timer
-    Private Shared HostPing As Integer = -1
-    Private Sub WatcherTimer15()
-    End Sub
-
-#End Region
-
-#Region "PanCert | 索引码输入页面"
-
-    '检测输入
-    Private Sub TextCert_ValidateChanged(sender As Object, e As EventArgs) Handles TextCert.ValidateChanged
-        BtnCertDone.IsEnabled = TextCert.ValidateResult = ""
-    End Sub
-    Private Sub TextCert_KeyDown(sender As Object, e As KeyEventArgs) Handles TextCert.KeyDown
-        If e.Key = Key.Enter AndAlso BtnCertDone.IsEnabled Then BtnCertDone_Click() '允许回车确认
-    End Sub
-
-    '确认
-    Private Sub BtnCertDone_Click() Handles BtnCertDone.Click
-        CurrentSubpage = Subpages.PanSelect
-    End Sub
-
-#End Region
-
-#Region "PanSelect | 种类选择页面"
-
-    '返回
-    Private Sub BtnSelectReturn_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectReturn.MouseLeftButtonUp
-        LabCertTitle.Text = "输入索引码"
-        LabCertDesc.Text = "你需要获取索引码才能使用 HiPer。" & vbCrLf & "如果实在没有索引码,可以在左侧选择 IOI 方式联机。"
-        CurrentSubpage = Subpages.PanCert
-    End Sub
-
-    '创建房间
-    Private Sub BtnSelectCreate_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectCreate.MouseLeftButtonUp
-    End Sub
-    Private Sub RoomCreate(Port As Integer)
-        '记录信息
-        HostIp = Nothing : HostPort = Port
-        IsServerSide = True
-        '启动
-        InitLoader.Start(IsForceRestart:=True)
-    End Sub
-
-    '加入房间
-    Private Sub BtnSelectJoin_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnSelectJoin.MouseLeftButtonUp
-    End Sub
-    Private Sub RoomJoin(Ip As String, Port As Integer)
-        '记录信息
-        HostIp = Ip : HostPort = Port
-        IsServerSide = False
-        '启动
-        InitLoader.Start(IsForceRestart:=True)
-    End Sub
-
-#End Region
-
-#Region "PanLoad | 加载中页面"
-
-    '承接状态切换的 UI 改变
-    Private Sub OnLoadStateChanged(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
-    End Sub
-    Private Shared LoadStep As String = "准备初始化"
-    Private Shared Sub SetLoadDesc(Intro As String, [Step] As String)
-        Log("[Hiper] 连接步骤:" & Intro)
-        LoadStep = [Step]
-        RunInUiWait(Sub()
-                        If FrmLinkHiper Is Nothing OrElse Not FrmLinkHiper.LabLoadDesc.IsLoaded Then Return
-                        FrmLinkHiper.LabLoadDesc.Text = Intro
-                        FrmLinkHiper.UpdateProgress()
-                    End Sub)
-    End Sub
-
-    '承接重试
-    Private Sub CardLoad_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles CardLoad.MouseLeftButtonUp
-        If Not InitLoader.State = LoadState.Failed Then Return
-        InitLoader.Start(IsForceRestart:=True)
-    End Sub
-
-    '取消加载
-    Private Sub CancelLoad() Handles BtnLoadCancel.Click
-        If InitLoader.State = LoadState.Loading Then
-            CurrentSubpage = Subpages.PanSelect
-            InitLoader.Abort()
-        Else
-            InitLoader.State = LoadState.Waiting
-        End If
-        HiperStop(False)
-    End Sub
-
-    '进度改变
-    Private Sub UpdateProgress(Optional Value As Double = -1)
-        If Value = -1 Then Value = InitLoader.Progress
-        Dim DisplayingProgress As Double = ColumnProgressA.Width.Value
-        If Math.Round(Value - DisplayingProgress, 3) = 0 Then Return
-        If DisplayingProgress > Value Then
-            ColumnProgressA.Width = New GridLength(Value, GridUnitType.Star)
-            ColumnProgressB.Width = New GridLength(1 - Value, GridUnitType.Star)
-            AniStop("Hiper Progress")
-        Else
-            Dim NewProgress As Double = If(Value = 1, 1, (Value - DisplayingProgress) * 0.2 + DisplayingProgress)
-            AniStart({
-                AaGridLengthWidth(ColumnProgressA, NewProgress - ColumnProgressA.Width.Value, 300, Ease:=New AniEaseOutFluent),
-                AaGridLengthWidth(ColumnProgressB, (1 - NewProgress) - ColumnProgressB.Width.Value, 300, Ease:=New AniEaseOutFluent)
-            }, "Hiper Progress")
-        End If
-    End Sub
-    Private Sub CardResized() Handles CardLoad.SizeChanged
-        RectProgressClip.Rect = New Rect(0, 0, CardLoad.ActualWidth, 12)
-    End Sub
-
-#End Region
-
-#Region "PanFinish | 加载完成页面"
-
-    '复制 IP
-    Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
-        ClipboardSet(LabFinishIp.Text)
-    End Sub
-
-    '退出
-    Private Sub BtnFinishExit_Click(sender As Object, e As EventArgs) Handles BtnFinishExit.Click
-        If IsServerSide AndAlso MyMsgBox("你确定要关闭联机房间吗?", "确认退出", "确定", "取消", IsWarn:=True) = 2 Then Return
-        HiperExit(False)
-    End Sub
-
-    '复制联机码
-    Private Sub BtnFinishCopy_Click(sender As Object, e As EventArgs) Handles BtnFinishCopy.Click
-    End Sub
-
-    'Ping 房主
-    Private Sub BtnFinishPing_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishPing.MouseLeftButtonUp
-        LabFinishPing.Text = "检测中"
-        If TaskPingHost.State = LoadState.Loading Then Return
-        TaskPingHost.Start(True, IsForceRestart:=True)
-    End Sub
-    Private Shared TaskPingHost As New LoaderTask(Of Boolean, Integer)("HiPer Ping Host",
-    Sub(Task As LoaderTask(Of Boolean, Integer))
-        HostPing = -1
-        HostPing = Ping(HostIp, 5000, Task.Input)
-    End Sub)
-
-#End Region
-
-#Region "子页面管理"
-
-    Public Enum Subpages
-        PanCert
-        PanSelect
-        PanFinish
-    End Enum
-    Private _CurrentSubpage As Subpages = Subpages.PanCert
-    Public Property CurrentSubpage As Subpages
-        Get
-            Return _CurrentSubpage
-        End Get
-        Set(value As Subpages)
-            If _CurrentSubpage = value Then Return
-            _CurrentSubpage = value
-            Log("[Hiper] 子页面更改为 " & GetStringFromEnum(value))
-            PageOnContentExit()
-            If value = Subpages.PanSelect Then
-                LabSelectCode.Text = "(" & TextCert.Text.Substring(0, Math.Min(TextCert.Text.Length, 3)) & "…)"
-            End If
-        End Set
-    End Property
-
-    Private Sub PageLinkHiper_OnPageEnter() Handles Me.PageEnter
-        FrmLinkHiper.PanCert.Visibility = If(CurrentSubpage = Subpages.PanCert, Visibility.Visible, Visibility.Collapsed)
-        FrmLinkHiper.PanSelect.Visibility = If(CurrentSubpage = Subpages.PanSelect, Visibility.Visible, Visibility.Collapsed)
-        FrmLinkHiper.PanFinish.Visibility = If(CurrentSubpage = Subpages.PanFinish, Visibility.Visible, Visibility.Collapsed)
-    End Sub
-
-    Private Shared Sub HiperExit(ExitToCertPage As Boolean)
-        Log("[Hiper] 要求退出 Hiper(当前加载器状态为 " & GetStringFromEnum(InitLoader.State) & ")")
-        HiperStop(False)
-        If InitLoader.State = LoadState.Loading Then InitLoader.Abort()
-        If InitLoader.State = LoadState.Failed Then InitLoader.State = LoadState.Waiting
-        RunInUi(Sub()
-                    If FrmLinkHiper Is Nothing OrElse Not FrmLinkHiper.IsLoaded Then Return
-                    FrmLinkHiper.CurrentSubpage = If(ExitToCertPage, Subpages.PanCert, Subpages.PanSelect)
-                    FrmLinkHiper.PageOnContentExit()
-                End Sub)
-    End Sub
-
-#End Region
-
-End Class

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

@@ -1,50 +0,0 @@
-<local:MyPageRight
-    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    xmlns:System="clr-namespace:System;assembly=mscorlib" 
-    xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-    mc:Ignorable="d" x:Class="PageLinkIoi"
-    PanScroll="PanBack">
-    <Grid>
-        <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
-            <StackPanel Orientation="Vertical" Margin="25,10">
-                <local:MyCard Margin="0,15,0,0" x:Name="CardUser" Title="已连接的玩家 (0)">
-                    <StackPanel Margin="20,37,20,15">
-                        <local:MyHint Margin="5,2,5,8" x:Name="LabHint" />
-                        <StackPanel x:Name="PanUserList" />
-                        <Grid Height="35" Margin="5,8,5,5">
-                            <Grid.ColumnDefinitions>
-                                <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                                <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                            </Grid.ColumnDefinitions>
-                            <local:MyButton x:Name="BtnLeftCreate" Grid.Column="0" MinWidth="140" Text="建立连接" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" />
-                            <local:MyButton x:Name="BtnLeftCopy" Grid.Column="1" MinWidth="140" Text="复制联机码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
-                        </Grid>
-                    </StackPanel>
-                </local:MyCard>
-                <local:MyCard Margin="0,15" Title="房间列表" x:Name="CardRoom">
-                    <StackPanel Margin="20,37,20,15">
-                        <StackPanel x:Name="PanRoom">
-                            <!--<local:MyListItem Type="Clickable" PaddingRight="60" Height="40" Title="00ll00" Info="已连接,1100 ms" Logo="pack://application:,,,/images/Blocks/Grass.png">
-                                <local:MyListItem.Buttons>
-                                    <x:Array Type="{x:Type local:MyIconButton}">
-                                        <local:MyIconButton ToolTip="刷新" ToolTipService.Placement="Bottom" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="5" ToolTipService.InitialShowDelay="200" LogoScale="0.85" Logo="M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z" />
-                                        <local:MyIconButton ToolTip="断开" ToolTipService.Placement="Bottom" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="5" ToolTipService.InitialShowDelay="200" LogoScale="0.85" Logo="F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z" />
-                                    </x:Array>
-                                </local:MyListItem.Buttons>
-                            </local:MyListItem>-->
-                        </StackPanel>
-                        <local:MyListItem x:Name="BtnCreate" Type="Clickable" Title="创建房间" Height="34" PaddingLeft="0"
-                          Logo="M512.277 954.412c-118.89 0-230.659-46.078-314.73-129.73S67.12 629.666 67.12 511.222s46.327-229.744 130.398-313.427 195.82-129.73 314.73-129.73 230.659 46.078 314.72 129.73S957.397 392.81 957.397 511.183 911.078 740.96 826.97 824.642s-195.8 129.77-314.692 129.77z m0-822.784c-101.972 0-197.809 39.494-269.865 111.222s-111.7 166.997-111.7 268.373 39.653 196.695 111.67 268.335S410.246 890.78 512.248 890.78s197.809-39.484 269.865-111.222 111.7-166.998 111.67-268.374c-0.03-101.375-39.654-196.665-111.67-268.303S614.22 131.628 512.277 131.628z m222.585 347.8H544.073V288.64c-0.76-17.561-15.613-31.18-33.173-30.419-16.495 0.714-29.704 13.924-30.419 30.419v190.787H289.703c-17.56 0.761-31.179 15.614-30.419 33.174 0.715 16.494 13.924 29.703 30.42 30.418H480.48v190.788c0.761 17.56 15.614 31.179 33.174 30.419 16.494-0.715 29.703-13.925 30.418-30.42V543.02h190.788c17.56 0.762 32.413-12.857 33.173-30.418 0.762-17.561-12.858-32.414-30.419-33.174a31.683 31.683 0 0 0-2.753 0z" />
-                    </StackPanel>
-                </local:MyCard>
-            </StackPanel>
-        </local:MyScrollViewer>
-        <local:MyCard Opacity="0" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True" x:Name="PanLoad" UseAnimation="False">
-            <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="联机服务由 速聚 授权提供" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.the.bb/zh-CN.html" />
-        </Grid>
-    </Grid>
-</local:MyPageRight>

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

@@ -1,411 +0,0 @@
-Imports System.Net.Sockets
-
-Public Class PageLinkIoi
-    Public Const RequestVersion As Integer = 4
-    Public Const IoiVersion As Integer = 10 '由于已关闭更新渠道,在提升 IoiVersion 时必须提升 RequestVersion
-    Public Shared PathIoi As String = PathAppdata & "联机模块\IOI 联机模块.exe"
-
-#Region "进程管理"
-
-    Private Shared IoiId As String, IoiPassword As String
-    Private Shared IoiProcess As Process = Nothing
-    Private Shared IoiState As LoadState = LoadState.Waiting
-
-    ''' <summary>
-    ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。
-    ''' </summary>
-    Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean
-        Return False
-    End Function
-    ''' <summary>
-    ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。
-    ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。
-    ''' 若 Ioi 正在运行,则会先停止其运行。
-    ''' </summary>
-    Public Shared Function IoiStart() As Boolean
-        Return False
-    End Function
-
-    'Ioi 日志
-    Private Shared Sub IoiLogLine(Content As String)
-    End Sub
-    Private Shared LogLinesCount As Integer = 0
-    Private Shared LastPortsId As String = "" '上一个收到 portssub 的 ID,用于记录端口
-
-#End Region
-
-#Region "时钟"
-
-    'UI 线程刷新
-    Private Shared UserListIdentifyCache As String = ""
-    Private Shared RoomListIdentifyCache As String = ""
-    Public Sub RefreshUi()
-    End Sub
-
-    '工作线程刷新
-    Public Sub RefreshWorker()
-    End Sub
-
-#End Region
-
-#Region "发送请求"
-
-    ''' <summary>
-    ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。
-    ''' </summary>
-    Private Shared Sub SendPortsubRequest(User As LinkUserIoi)
-    End Sub
-
-    ''' <summary>
-    ''' 向控制台发送 Connect 请求。
-    ''' </summary>
-    Private Shared Sub SendConnectRequest(User As LinkUserIoi)
-        Dim RawJson As New JObject()
-        RawJson("version") = RequestVersion
-        RawJson("name") = GetPlayerName()
-        RawJson("id") = IoiId
-        RawJson("type") = "connect"
-        User.Send(RawJson)
-    End Sub
-
-    ''' <summary>
-    ''' 向控制台发送 Update 请求。
-    ''' </summary>
-    Private Shared Sub SendUpdateRequest(User As LinkUserIoi, Stage As Integer, Optional Unique As Long = -1)
-        If Unique = -1 Then Unique = GetTimeTick()
-        Dim RawJson As New JObject
-        RawJson("name") = GetPlayerName()
-        RawJson("id") = IoiId
-        RawJson("type") = "update"
-        RawJson("stage") = Stage
-        RawJson("unique") = Unique
-        If Stage < 3 Then
-            Dim Rooms As New JArray
-            For Each Room In RoomListForMe
-                Dim RoomObject As New JObject
-                RoomObject("name") = Room.DisplayName
-                RoomObject("port") = Room.Port
-                Rooms.Add(RoomObject)
-            Next
-            RawJson("rooms") = Rooms
-            User.PingPending(Unique) = Date.Now
-        End If
-        User.Send(RawJson)
-    End Sub
-
-    ''' <summary>
-    ''' 尝试发送断开请求,并将其从用户列表中移除。
-    ''' </summary>
-    Private Shared Sub SendDisconnectRequest(User As LinkUserIoi, Optional Message As String = Nothing, Optional IsError As Boolean = False)
-    End Sub
-
-#End Region
-
-#Region "左边栏操作"
-
-    '刷新连接
-    Private Shared Sub BtnListRefresh_Click(sender As MyIconButton, e As EventArgs)
-    End Sub
-    '断开连接
-    Private Shared Sub BtnListDisconnect_Click(sender As MyIconButton, e As EventArgs)
-    End Sub
-    '复制联机码
-    Public Shared Sub BtnLeftCopy_Click() Handles BtnLeftCopy.Click
-    End Sub
-
-#End Region
-
-#Region "玩家名"
-
-    ''' <summary>
-    ''' 获取当前的玩家名。
-    ''' </summary>
-    Public Shared Function GetPlayerName() As String
-        Return ""
-    End Function
-    Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名
-    ''' <summary>
-    ''' 检查某个玩家名是否合法。
-    ''' </summary>
-    Private Shared Function IsPlayerNameValid(Name As String) As Boolean
-        Return True
-    End Function
-
-#End Region
-
-#Region "请求核心"
-
-    ''' <summary>
-    ''' 启动 Socket 监听核心。
-    ''' </summary>
-    Public Shared Sub StartSocketListener()
-    End Sub
-
-#End Region
-
-#Region "用户核心"
-
-    '用户基类
-    Public MustInherit Class LinkUserBase
-        Implements IDisposable
-
-        '基础数据
-        Public Uuid As Integer = GetUuid()
-        Public Id As String
-        Public DisplayName As String
-
-        '请求管理
-        Public Socket As Socket = Nothing
-        Public Sub Send(Request As JObject)
-        End Sub
-        Public ListenerThread As Thread = Nothing
-        Public Sub StartListener()
-        End Sub
-        Public Sub BindSocket(Socket As Socket)
-            If Me.Socket IsNot Nothing Then Throw New Exception("该用户已经绑定了 Socket")
-            Me.Socket = Socket
-            StartListener()
-        End Sub
-
-        'Ping
-        '0:与 Ping 计算无关,不回应
-        '1:A to B,2:B to A,3:A to B
-        Public PingPending As New Dictionary(Of Long, Date)
-        Public PingRecord As New Queue(Of Integer)
-
-        '心跳包
-        Public LastSend As Date = Date.Now
-        Public LastReceive As Date = Date.Now
-
-        '类型转换
-        Public Sub New(Id As String, DisplayName As String)
-            Me.Id = Id
-            Me.DisplayName = DisplayName
-            Log("[IOI] 无通信包的新用户对象:" & ToString())
-        End Sub
-        Public Sub New(Id As String, DisplayName As String, Socket As Socket)
-            Me.Id = Id
-            Me.DisplayName = DisplayName
-            Me.Socket = Socket
-            Log("[IOI] 新用户对象:" & ToString())
-            StartListener()
-        End Sub
-        Public Overrides Function ToString() As String
-            Return DisplayName & " @ " & Id & " #" & Uuid
-        End Function
-        Public Shared Widening Operator CType(User As LinkUserBase) As String
-            Return User.ToString
-        End Operator
-
-        '释放资源
-        Public IsDisposed As Boolean = False
-        Protected Overridable Sub Dispose(IsDisposing As Boolean)
-            If Socket IsNot Nothing Then Socket.Dispose()
-            If ListenerThread IsNot Nothing AndAlso ListenerThread.IsAlive Then ListenerThread.Interrupt()
-        End Sub
-        Public Sub Dispose() Implements IDisposable.Dispose
-            If Not IsDisposed Then
-                IsDisposed = True
-                Dispose(True)
-            End If
-            GC.SuppressFinalize(Me)
-        End Sub
-    End Class
-
-    '用户对象
-    Public Shared UserList As New Dictionary(Of String, LinkUserIoi)
-    Public Class LinkUserIoi
-        Inherits LinkUserBase
-        Public Sub New(Id As String, DisplayName As String, Socket As Socket)
-            MyBase.New(Id, DisplayName, Socket)
-        End Sub
-        Public Sub New(Id As String, DisplayName As String)
-            MyBase.New(Id, DisplayName)
-        End Sub
-
-        '基础数据
-        Public Ports As New Dictionary(Of Integer, String)
-        Public Rooms As New List(Of RoomEntry)
-
-        '进度与 UI
-        Public Progress As Double = 0
-        Public RelativeThread As Thread = Nothing
-
-        Public Function GetDescription() As String
-            Return If(Progress < 1,
-                "正在连接," & Math.Round(Progress * 100) & "%",
-                "已连接," & If(Not PingRecord.Any(), "检查延迟中", Math.Round(PingRecord.Average) & "ms"))
-        End Function
-        Public Function ToListItem() As MyListItem
-            Dim Item As New MyListItem With {
-                .Title = DisplayName, .Height = 42, .Tag = Me, .Type = MyListItem.CheckType.None,
-                .Logo = "pack://application:,,,/images/Blocks/Grass.png"}
-            '绑定图标按钮
-            Dim BtnRefresh As New MyIconButton With {.Logo = Logo.IconButtonRefresh, .LogoScale = 0.85, .ToolTip = "刷新", .Tag = Me}
-            AddHandler BtnRefresh.Click, AddressOf BtnListRefresh_Click
-            ToolTipService.SetPlacement(BtnRefresh, Primitives.PlacementMode.Bottom)
-            ToolTipService.SetHorizontalOffset(BtnRefresh, -10)
-            ToolTipService.SetVerticalOffset(BtnRefresh, 5)
-            ToolTipService.SetInitialShowDelay(BtnRefresh, 200)
-            Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "断开", .Tag = Me}
-            AddHandler BtnClose.Click, AddressOf BtnListDisconnect_Click
-            ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
-            ToolTipService.SetHorizontalOffset(BtnClose, -10)
-            ToolTipService.SetVerticalOffset(BtnClose, 5)
-            ToolTipService.SetInitialShowDelay(BtnClose, 200)
-            Item.Buttons = {BtnRefresh, BtnClose}
-            '刷新并返回
-            RefreshUi(Item)
-            Return Item
-        End Function
-        Public Sub RefreshUi(RelatedListItem As MyListItem)
-            RelatedListItem.Title = DisplayName
-            RelatedListItem.Info = GetDescription()
-            RelatedListItem.Buttons(0).Visibility = If(Progress = 1, Visibility.Visible, Visibility.Collapsed)
-        End Sub
-
-        '释放
-        Protected Overrides Sub Dispose(IsDisposing As Boolean)
-            Log("[IOI] 用户资源释放(IOI, " & DisplayName & ")")
-            If RelativeThread IsNot Nothing AndAlso RelativeThread.IsAlive Then RelativeThread.Interrupt()
-            UserList.Remove(Id)
-            MyBase.Dispose(IsDisposing)
-        End Sub
-    End Class
-
-    '房间对象
-    Private Shared RoomListForMe As New List(Of RoomEntry)
-
-    Private Function GetRoomList() As List(Of RoomEntry)
-        Dim RoomList As New List(Of RoomEntry)(RoomListForMe)
-        For i = 0 To UserList.Count - 1
-            If i > UserList.Count - 1 Then Exit For
-            RoomList.AddRange(UserList.Values(i).Rooms)
-        Next
-        Return RoomList
-    End Function
-    Public Class RoomEntry
-
-        '基础数据
-        Public Port As Integer
-        Public DisplayName As String
-        Public User As LinkUserIoi = Nothing '若 IsOwner = True,则此项为 Nothing
-        Public IsOwner As Boolean
-        Public ReadOnly Property Ip As String
-            Get
-                If IsOwner Then
-                    Return "localhost:" & Port
-                Else
-                    Return User.Ports(Port) & ":" & Port
-                End If
-            End Get
-        End Property
-
-        '类型转换
-        Public Sub New(Port As Integer, DisplayName As String, Optional User As LinkUserIoi = Nothing)
-            Me.IsOwner = User Is Nothing
-            Me.User = User
-            Me.DisplayName = DisplayName
-            Me.Port = Port
-        End Sub
-        Public Overrides Function ToString() As String
-            Return DisplayName & " - " & Port & " - " & IsOwner
-        End Function
-        Public Shared Widening Operator CType(Room As RoomEntry) As String
-            Return Room.ToString
-        End Operator
-        Public Shared Function SelectPort(Room As RoomEntry) As Integer
-            Return Room.Port
-        End Function
-
-        'UI
-        Public Function GetDescription() As String
-            If IsOwner Then
-                Return "由我创建,端口 " & Port
-            Else
-                Return "由 " & User.DisplayName & " 创建,端口 " & Port
-            End If
-        End Function
-        Public Function ToListItem() As MyListItem
-            Dim Item As New MyListItem With {
-                .Title = DisplayName, .Height = 42, .Info = GetDescription(), .Tag = Me,
-                .Type = If(IsOwner, MyListItem.CheckType.None, MyListItem.CheckType.Clickable),
-                .Logo = "pack://application:,,,/images/Blocks/" & If(IsOwner, "GrassPath", "Grass") & ".png"}
-            If IsOwner Then
-                '绑定图标按钮
-                Dim BtnEdit As New MyIconButton With {.Logo = Logo.IconButtonEdit, .LogoScale = 1, .ToolTip = "修改名称", .Tag = Me}
-                AddHandler BtnEdit.Click, AddressOf BtnRoomEdit_Click
-                ToolTipService.SetPlacement(BtnEdit, Primitives.PlacementMode.Bottom)
-                ToolTipService.SetHorizontalOffset(BtnEdit, -22)
-                ToolTipService.SetVerticalOffset(BtnEdit, 5)
-                ToolTipService.SetInitialShowDelay(BtnEdit, 200)
-                Dim BtnClose As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85, .ToolTip = "关闭", .Tag = Me}
-                AddHandler BtnClose.Click, AddressOf BtnRoomClose_Click
-                ToolTipService.SetPlacement(BtnClose, Primitives.PlacementMode.Bottom)
-                ToolTipService.SetHorizontalOffset(BtnClose, -10)
-                ToolTipService.SetVerticalOffset(BtnClose, 5)
-                ToolTipService.SetInitialShowDelay(BtnClose, 200)
-                Item.Buttons = {BtnEdit, BtnClose}
-            Else
-                '绑定点击事件
-                AddHandler Item.Click, AddressOf BtnRoom_Click
-            End If
-            Return Item
-        End Function
-        Public Sub RefreshUi(RelatedListItem As MyListItem)
-            RelatedListItem.Title = DisplayName
-            RelatedListItem.Info = GetDescription()
-        End Sub
-
-    End Class
-
-#End Region
-
-    '正向与反向连接
-    Public Shared Sub BtnLeftCreate_Click() Handles BtnLeftCreate.Click
-    End Sub
-    Private Shared Sub SendPortsubBack(User As LinkUserIoi, TargetVersion As Integer)
-    End Sub
-
-    '创建房间
-    Private Sub LinkCreate() Handles BtnCreate.Click
-    End Sub
-    Private Shared Sub SendUpdateRequestToAllUsers()
-        For i = 0 To UserList.Count - 1
-            If i > UserList.Count - 1 Then Exit For
-            Dim User = UserList.Values(i)
-            If User.Progress < 1 Then Continue For
-            Try
-                SendUpdateRequest(User, 1) '不需要使用多线程,发送实际会瞬间完成
-            Catch ex As Exception
-                Log(ex, "发送全局刷新请求失败(" & User.DisplayName & ")")
-            End Try
-        Next
-    End Sub
-    '修改房间名称
-    Private Shared Sub BtnRoomEdit_Click(sender As MyIconButton, e As EventArgs)
-    End Sub
-    '加入房间
-    Private Shared Sub BtnRoom_Click(sender As MyListItem, e As EventArgs)
-        Dim Room As RoomEntry = sender.Tag
-        If MyMsgBox("请在多人游戏页面点击直接连接,输入 " & Room.Ip & " 以进入服务器!", "加入房间", "复制地址", "确定") = 1 Then
-            ClipboardSet(Room.Ip)
-        End If
-    End Sub
-    '关闭房间
-    Private Shared Sub BtnRoomClose_Click(sender As MyIconButton, e As EventArgs)
-    End Sub
-
-    '获取数据包
-    Public Shared Sub ReceiveJson(JsonData As JObject, Optional NewSocket As Socket = Nothing)
-    End Sub
-    ''' <summary>
-    ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。
-    ''' </summary>
-    Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean)
-    End Sub
-
-    Public Shared Sub ModuleStopManually()
-    End Sub
-
-End Class

+ 0 - 44
Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml

@@ -1,44 +0,0 @@
-<local:MyPageLeft x:Class="PageLinkLeft"
-      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
-      xmlns:local="clr-namespace:PCL"
-      mc:Ignorable="d" AnimatedControl="PanItem" MinWidth="150">
-    <StackPanel Orientation="Vertical" x:Name="PanItem" Margin="0,12,0,0">
-        <TextBlock Text="联机方式" Margin="13,6,5,4" Opacity="0.6" FontSize="12" />
-        <local:MyListItem x:Name="ItemHiper" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="1" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="HiPer"
-                          LogoScale="0.85" Logo="M554.496 170.496c141.312 0 256 114.688 256 256s-114.688 256-256 256H402.432c-22.016-1.536-39.424-19.968-39.424-42.496a42.391 42.391 0 0 1 42.496-42.496h149.504c94.208 0 170.496-76.288 170.496-170.496S648.704 256 554.496 256h-256C204.288 256 128 332.288 128 426.496c0 38.4 12.8 73.728 33.792 102.4 5.632 7.168 8.704 15.872 8.704 25.6A42.391 42.391 0 0 1 128 596.992c-11.264 0-20.992-4.096-28.672-11.264l-0.512 0.512c-35.328-44.032-56.32-99.328-56.32-159.744 0-141.312 114.688-256 256-256h256z m64 192h3.072c22.016 1.536 39.424 19.968 39.424 42.496a42.391 42.391 0 0 1-42.496 42.496H469.504c-94.208 0-170.496 76.288-170.496 170.496S375.296 788.48 469.504 788.48h256C819.712 788.48 896 712.192 896 617.984c0-38.4-12.8-73.728-33.792-102.4-5.632-7.168-8.704-15.872-8.704-25.6A42.391 42.391 0 0 1 896 447.488c11.264 0 20.992 4.096 28.672 11.264l0.512-0.512c35.328 44.032 56.32 99.328 56.32 159.744 0 141.312-114.688 256-256 256h-256c-141.312 0-256-114.688-256-256s114.688-256 256-256h148.992z">
-            <local:MyListItem.Buttons>
-                <x:Array Type="{x:Type local:MyIconButton}">
-                    <local:MyIconButton ToolTip="关闭联机模块" Loaded="BtnHiperStop_Loaded" Click="BtnHiperStop_Click"
-                                        ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="5" ToolTipService.VerticalOffset="-2" ToolTipService.InitialShowDelay="200" LogoScale="0.7" 
-                                        Logo="M576 512l277.333333 277.333333-64 64-277.333333-277.333333L234.666667 853.333333 170.666667 789.333333l277.333333-277.333333L170.666667 234.666667 234.666667 170.666667l277.333333 277.333333L789.333333 170.666667 853.333333 234.666667 576 512z" />
-                </x:Array>
-            </local:MyListItem.Buttons>
-        </local:MyListItem>
-        <local:MyListItem x:Name="ItemIoi" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="2" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="IOI"
-                          LogoScale="0.85" Logo="M554.496 170.496c141.312 0 256 114.688 256 256s-114.688 256-256 256H402.432c-22.016-1.536-39.424-19.968-39.424-42.496a42.391 42.391 0 0 1 42.496-42.496h149.504c94.208 0 170.496-76.288 170.496-170.496S648.704 256 554.496 256h-256C204.288 256 128 332.288 128 426.496c0 38.4 12.8 73.728 33.792 102.4 5.632 7.168 8.704 15.872 8.704 25.6A42.391 42.391 0 0 1 128 596.992c-11.264 0-20.992-4.096-28.672-11.264l-0.512 0.512c-35.328-44.032-56.32-99.328-56.32-159.744 0-141.312 114.688-256 256-256h256z m64 192h3.072c22.016 1.536 39.424 19.968 39.424 42.496a42.391 42.391 0 0 1-42.496 42.496H469.504c-94.208 0-170.496 76.288-170.496 170.496S375.296 788.48 469.504 788.48h256C819.712 788.48 896 712.192 896 617.984c0-38.4-12.8-73.728-33.792-102.4-5.632-7.168-8.704-15.872-8.704-25.6A42.391 42.391 0 0 1 896 447.488c11.264 0 20.992 4.096 28.672 11.264l0.512-0.512c35.328 44.032 56.32 99.328 56.32 159.744 0 141.312-114.688 256-256 256h-256c-141.312 0-256-114.688-256-256s114.688-256 256-256h148.992z">
-            <local:MyListItem.Buttons>
-                <x:Array Type="{x:Type local:MyIconButton}">
-                    <local:MyIconButton ToolTip="关闭联机模块" Loaded="BtnIoiStop_Loaded" Click="BtnIoiStop_Click"
-                                        ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="5" ToolTipService.VerticalOffset="-2" ToolTipService.InitialShowDelay="200" LogoScale="0.7" 
-                                        Logo="M576 512l277.333333 277.333333-64 64-277.333333-277.333333L234.666667 853.333333 170.666667 789.333333l277.333333-277.333333L170.666667 234.666667 234.666667 170.666667l277.333333 277.333333L789.333333 170.666667 853.333333 234.666667 576 512z" />
-                </x:Array>
-            </local:MyListItem.Buttons>
-        </local:MyListItem>
-        <TextBlock Text="更多选项" Margin="13,18,5,4" Opacity="0.6" FontSize="12" />
-        <local:MyListItem x:Name="ItemSetup" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="设置"
-                          LogoScale="0.85" Logo="M940.4 463.7L773.3 174.2c-17.3-30-49.2-48.4-83.8-48.4H340.2c-34.6 0-66.5 18.5-83.8 48.4L89.2 463.7c-17.3 30-17.3 66.9 0 96.8L256.4 850c17.3 30 49.2 48.4 83.8 48.4h349.2c34.6 0 66.5-18.5 83.8-48.4l167.2-289.5c17.3-29.9 17.3-66.8 0-96.8z m-94.6 96.8L725.9 768.1c-17.3 30-49.2 48.4-83.8 48.4H387.5c-34.6 0-66.5-18.5-83.8-48.4L183.9 560.5c-17.3-30-17.3-66.9 0-96.8l119.8-207.5c17.3-30 49.2-48.4 83.8-48.4h254.6c34.6 0 66.5 18.5 83.8 48.4l119.8 207.5c17.3 30 17.3 66.9 0.1 96.8z M522.3 321.2c-2.5-0.1-5-0.2-7.5-0.2-119.9 0-214 110.3-186.3 235 15.8 70.9 71.5 126.6 142.4 142.4 17.5 3.9 34.7 5.4 51.4 4.7 102.1-3.9 183.6-87.9 183.6-191 0.1-103-81.5-187-183.6-190.9z m68.6 269.1c-18.5 18-43 28.9-68.6 30.7l-6 0.3c-30.2 0.4-58.6-11.4-79.7-33-19.5-20.1-30.7-47-30.9-75-0.3-29.6 11.1-57.4 32-78.3 20.6-20.6 48-32 77.2-32 2.5 0 5 0.1 7.5 0.3 26.7 1.8 51.5 13.2 70.5 32.5 19.6 20 30.8 46.9 31.2 74.9 0.2 30.2-11.5 58.6-33.2 79.6z">
-            <local:MyListItem.Buttons>
-                <x:Array Type="{x:Type local:MyIconButton}">
-                    <local:MyIconButton Tag="5" ToolTip="初始化设置" ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1" Click="Reset" 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" />
-                </x:Array>
-            </local:MyListItem.Buttons>
-        </local:MyListItem>
-        <local:MyListItem x:Name="ItemHelp" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="5" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="常见问题"
-                          LogoScale="0.97" Logo="M520.6 620.3c-11.3-0.6-20.1-4-26.9-10.5-6.6-6.3-8.1-11-10.1-20.8-1.9-9.5-1.5-16.7-1-24.9l0.3-5.3c0.5-9.9 3.5-19.6 8.8-28.1 5.7-9 12.5-17.2 20.8-25.1 8.6-8 17.7-15.7 27-22.9 9.4-7.2 18.7-14.8 27.7-22.7 9-7.9 16.2-15.8 22.1-24.3 6.2-9 9.3-18.8 9.3-29.4 1.2-20.3-6.5-37.7-22.8-51.3-16-13.3-37.4-20-63.5-20-13.7 0-26.3 3.3-37.4 10-10.9 6.5-20.2 14.5-27.7 23.8-7.5 9.2-13.5 19-17.9 29.2-4.5 10.6-6.7 19.4-6.7 26.8 0 9.2-3.3 16-10.1 20.8-6.9 4.8-14.6 7.3-22.8 7.3h-1.3c-9-0.3-17-3.3-24.6-9.2-7.3-5.6-11.1-14.6-11.6-27.5-0.6-11.5 1.9-26.1 7.2-43.5 5.4-17.4 14.8-34.4 28.1-50.5 13.4-16.3 31.2-30.4 52.8-41.8 21.6-11.4 49.2-17.2 81.9-17.2 26.4 0 49.6 3.9 69.1 11.7 19.4 7.7 35.4 18.2 47.5 31.1l1.6 1.7c11.8 12.6 20.3 21.7 29.4 44.4 11.6 28.9 7.1 52.1 5.5 58.5-5.8 22.6-12.6 37.7-22.7 50.4-11.7 14.6-24.9 28.2-39.2 40.4-14 11.7-39.2 33.3-39.2 33.3-13.3 11.4-19.4 22.1-18.6 32.9V587c0.5 8.7-2.4 16.1-8.8 22.8-6.2 6.4-14.8 10-26.2 10.5zM519 766.1c-13 0-23.6-4.2-32.2-12.9-8.7-8.7-13-19-13-31.4 0-12.9 4.2-23.5 12.9-32.2 8.7-8.7 19.2-12.9 32.2-12.9 13 0 23.5 4.2 32.2 12.9s12.9 19.2 12.9 32.2c0 12.4-4.2 22.7-12.9 31.4-8.6 8.6-19.2 12.9-32.1 12.9z M515 928.3c-228.1 0-413.7-185.6-413.7-413.7S286.9 100.9 515 100.9s413.7 185.6 413.7 413.7c0.1 228.1-185.5 413.7-413.7 413.7z m0-747.4c-184 0-333.7 149.7-333.7 333.7S331 848.3 515 848.3s333.7-149.7 333.7-333.7S699 180.9 515 180.9z" />
-        <local:MyListItem x:Name="ItemFeedback" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="6" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="关于与反馈"
-                          LogoScale="0.9" Logo="M613.717333 64.426667l3.413334 3.242666 331.861333 331.861334a85.333333 85.333333 0 0 1 3.2 117.269333l-3.2 3.413333L573.162667 896H960a21.333333 21.333333 0 0 1 21.333333 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333333 21.333333H64a21.333333 21.333333 0 0 1-21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h295.338667L74.069333 610.709333a85.333333 85.333333 0 0 1-3.221333-117.269333l3.2-3.413333 422.4-422.357334a85.333333 85.333333 0 0 1 117.269333-3.221333z m-20.629333 99.882666l-21.418667 321.493334a85.333333 85.333333 0 0 1-74.944 79.04l-4.522666 0.426666-321.493334 21.44 295.552 295.530667 422.4-422.378667L593.066667 164.309333z m-86.442667 13.824L184.533333 500.266667l301.994667-20.117334 20.117333-301.994666z" />
-    </StackPanel>
-</local:MyPageLeft>

+ 0 - 112
Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb

@@ -1,112 +0,0 @@
-Public Class PageLinkLeft
-
-    Private IsLoad As Boolean = False
-    Private IsPageSwitched As Boolean = False '如果在 Loaded 前切换到其他页面,会导致触发 Loaded 时再次切换一次
-    Private Sub PageLinkLeft_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
-        If IsLoad Then Return
-        IsLoad = True
-        '切换默认页面
-        If IsPageSwitched Then Return
-        ItemHiper.SetChecked(True, False, False)
-    End Sub
-    Private Sub PageOtherLeft_Unloaded(sender As Object, e As RoutedEventArgs) Handles Me.Unloaded
-        IsPageSwitched = False
-    End Sub
-
-#Region "页面切换"
-
-    ''' <summary>
-    ''' 当前页面的编号。
-    ''' </summary>
-    Public PageID As FormMain.PageSubType = FormMain.PageSubType.LinkHiper
-
-    ''' <summary>
-    ''' 勾选事件改变页面。
-    ''' </summary>
-    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemHiper.Check, ItemIoi.Check, ItemSetup.Check, ItemHelp.Check, ItemFeedback.Check
-        '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
-        '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
-        If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
-    End Sub
-
-    Public Function PageGet(Optional ID As FormMain.PageSubType = -1)
-        If ID = -1 Then ID = PageID
-        Select Case ID
-            Case 0, FormMain.PageSubType.LinkHiper
-                If FrmLinkHiper Is Nothing Then FrmLinkHiper = New PageLinkHiper
-                Return FrmLinkHiper
-            Case FormMain.PageSubType.LinkIoi
-                If FrmLinkIoi Is Nothing Then FrmLinkIoi = New PageLinkIoi
-                Return FrmLinkIoi
-            Case FormMain.PageSubType.LinkSetup
-                If FrmSetupLink Is Nothing Then FrmSetupLink = New PageSetupLink
-                Return FrmSetupLink
-            Case FormMain.PageSubType.LinkHelp
-                If FrmLinkHelp Is Nothing Then FrmLinkHelp = PageOtherHelp.GetHelpPage(PathTemp & "Help\启动器\联机.json")
-                Return FrmLinkHelp
-            Case FormMain.PageSubType.LinkFeedback
-                If FrmLinkFeedback Is Nothing Then FrmLinkFeedback = New PageLinkFeedback
-                Return FrmLinkFeedback
-            Case Else
-                Throw New Exception("未知的更多子页面种类:" & ID)
-        End Select
-    End Function
-
-    ''' <summary>
-    ''' 切换现有页面。
-    ''' </summary>
-    Public Sub PageChange(ID As FormMain.PageSubType)
-        If PageID = ID Then Return
-        AniControlEnabled += 1
-        IsPageSwitched = True
-        Try
-            PageChangeRun(PageGet(ID))
-            PageID = ID
-        Catch ex As Exception
-            Log(ex, "切换分页面失败(ID " & ID & ")", LogLevel.Feedback)
-        Finally
-            AniControlEnabled -= 1
-        End Try
-    End Sub
-    Private Shared Sub PageChangeRun(Target As MyPageRight)
-        AniStop("FrmMain PageChangeRight") '停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
-        If Target.Parent IsNot Nothing Then Target.SetValue(ContentPresenter.ContentProperty, Nothing)
-        FrmMain.PageRight = Target
-        CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnExit()
-        AniStart({
-                         AaCode(Sub()
-                                    CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnForceExit()
-                                    FrmMain.PanMainRight.Child = FrmMain.PageRight
-                                    FrmMain.PageRight.Opacity = 0
-                                End Sub, 130),
-                         AaCode(Sub()
-                                    '延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
-                                    FrmMain.PageRight.Opacity = 1
-                                    FrmMain.PageRight.PageOnEnter()
-                                End Sub, 30, True)
-                     }, "PageLeft PageChange")
-    End Sub
-
-#End Region
-
-    Public Sub Reset(sender As Object, e As EventArgs)
-        If MyMsgBox("是否要初始化联机页的所有设置?该操作不可撤销。", "初始化确认",, "取消", IsWarn:=True) = 1 Then
-            If IsNothing(FrmSetupLink) Then FrmSetupLink = New PageSetupLink
-            FrmSetupLink.Reset()
-            ItemSetup.Checked = True
-        End If
-    End Sub
-
-    Private Sub BtnHiperStop_Loaded(sender As Object, e As RoutedEventArgs)
-        sender.Visibility = If(PageLinkHiper.HiperState = LoadState.Finished OrElse PageLinkHiper.HiperState = LoadState.Loading, Visibility.Visible, Visibility.Collapsed)
-    End Sub
-    Private Sub BtnHiperStop_Click(sender As Object, e As EventArgs)
-        PageLinkHiper.ModuleStopManually()
-    End Sub
-    Private Sub BtnIoiStop_Loaded(sender As Object, e As RoutedEventArgs)
-    End Sub
-    Private Sub BtnIoiStop_Click(sender As Object, e As EventArgs)
-        PageLinkIoi.ModuleStopManually()
-    End Sub
-
-End Class

+ 202 - 0
Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml

@@ -0,0 +1,202 @@
+<local:MyPageRight
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             mc:Ignorable="d" x:Class="PageLinkMain"
+             d:DesignWidth="800" d:DesignHeight="800">
+    <Grid>
+        <Grid x:Name="PanLoad">
+            <local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="CardLoad" MinWidth="300">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="25" />
+                        <ColumnDefinition Width="Auto" />
+                        <ColumnDefinition Width="20" />
+                        <ColumnDefinition Width="Auto" />
+                        <ColumnDefinition Width="1*" />
+                        <ColumnDefinition Width="Auto" />
+                        <ColumnDefinition Width="20" />
+                    </Grid.ColumnDefinitions>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="20" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="1*" />
+                        <RowDefinition Height="20" />
+                    </Grid.RowDefinitions>
+                    <local:MyLoading Grid.Column="1" Grid.Row="1" x:Name="Load" Grid.RowSpan="3" Margin="0,-4,0,-29" HorizontalAlignment="Center"
+                                     Text="" TextError="" TextErrorInherit="False" AutoRun="False" IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5">
+                        <local:MyLoading.RenderTransform>
+                            <ScaleTransform ScaleX="0.9" ScaleY="0.9" />
+                        </local:MyLoading.RenderTransform>
+                    </local:MyLoading>
+                    <TextBlock x:Name="LabLoadTitle" Grid.Column="3" Grid.Row="1" Grid.ColumnSpan="2" VerticalAlignment="Center" Text="正在创建联机房间" FontSize="19"
+                               Margin="0,0,25,1" Foreground="{Binding Foreground, ElementName=Load}" IsHitTestVisible="False" />
+                    <TextBlock x:Name="LabLoadDesc" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" VerticalAlignment="Center" Margin="0,5,10,-5" Text="下载联机模块中……" FontSize="14" 
+                               LineHeight="19" MaxWidth="450" MaxHeight="220" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" IsHitTestVisible="False" />
+                    <local:MyIconButton Grid.Column="5" Grid.Row="1" x:Name="BtnLoadCancel" Height="25" Width="25" 
+                                        VerticalAlignment="Top" Margin="0,-13,-13,0"
+                                        Theme="Black" Opacity="0.3"
+                                        ToolTip="取消" ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="100" 
+                                        ToolTipService.VerticalOffset="-2" ToolTipService.HorizontalOffset="0"
+                                        LogoScale="0.72" Logo="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                    <Grid Height="12" VerticalAlignment="Bottom" Grid.ColumnSpan="100" Grid.RowSpan="100" Margin="0,0,0,-0.6">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="0*" x:Name="ColumnProgressA" />
+                            <ColumnDefinition Width="1*" x:Name="ColumnProgressB" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.Clip>
+                            <RectangleGeometry RadiusX="6" RadiusY="6" x:Name="RectProgressClip" />
+                        </Grid.Clip>
+                        <Rectangle Fill="{Binding Foreground, ElementName=Load}" VerticalAlignment="Bottom" Height="2" />
+                    </Grid>
+                </Grid>
+            </local:MyCard>
+        </Grid>
+        <Grid x:Name="PanContent">
+            <Grid x:Name="PanSelect">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="1*" />
+                    <ColumnDefinition Width="Auto" />
+                    <ColumnDefinition Width="1*" />
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="1*" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="35" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="10" />
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="1*" />
+                </Grid.RowDefinitions>
+                <local:MyCard x:Name="PanSelectCreate" Grid.Column="1" Grid.Row="1" CornerRadius="13" VerticalAlignment="Center">
+                    <Grid IsHitTestVisible="False">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="25" />
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="13" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="Auto" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="22" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="12" />
+                        </Grid.RowDefinitions>
+                        <Path Grid.Column="1" Grid.Row="1" Height="26" Width="26" Margin="0,-1,0,0"
+                            Stretch="Uniform" Fill="{DynamicResource ColorBrush3}"
+                            Data="M384 128v192h256a64 64 0 0 1 64 64v256h192V128H384zM320 320V128a64 64 0 0 1 64-64h512a64 64 0 0 1 64 64v512a64 64 0 0 1-64 64h-192v192a64 64 0 0 1-64 64H128a64 64 0 0 1-64-64V384a64 64 0 0 1 64-64h192z m320 384H384a64 64 0 0 1-64-64V384H128v512h512v-192z m0-64V384H384v256h256z" />
+                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1"
+                            Text="创建房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
+                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,10,30,10" 
+                            Text="邀请朋友加入你的单人游戏。"
+                            FontSize="15" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" />
+                    </Grid>
+                </local:MyCard>
+                <local:MyCard x:Name="PanSelectJoin" Grid.Column="1" Grid.Row="3" CornerRadius="13" VerticalAlignment="Center">
+                    <Grid IsHitTestVisible="False">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="25" />
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="13" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="Auto" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="22" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="Auto" />
+                            <RowDefinition Height="12" />
+                        </Grid.RowDefinitions>
+                        <Path Grid.Column="1" Grid.Row="1" Height="26" Width="26" Margin="0,-1,0,0"
+                            Stretch="Uniform" Fill="{DynamicResource ColorBrush3}"
+                            Data="m399,355l-92.5,101.6c-13.7,13.7-10.6,36.4,4.6,47,13.7,9.1,30.3,7.6,41-4.5l127.4-138.1c16.7-18.2,18.2-47,0-66.8l-125.9-141.1c-12.1-13.7-34.9-15.2-47,0-12.1,12.1-12.1,31.9,0,44l92.5,98.6H28.8c-16.7,0-28.8,13.7-28.8,28.8s13.7,28.8,28.8,28.8h370.2v1.5ZM625.7,0H224.6c-44,0-78.9,34.9-78.9,78.9v101.6c0,15.2,12.1,27.3,27.3,27.3s27.3-12.1,27.3-27.3v-98.6c0-15.2,12.1-27.3,27.3-27.3h387.5c15.2,0,27.3,12.1,27.3,27.3v493c0,15.2-12.1,27.3-27.3,27.3H229.2c-15.2,0-27.3-12.1-27.3-27.3v-101.6c0-16.7-13.7-28.8-28.8-28.8s-28.8,13.7-28.8,28.8v98.6c0,44,34.9,78.9,78.9,78.9h407.2c44,0,67.9-34.9,67.9-78.9V78.9c-4.6-44-28.4-78.9-72.4-78.9Z" />
+                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1"
+                            Text="加入房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
+                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,10,30,10" 
+                            Text="输入你收到的邀请码,加入朋友的房间。"
+                            FontSize="15" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" />
+                    </Grid>
+                </local:MyCard>
+            </Grid>
+            <Grid x:Name="PanFinish" Visibility="Collapsed" HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="300">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition Height="25" />
+                    <RowDefinition Height="Auto" />
+                </Grid.RowDefinitions>
+                <local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="12">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="25" />
+                            <ColumnDefinition Width="Auto" /> <!--Logo-->
+                            <ColumnDefinition Width="13" />
+                            <ColumnDefinition Width="1*" /> <!--文本区 ↓↓-->
+                            <ColumnDefinition Width="Auto" /> <!--按钮 1-->
+                            <ColumnDefinition Width="Auto" /> <!--按钮 2-->
+                            <ColumnDefinition Width="25" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="21" />
+                            <RowDefinition Height="Auto" /> <!--标题栏-->
+                            <RowDefinition Height="Auto" /> <!--文本区-->
+                            <RowDefinition Height="Auto" /> <!--按钮-->
+                            <RowDefinition Height="21" />
+                        </Grid.RowDefinitions>
+                        <Path Grid.Column="1" Grid.Row="1" Height="28" Width="28" x:Name="ImgFinishLogo"
+                              Stretch="Uniform" Fill="{DynamicResource ColorBrush3}" RenderTransformOrigin="0.5,0.54"
+                              Data="M845 547.45h65.54A400.6 400.6 0 0 0 712.7 201.77l-33.22 56.49C781.6 318.32 845 429.13 845 547.45zM347.72 253.92L316 196.58a401.21 401.21 0 0 0-207 350.87h65.54a335.66 335.66 0 0 1 173.18-293.53zM509.79 882.68a334 334 0 0 1-181.3-53.2L293 884.57a401.13 401.13 0 0 0 419.1 8.91L679 836.93a334.94 334.94 0 0 1-169.21 45.75z M821.51 625.1A108.64 108.64 0 0 0 752.2 650l-67.31-38.86a185.44 185.44 0 0 0 11.22-63.7c0-91.55-66.38-167.9-153.54-183.42V289a109.14 109.14 0 1 0-65.57 0v75c-87.16 15.53-153.54 91.87-153.54 183.42a185.46 185.46 0 0 0 11.22 63.7L267.39 650a109.17 109.17 0 1 0 35.81 55l64.47-37.23a186 186 0 0 0 284.23 0L716.38 705a109.13 109.13 0 1 0 105.13-79.9zM198.07 777.85a43.6 43.6 0 1 1 43.61-43.61 43.66 43.66 0 0 1-43.61 43.61z m311.72-636.53a43.61 43.61 0 1 1-43.61 43.61 43.65 43.65 0 0 1 43.61-43.61z m0 526.91a120.78 120.78 0 1 1 120.78-120.78 120.91 120.91 0 0 1-120.78 120.77z m311.72 109.62a43.6 43.6 0 1 1 43.61-43.61 43.65 43.65 0 0 1-43.61 43.61z" />
+                        <TextBlock x:Name="LabFinishTitle" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1" Margin="0,3,0,0"
+                                   Text="已加入房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
+                        <TextBlock x:Name="LabFinishDesc" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,9,10,10" 
+                                   Text="描述文本。"
+                                   FontSize="14" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray2}" />
+                        <local:MyButton x:Name="BtnFinishCopy" Grid.Column="4" Grid.Row="3" Margin="0,0,15,0" Text="复制" Padding="13,8" ColorType="Highlight" />
+                        <local:MyButton x:Name="BtnFinishExit" Grid.Column="5" Grid.Row="3" Text="退出" Padding="13,8" />
+                    </Grid>
+                </local:MyCard>
+                <local:MyCard Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="8">
+                    <StackPanel Orientation="Horizontal" Margin="3,0">
+                        <StackPanel x:Name="BtnFinishPing" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
+                                    ToolTip="延迟" ToolTipService.Placement="Center" ToolTipService.InitialShowDelay="50" ToolTipService.VerticalOffset="40" ToolTipService.HorizontalOffset="4">
+                            <Path IsHitTestVisible="False" Height="14" Width="14" Stretch="Uniform" Fill="#9C9C9C" Margin="13,0,8,0"
+                                Data="M60.9 966.2A60.3 60.3 0 0 1 0.8 906.1v-90.1a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v90.1a60.2 60.2 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V665.6a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v240.5a60.2 60.2 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V425.1a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v481.0a60.3 60.3 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V124.5a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v781.6a60.3 60.3 0 0 1-60.1 60.1z" />
+                            <TextBlock x:Name="LabFinishPing" IsHitTestVisible="False" Text="15 ms" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
+                        </StackPanel>
+                        <TextBlock Text="|" Opacity="0.2" Margin="0,8.8,0,8" RenderTransformOrigin="0.5,0.5">
+                            <TextBlock.RenderTransform>
+                                <ScaleTransform ScaleX="1.5" ScaleY="2.75" CenterX="0.5" CenterY="0.5" />
+                            </TextBlock.RenderTransform>
+                        </TextBlock>
+                        <StackPanel x:Name="BtnFinishPlayer" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
+                                    ToolTip="房间内人数" ToolTipService.Placement="Center" ToolTipService.InitialShowDelay="50" ToolTipService.VerticalOffset="40" ToolTipService.HorizontalOffset="4">
+                            <Path IsHitTestVisible="False" Height="18" Width="18" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="13,0,8,0"
+                                Data="m843.3,478.4c-.1-3.9-.2-7.7-.2-10.8,0-42.8-11.1-84.4-32-120.2-19.6-33.5-47.1-60.7-79.7-79,15-16.8,23.6-39.2,25-65.2,1.3-24.8-3.7-52.1-14.6-79-20.5-50.8-56-86.3-86.3-86.3s-25.1,4.1-37.5,23.5l-.4.7-.2.8c-4.1,16.6,3.9,33.4,20,41.7l.7.4h.8c34.9,5.6,50.6,38.7,53.5,65.3,3.3,30.7-7.8,67.2-40.1,80.1-7.4,1.1-9.9,7.7-11.5,12.1-.6,1.7-1.2,3.3-1.9,4.3l-.7,1v1.2c-3.4,27.8,19.6,39.8,46.3,53.6,41.4,21.4,93,48.1,93.1,144.9v.3c.3,4.3,0,9.9-.1,15.7-.6,17.9-1.4,40.2,10.3,52.4,5.5,5.7,13,8.6,22.5,8.7.7,0,1.4,0,2.1,0h0c8.1,0,14.7-2.6,19.8-7.9,12.7-13.1,11.9-39.4,11.4-58.6Z M204.7,295.9l.5-1.1v-30.1c0-3.9-2.1-6-3.3-7.1-.1-.1-.2-.2-.3-.3v-1.7l-8.9-9.2-.7-.4c-26.3-13.5-42.6-40.8-42.6-71.3s21.8-67.4,55.5-78.1c17-4.5,28.2-22.1,24.5-38.4v-.6c-.1,0-.4-.5-.4-.5-7.5-15.3-21.4-24.7-36.2-24.7s-5.1.3-7.7.8h-.2c-29.8,7.2-56.6,25.5-75.7,51.4-18.9,25.7-29.3,57.8-29.3,90.4s11.1,64.8,32.2,92.1C45.3,303.9,2.5,376.9,0,458.9v57.5s0,.2,0,.2c1.4,15.3,13.1,30.8,32.8,30.8s32.9-12.7,32.9-33.9v-43.3c0-76.2,45.9-139.2,114.3-157.1,10.5-.5,19.5-6.8,24.7-17.3Z M516,286.5c36.8-31.1,58.5-77.4,58.5-125.4C574.4,70.8,507.4,0,421.7,0s-152.7,70.8-152.7,161.2,20.4,95.1,56.4,125.6c-51.7,19.8-96.1,54.8-129,101.5-35.2,50.2-53.9,109.7-53.9,172v33.8c0,16.9,13.7,30.6,30.6,30.6h496.6c16.9,0,30.6-13.7,30.6-30.6v-33.8c0-61.4-19-120.6-54.9-171.3-32.9-46.4-78.6-82.5-129.5-102.4Zm-94.5-225.3c51.3,0,91.5,43.9,91.5,100s-41.9,100-91.5,100-91.5-43.9-91.5-100,40.2-100,91.5-100Zm218,502H203.8v-3.2c0-127,97.8-230.3,218-230.3s217.7,105.4,217.7,230.3v3.2Z" />
+                            <TextBlock x:Name="LabFinishPlayer" IsHitTestVisible="False" Text="2  人" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
+                        </StackPanel>
+                        <TextBlock Text="|" x:Name="LineFinishPort" Opacity="0.2" Margin="0,8.8,0,8" RenderTransformOrigin="0.5,0.5">
+                            <TextBlock.RenderTransform>
+                                <ScaleTransform ScaleX="1.5" ScaleY="2.75" CenterX="0.5" CenterY="0.5" />
+                            </TextBlock.RenderTransform>
+                        </TextBlock>
+                        <StackPanel x:Name="BtnFinishPort" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
+                                    ToolTip="端口" ToolTipService.Placement="Center" ToolTipService.InitialShowDelay="50" ToolTipService.VerticalOffset="40" ToolTipService.HorizontalOffset="4">
+                            <Path IsHitTestVisible="False" Height="16" Width="16" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="13,0,7,0"
+                                Data="m365,295.3c-15.1-3.6-36.5-1.8-39.6,17.5-1.2,12.4,0,107-.5,119.7.8,16.8-5.2,42.4,16.8,47.1,14.3,3.3,34.5,1.9,38.7-15.4,2.6-1.4.1-142.4,1-148.3-.7-10.9-6.2-17.8-16.4-20.6ZM234.1,311.6c-3.2-16.7-20-18.6-34.4-17.2-9.8.4-17.2,6.2-20,15.4-4.4,47.8-.2,97.3-1.6,144.8,0,16,9.8,26.6,24.6,26.1,14.4,1.4,29.1-2.9,31.7-19,2.2-8.8.8-63.9,1.1-74.1-1-25.3,2.1-50.6-1.5-75.9ZM521.4,300.6c-5.4-6.2-15.9-7.1-24.6-6.4-11.9-1-23,6.6-24.7,18.7-2.2,24.1-2,124-.2,148.1,2.6,19.7,24.1,22.4,40.1,18.4,11.9-2.4,16.6-15.2,16.3-27.1,0-33.6.1-69.3,0-103.1-1.2-15.1,4.2-37-6.9-48.5ZM708.8,194.2c.7-34.5-26.8-63.7-61.5-63.4-22.1-.1-52.4,0-75.6,0-13.4,1.1-8.7-12.1-9.6-20.9-1.6-30.6,7.5-67.2-17.5-90.9-11.7-11.9-27.4-18.4-44.3-18.4-64.8,0-129.7,0-194.5,0-37.7,1.4-76.5-3.1-113.8,2.2-48.1,14.7-46.7,55.3-45.4,97.3,0,8,.1,16.1,0,24.1.2,7-7.3,6.8-12.2,6.6-16,0-32,0-48,0-24.4-.7-51.3-1-68.6,18.4C5.8,161.2,0,176.1,0,194.7c0,0,0,350.9,0,350.9,0,22.9,5.5,41.7,16.8,57.5,19.2,27,45.9,40.7,79.5,40.7,37.4,0,77.4.1,129.9.1,42.5,0,85,0,127.5,0,11.2-1.3,273.6,2.4,278-1.8,46.1-9.1,77.1-46.8,77.1-93.8,0-117.8,0-237.9,0-354.1Zm-71-5.3c7.6.2,9.2,1.9,9.3,9.7,0,0,0,347.3,0,347.3,0,3.2.2,6.5-.3,9.9-2.8,19.3-18.9,32.3-38.5,31.9-4.5.4-508.9-.4-513.5,0-3.2,0-6.5.1-9.9-.4-19-2.9-31.8-17.8-31.8-37.1,0-117.9,0-233.9,0-350.9,0-9,2.3-10.3,10.9-10.3,0,0,131.2,0,131.2,0,1.6,0,3,0,4.5,0,0-1.5,0-2.7,0-4.3,0,0,0-115.7,0-115.7.1-8.3,1.9-10.1,10.3-10.1h280.3c8.3,0,10.1,1.8,10.1,10.3v114.1c0,1.9,0,3.7,0,5.6" />
+                            <TextBlock x:Name="LabFinishPort" IsHitTestVisible="False" Text="12345" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
+                        </StackPanel>
+                        <!--<StackPanel x:Name="BtnFinishIp" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
+                                    ToolTip="服务器 IP ( 点击复制 )" ToolTipService.Placement="Center" ToolTipService.InitialShowDelay="50" ToolTipService.VerticalOffset="40" ToolTipService.HorizontalOffset="4">
+                            <Path IsHitTestVisible="False" Height="17" Width="17" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="11,0,7,0"
+                                Data="m346.4,692.8c-92.5,0-179.5-36-244.9-101.5C36,525.9,0,438.9,0,346.4S36,166.9,101.5,101.5C166.9,36,253.9,0,346.4,0s179.5,36,244.9,101.5c65.4,65.4,101.5,152.4,101.5,244.9s-36,179.5-101.5,244.9c-65.4,65.4-152.4,101.5-244.9,101.5Zm0-645.1c-164.7,0-298.7,134-298.7,298.7s134,298.7,298.7,298.7,298.7-134,298.7-298.7S511.1,47.7,346.4,47.7Zm13.8,473.4c-14.5,0-28.2-9.8-28.2-24.2V207.9c0-14.5,11.8-26.2,26.2-26.2h78.9c57.9,0,105.1,47.1,105.1,105.1s-47.1,105.1-105.1,105.1h-52.6v105.1c0,14.5-9.8,24.2-24.2,24.2Zm24.2-181.7h52.6c29,0,52.6-23.6,52.6-52.6s-20-49.2-46.5-52.3h-.4s-5.3-.4-5.3-.4h-53v105.2Zm-158.8,181.7c-14.5,0-28.2-9.8-28.2-24.2V207.9c0-14.5,11.8-26.2,26.2-26.2s26.2,11.8,26.2,26.2v289c0,14.5-9.8,24.2-24.2,24.2Z" />
+                            <TextBlock x:Name="LabFinishIp" IsHitTestVisible="False" Text="6.0.0.1:12345" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
+                        </StackPanel>-->
+                    </StackPanel>
+                </local:MyCard>
+            </Grid>
+        </Grid>
+        <Grid x:Name="PanAlways" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="15" Opacity="0.5">
+        </Grid>
+    </Grid>
+</local:MyPageRight>

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

@@ -0,0 +1,844 @@
+Public Class PageLinkMain
+
+    '===============================
+    '  状态机与前端页面
+    '===============================
+
+#Region "状态管理"
+
+    Private Enum States
+        Waiting
+        Loading
+        Failed
+        Finished
+    End Enum
+    Private State As States = States.Waiting
+
+    ''' <summary>
+    ''' 切换到指定的状态。
+    ''' </summary>
+    Private Sub ChangeState(NewState As States)
+        If State = NewState Then Return
+        Dim OldState = State
+        State = NewState
+        Log($"[Link] 主状态由 {GetStringFromEnum(OldState)} 变更为 {GetStringFromEnum(NewState)}")
+        '触发状态切换
+        RunInUi(
+        Sub()
+            Select Case NewState
+                Case States.Waiting
+                    SwitchToWaiting(OldState)
+                Case States.Loading
+                    SwitchToLoading(OldState)
+                Case States.Failed
+                    SwitchToFailed(OldState)
+                Case States.Finished
+                    SwitchToFinished(OldState)
+            End Select
+        End Sub)
+    End Sub
+
+    '让加载器的 成功/失败 事件触发状态切换
+    Private Sub LoaderInit() Handles Me.Initialized
+        PageLoaderInit(Load, PanLoad, PanContent, PanAlways, LinkLoader, AutoRun:=False)
+        AddHandler LinkLoader.OnStateChangedUi,
+        Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
+            Select Case NewState
+                Case LoadState.Finished
+                    ChangeState(States.Finished)
+                Case LoadState.Failed
+                    Telemetry("联机失败", "Exception", FilterUserName(Loader.Error.GetDetail, "*"))
+                    ChangeState(States.Failed)
+            End Select
+        End Sub
+    End Sub
+
+    '页面动画兼容
+    Private Sub UpdatePanelVisibility() Handles Me.PageEnter
+        FrmLinkMain.PanSelect.Visibility = If(State = States.Waiting, Visibility.Visible, Visibility.Collapsed)
+        FrmLinkMain.PanFinish.Visibility = If(State = States.Finished, Visibility.Visible, Visibility.Collapsed)
+    End Sub
+
+#End Region
+
+#Region "初始页面 (Waiting)"
+
+    '由任意状态切换到 Waiting
+    Private Sub SwitchToWaiting(OldState As States)
+        '加载器与进程状态
+        LinkLoader.Abort()
+        SyncLock LinkLoader.LockState
+            LinkLoader.State = LoadState.Waiting
+        End SyncLock
+        ProcessStop()
+        '页面切换
+        If OldState <> States.Finished Then
+            '从完成页面退出时,PageOnContentExit 结束会触发 PageEnter 事件,这会调用 UpdatePanelVisibility
+            '如果在现在提前调用了,会丢失完成页面的退出动画
+            UpdatePanelVisibility()
+        End If
+        PageOnContentExit()
+    End Sub
+
+    '===============================
+    '  页面 UI
+    '===============================
+
+    '创建
+    Private Sub Create_MouseLeftButtonUp() Handles PanSelectCreate.MouseLeftButtonUp
+        '输入端口号
+        Dim Port As String = MyMsgBoxInput("输入端口", $"在单人游戏的暂停菜单选择 {vbLQ}对局域网开放{vbRQ},然后输入端口数字。{vbCrLf}其实也可以输入其他游戏的端口……",
+            ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateInteger(1024, 65535)},
+            HintText:="端口号")
+        If Port Is Nothing Then Return
+        '基础信息
+        IsServerSide = True
+        ServerPort = Port
+        Dim GenerateRandomCode =
+        Function() As String
+            Dim Result As String = ""
+            For i = 1 To 5
+                Result &= RadixConvert(RandomInteger(0, 35), 10, 36)
+            Next
+            Return Result.Replace("O", "0").Replace("I", "1")
+        End Function
+        NetworkName = $"P{RadixConvert(ServerPort, 10, 16).PadLeft(4, "0"c)}-{GenerateRandomCode()}"
+        NetworkSecret = GenerateRandomCode()
+        Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
+        '启动
+        ChangeState(States.Loading)
+    End Sub
+
+    '加入
+    Private LastCode As String = Nothing
+    Private Sub Join_MouseLeftButtonUp() Handles PanSelectJoin.MouseLeftButtonUp
+        '输入邀请码
+        Dim Code As String = MyMsgBoxInput("输入邀请码", "输入房主发给你的邀请码。",
+                                           HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码:" & LastCode))
+        If Not String.IsNullOrEmpty(LastCode) AndAlso Code IsNot Nothing AndAlso Code = "" Then Code = LastCode
+        If Code Is Nothing Then Return
+        '基础格式校验
+        Code = Code.Between("【", "】").Between("[", "]") '从完整消息中提取
+        Code = Code.ToUpper.Replace("O", "0").Replace("I", "1") '输入修正
+        If Not (Code.Length >= 14 AndAlso Code(0) = "P"c AndAlso Code(5) = "-"c AndAlso Code(11) = "-"c) Then
+            If Code.StartsWithF("U/") Then 'HMCL
+                Hint("请让房主使用 PCL 创建房间!", HintType.Red)
+            ElseIf Code.Length = 10 Then 'PCL CE
+                Hint("请让房主使用非社区版的 PCL 创建房间!", HintType.Red)
+            ElseIf Code.StartsWithF("P") Then '远古版 PCL
+                Hint("你的 PCL 版本与房主的 PCL 版本不一致,或输入的邀请码有误!", HintType.Red)
+            Else
+                Hint("邀请码有误,请让房主使用 PCL 创建房间!", HintType.Red)
+            End If
+            Return
+        End If
+        '离线登录警告
+        If McLoginLoader.State = LoadState.Finished AndAlso McLoginLoader.Output.Type = "Legacy" AndAlso Not Setup.Get("LinkOfflineHint") Then
+            Dim Solution As String
+            If McVersionCurrent IsNot Nothing AndAlso McVersionCurrent.Version.McVersion >= New Version(1, 21, 5) Then
+                Solution = $"请换用正版登录,或是让房主安装 {vbLQ}更高级联机设置{vbRQ} Mod 并禁用正版验证。"
+            ElseIf McVersionCurrent IsNot Nothing AndAlso McVersionCurrent.Version.McVersion >= New Version(1, 12, 0) Then
+                Solution = $"请换用正版登录,或是让房主安装 {vbLQ}自定义局域网联机{vbRQ} Mod 并关闭在线模式。"
+            Else
+                Solution = $"请换用正版登录,或是让房主用服务端开服,关闭 online-mode,然后在创建房间时输入服务端的端口号。"
+            End If
+            Select Case MyMsgBox($"如果不用正版登录可能会进不了服,显示 {vbLQ}无效会话{vbRQ}。" & vbCrLf & Solution,
+                                 "离线登录警告", "继续", "继续且不再提示", "取消", IsWarn:=True)
+                Case 2
+                    Setup.Set("LinkOfflineHint", True)
+                Case 3
+                    Return
+            End Select
+        End If
+        '基础信息
+        IsServerSide = False
+        ServerPort = RadixConvert(Code.Substring(1, 4), 16, 10)
+        NetworkName = Code.Substring(0, 11)
+        NetworkSecret = Code.Substring(12, 5)
+        Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
+        '启动
+        LastCode = Code
+        ChangeState(States.Loading)
+    End Sub
+
+#End Region
+
+#Region "加载页面 (Loading | Failed)"
+
+    '由 Waiting 或 Failed 状态切换到 Loading
+    Private Sub SwitchToLoading(OldState As States)
+        '清理变量
+        Peers = Nothing
+        FailCount = 0
+        '加载器与进程状态
+        LinkLoader.Start(IsForceRestart:=True)
+        '页面切换会由 Loader 调用 MyPageRight 来触发
+        'UI 更新
+        UpdateProgressBar(0)
+        LabLoadTitle.Text = If(IsServerSide, "创建房间中", "加入房间中")
+        UpdateLoadingPage("正在初始化……", "准备初始化")
+    End Sub
+
+    '由 Loading 状态切换到 Failed
+    Private Sub SwitchToFailed(OldState As States)
+        '加载器与进程状态
+        LinkLoader.Abort()
+        ProcessStop()
+        'UI 更新
+        UpdateProgressBar(1)
+        '显示错误信息
+        LabLoadTitle.Text = FailReason
+        Dim Brief As String = LinkLoader.Error.GetBrief
+        Dim InnerEx As Exception = LinkLoader.Error
+        If InnerEx.Message.StartsWithF("$") Then Brief = InnerEx.Message
+        Do Until InnerEx.InnerException Is Nothing
+            InnerEx = InnerEx.InnerException
+            If InnerEx.Message.StartsWithF("$") Then Brief = InnerEx.Message
+        Loop
+        LabLoadDesc.Text = If(Brief.StartsWithF("$"), Brief.TrimStart("$"), LinkLoader.Error.GetDetail)
+        Log(LinkLoader.Error, LabLoadTitle.Text)
+    End Sub
+
+    '===============================
+    '  页面 UI
+    '===============================
+
+    Private Sub UpdateLoadingPage(Title As String, FailBrief As String)
+        If FailReason = FailBrief & "失败" Then Return
+        FailReason = FailBrief & "失败"
+        Log("[Link] 开始步骤:" & Title)
+        RunInUiWait(
+        Sub()
+            If FrmLinkMain Is Nothing OrElse Not FrmLinkMain.LabLoadDesc.IsLoaded Then Return
+            FrmLinkMain.LabLoadDesc.Text = Title
+        End Sub)
+    End Sub
+    Private FailReason As String = "准备初始化"
+
+    '取消加载
+    Private Sub BtnLoadCancel_Click() Handles BtnLoadCancel.Click
+        ChangeState(States.Waiting)
+    End Sub
+
+    '点击重试
+    Private Sub CardLoad_MouseLeftButtonUp() Handles CardLoad.MouseLeftButtonUp
+        ChangeState(States.Loading)
+    End Sub
+
+    '进度条
+    Private Sub UpdateProgressBar(Optional Value As Double = -1)
+        If Value = -1 Then Value = LinkLoader.Progress
+        Dim DisplayingProgress As Double = ColumnProgressA.Width.Value
+        If Math.Round(Value - DisplayingProgress, 3) = 0 Then Return
+        If DisplayingProgress <= Value Then
+            Dim NewProgress As Double = If(Value = 1, 1, DisplayingProgress +
+                (Value - DisplayingProgress) ^ 2 * 0.5)
+            AniStart({
+                AaGridLengthWidth(ColumnProgressA, NewProgress - ColumnProgressA.Width.Value, 200),
+                AaGridLengthWidth(ColumnProgressB, (1 - NewProgress) - ColumnProgressB.Width.Value, 200)
+            }, "Link Progress")
+        Else
+            ColumnProgressA.Width = New GridLength(Value, GridUnitType.Star)
+            ColumnProgressB.Width = New GridLength(1 - Value, GridUnitType.Star)
+            AniStop("Link Progress")
+        End If
+    End Sub
+    Private Sub CardLoad_SizeChanged() Handles CardLoad.SizeChanged
+        RectProgressClip.Rect = New Rect(0, 0, CardLoad.ActualWidth, 12)
+    End Sub
+
+#End Region
+
+#Region "完成页面 (Finished)"
+
+    '由 Loading 状态切换到 Finished
+    Private Sub SwitchToFinished(OldState As States)
+        '由于只可能从加载器的完成事件触发,不需要管加载器
+        UpdatePanelVisibility() '页面的实际切换会由 Loader 调用 MyPageRight 来触发
+
+        'UI 更新
+        If IsServerSide Then
+            LabFinishTitle.Text = "已创建房间"
+            LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{NetworkName}-{NetworkSecret}"
+            BtnFinishCopy.Text = "复制邀请码"
+            BtnFinishExit.Text = "关闭"
+            '下边栏
+            BtnFinishPing.ToolTip = "网络延迟"
+            LineFinishPort.Visibility = Visibility.Visible
+            BtnFinishPort.Visibility = Visibility.Visible
+            LabFinishPort.Visibility = Visibility.Visible
+            LabFinishPort.Text = ServerPort
+        Else
+            LabFinishTitle.Text = "已加入房间"
+            LabFinishDesc.Text = $"在多人游戏中选择直接连接,粘贴地址即可加入游戏!{vbCrLf}服务器地址:{ClientAddress}" '"如果提示无效会话,不要退出联机房间,重新启动游戏即可!"
+            BtnFinishCopy.Text = "复制地址"
+            BtnFinishExit.Text = "离开"
+            '下边栏
+            BtnFinishPing.ToolTip = "与房主的延迟"
+            LineFinishPort.Visibility = Visibility.Collapsed
+            BtnFinishPort.Visibility = Visibility.Collapsed
+            LabFinishPort.Visibility = Visibility.Collapsed
+        End If
+        Copy() '复制信息
+        Update() '立即刷新
+    End Sub
+
+    '===============================
+    '  页面 UI
+    '===============================
+
+    '退出
+    Private Sub BtnFinishExit_Click() Handles BtnFinishExit.Click
+        TryExit(True)
+    End Sub
+    ''' <summary>
+    ''' 退出联机。
+    ''' 返回是否弹出了警告窗口并且玩家选择了取消。
+    ''' </summary>
+    Public Function TryExit(SendWarning As Boolean) As Boolean
+        If State = States.Waiting OrElse State = States.Failed Then Return False
+        If IsServerSide AndAlso PeopleCount > 1 Then
+            If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "关闭房间", "确定", "取消", IsWarn:=True) = 2 Then Return True
+        End If
+        ChangeState(States.Waiting)
+        Return False
+    End Function
+
+    '复制
+    Private Sub Copy() Handles BtnFinishCopy.Click
+        If IsServerSide Then
+            ClipboardSet($"在 PCL 启动器中输入邀请码【{NetworkName}-{NetworkSecret}】,即可加入联机房间!", False)
+            Hint("已复制邀请码!", HintType.Green)
+        Else
+            ClipboardSet(ClientAddress, False)
+            Hint("已复制服务器地址!", HintType.Green)
+        End If
+    End Sub
+
+#End Region
+
+    '===============================
+    '  后端
+    '===============================
+
+    ''' <summary>
+    ''' EasyTier 所在的文件夹路径,以 \ 结尾。
+    ''' </summary>
+    Private PathEasyTier As String = PathAppdata & "EasyTier\"
+    ''' <summary>
+    ''' 当前是服务端还是客户端。
+    ''' </summary>
+    Private IsServerSide As Boolean
+    ''' <summary>
+    ''' 服务端映射前的端口号。
+    ''' </summary>
+    Private ServerPort As Integer
+    ''' <summary>
+    ''' 在客户端映射后的地址。
+    ''' 除非已作为客户端启动 EasyTier,否则一直为 Nothing。
+    ''' </summary>
+    Private ClientAddress As String = Nothing
+    ''' <summary>
+    ''' 网络信息。
+    ''' </summary>
+    Private NetworkName As String, NetworkSecret As String
+
+#Region "加载"
+
+    Private WithEvents LinkLoader As New LoaderCombo(Of Integer)("联机", {
+        New LoaderTask(Of Integer, Integer)("获取配置", AddressOf InitConfig) With {.Block = False, .ProgressWeight = 8},
+        New LoaderTask(Of Integer, List(Of NetFile))("准备下载联机模块", AddressOf InitPrepareDownload) With {.ProgressWeight = 2},
+        New LoaderDownload("下载联机模块", New List(Of NetFile)) With {.ProgressWeight = 40},
+        New LoaderTask(Of Integer, Integer)("启动联机模块", AddressOf InitLaunch) With {.ProgressWeight = 50}
+    })
+
+    '1. 获取服务器配置
+    Private Sub InitConfig(Task As LoaderTask(Of Integer, Integer))
+        UpdateLoadingPage("正在联网获取配置……", "联网获取配置")
+        If VersionBranchMain <> "Official" Then
+            Throw New Exception($"$开源版无法联网获取配置。{vbCrLf}你可以在 PCL 官方版的缓存文件夹下查看 ServerConfig 的当前内容, 并在代码中进行相应修改。")
+        End If
+        ServerLoader.WaitForExit(LoaderToSyncProgress:=Task)
+        If ServerConfig Is Nothing Then Throw New Exception("无法从服务器获取配置")
+        If Not String.IsNullOrEmpty(ServerConfig("Link")("DisableReason")) Then '检查是否已禁用联机功能
+            Throw New Exception("$" & ServerConfig("Link")("DisableReason").ToString)
+        End If
+    End Sub
+
+    '2. 获取需要下载的文件
+    Private ServerVersion As Integer
+    Private Sub InitPrepareDownload(Task As LoaderTask(Of Integer, List(Of NetFile)))
+        '获取 CPU 架构
+        UpdateLoadingPage("正在获取 CPU 架构……", "获取 CPU 架构")
+        Dim Architecture As String = GetType(String).Assembly.GetName().ProcessorArchitecture
+        Select Case Architecture
+            Case Reflection.ProcessorArchitecture.X86
+                Architecture = "i686"
+            Case Reflection.ProcessorArchitecture.Amd64
+                Architecture = "x86_64"
+            Case Reflection.ProcessorArchitecture.Arm
+                Architecture = "arm64"
+            Case Else
+                Log($"[Link] CPU 是不支持的 {Architecture} 架构,这可能会导致联机模块无法启动!", LogLevel.Debug)
+                Architecture = "arm64"
+        End Select
+        Log("[Link] CPU 架构:" & Architecture)
+        Telemetry("联机开始")
+        '检查 EasyTier 版本
+        UpdateLoadingPage("正在检查联机模块版本……", "检查联机模块版本")
+        If Not (File.Exists(PathEasyTier & "联机模块 CLI.exe") AndAlso File.Exists(PathEasyTier & "联机模块.exe") AndAlso
+                File.Exists(PathEasyTier & "Packet.dll")) Then
+            Setup.Set("LinkEasyTierVersion", -1)
+        End If
+        Dim LocalVersion As Integer = Setup.Get("LinkEasyTierVersion")
+        ServerVersion = ServerConfig("Link")("EasyTierVersion")
+        Dim RequiredFiles As New List(Of NetFile)
+        Log($"[Link] EasyTier 本地版本:{LocalVersion},需求版本:{ServerVersion}")
+        If LocalVersion < ServerVersion Then
+            RequiredFiles.Add(New NetFile(
+                ServerConfig("Link")("Downloads").Select(Function(UrlEntry) UrlEntry.ToString.Replace("{arch}", Architecture)),
+                PathEasyTier & "EasyTier.zip",
+                New FileChecker(MinSize:=1024 * 1024 * 2)))
+        End If
+        '开始下载
+        UpdateLoadingPage("正在下载联机模块……", "下载联机模块")
+        Task.Output = RequiredFiles
+    End Sub
+
+    '3. 启动联机模块
+    Private Shared HostName As String
+    Private Sub InitLaunch(Task As LoaderTask(Of Integer, Integer))
+        '解压文件
+        UpdateLoadingPage("正在解压联机模块……", "解压联机模块")
+        If File.Exists(PathEasyTier & "EasyTier.zip") Then
+            '解压
+            Dim ExtractPath As String = RequestTaskTempFolder()
+            ExtractFile(PathEasyTier & "EasyTier.zip", ExtractPath,
+                ProgressIncrementHandler:=Sub(Progress) Task.Progress += Progress * 0.06)
+            Dim ExtractedPath As String = New DirectoryInfo(ExtractPath).EnumerateDirectories.FirstOrDefault?.FullName
+            Log("[Link] 联机模块解压时的临时路径:" & ExtractedPath)
+            CopyDirectory(ExtractedPath, PathEasyTier)
+            '重命名
+            File.Delete(PathEasyTier & "联机模块.exe")
+            Rename(PathEasyTier & "easytier-core.exe", PathEasyTier & "联机模块.exe")
+            File.Delete(PathEasyTier & "联机模块 CLI.exe")
+            Rename(PathEasyTier & "easytier-cli.exe", PathEasyTier & "联机模块 CLI.exe")
+            '清理
+            File.Delete(PathEasyTier & "EasyTier.zip")
+            Setup.Set("LinkEasyTierVersion", ServerVersion)
+        End If
+        Task.Progress = 0.1
+        '获取启动参数
+        UpdateLoadingPage("正在启动联机模块……", "启动联机模块")
+        Dim Arguments As String = ServerConfig("Link")("Argument")
+        Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret}"
+        HostName = If(IsServerSide, "Server-", "Client-") & RadixConvert(Math.Abs(Identify.GetHashCode), 10, 36)
+        If IsServerSide Then
+            Arguments += $" -i 10.114.114.114 --hostname={HostName} --tcp-whitelist={ServerPort} --udp-whitelist={ServerPort}"
+        Else
+            Arguments += $" -d --hostname={HostName} --tcp-whitelist=0 --udp-whitelist=0"
+            '端口转发
+            Dim Port As Integer = GetAvailablePort()
+            ClientAddress = $"localhost:{Port}"
+            Arguments += $" --port-forward tcp://[{IPAddress.IPv6Loopback}]:{Port}/10.114.114.114:{ServerPort}"
+            Arguments += $" --port-forward udp://[{IPAddress.IPv6Loopback}]:{Port}/10.114.114.114:{ServerPort}"
+            Arguments += $" --port-forward tcp://{IPAddress.Loopback}:{Port}/10.114.114.114:{ServerPort}"
+            Arguments += $" --port-forward udp://{IPAddress.Loopback}:{Port}/10.114.114.114:{ServerPort}"
+        End If
+        For Each Peer As String In ServerConfig("Link")("Peers")
+            Arguments += $" -p=""{Peer}"""
+        Next
+        If Not Setup.Get("LinkShareMode") Then Arguments += " --private-mode true"
+        '启动进程
+        ProcessStart(Arguments)
+        Task.Progress = 0.15
+        '等待连接
+        Dim TimeoutCounter As Integer = 0
+        Do
+            Thread.Sleep(500) '为避免还没启动好先跑 CLI 了,先等半秒
+            '刷新信息
+            CheckCrash()
+            RefreshPeerLoader.WaitForExit(IsForceRestart:=True)
+            '查找目标节点
+            Dim Ping = GetPeerPing()
+            If Ping <> 0 Then
+                Log($"[Link] 已与目标建立连接,当前 Ping 为 {Ping:0.0}ms")
+                Telemetry("联机成功", "Server", IsServerSide, "Ping", Ping)
+                Exit Do '退出循环
+            End If
+            '更新进度
+            Dim LastProgress = Task.Progress
+            Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Count)
+            Select Case PeerCount
+                Case -1 'CLI 无返回
+                    Task.Progress = MathClamp(Task.Progress + 0.02, 0.15, 0.25)
+                Case 0 'CLI 有返回,但未连接到任何节点
+                    UpdateLoadingPage("正在连接到节点……", "连接节点")
+                    Task.Progress = MathClamp(Task.Progress + 0.02, If(IsServerSide, 0.5, 0.3), If(IsServerSide, 0.99, 0.5))
+                Case Else '已连接到节点,但未连接到房主
+                    UpdateLoadingPage("正在连接到房主……", "连接房主")
+                    Task.Progress = MathClamp(Task.Progress + 0.02, Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.99)
+            End Select
+            '超时判定
+            If LastProgress <> Task.Progress Then
+                TimeoutCounter = 0
+            ElseIf TimeoutCounter < 20 Then
+                TimeoutCounter += 1
+            Else '进度停滞超过 10s
+                Select Case PeerCount
+                    Case -1 'CLI 无返回
+                        Panic("无法启动联机模块", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf))
+                    Case 0 'CLI 有返回,但未连接到任何节点
+                        Panic("无法连接到节点", $"请检查你的网络环境是否良好。")
+                    Case Else '已连接到节点,但未连接到房主
+                        Panic("无法连接到房主", $"可能的原因:{vbCrLf}- 你或者房主的网络环境不佳{vbCrLf}- 房主已关闭房间{vbCrLf}- 邀请码输错了")
+                End Select
+            End If
+        Loop Until Task.IsAborted
+        If Task.IsAborted Then Throw New ThreadInterruptedException
+    End Sub
+
+#End Region
+
+#Region "进程与日志"
+
+    ''' <summary>
+    ''' EasyTier Core 进程。
+    ''' </summary>
+    Private Shared ProcessCore As Process
+    Private Shared ProcessOutputHandle As AutoResetEvent, ProcessErrorHandle As AutoResetEvent
+
+    ''' <summary>
+    ''' 若程序正在运行,则结束程序进程。
+    ''' 即使失败也不会抛出异常。
+    ''' </summary>
+    Public Shared Sub ProcessStop()
+        Try
+            '关闭未捕获的进程
+            For Each ProcessObject In Process.GetProcesses
+                If ProcessObject.Id = ProcessCore?.Id Then Continue For
+                If ProcessObject.ProcessName <> "联机模块" AndAlso ProcessObject.ProcessName <> "联机模块 CLI" Then Continue For
+                Try
+                    Log("[Link] 停止残留的联机模块,PID:" & ProcessObject.Id & ",进程名:" & ProcessObject.ProcessName)
+                    ProcessObject.Kill()
+                    ProcessObject.Close()
+                Catch exx As Exception
+                    Log(exx, $"结束进程失败({ProcessObject.ProcessName},PID {ProcessObject.Id})")
+                End Try
+            Next
+            '关闭由自身启动的进程
+            If ProcessCore IsNot Nothing AndAlso Not ProcessCore.HasExited Then
+                Log("[Link] 停止所启动的联机模块,PID:" & ProcessCore.Id)
+                ProcessCore.Kill()
+                ProcessCore.Close()
+                ProcessCore = Nothing
+            End If
+            '释放资源
+            ProcessOutputHandle?.Dispose()
+            ProcessErrorHandle?.Dispose()
+        Catch ex As Exception
+            Log(ex, "停止联机模块失败")
+        End Try
+    End Sub
+    ''' <summary>
+    ''' 出现意外错误,给出错误信息并结束联机。
+    ''' </summary>
+    Private Sub Panic(Brief As String, Detail As String)
+        If State = States.Loading Then
+            FailReason = Brief
+            LinkLoader.Failed(New Exception(Detail))
+        Else
+            MyMsgBox(Detail, "联机出错:" & Brief, IsWarn:=True)
+            ChangeState(States.Waiting)
+            Telemetry("联机失败", "Exception", FilterUserName(Brief & ":" & Detail, "*"))
+        End If
+        '收集到的 CLI 错误:
+
+        'Error: Timeout : deadline has elapsed
+        'Caused by
+        '    deadline has elapsed
+
+        'Error: failed to get peer manager client
+        'Caused by
+        '    0:  rust tun error failed to connect to server: Url {scheme:  "tcp", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("127.0.0.1")), port: Some(15888), path: "", query: None, fragment: None }
+        '    1:  failed to connect to server: Url {scheme:  "tcp", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("127.0.0.1")), port: Some(15888), path: "", query: None, fragment: None }
+        '    2:  IO error
+        '    3:  由于目标计算机积极拒绝, 无法连接。 (os error 10061)
+    End Sub
+
+    ''' <summary>
+    ''' 启动进程。
+    ''' </summary>
+    Private Sub ProcessStart(Arguments As String)
+        Dim Info = New ProcessStartInfo With {
+            .FileName = PathEasyTier & "联机模块.exe",
+            .Arguments = Arguments,
+            .UseShellExecute = False,
+            .CreateNoWindow = True,
+            .RedirectStandardError = True,
+            .RedirectStandardOutput = True,
+            .StandardOutputEncoding = Encoding.UTF8,
+            .StandardErrorEncoding = Encoding.UTF8
+        }
+        Log("[Link] 正在启动 EasyTier:" & Arguments)
+        ProcessCore = New Process With {.StartInfo = Info}
+        Dim LogLineHandler =
+        Function(sender As Object, e As DataReceivedEventArgs, Handle As AutoResetEvent)
+            Try
+                If e.Data Is Nothing Then
+                    Handle.[Set]()
+                    Return Nothing
+                End If
+                ProcessLogLine(e.Data)
+            Catch unused As ObjectDisposedException
+            Catch ex As Exception
+                Log(ex, "读取 EasyTier 信息失败")
+            End Try
+            Return Nothing
+        End Function
+        ProcessOutputHandle = New AutoResetEvent(False)
+        AddHandler ProcessCore.OutputDataReceived, Function(sender, e) LogLineHandler(sender, e, ProcessOutputHandle)
+        ProcessErrorHandle = New AutoResetEvent(False)
+        AddHandler ProcessCore.ErrorDataReceived, Function(sender, e) LogLineHandler(sender, e, ProcessErrorHandle)
+        ProcessCore.Start()
+        ProcessCore.BeginOutputReadLine()
+        ProcessCore.BeginErrorReadLine()
+    End Sub
+    Private Sub ProcessLogLine(Line As String)
+        '记录日志
+        Log("[EasyTier] " & Line)
+        LogHistory.Enqueue(Line)
+        If LogHistory.Count >= 10 Then LogHistory.Dequeue()
+        '检查日志内容
+        If Line.ContainsF("new peer connection added", True) Then
+            Log("[Link] 已建立连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), LogLevel.Debug)
+            Update()
+        ElseIf Line.ContainsF("peer connection removed", True) Then
+            Log("[Link] 已断开连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), LogLevel.Debug)
+            Update()
+        End If
+    End Sub
+    Private LogHistory As New Queue(Of String)(10)
+
+#End Region
+
+#Region "CLI 节点信息获取"
+
+    Private Class Peer
+        ''' <summary>
+        ''' 该节点的类型。
+        ''' </summary>
+        Public ReadOnly Type As Types
+        Public Enum Types
+            ''' <summary>
+            ''' 客户端。
+            ''' </summary>
+            Client
+            ''' <summary>
+            ''' 服务器。
+            ''' </summary>
+            Server
+            ''' <summary>
+            ''' 公共节点。
+            ''' </summary>
+            Misc
+            ''' <summary>
+            ''' 自己,或者自己的重复(它可能返回多个自己)。
+            ''' </summary>
+            Self
+        End Enum
+        ''' <summary>
+        ''' 与该节点的延迟 (ms)。
+        ''' 若是自己则为 0。
+        ''' </summary>
+        Public ReadOnly Ping As Double
+        ''' <summary>
+        ''' 名称。
+        ''' </summary>
+        Public ReadOnly Name As String
+
+        ''' <summary>
+        ''' 从 CLI 给出的信息行分析对应的数据。
+        ''' </summary>
+        Public Sub New(Info As String)
+            '具体每个部分的索引见 GetPeerData()
+            Dim Parts = Info.Split("|").Select(Function(s) If(s.All(Function(c) c = " "c), s, s.Trim)).Where(Function(s) s <> "").ToList
+            If Parts.Count <> 10 Then Throw New Exception($"信息列数有误")
+            '延迟
+            Ping = If(Parts(3) = "-", 0, Parts(3))
+            '类别
+            Dim PeerName = Parts(1)
+            If PeerName = HostName Then
+                Type = Types.Self
+            ElseIf PeerName.StartsWithF("Client") Then
+                Type = Types.Client
+            ElseIf Parts(0).StartsWithF("10.114.114.114") Then '只允许这个 IP 作为房主,避免网络被多个房主搞炸的情况
+                Type = Types.Server
+            Else
+                Type = Types.Misc
+            End If
+            '主机名
+            Name = PeerName
+        End Sub
+        Public Overrides Function ToString() As String
+            Return $"{Type} - {Name} - Ping {Ping:0.0}ms"
+        End Function
+    End Class
+
+    ''' <summary>
+    ''' 当前的节点列表,使用 RefreshPeerLoader 来刷新。
+    ''' 若尚未成功获取过则为 Nothing,但保证在加载完成后至少是一个列表。
+    ''' </summary>
+    Private Peers As List(Of Peer) = Nothing
+
+    ''' <summary>
+    ''' 调用 EasyTier CLI 获取已连接节点信息。
+    ''' 这会过滤掉尚未完成连接的其他节点以及自己。
+    ''' 若在加载完成后连续获取失败 3 次,则会强制断开连接。
+    ''' </summary>
+    Private RefreshPeerLoader As New LoaderTask(Of Integer, List(Of Peer))("EasyTier CLI", AddressOf RefreshPeer)
+    Private Sub RefreshPeer()
+        '| ipv4              | hostname              | cost     | lat(ms) | loss | rx      | tx      | tunnel | NAT            | version        |
+        '|-------------------|-----------------------|----------|---------|------|---------|---------|--------|----------------|----------------|
+        '| 10.114.114.1/24   | Client-RJ458A         | Local    | -       | -    | -       | -       | -      | Unknown        | 2.4.5-4c4d172e |
+        '|                   | PublicServer_公用服务器  | p2p      | 48.40   | 0.0% | 875 B   | 1.26 kB | tcp    | NoPat          | 2.4.5-4c4d172e |
+        '| 10.114.114.114/24 | Server-J6P6IW         | p2p      | 5.63    | 0.0% | 1.65 kB | 1.64 kB | udp    | PortRestricted | 2.4.5-4c4d172e |
+        '| 10.114.114.114/24 | Server-J6PHIW (连接中) | relay(2) | 1000.00 | 0.0% | 0 B     | 0 B     |        | PortRestricted | 2.4.5-4c4d172e |
+        Try
+            Dim CliResult = StartProcessAndGetOutput(PathEasyTier & "联机模块 CLI.exe", "peer", 2000, Encoding:=Encoding.UTF8, PrintLog:=False)
+            '解析
+            If Not CliResult.Contains("lat(ms)") Then Throw New Exception("CLI 调用失败:" & vbCrLf & CliResult)
+            If GetUuid() Mod If(ModeDebug, 4, 30) = 0 Then Log("[EasyTier] CLI 输出抽样:" & vbCrLf & CliResult)
+            Dim NewPeers As New List(Of Peer)
+            For Each Line In CliResult.Split(vbCrLf.ToCharArray, StringSplitOptions.RemoveEmptyEntries).Skip(2)
+                Try
+                    Dim Peer = New Peer(Line)
+                    If Peer.Type = Peer.Types.Self Then Continue For '自己
+                    NewPeers.Add(Peer)
+                Catch exx As Exception
+                    Log(exx, $"错误的信息行({Line})")
+                End Try
+            Next
+            '完成
+            Peers = NewPeers
+            FailCount = 0
+        Catch ex As Exception
+            Log(ex, "获取节点信息失败")
+            If State = States.Finished Then
+                FailCount += 1
+                If FailCount >= 4 Then Panic("获取节点信息失败", ex.Message)
+            End If
+        End Try
+    End Sub
+    ''' <summary>
+    ''' CLI 调用连续失败的次数。
+    ''' </summary>
+    Private FailCount As Integer = 0
+
+    ''' <summary>
+    ''' 根据当前的节点列表获取 Ping 值。
+    ''' 服务端会返回所有节点中最低的 Ping,客户端会返回服务端的 Ping。
+    ''' 0 代表尚不可用。
+    ''' </summary>
+    Private Function GetPeerPing() As Double
+        If Peers Is Nothing Then Return 0
+        Dim PingTargets As IEnumerable(Of Peer)
+        If IsServerSide Then
+            PingTargets = Peers
+        Else
+            PingTargets = Peers.Where(Function(p) p.Type = Peer.Types.Server)
+        End If
+        If Not PingTargets.Any Then
+            Return 0
+        Else
+            Return Math.Max(1, PingTargets.Select(Function(p) p.Ping).Min())
+        End If
+    End Function
+
+    ''' <summary>
+    ''' 获取当前房间中的人数,包括自己。
+    ''' 0 代表不可用。
+    ''' </summary>
+    Private ReadOnly Property PeopleCount As Integer
+        Get
+            If Peers Is Nothing Then Return 0
+            Return Peers.Where(Function(p) p.Type <> Peer.Types.Misc).Count() + 1 '加上自己
+        End Get
+    End Property
+
+#End Region
+
+#Region "定时状态更新"
+
+    '启动
+    Private IsTimerStarted As Boolean = False
+    Private Sub StartTimerThread() Handles Me.Loaded
+        If IsTimerStarted Then Return
+        RunInNewThread(
+        Sub()
+            Dim Counter As Integer = 0
+            Do While True
+                Try
+                    Thread.Sleep(200)
+                    Counter += 200
+                    '每 4 秒执行一次
+                    If Counter >= 4000 Then
+                        Counter = 0
+                        Update()
+                    End If
+                    '每 200ms 更新进度条
+                    If State = States.Loading Then RunInUi(AddressOf UpdateProgressBar)
+                Catch ex As Exception
+                    Log(ex, "联机模块主时钟出错", LogLevel.Feedback)
+                    Thread.Sleep(10000)
+                End Try
+            Loop
+        End Sub, "Link Timer")
+    End Sub
+
+    '每 4 秒或进入页面时更新一次信息
+    Private Sub Update() Handles Me.Loaded
+        If State <> States.Finished Then Return
+        '重新获取信息
+        SyncLock RefreshPeerLoader.LockState
+            If RefreshPeerLoader.State <> LoadState.Loading Then RefreshPeerLoader.Start(IsForceRestart:=True)
+        End SyncLock
+        '更新 Ping 与人数显示
+        If FrmMain.PageCurrent = FormMain.PageType.Link Then
+            RunInUi(
+            Sub()
+                Dim Ping As Double = GetPeerPing()
+                If Ping Mod 500 = 0 OrElse $"{Ping:0.0}" = "1.0" OrElse FailCount > 0 Then
+                    LabFinishPing.Text = "连接中"
+                    AniStop("Link Logo Rotation")
+                Else
+                    LabFinishPing.Text = If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms")
+                    AniStart(AaRotateTransform(ImgFinishLogo, 500, 5000), "Link Logo Rotation") 'Logo 旋转动画
+                End If
+                LabFinishPlayer.Text = PeopleCount & "  人"
+            End Sub)
+        End If
+        '检查核心状态
+        CheckCrash()
+        '检查节点状态
+        If Not Peers.Any Then
+            Panic("网络连接已断开", "请检查你的网络环境是否正常。")
+            Return
+        End If
+        If Not IsServerSide AndAlso Not Peers.Any(Function(p) p.Type = Peer.Types.Server) Then
+            MyMsgBox("房主已离开房间!", "联机结束")
+            ChangeState(States.Waiting)
+            Return
+        End If
+    End Sub
+    ''' <summary>
+    ''' 检查联机模块是否崩溃。
+    ''' </summary>
+    Private Sub CheckCrash()
+        If ProcessCore IsNot Nothing AndAlso Not ProcessCore.HasExited Then Return
+        Panic("联机模块已崩溃", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf))
+    End Sub
+
+#End Region
+
+End Class

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

@@ -337,7 +337,7 @@
                     <TextBlock Text="匿名数据收集" TextWrapping="Wrap" Margin="0,6,0,4" FontWeight="Bold" />
                     <TextBlock Text="匿名数据收集" TextWrapping="Wrap" Margin="0,6,0,4" FontWeight="Bold" />
                     <TextBlock Margin="0,0,0,2" LineHeight="17" Text="当下载失败、PCL 出错、游戏崩溃等事件时,PCL 会匿名上报信息,以通知龙猫 “哦吼,出错了”,让龙猫能更好地修 Bug……(PCL 不会上传或收集任何个人信息,包括上报的东西里面也没有!)&#xa;你可以在 设置 → 其他 → 启动器 中关闭上报,不过龙猫就不知道你遇到了什么 Bug 了……&#xa;详情:https://github.com/Meloong-Git/PCL/discussions/7036" TextWrapping="Wrap" />
                     <TextBlock Margin="0,0,0,2" LineHeight="17" Text="当下载失败、PCL 出错、游戏崩溃等事件时,PCL 会匿名上报信息,以通知龙猫 “哦吼,出错了”,让龙猫能更好地修 Bug……(PCL 不会上传或收集任何个人信息,包括上报的东西里面也没有!)&#xa;你可以在 设置 → 其他 → 启动器 中关闭上报,不过龙猫就不知道你遇到了什么 Bug 了……&#xa;详情:https://github.com/Meloong-Git/PCL/discussions/7036" TextWrapping="Wrap" />
                     <TextBlock Text="隐私声明与个人信息保护政策" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
                     <TextBlock Text="隐私声明与个人信息保护政策" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
-                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="PCL 没有收集个人信息,所以这里其实啥都不用写……&#xa;留个这个其实只是为了让你知道 PCL 没有收集个人信息,诶嘿。" TextWrapping="Wrap" />
+                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="PCL 没有收集个人信息,包括联机功能也没有收集,所以这里其实啥都不用写……&#xa;留个这个其实只是为了让你知道 PCL 没有收集个人信息,诶嘿。" TextWrapping="Wrap" />
                     <TextBlock Text="其他信息" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
                     <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" />
                     <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">
                     <StackPanel Orientation="Horizontal" Height="35" HorizontalAlignment="Left" Margin="0,12,0,0">
@@ -370,6 +370,8 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="60" />
                         <RowDefinition Height="60" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     </Grid.RowDefinitions>
                     <TextBlock Grid.Row="0" Grid.Column="0" Text="Java Launch Wrapper" TextWrapping="Wrap" FontWeight="Bold" />
                     <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" />
                     <TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2022 00ll00&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
@@ -391,7 +393,7 @@
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.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" 
                     <local:MyButton Grid.Row="5" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.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="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" />
                     <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"
                     <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
@@ -418,6 +420,12 @@
                     <local:MyButton Grid.Row="13" Grid.Column="2" MinWidth="140" Text="查看原网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="13" Grid.Column="2" MinWidth="140" Text="查看原网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.iconfont.cn/" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.iconfont.cn/" />
 
 
+                    <TextBlock Grid.Row="14" Grid.Column="0" Text="EasyTier" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2024-present Easytier Programme within The Commons Conservancy&#xa;Licensed under the GNU Lesser General Public License." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="15" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://easytier.cn/" />
+                    <local:MyButton Grid.Row="15" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/EasyTier/EasyTier/blob/main/LICENSE" />
                 </Grid>
                 </Grid>
             </local:MyCard>
             </local:MyCard>
         </StackPanel>
         </StackPanel>

+ 6 - 9
Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb

@@ -24,7 +24,8 @@
         {"HintMoreAdvancedSetup", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintMoreAdvancedSetup", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintIndieSetup", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintIndieSetup", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintExportConfig", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintExportConfig", New SetupEntry(False, Source:=SetupSource.Registry)},
-        {"SystemEula", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"SystemEula", New SetupEntry(False, Source:=SetupSource.Registry)}, '仅老版本兼容,现已换用 SystemEulaVersion
+        {"SystemEulaVersion", New SetupEntry(0, Source:=SetupSource.Registry)},
         {"SystemCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLaunchCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLaunchCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLastVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
         {"SystemLastVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
@@ -42,6 +43,7 @@
         {"SystemSystemUpdate", New SetupEntry(0)},
         {"SystemSystemUpdate", New SetupEntry(0)},
         {"SystemSystemActivity", New SetupEntry(0)},
         {"SystemSystemActivity", New SetupEntry(0)},
         {"SystemSystemTelemetry", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"SystemSystemTelemetry", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"CacheConfig", New SetupEntry(0, Source:=SetupSource.Registry)},
         {"CacheExportConfig", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheExportConfig", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageUrl", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageUrl", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageVersion", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageVersion", New SetupEntry("", Source:=SetupSource.Registry)},
@@ -109,12 +111,9 @@
         {"LaunchAdvanceGraphicCard", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"LaunchAdvanceGraphicCard", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"LaunchRamType", New SetupEntry(0)},
         {"LaunchRamType", New SetupEntry(0)},
         {"LaunchRamCustom", New SetupEntry(15)},
         {"LaunchRamCustom", New SetupEntry(15)},
-        {"LinkEula", New SetupEntry(False, Source:=SetupSource.Registry)},
-        {"LinkName", New SetupEntry("", Source:=SetupSource.Registry)},
-        {"LinkHiperCertLast", New SetupEntry("", Source:=SetupSource.Registry)},
-        {"LinkHiperCertTime", New SetupEntry("", Source:=SetupSource.Registry)},
-        {"LinkHiperCertWarn", New SetupEntry(True, Source:=SetupSource.Registry)},
-        {"LinkIoiVersion", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"LinkOfflineHint", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"LinkShareMode", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"LinkEasyTierVersion", New SetupEntry(-1, Source:=SetupSource.Registry)},
         {"ToolHelpChinese", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"ToolHelpChinese", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"ToolDownloadThread", New SetupEntry(63, Source:=SetupSource.Registry)},
         {"ToolDownloadThread", New SetupEntry(63, Source:=SetupSource.Registry)},
         {"ToolDownloadSpeed", New SetupEntry(42, Source:=SetupSource.Registry)},
         {"ToolDownloadSpeed", New SetupEntry(42, Source:=SetupSource.Registry)},
@@ -419,8 +418,6 @@
 #If BETA Then
 #If BETA Then
         If Key = "UiLauncherTheme" Then Return "0"
         If Key = "UiLauncherTheme" Then Return "0"
 #End If
 #End If
-        If Key = "UiHiddenPageLink" Then Return True
-        If Key = "UiHiddenSetupLink" Then Return True
         Return Nothing
         Return Nothing
     End Function
     End Function
 
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml


+ 13 - 11
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.vb

@@ -126,17 +126,19 @@
         FrmMain.PageRight = Target
         FrmMain.PageRight = Target
         CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnExit()
         CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnExit()
         AniStart({
         AniStart({
-                         AaCode(Sub()
-                                    CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnForceExit()
-                                    FrmMain.PanMainRight.Child = FrmMain.PageRight
-                                    FrmMain.PageRight.Opacity = 0
-                                End Sub, 130),
-                         AaCode(Sub()
-                                    '延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
-                                    FrmMain.PageRight.Opacity = 1
-                                    FrmMain.PageRight.PageOnEnter()
-                                End Sub, 30, True)
-                     }, "PageLeft PageChange")
+            AaCode(
+            Sub()
+                CType(FrmMain.PanMainRight.Child, MyPageRight).PageOnForceExit()
+                FrmMain.PanMainRight.Child = FrmMain.PageRight
+                FrmMain.PageRight.Opacity = 0
+            End Sub, 130),
+            AaCode(
+            Sub()
+                '延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
+                FrmMain.PageRight.Opacity = 1
+                FrmMain.PageRight.PageOnEnter()
+            End Sub, 30, True)
+        }, "PageLeft PageChange")
     End Sub
     End Sub
 
 
 #End Region
 #End Region

+ 3 - 24
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml

@@ -6,31 +6,10 @@
     PanScroll="PanBack">
     PanScroll="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
         <StackPanel x:Name="PanMain" Margin="25,10">
         <StackPanel x:Name="PanMain" Margin="25,10">
-            <local:MyCard Margin="0,15" Title="HiPer">
+            <local:MyCard Margin="0,15" Title="联机">
                 <StackPanel Margin="25,40,25,20" Orientation="Vertical">
                 <StackPanel Margin="25,40,25,20" Orientation="Vertical">
-                    <local:MyCheckBox Text="在索引码还有 5 分钟到期时提醒我" x:Name="CheckHiperCertWarn" Tag="LinkHiperCertWarn" Height="22" />
-                    <local:MyButton x:Name="BtnHiperLog" Height="35" MinWidth="140" Text="查看日志" Padding="13,0" Margin="0,10,20,0" HorizontalAlignment="Left" />
-                </StackPanel>
-            </local:MyCard>
-            <local:MyCard Margin="0,0,0,15" Title="IOI">
-                <StackPanel Margin="25,40,25,20">
-                    <Grid>
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
-                            <ColumnDefinition Width="1*" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="28" />
-                        </Grid.RowDefinitions>
-                        <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="玩家名" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextLinkName" Grid.Row="0" Grid.ColumnSpan="2" Tag="LinkName" Grid.Column="1">
-                            <local:MyTextBox.ValidateRules>
-                                <local:ValidateNullable />
-                                <local:ValidateNullOrWhiteSpace />
-                                <local:ValidateLength Max="20" />
-                            </local:MyTextBox.ValidateRules>
-                        </local:MyTextBox>
-                    </Grid>
+                    <local:MyCheckBox Text="老好人模式" x:Name="CheckShareMode" Tag="LinkShareMode" Height="22"
+                        ToolTip="启动联机后,用你的网络帮助其他玩家建立连接、降低延迟!&#xa;即使你不开启,开启了这个选项的其他玩家也会帮助你,不过,人人为我,我为人人嘛……" />
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </local:MyCard>
         </StackPanel>
         </StackPanel>

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

@@ -17,15 +17,13 @@
 
 
     End Sub
     End Sub
     Public Sub Reload()
     Public Sub Reload()
-        TextLinkName.Text = Setup.Get("LinkName")
-        CheckHiperCertWarn.Checked = Setup.Get("LinkHiperCertWarn")
+        CheckShareMode.Checked = Setup.Get("LinkShareMode")
     End Sub
     End Sub
 
 
     '初始化
     '初始化
     Public Sub Reset()
     Public Sub Reset()
         Try
         Try
-            Setup.Reset("LinkName")
-            Setup.Reset("LinkHiperCertWarn")
+            Setup.Reset("LinkShareMode")
 
 
             Log("[Setup] 已初始化联机页设置")
             Log("[Setup] 已初始化联机页设置")
             Hint("已初始化联机页设置!", HintType.Green, False)
             Hint("已初始化联机页设置!", HintType.Green, False)
@@ -37,14 +35,11 @@
     End Sub
     End Sub
 
 
     '将控件改变路由到设置改变
     '将控件改变路由到设置改变
-    Private Shared Sub TextBoxChange(sender As MyTextBox, e As Object) Handles TextLinkName.ValidatedTextChanged
+    Private Shared Sub TextBoxChange(sender As MyTextBox, e As Object) 'Handles TextLinkName.ValidatedTextChanged
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Text)
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Text)
     End Sub
     End Sub
-    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckHiperCertWarn.Change
+    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckShareMode.Change
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Checked)
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Checked)
     End Sub
     End Sub
 
 
-    Private Sub BtnHiperLog_Click(sender As Object, e As EventArgs) Handles BtnHiperLog.Click
-    End Sub
-
 End Class
 End Class

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

@@ -280,14 +280,14 @@
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <TextBlock Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="主页面" />
                         <TextBlock Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="主页面" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="下载" x:Name="CheckHiddenPageDownload" Tag="UiHiddenPageDownload" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="下载" x:Name="CheckHiddenPageDownload" Tag="UiHiddenPageDownload" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Visibility="Collapsed" Text="联机" x:Name="CheckHiddenPageLink" Tag="UiHiddenPageLink" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="设置" x:Name="CheckHiddenPageSetup" Tag="UiHiddenPageSetup" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="更多" x:Name="CheckHiddenPageOther" Tag="UiHiddenPageOther" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="联机" x:Name="CheckHiddenPageLink" Tag="UiHiddenPageLink" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="设置" x:Name="CheckHiddenPageSetup" Tag="UiHiddenPageSetup" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="5" Text="更多" x:Name="CheckHiddenPageOther" Tag="UiHiddenPageOther" />
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="设置 子页面" />
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="设置 子页面" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="启动" x:Name="CheckHiddenSetupLaunch" Tag="UiHiddenSetupLaunch" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="启动" x:Name="CheckHiddenSetupLaunch" Tag="UiHiddenSetupLaunch" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="个性化" x:Name="CheckHiddenSetupUI" Tag="UiHiddenSetupUi" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="其他" x:Name="CheckHiddenSetupSystem" Tag="UiHiddenSetupSystem" />
-                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="5" Text="联机" x:Name="CheckHiddenSetupLink" Tag="UiHiddenSetupLink" Visibility="Collapsed" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="联机" x:Name="CheckHiddenSetupLink" Tag="UiHiddenSetupLink" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="个性化" x:Name="CheckHiddenSetupUI" Tag="UiHiddenSetupUi" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="5" Text="其他" x:Name="CheckHiddenSetupSystem" Tag="UiHiddenSetupSystem" />
                         <TextBlock Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" Text="更多 子页面" />
                         <TextBlock Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" Text="更多 子页面" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="帮助" x:Name="CheckHiddenOtherHelp" Tag="UiHiddenOtherHelp" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="帮助" x:Name="CheckHiddenOtherHelp" Tag="UiHiddenOtherHelp" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="关于与鸣谢" x:Name="CheckHiddenOtherAbout" Tag="UiHiddenOtherAbout" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="关于与鸣谢" x:Name="CheckHiddenOtherAbout" Tag="UiHiddenOtherAbout" />

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

@@ -523,7 +523,7 @@ Refresh:
                 '顶部栏未被全部隐藏
                 '顶部栏未被全部隐藏
                 FrmMain.PanTitleSelect.Visibility = Visibility.Visible
                 FrmMain.PanTitleSelect.Visibility = Visibility.Visible
                 FrmMain.BtnTitleSelect1.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageDownload"), Visibility.Collapsed, Visibility.Visible)
                 FrmMain.BtnTitleSelect1.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageDownload"), Visibility.Collapsed, Visibility.Visible)
-                FrmMain.BtnTitleSelect2.Visibility = Visibility.Collapsed 'If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageLink"), Visibility.Collapsed, Visibility.Visible)
+                FrmMain.BtnTitleSelect2.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageLink"), Visibility.Collapsed, Visibility.Visible)
                 FrmMain.BtnTitleSelect3.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageSetup"), Visibility.Collapsed, Visibility.Visible)
                 FrmMain.BtnTitleSelect3.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageSetup"), Visibility.Collapsed, Visibility.Visible)
                 FrmMain.BtnTitleSelect4.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageOther"), Visibility.Collapsed, Visibility.Visible)
                 FrmMain.BtnTitleSelect4.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageOther"), Visibility.Collapsed, Visibility.Visible)
             End If
             End If
@@ -536,7 +536,7 @@ Refresh:
             If FrmSetupLeft IsNot Nothing Then
             If FrmSetupLeft IsNot Nothing Then
                 FrmSetupLeft.ItemLaunch.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLaunch"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemLaunch.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLaunch"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemUI.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupUi"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemUI.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupUi"), Visibility.Collapsed, Visibility.Visible)
-                FrmSetupLeft.ItemLink.Visibility = Visibility.Collapsed 'If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLink"), Visibility.Collapsed, Visibility.Visible)
+                FrmSetupLeft.ItemLink.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLink"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemSystem.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupSystem"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemSystem.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupSystem"), Visibility.Collapsed, Visibility.Visible)
                 '隐藏左边选择卡
                 '隐藏左边选择卡
                 Dim AvaliableCount As Integer = 0
                 Dim AvaliableCount As Integer = 0

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

@@ -206,7 +206,6 @@
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
     </ApplicationDefinition>
     </ApplicationDefinition>
     <Compile Include="Controls\Behaviors\ClipboardInterceptor.vb" />
     <Compile Include="Controls\Behaviors\ClipboardInterceptor.vb" />
-    <Compile Include="Controls\Behaviors\IMyRadio.vb" />
     <Compile Include="Controls\Behaviors\Interfaces.vb" />
     <Compile Include="Controls\Behaviors\Interfaces.vb" />
     <Compile Include="Controls\MyComboBox.vb" />
     <Compile Include="Controls\MyComboBox.vb" />
     <Compile Include="Controls\MyComboBoxItem.vb" />
     <Compile Include="Controls\MyComboBoxItem.vb" />
@@ -307,21 +306,12 @@
     <Compile Include="Pages\PageLaunch\PageLoginNideSkin.xaml.vb">
     <Compile Include="Pages\PageLaunch\PageLoginNideSkin.xaml.vb">
       <DependentUpon>PageLoginNideSkin.xaml</DependentUpon>
       <DependentUpon>PageLoginNideSkin.xaml</DependentUpon>
     </Compile>
     </Compile>
-    <Compile Include="Pages\PageLink\PageLinkFeedback.xaml.vb">
-      <DependentUpon>PageLinkFeedback.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\PageLink\PageLinkHiper.xaml.vb">
-      <DependentUpon>PageLinkHiper.xaml</DependentUpon>
+    <Compile Include="Pages\PageLink\PageLinkMain.xaml.vb">
+      <DependentUpon>PageLinkMain.xaml</DependentUpon>
     </Compile>
     </Compile>
     <Compile Include="Pages\PageOther\PageOtherHelpDetail.xaml.vb">
     <Compile Include="Pages\PageOther\PageOtherHelpDetail.xaml.vb">
       <DependentUpon>PageOtherHelpDetail.xaml</DependentUpon>
       <DependentUpon>PageOtherHelpDetail.xaml</DependentUpon>
     </Compile>
     </Compile>
-    <Compile Include="Pages\PageLink\PageLinkIoi.xaml.vb">
-      <DependentUpon>PageLinkIoi.xaml</DependentUpon>
-    </Compile>
-    <Compile Include="Pages\PageLink\PageLinkLeft.xaml.vb">
-      <DependentUpon>PageLinkLeft.xaml</DependentUpon>
-    </Compile>
     <Compile Include="Pages\PageVersion\PageVersionExport.xaml.vb">
     <Compile Include="Pages\PageVersion\PageVersionExport.xaml.vb">
       <DependentUpon>PageVersionExport.xaml</DependentUpon>
       <DependentUpon>PageVersionExport.xaml</DependentUpon>
     </Compile>
     </Compile>
@@ -576,11 +566,7 @@
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
     </None>
     </None>
-    <Page Include="Pages\PageLink\PageLinkFeedback.xaml">
-      <Generator>MSBuild:Compile</Generator>
-      <SubType>Designer</SubType>
-    </Page>
-    <Page Include="Pages\PageLink\PageLinkHiper.xaml">
+    <Page Include="Pages\PageLink\PageLinkMain.xaml">
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
     </Page>
     </Page>
@@ -588,14 +574,6 @@
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
     </Page>
     </Page>
-    <Page Include="Pages\PageLink\PageLinkIoi.xaml">
-      <SubType>Designer</SubType>
-      <Generator>MSBuild:Compile</Generator>
-    </Page>
-    <Page Include="Pages\PageLink\PageLinkLeft.xaml">
-      <Generator>MSBuild:Compile</Generator>
-      <SubType>Designer</SubType>
-    </Page>
     <Page Include="Pages\PageVersion\PageVersionExport.xaml">
     <Page Include="Pages\PageVersion\PageVersionExport.xaml">
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>

+ 38 - 9
Plain Craft Launcher 2/Resources/Custom.xaml

@@ -55,24 +55,22 @@
 
 
 <local:MyCard Title="自定义事件" 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">
 	<StackPanel Margin="25,40,23,15">
-		<TextBlock Margin="0,0,0,10"
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
                     Text="将按钮的 EventType 属性设为 打开网页,然后在 EventData 属性中写入网址,即可通过点击按钮打开网页。" />
                     Text="将按钮的 EventType 属性设为 打开网页,然后在 EventData 属性中写入网址,即可通过点击按钮打开网页。" />
 		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0"
 		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0"
                     Text="打开 Minecraft Wiki" EventType="打开网页" EventData="https://zh.minecraft.wiki/" />
                     Text="打开 Minecraft Wiki" EventType="打开网页" EventData="https://zh.minecraft.wiki/" />
-		<TextBlock Margin="0,10,0,10"
+		<TextBlock TextWrapping="Wrap" Margin="0,10,0,10"
                     Text="或者将 EventType 属性改为 弹出窗口,然后在 EventData 属性中写入弹窗的标题与内容……" />
                     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你也可以直接在其中换行。" />
                     Text="显示一个弹窗" EventType="弹出窗口" EventData="这是标题|标题与内容以竖线间隔。\n你也可以直接在其中换行。" />
-		<TextBlock Margin="0,10,0,0"
+		<TextBlock TextWrapping="Wrap" Margin="0,10,0,0"
                     Text="local:MyTextButton 同样可以设置 EventType 与 EventData 让它具有特定功能。" />
                     Text="local:MyTextButton 同样可以设置 EventType 与 EventData 让它具有特定功能。" />
 		<local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
 		<local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
                     Text="点击打开哔哩哔哩" EventType="打开网页" EventData="https://www.bilibili.com/" />
                     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/自定义事件" />
+		<TextBlock TextWrapping="Wrap" Margin="0,30,0,0"
+                    Text="自定义事件的详细用法请参考下方的页面。" />
+		<local:MyListItem Margin="-5,4,-5,5"
+		                  EventType="打开帮助" EventData="个性化/自定义事件.json" />
 	</StackPanel>
 	</StackPanel>
 </local:MyCard>
 </local:MyCard>
 
 
@@ -83,6 +81,37 @@
 	</StackPanel>
 	</StackPanel>
 </local:MyCard>
 </local:MyCard>
 
 
+<local:MyCard Title="进阶:高级自定义事件" Margin="0,0,0,15" CanSwap="True" IsSwapped="{variable:TutorialCardSwap:True}">
+	<StackPanel Margin="25,40,23,10">
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
+            Text="你可以让一个控件触发多个事件。下方的例子展示了连续触发多个事件搭配 {variable} 替换标记的效果。" />
+		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0" Margin="0,0,0,10"
+            Text="隐藏下方的百度 Logo" Visibility="{variable:TutorialVisibility1:Visible}">
+			<local:CustomEventService.Events>
+				<local:CustomEventCollection>
+					<local:CustomEvent Type="修改变量" Data="TutorialVisibility1|Collapsed|-" />
+					<local:CustomEvent Type="修改变量" Data="TutorialVisibility2|Visible|-" />
+					<local:CustomEvent Type="修改变量" Data="TutorialCardSwap|False|-" />
+					<local:CustomEvent Type="刷新页面" Data="-" />
+				</local:CustomEventCollection>
+			</local:CustomEventService.Events>
+		</local:MyButton>
+		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0" Margin="0,0,0,10"
+            Text="再次显示 Logo 并折叠卡片" Visibility="{variable:TutorialVisibility2:Collapsed}">
+			<local:CustomEventService.Events>
+				<local:CustomEventCollection>
+					<local:CustomEvent Type="修改变量" Data="TutorialVisibility1|Visible|-" />
+					<local:CustomEvent Type="修改变量" Data="TutorialVisibility2|Collapsed|-" />
+					<local:CustomEvent Type="修改变量" Data="TutorialCardSwap|True|-" />
+					<local:CustomEvent Type="刷新页面" Data="-" />
+				</local:CustomEventCollection>
+			</local:CustomEventService.Events>
+		</local:MyButton>
+		<local:MyImage Height="50" HorizontalAlignment="Left" Margin="0,0,0,10" Visibility="{variable:TutorialVisibility1:Visible}"
+            Source="https://www.baidu.com/img/flexible/logo/pc/result.png" />
+	</StackPanel>
+</local:MyCard>
+
 <local:MyCard Title="横向布局" 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">
     <StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"

BIN
Plain Craft Launcher 2/Resources/Help.zip


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