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