From 8d2eb85aec219f6be24b3c3462d9ab4a506baba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E8=85=BE=E7=8C=AB=E8=B7=83?= <1043137532@qq.com> Date: Fri, 24 Oct 2025 22:39:55 +0800 Subject: [PATCH] 2.11.0 --- Plain Craft Launcher 2/Application.xaml.vb | 3 +- .../Controls/Behaviors/IMyRadio.vb | 4 - .../Controls/Behaviors/Interfaces.vb | 8 + Plain Craft Launcher 2/Controls/MyImage.vb | 4 +- .../Controls/MyLoading.xaml.vb | 47 +- .../Controls/MyPageRight.vb | 13 +- .../Controls/MyRadioBox.xaml.vb | 6 +- Plain Craft Launcher 2/FormMain.xaml.vb | 71 +- .../Modules/Base/ModAnimation.vb | 82 +- .../Modules/Base/ModBase.vb | 29 +- .../Modules/Base/ModLoader.vb | 57 +- Plain Craft Launcher 2/Modules/Base/ModNet.vb | 34 +- .../Modules/Minecraft/ModComp.vb | 4 +- .../Modules/Minecraft/ModLaunch.vb | 4 +- .../Modules/Minecraft/ModMinecraft.vb | 35 +- Plain Craft Launcher 2/Modules/ModEvent.vb | 36 +- Plain Craft Launcher 2/Modules/ModMain.vb | 20 +- Plain Craft Launcher 2/Modules/ModSecret.vb | 11 +- .../My Project/AssemblyInfo.vb | 4 +- .../Pages/PageDownload/ModDownloadLib.vb | 3 +- .../PageDownload/PageDownloadInstall.xaml.vb | 10 +- .../Pages/PageDownload/PageDownloadLeft.xaml | 2 +- .../Pages/PageLaunch/MySkin.xaml.vb | 2 +- .../Pages/PageLaunch/PageLaunchLeft.xaml.vb | 6 +- .../Pages/PageLaunch/PageLaunchRight.xaml.vb | 15 +- .../Pages/PageLink/PageLinkFeedback.xaml | 19 - .../Pages/PageLink/PageLinkFeedback.xaml.vb | 2 - .../Pages/PageLink/PageLinkHiper.xaml | 234 ----- .../Pages/PageLink/PageLinkHiper.xaml.vb | 395 -------- .../Pages/PageLink/PageLinkIoi.xaml | 50 -- .../Pages/PageLink/PageLinkIoi.xaml.vb | 411 --------- .../Pages/PageLink/PageLinkLeft.xaml | 44 - .../Pages/PageLink/PageLinkLeft.xaml.vb | 112 --- .../Pages/PageLink/PageLinkMain.xaml | 202 +++++ .../Pages/PageLink/PageLinkMain.xaml.vb | 844 ++++++++++++++++++ .../Pages/PageOther/PageOtherAbout.xaml | 12 +- .../Pages/PageSetup/ModSetup.vb | 15 +- .../Pages/PageSetup/PageSetupLeft.xaml | 14 +- .../Pages/PageSetup/PageSetupLeft.xaml.vb | 24 +- .../Pages/PageSetup/PageSetupLink.xaml | 27 +- .../Pages/PageSetup/PageSetupLink.xaml.vb | 13 +- .../Pages/PageSetup/PageSetupUI.xaml | 12 +- .../Pages/PageSetup/PageSetupUI.xaml.vb | 4 +- .../Plain Craft Launcher 2.vbproj | 28 +- Plain Craft Launcher 2/Resources/Custom.xaml | 47 +- Plain Craft Launcher 2/Resources/Help.zip | Bin 83977 -> 84738 bytes 46 files changed, 1435 insertions(+), 1584 deletions(-) delete mode 100644 Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml.vb delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml delete mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb create mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml create mode 100644 Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb diff --git a/Plain Craft Launcher 2/Application.xaml.vb b/Plain Craft Launcher 2/Application.xaml.vb index cdde3cd..a025af9 100644 --- a/Plain Craft Launcher 2/Application.xaml.vb +++ b/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) diff --git a/Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb b/Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb deleted file mode 100644 index 8649305..0000000 --- a/Plain Craft Launcher 2/Controls/Behaviors/IMyRadio.vb +++ /dev/null @@ -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 diff --git a/Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb b/Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb index 19a2f34..c508b94 100644 --- a/Plain Craft Launcher 2/Controls/Behaviors/Interfaces.vb +++ b/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 + +''' +''' 指示该控件是可勾选的。 +''' +Public Interface IMyRadio + Event Check(sender As Object, e As RouteEventArgs) + Event Changed(sender As Object, e As RouteEventArgs) +End Interface diff --git a/Plain Craft Launcher 2/Controls/MyImage.vb b/Plain Craft Launcher 2/Controls/MyImage.vb index d4c5cf1..f869400 100644 --- a/Plain Craft Launcher 2/Controls/MyImage.vb +++ b/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 '保存缓存并显示 diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb b/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb index 5a735ba..e4f6785 100644 --- a/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb +++ b/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 ''' diff --git a/Plain Craft Launcher 2/Controls/MyPageRight.vb b/Plain Craft Launcher 2/Controls/MyPageRight.vb index 151ad44..a3e85a6 100644 --- a/Plain Craft Launcher 2/Controls/MyPageRight.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb b/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb index ba77d6f..e629808 100644 --- a/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 7f1c0b1..04298d5 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/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 ''' ''' 是否在还有下载任务未完成时发出警告。 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)) diff --git a/Plain Craft Launcher 2/Modules/Base/ModAnimation.vb b/Plain Craft Launcher 2/Modules/Base/ModAnimation.vb index 06bf1a6..4248ea4 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModAnimation.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModAnimation.vb @@ -467,7 +467,7 @@ Public Module ModAnimation ''' 按照 WPF 方式旋转控件的动画。 ''' ''' 动画的对象。它必须已经拥有了单一的 ScaleTransform 值。 - ''' 大小改变的百分比(如-0.6)。 + ''' 旋转角度(度)。 ''' 动画长度(毫秒)。 ''' 动画延迟执行的时间(毫秒)。 ''' 插值器类型。 @@ -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 ''' @@ -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)) diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index e9083a4..1819a52 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/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: ''' 运行参数。 ''' 等待该程序结束的最长时间(毫秒)。超时会抛出错误。 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 ''' - ''' 取随机整数。 + ''' 取随机整数(包含)。 ''' Public Function RandomInteger(min As Integer, max As Integer) As Integer Return Math.Floor((max - min + 1) * Random.NextDouble) + min diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb index 88a5d09..238c969 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb +++ b/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"} '基础属性 ''' @@ -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 Abort() - If State <> LoadState.Loading Then Return + 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() + 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 ''' ''' 子任务状态变更。 @@ -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 ''' diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index e19c8b9..0c17c64 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/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 + ''' + ''' 随机获取一个可用的端口。 + ''' + 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 + + ''' + ''' 带有 Timeout 的读取 Stream 方法。 + ''' + 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 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb index d7f0dc8..f36ed60 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb index c4c8fdb..c4bde9b 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb +++ b/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 & diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb index 2382a0b..00f2b11 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb +++ b/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: ''' 是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。 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 ''' diff --git a/Plain Craft Launcher 2/Modules/ModEvent.vb b/Plain Craft Launcher 2/Modules/ModEvent.vb index 02849de..4dffc4f 100644 --- a/Plain Craft Launcher 2/Modules/ModEvent.vb +++ b/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") diff --git a/Plain Craft Launcher 2/Modules/ModMain.vb b/Plain Craft Launcher 2/Modules/ModMain.vb index 767bbd0..c8c325c 100644 --- a/Plain Craft Launcher 2/Modules/ModMain.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Modules/ModSecret.vb b/Plain Craft Launcher 2/Modules/ModSecret.vb index f4bd63c..62d425c 100644 --- a/Plain Craft Launcher 2/Modules/ModSecret.vb +++ b/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) + ''' + ''' 联网获取的配置信息。 + ''' 若获取失败或仍在获取中,可能为 Nothing。 + ''' + 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 diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index 5813168..a3e04d0 100644 --- a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb +++ b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb @@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 ' 方法是按如下所示使用“*” - - + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb index 6e1d801..62d5c69 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb +++ b/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, diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb index 51b25e1..6718397 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml index 2aa4592..6d85387 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml @@ -5,7 +5,7 @@ - diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb index 40daeca..36e3f0c 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb +++ b/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 = "无披风"}} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb index 64f43b6..a1d5b7e 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb index 260fed3..374ce8d 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb +++ b/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 ''' Private Sub ClearCache() LoadedContentHash = -1 - OnlineLoader.Input = "" + OnlineLoader.Input = New Tuple(Of String, Boolean)("", True) Setup.Set("CacheSavedPageUrl", "") Setup.Set("CacheSavedPageVersion", "") Log("[Page] 已清空主页缓存") diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml deleted file mode 100644 index 143d253..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml.vb deleted file mode 100644 index 304bb68..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkFeedback.xaml.vb +++ /dev/null @@ -1,2 +0,0 @@ -Public Class PageLinkFeedback -End Class diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml deleted file mode 100644 index fd4a3b0..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb deleted file mode 100644 index b4f7a3b..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb +++ /dev/null @@ -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 - - ''' - ''' 若程序正在运行,则结束程序进程,同时初始化状态数据。返回是否关闭了相关进程。 - ''' - Public Shared Function HiperStop(SleepWhenKilled As Boolean) As Boolean - Return False - End Function - ''' - ''' 启动程序,并等待初始化完成后退出运行,同时更新 HiperIp。 - ''' 若启动失败,则会直接抛出异常。 - ''' 若程序正在运行,则会先停止其运行。 - ''' - 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 diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml deleted file mode 100644 index 7a63bb3..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb deleted file mode 100644 index b398ac3..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb +++ /dev/null @@ -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 - - ''' - ''' 若 Ioi 正在运行,则结束 Ioi 进程,同时初始化状态数据。返回是否关闭了对应进程。 - ''' - Public Shared Function IoiStop(SleepWhenKilled As Boolean) As Boolean - Return False - End Function - ''' - ''' 启动 Ioi,并等待初始化完成后退出运行,同时更新 IoiId 与 IoiPassword。 - ''' 正常初始化返回 True,需要更新返回 False,其余情况抛出异常。 - ''' 若 Ioi 正在运行,则会先停止其运行。 - ''' - 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 "发送请求" - - ''' - ''' 发送 Portsub 请求并等待获取控制台端口。进度将从 0 变化至 80%。 - ''' - Private Shared Sub SendPortsubRequest(User As LinkUserIoi) - End Sub - - ''' - ''' 向控制台发送 Connect 请求。 - ''' - 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 - - ''' - ''' 向控制台发送 Update 请求。 - ''' - 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 - - ''' - ''' 尝试发送断开请求,并将其从用户列表中移除。 - ''' - 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 "玩家名" - - ''' - ''' 获取当前的玩家名。 - ''' - Public Shared Function GetPlayerName() As String - Return "" - End Function - Private Shared AutogenPlayerName As String = Nothing '并非由玩家自定义,而是自动生成的玩家名 - ''' - ''' 检查某个玩家名是否合法。 - ''' - Private Shared Function IsPlayerNameValid(Name As String) As Boolean - Return True - End Function - -#End Region - -#Region "请求核心" - - ''' - ''' 启动 Socket 监听核心。 - ''' - 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 - ''' - ''' 从用户列表中移除一位用户。提示信息视作该用户主动离开。 - ''' - Public Shared Sub UserRemove(User As LinkUserIoi, ShowLeaveMessage As Boolean) - End Sub - - Public Shared Sub ModuleStopManually() - End Sub - -End Class diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml deleted file mode 100644 index 2d06a86..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb deleted file mode 100644 index 6ff4fa5..0000000 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkLeft.xaml.vb +++ /dev/null @@ -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 "页面切换" - - ''' - ''' 当前页面的编号。 - ''' - Public PageID As FormMain.PageSubType = FormMain.PageSubType.LinkHiper - - ''' - ''' 勾选事件改变页面。 - ''' - 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 - - ''' - ''' 切换现有页面。 - ''' - 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 diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml new file mode 100644 index 0000000..9e0c21f --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb new file mode 100644 index 0000000..b0ae907 --- /dev/null +++ b/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 + + ''' + ''' 切换到指定的状态。 + ''' + 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 + ''' + ''' 退出联机。 + ''' 返回是否弹出了警告窗口并且玩家选择了取消。 + ''' + 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 + + '=============================== + ' 后端 + '=============================== + + ''' + ''' EasyTier 所在的文件夹路径,以 \ 结尾。 + ''' + Private PathEasyTier As String = PathAppdata & "EasyTier\" + ''' + ''' 当前是服务端还是客户端。 + ''' + Private IsServerSide As Boolean + ''' + ''' 服务端映射前的端口号。 + ''' + Private ServerPort As Integer + ''' + ''' 在客户端映射后的地址。 + ''' 除非已作为客户端启动 EasyTier,否则一直为 Nothing。 + ''' + Private ClientAddress As String = Nothing + ''' + ''' 网络信息。 + ''' + 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 "进程与日志" + + ''' + ''' EasyTier Core 进程。 + ''' + Private Shared ProcessCore As Process + Private Shared ProcessOutputHandle As AutoResetEvent, ProcessErrorHandle As AutoResetEvent + + ''' + ''' 若程序正在运行,则结束程序进程。 + ''' 即使失败也不会抛出异常。 + ''' + 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 + ''' + ''' 出现意外错误,给出错误信息并结束联机。 + ''' + 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 + + ''' + ''' 启动进程。 + ''' + 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 + ''' + ''' 该节点的类型。 + ''' + Public ReadOnly Type As Types + Public Enum Types + ''' + ''' 客户端。 + ''' + Client + ''' + ''' 服务器。 + ''' + Server + ''' + ''' 公共节点。 + ''' + Misc + ''' + ''' 自己,或者自己的重复(它可能返回多个自己)。 + ''' + Self + End Enum + ''' + ''' 与该节点的延迟 (ms)。 + ''' 若是自己则为 0。 + ''' + Public ReadOnly Ping As Double + ''' + ''' 名称。 + ''' + Public ReadOnly Name As String + + ''' + ''' 从 CLI 给出的信息行分析对应的数据。 + ''' + 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 + + ''' + ''' 当前的节点列表,使用 RefreshPeerLoader 来刷新。 + ''' 若尚未成功获取过则为 Nothing,但保证在加载完成后至少是一个列表。 + ''' + Private Peers As List(Of Peer) = Nothing + + ''' + ''' 调用 EasyTier CLI 获取已连接节点信息。 + ''' 这会过滤掉尚未完成连接的其他节点以及自己。 + ''' 若在加载完成后连续获取失败 3 次,则会强制断开连接。 + ''' + 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 + ''' + ''' CLI 调用连续失败的次数。 + ''' + Private FailCount As Integer = 0 + + ''' + ''' 根据当前的节点列表获取 Ping 值。 + ''' 服务端会返回所有节点中最低的 Ping,客户端会返回服务端的 Ping。 + ''' 0 代表尚不可用。 + ''' + 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 + + ''' + ''' 获取当前房间中的人数,包括自己。 + ''' 0 代表不可用。 + ''' + 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 + ''' + ''' 检查联机模块是否崩溃。 + ''' + Private Sub CheckCrash() + If ProcessCore IsNot Nothing AndAlso Not ProcessCore.HasExited Then Return + Panic("联机模块已崩溃", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf)) + End Sub + +#End Region + +End Class diff --git a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml index 7e3bf33..10f5535 100644 --- a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml +++ b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml @@ -337,7 +337,7 @@ - + @@ -370,6 +370,8 @@ + + @@ -391,7 +393,7 @@ local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio" /> - + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb b/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb index b18c083..724fcb8 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml index a4c7d3e..7c81663 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml @@ -5,31 +5,31 @@ d:DesignWidth="152" AnimatedControl="PanItem"> + LogoScale="0.8" Logo="M52.1,164.5c-1.4,0-3.1-0.5-4.2-1.3c-2.6-1.7-4-4.2-4-7V43.8c0-2.9,1.6-5.8,4.1-7c1.2-0.8,2.7-1.2,4.1-1.2c1.5,0,2.9,0.4,4.2,1.2L153.1,93c0,0,0.1,0,0.1,0.1c2.6,1.7,4,4.2,4,7c0,3-1.7,5.8-4.2,7.1l-96.8,56.2C55.1,164,53.5,164.5,52.1,164.5z M60.4,142.1l72.1-42.1L60.4,58.2V142.1z"> - + - + - + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.vb index 5744a66..b8b8030 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml index 9403796..daf7f96 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml @@ -6,31 +6,10 @@ PanScroll="PanBack"> - + - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb index c4cc82e..0dc33c1 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml index 06913f0..a32d1a6 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml @@ -280,14 +280,14 @@ - - - + + + - - - + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb index a992749..3d42789 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb +++ b/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 diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj index e40bfb6..ffeaa15 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj @@ -206,7 +206,6 @@ Designer - @@ -307,21 +306,12 @@ PageLoginNideSkin.xaml - - PageLinkFeedback.xaml - - - PageLinkHiper.xaml + + PageLinkMain.xaml PageOtherHelpDetail.xaml - - PageLinkIoi.xaml - - - PageLinkLeft.xaml - PageVersionExport.xaml @@ -576,11 +566,7 @@ MSBuild:Compile Designer - - MSBuild:Compile - Designer - - + MSBuild:Compile Designer @@ -588,14 +574,6 @@ Designer MSBuild:Compile - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - Designer MSBuild:Compile diff --git a/Plain Craft Launcher 2/Resources/Custom.xaml b/Plain Craft Launcher 2/Resources/Custom.xaml index 65c3675..4a737b7 100644 --- a/Plain Craft Launcher 2/Resources/Custom.xaml +++ b/Plain Craft Launcher 2/Resources/Custom.xaml @@ -55,24 +55,22 @@ - - - - - + + @@ -83,6 +81,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + *^-fr$9(KPvNvV#nO#PN?2I0iva%kzjqHrb%FL!DWhI1&vLi%7 zq2J@V@$2*Xet)n3|GZq@w{u_D`&`$#&N=tF-6tvmH#`NGL{AeN=Nt@10E7J~F-s(2 z$NrLMR3)Yg9AR|`U`&R&7$L`lYCciW<6>$KsLw(*XK8hbFf~_MNuU}ZwicYD8k8v4 zHs%g5w>kl)!cTY%Q=u<1gpax4kd^{S(W;hxB~+gV(N65Mp@gKlszDjjGZ>3v)qRX& zky-{3MiJqCNv0eIgZ05sc7|}2r@<`dzKNj~9_FIX+!^BqirI1mQ?X~63wfMr=K=Xg z=D-47h+cKU$0{*HWxHO)s7|;t0hCAH{D|edG0Xsiv3S5>bTC%1iU*)UvEJZ9x*A%3 zsLlSEc6dMJ;q!yRn8Dv+7o4u6r1}}&JGx0MXFj*N%3MjuER^Jl{)@mt0qd@3z@5kr zyjUgzdU^7*;eWnd&t?tTv}tht=G*@+ksK~qH6^VrnB)D^;aly8po*5=AM&wkCcM0x z!)iip2E401l&(+3=twL7*wfHX2O%xurogp|OHotES8M0|@9%xhte0jj`25gI%EaQ^ zLyo#z*VWSLl7|M~kPQ{Q9N0+db}Fc<+1~_$C^g@|m#D&hg%Yr(roDImLH5DIhqk@# zqOgPylo#Lom5xxlJDDE53tfL2hDa)?O%1EP)tPxXV}9rPnLqUxi3cM(klEHz$iU|5 z+w*6BVOd-#&F^t{e>UOMr}DnEHRIiuU{AeR{y5315+`lTmo^eg3pYGJp>sc{?;*0< zpNF=IjLj3N4bWM9ZW4&qe$m?RS&SIFJ-V?o;DIY^b$k7q4cjo~&tGP)oaBVrWIHP5 zmbV3s3P=Ck*Lx%e3!i(SC`mcn(_@dkZPt&7c@AV*JJ{4(+jp%thW~2Ia&3C45wulM zkT^6$nU%)lj(DS4@rHxuNuRs3^tQc5wG4I{OrDhKFxW`7B>AMbL=>YfouH!q((*WX48~dgT)B z<9A;tpZUIoS5nBcAaC?X9shMea6Dn=?jHzx9MCF9AtrRQr{OLqirq7iOhnWS+Aj z@WSFm)8IGe_sA2p1mXo!oWL{rn_Iy*o$x)olZ77R7BK5EYbm^dWpZs9hmIgQP{I`i zR=;`@m@D~a2aw^JDy?JI^Vrp0!L4SgcO4C;iFOGnDaReAzx^Jj*~M+Rmy@tyg9RtZ z7T>y^=b6XH$`2V4ut5{rciEX zXgIVOT-UPhzf7dLe*e3X8UL?4`ndka@sXWkIi%EedA_y9wc*K)Yb)U+$V@Vr&;7Lz z3!&oo;XbQH8xNEuF5=BBB7?&emZUklipH+lH09lo;(U8>dE&wJZLh7#^~?C*%Z$g= zX`clzGTW$VBxjdhUp@1Pb4vUBk3!K}KK6TigXzh^u({9@r|q~OhUvuEbQ;#W1fAdJ zrBNG1!O4^#4ICq1z2ukbf9QntzvXWHE;Pa^XSdkgA$H{@gFL-mv~$c#)bckSB{agyTIEHuR_)E$gBH!@3Ze`+m zJVfjS6e?7I{@J(QnIvChZCFO0F%lfw<9D79^ zBS)+e@n4bMHrQ%!bQ8~h?JXN3eW2baQTH}Nh&=7;rIMzDW~LfCw~El(&4eOr=f>O` ziBcE!L4wNaVVLu?n|up0=g5#>3z6gV%fkfvtHMtlzSzSx?6LeJdabniwhD@hQ>qn8 zb>})|TgkD%D-~XGalNztNfoRAN7$Ueg1Y7fp5Q%q7oV7-Lg%xs2Nvn$xSw%XfWDq; z^OiBa?>1Qsl+Nk|uq`+PDRp5D`G~l0YP{fA7g;L9t)}%>*Vcva^*f@-AZ05Kq-oI; z)|sZ|HtG$a+vlFvl(pd)&GH$QjHWx8MbEI=_!Os;x>PQyAT(>8UgvRM^P5+?7uHU2 z1)H&C0zXG9un)x7_bifdAz*$IU`MZ~`$CwgwDWLtYv?VNoOpB)N#l9?2f4Kx4z9+t z@5vEEm-6-<7V_(2F5?XEA{8HV7d+A4D2W_(VQr0H^7Z>^T=bNvFhwL`$!#PUKgZN|K z^tmGXXZzfiz(c>H`UQsB-IG^Q})=4 zlSyUdQ8;f7N=xp9#xitK4A!lB7gY-h2!&3>Jk8(P>mw1TESc z?z+?aD!H54UM(eHYvEE-4HXX{B z<0DxmGz%>-lV9yaw({xk>$hSz^!Df$-ui0iV2LH0^L!y9%q*uNo_m7yBTV(B#VTSj zj`{-s2ftxQ0&NB1aCq6onFFs5qpD2kQ1h&Xvk1wPQPj|OUC`6tSB)H$sv@t9HnNl>=_TxK(^2BEf;{^|lCFNmK zx`9IG6JvQ7LxiMxwWDZbBM+Ej+cgg_{dikkp?Rm1AgS(5tDWbeY1;(ZD>?cNw|iu= z<(-P`vDVn`ThlDa+?l2z?5!Y$I)$Br-UWdI$KHEtRoB$qp6lXT{))&XEos;09B9Uq zrkS*n+*!)>`7!Mg(0Q9IpvI~Er%G784DjfTO?F3}{3Dzyl9hEj9wzM@n=EQfcCTmN zaqCX8s;8#hV@!F2O?%FqX?6giKem9U5hUNUw|!vsxXRa)2$@>f%anRsUhlrvtB%=2 zTq=KuOPm7_9u^u-d`+%gEYNd$-QN0B{qD=CPbKv>59?DzzU(Mm`*7Xl{RCyyy+)je z*54yMc3WBdy|&sIUR7lP$pGa&^|$G1{p6B z(8${QbN#_c)6M8=q*E2vOH01}V0xL~ZrDV&8ucNmODVlGdFGP_5s%CkN~ST&COt6tqSafcS~Ch@$}00HltC=R3O^^r@|r|I^o>(?4)a^6?!m1T<#ALj z_`n&Y#Te2nQn9!Bb~6d<{6a}VR`Na5y}{;l8I%42--QxI?ro1cMN5kWUoD{njRoXP zHn>ca9vQ{fp|S%XA$j}#B`u~0+7a5+F}h&N=o`(Cy_?jRG>cc`_hc-XHjRg#)lfA@ z6}Vzs1igI?EgWr5pw7?`(F1Oy~J<2e}v^S^oKKi$0g?HAIJ@gvJ1OqkPM&9!~~ zfcn9jp=38}g|TGoYnP@7yf6FZHIcW(hF-%5Y)wj%I7F-U8K)nrnbu^!G7KxBPrs$1 z^+Kj%Ufst4HVYqpDI)V&BeMBU@-JbX?{i;1B@H)y_d|p^MBbrIbZ03Fn(v|NotZX! zr!e>3JnK=ix8NsAB>S7fAB>9C^@9D$X2>`fP6q=P^RGO5=X9bPTtC;|V;vzGG?Eac zTZIcUYw^`=q!ku&kh9JNscPA0uoMb(W?fQ=jK^~+(4hqxjafeZFh4VUcorug*d8$R zm?u_sFpwiSYilIEy+E0KFLBNxHBi&#?gVv8AY$W(motTm5E9VwVbgAVh3ee8Z`(;}!7p zFFLbe8xtW!U*1HP<(Bj7R$2w0jddy^vJ6yc{qrS6$(N)-1nXzgx^2#zJ3F6vN?HkU z<2ntw(gLP0m{FwRE8-9?$==y|hzLAL2)VHSkcO#6KA|7iV;39$4(lQ#>_*!MS!{fr z!JibvDhWNs?Xz9+Uqn6!lM^GjeB)roj?%m?~CN<9&%SVM5d)w$}L)*JC@By-iL$Wyh@i6-Bd(KNBGwc(wY)oXOHVHZA`l| zo$t&_M646eW?ht?v#6TI=NOva7%ezEPK8(U+WXZUL6zgTwOX$af(OYSYthN~wVPFH zMY;?vmu!a8GM#x_=qiG8zYHs7yU*lUWL4zEdUg;HI@%$P-8^g;fyh_vO3Hq>pvAOrh)3g}Uiachjgu*v#!;3Je}5q~xGeLTt$$YQi_~Yf zax}<&W?N&T%MWWbe?rPFG-#WKJJOyAS6G${J`xY>Tk8%A$oS%`l$&tp*GLe(f@#b=ZyJWx_ zrePW$A!JBl1IL#fQ_XMfBScAq&-i{+&=PkA@Ny+c>(d2yNUY~d60xrBIFR1&$-NUs zz)+{D5nDY?VLU78zcuvCgf=XPb)N6}@dCW4O!}ypR%V+h{=#&~^xT>U9 zF1bPRjagqGlH4>2^Y5h;g2Qom<+O+I`67Q#rOF{UG=4B=WqZ{;$)MkTt+!abbys3e zPEYvqNFcBJ(?uIe<;T}^NByrXxfF&hu?;nG>qpxL@6)7ybPri3!7Z(@3O7SBvETBe z60j**wAehrpYJ%wSIRPBC}NBIE;FQYgH_w@5^D>-_4Yf`sN27LvEOp4L zd2Em!f@XblTEs?c-`O;p*6#_BfBhMe(WF#=5Efb_d2UT~s@2`?0QE$6?m=qncPo(& zVh%B?;k$;aq>}7Z7gg#cuqh2-#eMefRf1cpU=wm=!PartX5aIYX1C;=T;Iqh~WE$A1p0a>pxENkJ?v< z-0&UyzC}`0iffbMQUCm@pJR0fwp!_U54|6VUwF2uI<)Z2KV;O}ITP8<40w6WOauf|@E1)6Hv~@!nlA?E3*u){}T zz|muH7$)u7yjq!R9+ThY>SGpa9P3y8?we@is#*-y5?S$(W6i}WwW9aV5%ts=O(RRT ze)o|v<`$27U(xf4F9-*HW~iP@zqu4EZy8E9>6riadd(OEjL7Rk==@8eE7Xo2 zfURA?Rf+rO8!auHO=74D??1nyc0u7P$6#MOazn= zQzKX8Q-e)mDc90w`YnXVKkXvCG1VSK!@!UKl)7PJNxacYO*)ofR(O|~DBPxu-z}JX z{}P9+u598j>m2-2?u%U-)Kmj^u_X+RZBfOkoV4E3!Xab16*(MEM%*(|z2S)PJ1_Yh zgpgrLyrM~Z=XJP-@&$mxwn`ph|Aflz)t>`q>k=Oh^tFDIKP8YC_&q_k*wiRfRN<&^ zC%@8D^VDLW71@e)O0>A#RRb-{X=5NS&g3LF|7HnpCFxV_|3alLz!(w z3k1Bdf;-T6l3#iFPK0eMYBcD(NQNr+QqV_!4!|-Hh{gGWrLOd_@F|!5%k8v87fW*F zs0oo5?F6k~(?hlBnK!m<7S%K<3+Do9@C^c|++AU#QnP|tXq&?k>D0&Hj`E5y0POwOC`kaJC(D~Sl(xcIP| z<$S7E-1gl^*&W|sT3Hjrr&#CEA3g zuPwQ-l0%^{N%W?AT&v&Z?2ym#*2vZ8uQbwX4LH>2k$2~fxlqjweC#4d$*RN2)aHvl?v{nl6#)54sGoW`S zt@T4{leq$7JQs;ND$cI3d#Q7@>^;5RTNyfLRmId{-CJ2w2k%y7Kd9{2_91BqBEmD( zXvtTQ850K-9=dGdiSDQoJ71=lB_Zm#wvEGDuW34425(`XkvKazs|$%>{2ArfBt|~^ zIA3GN^|`gV(z%&U${rY8cE6esKZPo+nv7|6f3&1@AxxHYsaB@IL_dzloXJem#kKZH zD#P_JPiQv0&7^Igj;oadn$*bXe8PLa3q;Ax!cXH;$wfxTtw?O=O)5C8H0sqpt@S!= zzvGjUePO9$Mr!iJuEa>Z0Ff8EJbBIQQ_%8V?RAfKO;uMycRLv~b362&&Q-a= z&e~b8@9gDYsU}ywT`ifIUXg87_A{feBjM7G^h+;_Q|Bt=IG9oV`wJcgt5JuuetdTX6tSuBoYYy#dVRIWEg98de*^TY9#x*qSS3U0wb+t13Ko z`-`3k#hNpV4;l)lKO4R%b*2;K*uESRwHh^ebDB(7O67%|{VziTq|4B|*1qd@ zKn2l0Nd;#@D3cQPeNGjk4J7<9l3fD--0(NPjqUS{(`R@R^3hm@?uc`KOy3(w$Kj3y6-P_b`mIAHYwLfL# zPDdz}Xcfp5_bT*lZ&BWsNvzWw09H!hE!N0kU8b@{QzAtv1B%7nu~CTqGTi^!+2K@SuTJnxex13n1xWqvNEuSG`v3`yerfD#Ov}V3xw? z#e-cHe;GX*cdJ?(g}h5)1QXWFFWuwnxbtEp&I)B=1m{fy@_u<*>SpzB zYI?qSVt{1VP{&J5VL?`9lj_oN-H)nG3~{1=FcY!Vs-)ygs;U#piR(Ud?|z1@Ms#wU z-R+9@?j(iR^UEN2in||fIE9x_hhKCk^YDC>sQ+HeXFaxI3M&3}|3lkuZPbXHU;2iC zd0OL(*2r{)8^y6+99~3fn|PhCa*E#40N%a+3vbE@E=5lVu}~mwKGe3CRa;SbaMEws zy!UUq)x5lIg)2MUe@4yyT4x0oqkPJrcF}yztSophxuXd!_MepDUtTwZQsbhRVX^2f zYVv+EqowTttBLC$&nta4gZg*$u0Olhcy6Wp(ZJNW!Jl%O&6=7K{EQvbHiSN(;Uk`7cR^iPX}}grVI|BySyt8eLKe@Ab=fxEa#7AfIchh#N=R`9oJgc zewXpV0$?z0v@pUDu(^hkh0vn;*c5@K`z7<`>2uGc<{D$>n(FwkdwaP1da}X*Kfh-V z|EC(Ny^jNx9Zih#{7r~NSxxEsc?3-e9dvb4*|PIL>FQBU!(~s~y9JNXU3{ugJXQ63 zzOzghu`M%IwdV5S4MKWUZoKKwUNY5yQ*BNf{y9bAA?ey@GHsj%v{$X~=2ygfyWG5d zyWG|My7ift#t(6yz&uxbi&FFp^Fa{AaqLMX3ZbW*MIL?32e+oS;G%V<#A&t zaCAPvxmg%7ifU{M*)G4X^+XqX^+Y3>F9tUNGtUMAcfHy zrU%3j@7MU9msKVK-3oEHBdjXo!u+kL31FRror4{Zb5M{0xJ(_&H>=*PI0-z(;l?OI z2*xu2Vn^YD-DoA+6QxZBNa+MoPXPp|%Lou-RIM2SwNuqJ6r_5BD1d?uoHh4 ziP->y(+*myhg44xWi2g8_U~~~i?%rJcfwW30z#019gu^P&Hk60tkis%SRe#j*pJ=Z zMk_I&44%ivkkSca7S6kGK2M-#RO!H58~|pffw3Ha5noJ%Y#;*@7}$}^_M^EQII`^Uztim&`Nq;!HP zIR^xIoBPSlK_O^8V?|ZIm`9G$E-j;UO?(pUyBMNhY$?m1%xo9RlL9jfDyFj1L(j- zUV!}6z)2Z0fDk0%1BBs6=eUYJWPLQSVK6>?80-S<68hNKQGugHE*7-m1Gr8Z=o}yh z2*E5qfFJqaAK{uu6zBp+%X~cS|M5U4#OT3b0E`xPGIkeULT#P&;s5c_m-+YR;3WCS zhxIIvyf`U@Fp1+!ax`irf*X7QEj66-;y*#`I6iP`ukqwEkDL2*Yg9 zzmEzWEfI;J4L_i8IyNhGk1QvF6ZG|zfB~ss_Y0I_yf7$pGM)he-~#q=5)7pfF97NY z0OY3xuF(*|31SFjg0+943HZS>v??aW?Pvn5cj@4i03d(5&=gfcwkL>cO_c)O1&_yU zh#=Y|^wE)t0N5k=*RW{+1KHJ%9U-_b2wcK6A|-_O@?IVcrIlUOOmRgRO~L;Jh9WIy1XG3o@|oQpY8pbY zRT#i*guuNc3&WBl3l>mP1Ry@`9q&rW;sjBG#oT-+W94KFKmDw%O$oqYo@hb9jE>Tb zE^xHyRKQSY$~eKvzd3h5K%6It5-Jk}1x1g&mls9*GFk~kC6=>*NObM#G{WbQz;qmh zV6Nz~_mf8!yw8p-7(rUGzvBI72U&zl9U&+uc5HD=3~lkM4u%S=VF90u{ndt16l4J* z_z`Wvd^Fk1glTNvVZmT*=wqbJFrB0L&;^c`*Y!s7%V%(ZT^U zJTgZYI9m3aj~Fh1aT37!Q<{?r3nADd0mvOKxd#%!1=p5V^n%k+J|F!=;O`hng`vfD z&;eSBJ#PqNQsC=J3u;ichW29;EnF}Y^zWkrN6Tg_C@2XaQX~N! zu>9iRJ&%H@jW7rfUIY{{L5i`-3-sZ_U|)#-O~ufDL?v+=U(61~03jG62`E6*UJ^Zr z9bO$N(}FqZGr=53Z$p2Fd9rvaunA$pgAnAB0^~6au2N`*wC+;|#?$%n;5uaCqYNP` zq)UkR-@WmkRJi_V6(mRJ>7RqAbpRZc2AIGcX#nTHE*3mV5bp`1GWxjxgMUt#Dc|4> z40f0H-~1Hsj`#&m-94c>LDX75_kZlF^kmS{_D*1;MP-dq{8z_pcOl1^-W>;CF(wFp zl{ucodq)%-AE6H8mjxJ3ljO@J42>WRf_Aci945`NqOy0Z(S0hE&tqgTsk=7Iu9l&pr_V9 zI_h6s?h3~{7#aAy3_E)FVtn@BTvmriT>ra+Q8S0u0cbq{|0n<=Crby`G%l#G2vAWk z0aBREp6uEVZIAW`V6Y;(VkdKm^Vx0>r0}aMEYcZsG)eJ-J{2 z-_6nAAO$V}2RE<*oc|c83QA~3V|)xFH!eWL_uuBUKTHzi)(K*WqtdT*9uc$sN3QlS z9Vi`*!40(hb4?{CMK>gRnt3M##Cw9^8WDPnuqFWz;Gevz|a|Fg#Ep9 zyh{Us@>BrRf1S9a;}G2mg4R^%S7SZZl)e zIsmqq*1A}f_>U*YV*+R~K6!F{;Szx6{n3*HiS;@C*yi7BWx5VN-!3{Resp9MFgf(q zQGug{|H5fQ+RECxDwzLi5wc?L{c{g|>lW1334*_A0lNRlbW8Ovk`>*GGg|0i>PO9> z3mh#T^xz*gfb%qb{nrp5grKbY@hihkb#z~SV*umS|C%Z%fkO!PsRR6^8ZZr57Y}-K J#Ds1O_CF_=US0qI delta 12270 zcmZ`<2Rv2p|G&rHd+)vXUfFw7D3VoHp|T~{CXsRFSlN4okiAz(qGgn#h(cD`{I7fH z=lA`6|L5i0_wk(f`*S|y8HeYFreHouz$7%*L&u64d4W8 z?*nGUsEcMu*^bI*BA(T8mKKspr}E3S?F1=d(u44m18_9HkXJM2qiG079hP6t6j@aU=;c!&H|8cW_+1Nh~i#&?K}4D=9(;|IfY zUw2jKocxBd9Sg?%{eOzyqs}fa{xydm%WcymAVMIX)_(N6D(JAhD%37pdgsRGCoYeI z2y5cr&?8T8g+zghzMq$CrN^moo|M zBK~Lcg+~b%? z<-6-%Hv6c79e#P>X;e?w&yZK_Di=jJ9yh5wtS$1_W*Np8U6pN7i8T)KyMmDRT6s>r z(9X29@u9{eZ>RLl=R5_Oi*rwPBk6gS$;3N}e+mm!&Wqn^Z+U^xyPLYI)l=6^_XucN#gJipDAllFF#PO-aJ{m+E&chcXOLBLa1o`NPp7{=@yM@9Xu`UluI zz4j?jQd~PzMNEdMcfZ(eYl;yrct22 zJ73nH)%jpNj3WDUGM8^bFA4KiL{fYRgYoZ*889fFyC*m@=>vJ_?UvnDi~}_0`QAs# zOa-Df4+m-LMxSR3Tvx(D@d9(A&R4rC4Re2vkiYr z_7nQAg1)12dZ?L3#A&DxsuX#so&7QV4ktPbiZ5c z&Vyh}m-}f-LqmFZzKHf3*qDmqN#vOolOAA+ciFyfNzH%@6@c}-uRq#Ri9eU2WNU~a9q?gT|}BUal>?=w@*2_7h{<&ttH)ZK~LCN&?(qFd-xvlt|~(> ze%jrL6~gvo7_Q7G^dzNhdfrKE7?b$f;5QS6X*G(wK_cyMkhjil1U@(a-qZ@Oz zEes#>G=7(M!C@`knW4$aio9X&*m<;3-u)hrnX#)4>l4AF{x6oLH6yQg1bQDxWbz!P z9#yQ^lL0s2K_sQRE_caW`SQFiaorI^?AirOSJ+J3)8^@E6IgcJIBwx@biT5h()v@` zP>7jHYZlJ@GMBLR>4OxeXD)-Gav@<0%Ib7&&xVrWeV^&dFXoagqA$zFkEQWED3adN zgLh0@|EX~-Z|0IuUnbiqA2WEH*!(&`LU2-$#Y(uR3?3Gs(=6RjJ{ZV_Etio;>b&t< zUUokEmMve&w9xguw<3|&|nBEd*%{QvC zE>l|(n3i+<^!-O&p&9qbE)_J;cND#%+OF@&PH_!QX3sj1vfV34_=0lWcrEuR@p_1X z^Q(*e5pnQ{+9@sX**s}MCLHZOe_Y0ghm0h`-OH%Q*R&GIU!k8tz@j=l}UH6o$?E7s}XEpnAHr;g6 z_V`!v$uwVE?ijoD6-ozd?{--k`$cOAzAmVB+;jB%g;%Pi-b}L3(J;R+->RflyA-96 z^4&-v;m)V$Uo&aYLL{+OgKT=I>Cja+?st-yy+OYsTIN&LmaDchbDO?7<7SS-O~Mv5 zDqGeWeE2<2XFUpd@-H-@+Ko-#tN_#L6lG%Pr~Y;v>Ic>O9HL#VPy@Nu}2F?#6A0Q=>R$Ic!JypkIfhegHZ3cnLt zfuyeB2C-6tOyh~mk3H|_uor$HkBD=N`T1R3ZJeY3R=9!;-DeB5u?cBWcobIMAnszM z$9&BJd`LGWt@jS*V5Wy^r5)jPym@Di41s_Vu|t8qt!{5%7wN~OBCVmKX0)SsP0fa{ zX@h-Ub9V)g(&LA#*}VVoA&eW#-TJM72&1HQguGm9F4g)22GOYzuEotZ>P$(Rz0XI4 z*cS%P8!+uxtfl+7r1!&SZy$r z>SIuRq|DcBHRo?>63l^)$_`7fH9D%1>wHfxvDUjOPsP0ir+}UE9VfR$=hYmtuGb?x z3OKQ;`F8DFc$HPg~uZVPAQ4-vgEW2V%`3qzC$%blS8j zE^gJ$bj$ExYm#*rr4{_&G3Sz{fy>4}I)}c)_paVi-N(O+cfDEW*@e1o%2$PIv#A|# zrcL`zH3xk#if=@XX1yWG@W}>#Ay-0%hk@;xy{Q&IIGJr0h_^SIZm^rgeE4ytndsQ@ zn6aaZf5B;nXTJ^fcUaywp<2SLakkLHuBhUj+xFdKUuIrrmij}?SU;yIAh1;O*s(AF zJ$zSEE-yu^)93y$*bt+KcioRAE8rT9<$Rc@Y2ci3_%}~Br!B_!#hWc}aw6jzC0ci` zb`^&c$idXvsSLUig)xEJ2S+04*?-h6Cz|>S86EEv7jRzs^POSqy*&HSWYzOf-}T+Q z<)rzBel$|P38NQGdO9d-d)mJQuUXp`l5)Z&*iF@4Z)EfEsAg{~`SI_BzhWzvj&ax@e-ZQi4NZUu!`qRuC21xuVWq;`7_niO$wx)V z;3*_g^t`|4VY0cqVVtG+z8<=x4Umj6-6`T6^&KYXPvvT?$qRXb?dW$`^;Ngu=x#Y~ zYz9wT)Y$STdIw!))?tZcQQky>t>r|A3Ym0rbP;q@vyme=LJ2d(ra$jooMyyMNuoL5 zj_y{;Hr_I>4uO0=_?Hz z<5dh3{a|uNor@kdlPVr}c(r(0bu+bJQ|iYSU?@?Dcu53uxhSWrpnF z+^Ke3nh4twr+t#|ld@0zL-V}s@O*R(6}~!rn(As*#L!mVCgFZ-f2l0k@-aGDv0(!< zT#9Whf%D#jPBF_GN}}0!5`=m#Tuv8gOQxFkm%1-3#8i)L55`}E>zBBr`D%7u+h8WV zRC7G5rSY!t_L__WWzvKAwh+!le+u=SU_~*R&jAiAW`Eo&tgZ#_tt-nhH&fy;5kAVi zPf&MgvQ5AsqcS~UOs>L}kH-AMsf$EEFc3~svylP1nSfWyGAAUPM!F zJoW_h^iA`f(F)(Yz(5FAt;KS72q+4Pe zc!gq=D7#&>MsCT^zkE)dH5oLNw!*AX`!!iy-ZgEPM|59jamat@u^e0mkh3n%?DbC4 z`G|!3NyynLvv*U?O~P38cc0ES&CS-~2Z0vyLFI$9$iGHrIId(5{fc}1typLk&iA%C z&qQQhRNi!vHpNj&y~tI%P~TL`|AI=#+S~giO;h(jc6Slbp!0+{F&m@5jw$=n#gj=> zwe`}QYcdquv(H6D&rw(dO>o&$Cdc<}f?K==U9U@1BXepp)oQ*SYjE+sVTxYUODsZ! zw>9`LiTm)lr;N50RP5RxDAG72e}zX#J3cE9M$FyP4M<3eB-{NIYg}MCltJcg6*MG$ z(Q@Ge@5?C6T^zqw$2ri0onN1AH96rtmH&!p<0L#$2TxEA(6#-u~i;sKiaUnLqo= z)t?Pfm=?c@2!$5M8E}?7)H6EFuae(+U0&!DeZb9uD>$us9MfeSJAgoI`WkWcA#js5 zy;fw-PN_&v-;j|&eX-iw)IRnwrG#!&7V*$ofAq%VS|H+*UW1eWX9D@pl=xq9iErVK zd1FQmT_qpK#XSo%1*LiC zi_)@dCe{)8uPzqN!t<_^^#oK5RD3`7jEuI+eNKFJ@k-JCMUL?(K9d;Rz@vtO^j|?= z@uOmW<&`T2jSvd8G13ZeJq35izS_fMQq0I!N?oc$7wuVYQ0jf9AHThwxZ4Y#Vjc5n zZ~S`rWL7a#=xJ_C_GG>i&b#_ntj2BT=2f)@XEU1lPfp`I?RBC=a#cf;YG3!_sP8FA ze$#%N(5XLIw>;$w(qNURWj+cUx{#*svi^AkyXVP~wwl(?H5QsJsc?3;g?lSSMM`N_ z-R&j2k;g@}jv_YLY0S8OVUC%6aLf7ROJ7JPXVCPufgH#3!m%L%@KY973*yR`s-4+C zA2%A874bbM)Md7@{Pgl?92mKXHyb@vjCP0& zrs?S!x6?1UzjtncJ9)~jZ3==dq&8Nok}YmTj=q;exib3tGKm_fi^yub;L%+1Bi9?S zX5&hi2{$KF83U%R<6aNaO895$ABx3(c_s=9IzO|t-We4M$GnEc-`1&UemQ#BRdh1$ zjpf_1%U8HFwR2v{v<|qKb_^96a}C^Ced3)IV(yGiW3832CedEr!BgSbri$gsn zVYFh*J0Z|L{Bp(GLP#rsT}&d)T2p8EGyEr+PhU8tsE|GOlE!(DuaXcgLT{ZI<-Zf(qyJ_xJ7$F6l3J5p zRbymu1xG*3B_fNkJ)sH9=e^&gOMSJbQ~v0;I<)ds1}enBjl1&t=Jc=O%*KVEu6z6I z3xR`J9+f^uh`~VyL+M(n=F}&$^eZpj*1O~#J+2cy%$T(HarCtOLTJphRxxnhZ42hn z19O+Iy|N-gQrlJ18ETFo-x&$2n*8AdxJ(BYX-0NTF(}WyriDwPG5ZN{HH=*MmI0`~ zNx_CA92zTMj^T86uf1@G8{&I4NbcP-r`IR;Y%=f(exaIU6+CNQq=~O43w&Og=)-^k z*jMiRT*mg4(Ps&zGk`TyM!HHgF}x&XzLHNk6%f2FB>&0qvf%CdL5Gb-10ovI{u^tw zR2UsoA18M7cM|mjvI2yDw`vlbo869iEHG4tgY7a>X)Dag}YH)ZzTv*H>BWeon3+mu>o$5SpU*ounHcqv6D&8s3773+2+T zU&^c6pW@=U08o!e>8B?~gjqWeO-XYNpvk9rarI>-g!?>5>EJ6MV%M^A^Rh}k} zsY^yxtdm3}eVkerXt$LhC(`{Y-Newj$HJ*lQkU7un!-cWds_hxloD&A-3ejO)xG$h zLUMLb;6+^=&P@cREP)$ARF5XR#+@4?WR;WHdY-s#57BwsRwbpIYf6bRf(gICIDb^j z(HxK~t*2os&C(aOHZ5o-qVIU&`S|RrTj2YM@S=a|xK}ZWyYXt__Kq@9Cx62r;Razl zYqzq`#20e(v@d8baI}ueHNjQJ33-+P2HU$lHa;P=Q926f>>6IuxGey-SK}{9d6kK# zmFHGm83Vx0nx4DWg&Ps*wo2;+{f(TKjM5{MdOSUa+D}SUXE|4~LwNyH9E^txG|p)T zG2gnCDa(#ywXwJm4i{Lz7u`{~V?l(jP0X}DQ`tSuJ!kka6rPKfTA~>JZXn>==Biui z@qp>Z*DpCLA~9;?9(f>o&Ces}sQK*7xn(dy@V>$XT}l^7KfEb z(MoKwV2AoFxamf7?J<@4w?YXl7ik{~p1zRC)NdHxMsNY{%?!gURJjZx-cH`cjhrOE zy*2V_mZYXLrA8k_Gdaxe;QzAb|5E|H7v)y7FeSwjOMDO8Oce?LsQmbedX8c?ZSI~$ zUMZRVyD6S|*U0_dMKu?^D(jyQ-$V=5kxqn#UK>WZmi7ajKhcN9Z&khjE*|z<;Wu3n zkqdV0jcI3ic~7(UTuG>%==6ml5?WI&{b*YL<)>KHcgro_YUNw8|MLA_-8(6jl{G8K zRv}wVp?h(tc#PbH)9nwR?5|H=-9_8dUkdXiF-bn}veyJxk>T9^W1vBzzSK7PbM{p} z3rT~Pqi}dgySyuq`|-Ilj+FU}nB0x~fdYfIUw4=9D#G{Kx%gu45y{j|7%Mh%#eeTS zFnM|yXlv$&9Vhp$Jl4*-6 zs4&O2HYu#z)-_ey!kKzpO#n0@_(fKTCF z3VHbNaZF8B>R1A&w)z=!53ORln+AgD_$4>o5Pg2S=}fsj^$)50g0@n!dek0j_?IZ^ zd~v7UpdeM|oVS@4(%tI$9XwpMN>$(erQWg`>tSFDe3h!i|L*13SP=&v_9*&i(nSj- zR2JNYpq|&8M|{1c0p5}ax@HT>RxI{`$bXMlQ)51ts;)ayzZytQz5j=4sL9`zz#%1? zaEl_gNWdHR1Y=eYu~*Qp{zrYW@z*;9(O7kS`Y}a+cB&hX^1cHO`Ehz{yH7|*E+r<} zn%avw68O7S7*j8E%{RyF(B?4AcEu5FDcno`z66SSM_D!w?)_jkkhR{dZ}V&-8{pWnNF66FUfodj@M=Fqr41I?H`%#gz0u4t6I0ij-b zreQ(4f;ns$3%Ji=Y52;WB*SJJFL`cK|KyD0EB>&%L3Qm(hE1n|VrtRn2vt6VLIs`? z<&oc;N_Q$nK+N&*mB=a2aHtKb2Y&4o3I4ZA4 z@eFFo!kgJI*X-R+Yh;%v?J`9JB)d51c&O}kZj|(qCqAUia8$1}Hjw@pZR9n8PC5Jj zF$D%k48>(RjPK$%qde_2{JZp1T=Bb|3`{9KiTlXAFZJE?!vQ|IQS>G843%gZE#aV< z6qGCYlBqv&ca@}O&`1jCF_h-Ni!Y`dL0^Jy$_5YG&?~_Ws}_$fzoyGcO6e+?J>f@d zmMP@%2e-1G&DX=8>RBhAE|E9SMw7E{1VyCzw+FPlSExMG7W2y1Q_6&fDnA*S8OwkG zy5SVsubU$|!-P@V(Jx-QPA2Z?{4r7Szk*I@e3M1BpEFB@$^GXb;tCwsUFPBsVU491 zEaRqd4x%@+lWc|Qm+2L~`c3e!MX%~{T$(lhMiFiL1ZaLZq*Zd4D4V~?#Po5bhh%W&U% z7n-!k&O{^zc181kTUpcUV%UWHueeii^!;ibgO_xCbzEC*dQ>9JfQ#k+qvezApcPAZbXL^yFyOS_PM?NE>CGQVvU$!nk9NNN_FFoD4wJqC0 zZ86tcRYgEHzE)p^E@Yp97${>4Hs-I=kjAQDh%XK72%upMW$aypG&<^nxMd4cV6}Av zC)>b7?P91K$L&1O4Zcn_XjA!izc|!VG{A=1$*vt9HHKQ4#u`yO)wPpTtWeA0Gyw_9 z4#6@u)Izl)jk1F<-aJCxG1&qjk=fztF1qVc@X1y*xQq#)oM>Rh0u)i|277Fv4kd4( zB?6S8wyVUz52#It3V=528xpC3CFt&bS|I8qA`pxI)S5i~nYBO$;2y-myk@3YPJAkrBk6lmDM6*hnXMLl3U z=ccfh)7`P0E5_1sd4P*sD&vI^$Za?92_7q$0?PG;~doqp9W?13=y`x9AM5F zwF*i78&K30h&((-H1R_BQW^TzRBCR(^sj%yHz6v7AeBnG~7iDfH*NDzX4 ze5Z-6goh#086xzo*g$^%(?rPepX+)0w9w#!5KQJj?Rf+I=L&DG43W+dF=)pD z>Ij_LyDV_7Tz8!y>KP&w9k{_SXVhOv>faiijcW)&LqS05WM+6N0?>oGf&d-W+ay+0 zYS-T1Kn?u55SEk9G$shJqKfrf5YYH*?e*x%&5ru;%im*! zj6(>1M)FxszF>2VU}@<=&du+UCp}CX`FV1%AWzm9uuB-A1$Bf0lD}qJ)gdzwf*!(v z7|KkE#|1B4bQnw!`=4f19-f%t{cB;N39`q z@ldY7U;vB;_L!0r74pm>lpkvxwv+r&i=O63UKF55XOD*=){}(6Xwh?CdzqgwKLQYf zPelP8R7Uqs6n7I&6yl(s7(npX%cKHG0U_uncB*I+Ln=H{VTjdqF%VZAAox2P3x*-Z z86sFR*GSq6?Aa8?3YO&5uCc?nczN9DzX_5{IriPLJ1CTX_uA>Gs<{HGa> z+!HhZspdB)$O42QizFb%deW`X>PtOZkzq|DPga;A^7G_iL7p#*V2ImgOgyfh9`64Z zCw4q!@eC1erF^Kthw5h*Ve6Sm7%y1{>5xc+JXdL0-qb1q8+Igq(yKL}A-dhzXI3z#Sau%^hKW{Fr9nI*^i z6D>2SCws07c@LZlQ3&3V1yC=UU3mG-e@!5uI5cOT zy;-d*FrIXKMR|Y;^=un?WRwZ5V8{i42K*rpkhA?;1#qvQs6GM^f`jsaGD=OLfK<1% z{Z$i##tP>W#qeZPFA6!;ES6x4ENChECq;#s%Q9`O*Cs3-SX;Ls=>6|E`2NFRD z)+hmrs4>3>*XvFvhQZ1OU@&gjrIYxO2Mh9iG5!^6Ru)T;Ok&bMaV%kOD%G zUiGx^TvJ6V=GIRXd|;ce<$1OIDevdD0w z3U;=%_r^d=z4vVb7$CeXiFc~`=X%$8v^X`6%<~5F7LDi)H7ZXg2qIE@c;Cp;K@Ux04x~nGxAH|T(FBLfgleSOg#2$S9`B0Wg;u`A(UveXa*x`Ts-u^ zq)<&D1X;8JNz~FVIm5)q0>EI#$n(FKb|Pv3jH3nEz*OyXT~W{mvU65ru!tkwZ|P)V9bk}(MB02rvE3()=R3@=~`Ng)KKbOBM6)KwQLeMpa#jv=r9ReI(s YgkZleAVj1C(}4-`!(e?3$c(}M4=U~ybN~PV