龙腾猫跃 hace 1 año
padre
commit
8d2eb85aec
Se han modificado 46 ficheros con 1427 adiciones y 1459 borrados
  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()}")
             '检测异常环境
             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
             If Is32BitSystem Then
                 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
     Sub DispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs)
 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
         RunInNewThread(
         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
                 '下载
                 ActualSource = LoadingSource '显示加载中的占位图片
                 NetDownloadByLoader(
-                    If(String.IsNullOrEmpty(FallbackSource) OrElse IsLocalFallback, {Source}, {Source, FallbackSource}),
+                    If(FallbackSource?.StartsWithF("http"), {Source, FallbackSource}, {Source}),
                     TempPath, SimulateBrowserHeaders:=True)
                 If EnableCache Then
                     '保存缓存并显示

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

@@ -183,30 +183,29 @@ Public Class MyLoading
         IsLooping = True
         ErrorAnimationWaiting = True
         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
 
     ''' <summary>

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

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

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

@@ -89,15 +89,15 @@ Public Class MyRadioBox
                     End If
             End Select
 
-            '触发事件
             If IsChanged Then
+                '触发事件
                 If Checked Then RaiseEvent Check(Me, New RouteEventArgs(user))
                 RaiseEvent Changed(Me, New RouteEventArgs(user))
                 RaiseCustomEvent()
+                '更改动画
+                SyncUI()
             End If
 
-            '更改动画
-            SyncUI()
         Catch ex As Exception
             Log(ex, "单选框勾选改变错误", LogLevel.Hint)
         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))
         '统计更新日志条目
 #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
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
@@ -104,6 +113,11 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '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
             FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
@@ -407,11 +421,16 @@ Public Class FormMain
         RunInNewThread(
         Sub()
             '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"))
                     Case 1
-                        Setup.Set("SystemEula", True)
+                        Setup.Set("SystemEulaVersion", EulaVersion)
                     Case 2
                         EndProgram(False)
                 End Select
@@ -476,6 +495,8 @@ Public Class FormMain
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
         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)
         '修改主题设置项名称
@@ -554,20 +575,20 @@ Public Class FormMain
     ''' </summary>
     ''' <param name="SendWarning">是否在还有下载任务未完成时发出警告。</param>
     Public Sub EndProgram(SendWarning As Boolean)
-        '发出警告
-        If SendWarning AndAlso HasDownloadingTask() Then
-            If MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 1 Then
-                '强行结束下载任务
-                RunInNewThread(
-                Sub()
-                    Log("[System] 正在强行停止任务")
-                    For Each Task As LoaderBase In LoaderTaskbar.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
         '关闭
         RunInUiWait(
@@ -1063,14 +1084,10 @@ Public Class FormMain
         DownloadResourcePack = 5
         DownloadShader = 6
         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
         OtherAbout = 1
         OtherTest = 2
@@ -1298,8 +1315,8 @@ Public Class FormMain
                     'PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
                     PageChangeAnim(FrmDownloadLeft, FrmDownloadLeft.PageGet(SubType))
                 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 '设置
                     If FrmSetupLeft Is Nothing Then FrmSetupLeft = New PageSetupLeft
                     PageChangeAnim(FrmSetupLeft, FrmSetupLeft.PageGet(SubType))

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

@@ -467,7 +467,7 @@ Public Module ModAnimation
     ''' 按照 WPF 方式旋转控件的动画。
     ''' </summary>
     ''' <param name="Obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
-    ''' <param name="Value">大小改变的百分比(如-0.6)。</param>
+    ''' <param name="Value">旋转角度(度)。</param>
     ''' <param name="Time">动画长度(毫秒)。</param>
     ''' <param name="Delay">动画延迟执行的时间(毫秒)。</param>
     ''' <param name="Ease">插值器类型。</param>
@@ -792,44 +792,46 @@ Public Module ModAnimation
         AniFPSTimer = AniLastTick
         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:
-                                   '控制 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
 
     ''' <summary>
@@ -996,7 +998,7 @@ NextAni:
                 Case AniType.ScaleTransform
                     Dim Obj As FrameworkElement = Ani.Obj
                     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)
                     End If
                     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
                     Dim Obj As FrameworkElement = Ani.Obj
                     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)
                     End If
                     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 "声明"
 
     '下列版本信息由更新器自动修改
-    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 自动替换
 #If BETA Then
-    Public Const VersionCode As Integer = 369 'Release
+    Public Const VersionCode As Integer = 372 'Release
 #Else
-    Public Const VersionCode As Integer = 371 'Snapshot
+    Public Const VersionCode As Integer = 373 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -498,6 +498,7 @@ Public Module ModBase
             RealNum += Digit * Scale
             Scale *= FromRadix
         Next
+        If RealNum = 0 Then Return "0"
         '转换为指定进制
         Dim Result = ""
         While RealNum > 0
@@ -1300,10 +1301,12 @@ Re:
                 Dim TotalCount As Long = Archive.Entries.Count
                 For Each Entry As ZipArchiveEntry In Archive.Entries
                     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
             End Using
         End If
@@ -2368,7 +2371,7 @@ NextElement:
     ''' <param name="Arguments">运行参数。</param>
     ''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
     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 {
             .Arguments = Arguments,
             .FileName = ShortenPath(FileName),
@@ -2376,7 +2379,9 @@ NextElement:
             .CreateNoWindow = True,
             .RedirectStandardError = 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 Info.EnvironmentVariables.ContainsKey("appdata") Then
@@ -2385,7 +2390,7 @@ NextElement:
                 Info.EnvironmentVariables.Add("appdata", WorkingDirectory)
             End If
         End If
-        Log("[System] 执行外部命令并等待返回结果:" & FileName & " " & Arguments)
+        If PrintLog Then Log("[System] 执行命令并等待返回结果:" & FileName & " " & Arguments)
         Using Program As New Process With {.StartInfo = Info}
             Program.Start()
             Dim Result As String = Program.StandardOutput.ReadToEnd & Program.StandardError.ReadToEnd
@@ -3111,7 +3116,7 @@ Retry:
     End Function
 
     ''' <summary>
-    ''' 取随机整数。
+    ''' 取随机整数(包含)。
     ''' </summary>
     Public Function RandomInteger(min As Integer, max As Integer) As Integer
         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
 
         Public ReadOnly Property IsLoader As Boolean = True Implements ILoadingTrigger.IsLoader
+        Protected Shared LogBlackList As String() = {"EasyTier CLI"}
 
         '基础属性
         ''' <summary>
@@ -89,7 +90,7 @@
                 Dim OldState = _State
                 If value = LoadState.Finished AndAlso Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
                 _State = value
-                Log("[Loader] 加载器 " & Name & " 状态改变:" & GetStringFromEnum(value))
+                If Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载器 {Name} 状态改变:{GetStringFromEnum(value)}")
                 '实现 ILoadingTrigger 接口与 OnStateChanged 回调
                 RunInUi(
                 Sub()
@@ -170,6 +171,7 @@
         '状态变化
         Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
         Public MustOverride Sub Abort()
+        Public MustOverride Sub Failed(Ex As Exception)
 
         '等待结束
         Public Const WaitForExitTimeoutMessage As String = "等待加载器执行超时。"
@@ -290,6 +292,7 @@
             '检验输入以确定情况
             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 (State = LoadState.Loading OrElse State = LoadState.Finished) AndAlso '正在加载或已结束…
                (IgnoreReloadTimeout OrElse ReloadTimeout = -1 OrElse
                LastFinishedTime = 0 OrElse GetTimeTick() - LastFinishedTime < ReloadTimeout) Then '…且没有超时…
@@ -316,13 +319,13 @@
             Sub()
                 Try
                     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)
                     If IsAborted Then
                         Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
                         Return
                     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()
                     State = LoadState.Finished
                     LastFinishedTime = GetTimeTick() '未中断,本次输出有效
@@ -335,25 +338,33 @@
                     '如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
                     If Not IsAborted Then State = LoadState.Aborted
                 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 Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority}
             LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
         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()
-            If State <> LoadState.Loading Then Return
             SyncLock LockState
+                If State <> LoadState.Loading Then Return
                 State = LoadState.Aborted
             End SyncLock
             TriggerThreadAbort()
         End Sub
         Private Sub TriggerThreadAbort()
             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
         End Sub
 
@@ -452,6 +463,17 @@
                 Next
             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>
         ''' 子任务状态变更。
@@ -469,18 +491,9 @@
                 Case LoadState.Aborted
                     '被中断,这个任务也中断
                     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 Sub
         ''' <summary>

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

@@ -1063,7 +1063,7 @@ NotSupportRange:
                 ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
                 If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
                 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 '判断是否下载完成
                             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("操作超时,无数据。")
                     End If
-                    HttpDataCount = ResponseStream.Read(ResponseBytes, 0, 16384)
+                    HttpDataCount = ReadWithTimeout(ResponseStream, ResponseBytes, 0, 16384, Timeout)
                 End While
 SourceBreak:
                 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
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
                         '若所有源均不支持 Range,也会走到这里重试
-                        If Not IsRangeNotSupported Then Hint($"{LocalName}:文件下载失败,正在自动重试……", If(ModeDebug, HintType.Red, HintType.Blue)) '在调试模式下给出红色提示
+                        If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", LogLevel.Debug)
                         Retried = True
                         SyncLock LockSource
                             SourcesOnce.Clear()
@@ -1642,9 +1642,12 @@ Retry:
             Next
             OnFail(File.Ex)
         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))
             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("未知错误!")}
                 '寻找有效的错误信息
                 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))
     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

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

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

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

@@ -1463,9 +1463,7 @@ Retry:
         'Authlib-Injector
         If McLoginLoader.Output.Type = "Auth" Then
             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
                 Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8)
                 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"),
                              .Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
                              .LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
-                                 McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
-                                 CustomMcFolder & "libraries\" & Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")),
+                                 McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
+                                    Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").
+                                    Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
+                                    $"{CustomMcFolder}libraries\{Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")}"),
                              .Size = Val(Library("downloads")("classifiers")("natives-windows")("size").ToString),
                              .IsNatives = True,
                              .SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
                     Else
-                        BasicArray.Add(New McLibToken With {.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
                 Catch ex As Exception
                     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 If
 
@@ -2130,19 +2142,12 @@ OnLoaded:
     ''' <param name="WithHead">是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。</param>
     Public Function McLibGet(Original As String, Optional WithHead As Boolean = True, Optional IgnoreLiteLoader As Boolean = False, Optional CustomMcFolder As String = Nothing) As String
         CustomMcFolder = If(CustomMcFolder, PathMcFolder)
+        '有时候原始的有四段,例如 net.minecraftforge:forge:1.21.4-54.0.34:client
+        '文件夹名应该忽略最后一段(client),但文件名需要保留
+        '参见 #7306(以及 #5376 也是因为这里没有处理好导致的)
         Dim Splited = Original.Split(":")
         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
 
     ''' <summary>

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

@@ -207,7 +207,14 @@ Public Class CustomEvent
                 Case EventType.复制文本
                     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
                         RunInUiWait(Sub() CType(FrmMain.PageRight, IRefreshable).Refresh())
                         If String.IsNullOrEmpty(Arg) Then Hint("已刷新!", HintType.Green)
@@ -250,18 +257,21 @@ Public Class CustomEvent
                         Return
                     End If
                     If Not EventSafetyConfirm("即将从该网址下载文件:" & vbCrLf & Args(0)) Then Return
-                    Try
-                        Select Case Args.Length
-                            Case 1
-                                PageOtherTest.StartCustomDownload(Args(0), GetFileNameFromPath(Args(0)))
-                            Case 2
-                                PageOtherTest.StartCustomDownload(Args(0), Args(1))
-                            Case Else
-                                PageOtherTest.StartCustomDownload(Args(0), Args(1), Args(2))
-                        End Select
-                    Catch
-                        PageOtherTest.StartCustomDownload(Args(0), "未知")
-                    End Try
+                    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.写入设置
                     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}
         WaitingMyMsgBox.Add(Converter)
-        If RunInUi() Then
-            '若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗
-            MyMsgBoxTick()
-        End If
         If Button2.Length > 0 OrElse ForceWait Then
             '若有多个按钮则开始等待
             If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing AndAlso RunInUi() Then
@@ -264,6 +260,7 @@ EndHint:
             Else
                 Try
                     FrmMain.DragStop()
+                    If RunInUi() Then MyMsgBoxTick()
                     ComponentDispatcher.PushModal()
                     Dispatcher.PushFrame(Converter.WaitFrame)
                 Finally
@@ -295,6 +292,7 @@ EndHint:
         '虽然我也不知道这是啥但是能用就成了 :)
         Try
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            If RunInUi() Then MyMsgBoxTick()
             ComponentDispatcher.PushModal()
             Dispatcher.PushFrame(Converter.WaitFrame)
         Finally
@@ -317,6 +315,7 @@ EndHint:
         '虽然我也不知道这是啥但是能用就成了 :)
         Try
             If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            If RunInUi() Then MyMsgBoxTick()
             ComponentDispatcher.PushModal()
             Dispatcher.PushFrame(Converter.WaitFrame)
         Finally
@@ -339,7 +338,7 @@ EndHint:
             ElseIf WaitingMyMsgBox.Any Then
                 '没有弹窗,显示一个等待的弹窗
                 FrmMain.PanMsg.Visibility = Visibility.Visible
-                Select Case CType(WaitingMyMsgBox(0), MyMsgBoxConverter).Type
+                Select Case WaitingMyMsgBox(0).Type
                     Case MyMsgBoxType.Input
                         FrmMain.PanMsg.Children.Add(New MyMsgInput(WaitingMyMsgBox(0)))
                     Case MyMsgBoxType.Select
@@ -352,7 +351,7 @@ EndHint:
                 WaitingMyMsgBox.RemoveAt(0)
             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
         Catch ex As Exception
             Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
@@ -377,11 +376,7 @@ EndHint:
     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
@@ -863,7 +858,7 @@ NextFile:
         '登录信息
         If McLoginLoader.State = LoadState.Finished Then
             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
                 Case McLoginType.Legacy
                     Text = Text.Replace("{login}", Replacer("离线"))
@@ -884,6 +879,7 @@ NextFile:
         Text = Text.RegexReplaceEach("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
         Text = Text.RegexReplaceEach("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Setup.GetSafe(m.Groups(1).Value, McVersionCurrent)))
         Text = Text.RegexReplaceEach("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
+        Text = Text.RegexReplaceEach("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
         Return Text
     End Function
 

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

@@ -281,9 +281,16 @@ Friend Module ModSecret
 
 #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
 

+ 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("")>

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

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

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

@@ -613,16 +613,16 @@
             PanMinecraft.Children.Clear()
             '添加最新版本
             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()
             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
                 Dim Snapshot As JObject = Dict("预览版")(0).DeepClone()
                 Snapshot("lore") = "最新预览版,发布于 " & Snapshot("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
-                TopestVersions.Add(Snapshot)
+                PinnedVersions.Add(Snapshot)
             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)
             CardInfo.Children.Add(PanInfo)
             PanMinecraft.Children.Insert(0, CardInfo)
@@ -630,7 +630,7 @@
             For Each Pair As KeyValuePair(Of String, List(Of JObject)) In Dict
                 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}
                 NewCard.Children.Add(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}">
         <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">
                 <local:MyListItem.Buttons>
                     <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 参与者披风"},
                             {"Cherry Blossom", "樱花披风"}, {"15th Anniversary", "15 周年纪念披风"}, {"Purple Heart", "紫色心形披风"},
                             {"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", "铜披风"}
                         }
                         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")
             Case McLoginType.Legacy
-                If PageLinkHiper.HiperState = LoadState.Finished Then
-                    LabLaunchingMethod.Text = "联机离线登录"
-                Else
-                    LabLaunchingMethod.Text = "离线登录"
-                End If
+                LabLaunchingMethod.Text = "离线登录"
             Case McLoginType.Ms
                 LabLaunchingMethod.Text = "正版登录"
             Case McLoginType.Nide

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

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

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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 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 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 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">
@@ -370,6 +370,8 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="60" />
                         <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     <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" />
@@ -391,7 +393,7 @@
                                     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:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio/blob/master/license.txt" />
-                    
+
                     <TextBlock Grid.Row="6" Grid.Column="0" Text="Ookii.Dialogs" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2009 Sven Groot (Ookii.org)" TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
@@ -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: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>
             </local:MyCard>
         </StackPanel>

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

@@ -24,7 +24,8 @@
         {"HintMoreAdvancedSetup", New SetupEntry(False, Source:=SetupSource.Registry)},
         {"HintIndieSetup", 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)},
         {"SystemLaunchCount", 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)},
         {"SystemSystemActivity", New SetupEntry(0)},
         {"SystemSystemTelemetry", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"CacheConfig", New SetupEntry(0, Source:=SetupSource.Registry)},
         {"CacheExportConfig", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageUrl", New SetupEntry("", Source:=SetupSource.Registry)},
         {"CacheSavedPageVersion", New SetupEntry("", Source:=SetupSource.Registry)},
@@ -109,12 +111,9 @@
         {"LaunchAdvanceGraphicCard", New SetupEntry(True, Source:=SetupSource.Registry)},
         {"LaunchRamType", New SetupEntry(0)},
         {"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)},
         {"ToolDownloadThread", New SetupEntry(63, Source:=SetupSource.Registry)},
         {"ToolDownloadSpeed", New SetupEntry(42, Source:=SetupSource.Registry)},
@@ -419,8 +418,6 @@
 #If BETA Then
         If Key = "UiLauncherTheme" Then Return "0"
 #End If
-        If Key = "UiHiddenPageLink" Then Return True
-        If Key = "UiHiddenSetupLink" Then Return True
         Return Nothing
     End Function
 

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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
         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")
+            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

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

@@ -6,31 +6,10 @@
     PanScroll="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
         <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">
-                    <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>
             </local:MyCard>
         </StackPanel>

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

@@ -17,15 +17,13 @@
 
     End Sub
     Public Sub Reload()
-        TextLinkName.Text = Setup.Get("LinkName")
-        CheckHiperCertWarn.Checked = Setup.Get("LinkHiperCertWarn")
+        CheckShareMode.Checked = Setup.Get("LinkShareMode")
     End Sub
 
     '初始化
     Public Sub Reset()
         Try
-            Setup.Reset("LinkName")
-            Setup.Reset("LinkHiperCertWarn")
+            Setup.Reset("LinkShareMode")
 
             Log("[Setup] 已初始化联机页设置")
             Hint("已初始化联机页设置!", HintType.Green, False)
@@ -37,14 +35,11 @@
     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)
     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)
     End Sub
 
-    Private Sub BtnHiperLog_Click(sender As Object, e As EventArgs) Handles BtnHiperLog.Click
-    End Sub
-
 End Class

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

@@ -280,14 +280,14 @@
                         </Grid.RowDefinitions>
                         <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="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="设置 子页面" />
                         <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="更多 子页面" />
                         <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" />

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

@@ -523,7 +523,7 @@ Refresh:
                 '顶部栏未被全部隐藏
                 FrmMain.PanTitleSelect.Visibility = 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.BtnTitleSelect4.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageOther"), Visibility.Collapsed, Visibility.Visible)
             End If
@@ -536,7 +536,7 @@ Refresh:
             If FrmSetupLeft IsNot Nothing Then
                 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.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)
                 '隐藏左边选择卡
                 Dim AvaliableCount As Integer = 0

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

@@ -206,7 +206,6 @@
       <SubType>Designer</SubType>
     </ApplicationDefinition>
     <Compile Include="Controls\Behaviors\ClipboardInterceptor.vb" />
-    <Compile Include="Controls\Behaviors\IMyRadio.vb" />
     <Compile Include="Controls\Behaviors\Interfaces.vb" />
     <Compile Include="Controls\MyComboBox.vb" />
     <Compile Include="Controls\MyComboBoxItem.vb" />
@@ -307,21 +306,12 @@
     <Compile Include="Pages\PageLaunch\PageLoginNideSkin.xaml.vb">
       <DependentUpon>PageLoginNideSkin.xaml</DependentUpon>
     </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 Include="Pages\PageOther\PageOtherHelpDetail.xaml.vb">
       <DependentUpon>PageOtherHelpDetail.xaml</DependentUpon>
     </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">
       <DependentUpon>PageVersionExport.xaml</DependentUpon>
     </Compile>
@@ -576,11 +566,7 @@
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
     </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>
       <SubType>Designer</SubType>
     </Page>
@@ -588,14 +574,6 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </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">
       <SubType>Designer</SubType>
       <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">
 	<StackPanel Margin="25,40,23,15">
-		<TextBlock Margin="0,0,0,10"
+		<TextBlock TextWrapping="Wrap" Margin="0,0,0,10"
                     Text="将按钮的 EventType 属性设为 打开网页,然后在 EventData 属性中写入网址,即可通过点击按钮打开网页。" />
 		<local:MyButton Height="35" HorizontalAlignment="Left" Padding="20,0,20,0"
                     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 属性中写入弹窗的标题与内容……" />
 		<local:MyButton Width="140" Height="35" HorizontalAlignment="Left" Padding="13,0,13,0"
                     Text="显示一个弹窗" EventType="弹出窗口" EventData="这是标题|标题与内容以竖线间隔。\n你也可以直接在其中换行。" />
-		<TextBlock Margin="0,10,0,0"
+		<TextBlock TextWrapping="Wrap" Margin="0,10,0,0"
                     Text="local:MyTextButton 同样可以设置 EventType 与 EventData 让它具有特定功能。" />
 		<local:MyTextButton Margin="0,8,0,0" HorizontalAlignment="Center"
                     Text="点击打开哔哩哔哩" EventType="打开网页" EventData="https://www.bilibili.com/" />
-		<TextBlock Margin="0,30,0,0"
-                    Text="你也可以让一个控件触发多个事件。自定义事件的详细用法请参考下方的 Wiki。" />
-		<local:MyListItem Margin="-5,8,-5,5"
-            Title="自定义事件 - PCL Wiki" Info="点击查看自定义事件的完整列表与说明"
-            Logo="pack://application:,,,/images/Blocks/CommandBlock.png" Type="Clickable"
-		    EventType="打开网页" EventData="https://github.com/Meloong-Git/PCL/wiki/自定义事件" />
+		<TextBlock TextWrapping="Wrap" Margin="0,30,0,0"
+                    Text="自定义事件的详细用法请参考下方的页面。" />
+		<local:MyListItem Margin="-5,4,-5,5"
+		                  EventType="打开帮助" EventData="个性化/自定义事件.json" />
 	</StackPanel>
 </local:MyCard>
 
@@ -83,6 +81,37 @@
 	</StackPanel>
 </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">
     <StackPanel Margin="25,40,23,15">
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,10"

BIN
Plain Craft Launcher 2/Resources/Help.zip


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio