diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 04298d5..3aead7d 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -113,6 +113,17 @@ Public Class FormMain '3:BUG+ IMP* FEAT- '2:BUG* IMP- '1:BUG- + If LastVersion < 374 Then 'Snapshot 2.11.1 + If LastVersion >= 373 Then + FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:使用离线登录也可以直接加入联机房间了")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:会从所有共享节点中自动选择负载最低的进行中继连接")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若复制了邀请码,则可以直接快速加入房间")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:关闭 PCL 时总是会提示是否退出联机,防止在关闭 PCL 时无意地关闭或退出了房间")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许自定义要连接的节点")) + End If + FeatureCount += 9 + BugCount += 7 + End If If LastVersion < 373 Then 'Snapshot 2.11.0 FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!")) FeatureCount += 7 @@ -587,9 +598,7 @@ Public Class FormMain End Sub, "强行停止下载任务") End If '关闭联机? - If FrmLinkMain IsNot Nothing Then - If FrmLinkMain.TryExit(SendWarning) Then Return - End If + If FrmLinkMain?.TryExit(Not SendWarning, True) Then Return '关闭 RunInUiWait( Sub() @@ -765,6 +774,21 @@ Public Class FormMain Catch ex As Exception Log(ex, "切回窗口时出错", LogLevel.Feedback) End Try + '读取剪贴板,自动加入联机房间 + If PageLinkMain.LinkState <> PageLinkMain.LinkStates.Waiting Then Return '已启动联机 + Dim Code = ClipboardGetText() : If Code Is Nothing Then Return '剪贴板无文本 + If Setup.Get("LinkLastAutoJoinInviteCode") = Code Then Return + If PageLinkMain.ValidateCodeFormat(Code) IsNot Nothing Then Return '不是邀请码 + Setup.Set("LinkLastAutoJoinInviteCode", Code) + RunInThread( + Sub() + If MyMsgBox("嘿,是否使用复制的邀请码加入房间?", "加入联机房间", "加入", "取消") = 2 Then Return '防止弹窗阻碍主线程,所以必须放在工作线程 + RunInUi( + Sub() + PageLinkMain.Join(Code) + ClipboardSet(Nothing, False) + End Sub) + End Sub) End Sub '文件拖放 diff --git a/Plain Craft Launcher 2/Images/Heads/EasyTier.png b/Plain Craft Launcher 2/Images/Heads/EasyTier.png new file mode 100644 index 0000000..3460251 Binary files /dev/null and b/Plain Craft Launcher 2/Images/Heads/EasyTier.png differ diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index 1819a52..aa66d98 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -5,6 +5,7 @@ Imports System.Runtime.CompilerServices Imports System.Security.Cryptography Imports System.Security.Principal Imports System.Text.RegularExpressions +Imports System.Threading.Tasks Imports System.Xaml Imports Newtonsoft.Json @@ -13,13 +14,13 @@ Public Module ModBase #Region "声明" '下列版本信息由更新器自动修改 - Public Const VersionBaseName As String = "2.11.0" '不含分支前缀的显示用版本名 - Public Const VersionStandardCode As String = "2.11.0." & VersionBranchCode '标准格式的四段式版本号 + Public Const VersionBaseName As String = "2.11.1" '不含分支前缀的显示用版本名 + Public Const VersionStandardCode As String = "2.11.1." & VersionBranchCode '标准格式的四段式版本号 Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换 #If BETA Then Public Const VersionCode As Integer = 372 'Release #Else - Public Const VersionCode As Integer = 373 'Snapshot + Public Const VersionCode As Integer = 374 'Snapshot #End If '自动生成的版本信息 Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName @@ -1727,6 +1728,7 @@ RetryDir: ''' Public Function StartsWithF(Str As String, Prefix As String, Optional IgnoreCase As Boolean = False) As Boolean + If Str Is Nothing Then Return False Return Str.StartsWith(Prefix, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)) End Function ''' @@ -1734,6 +1736,7 @@ RetryDir: ''' Public Function EndsWithF(Str As String, Suffix As String, Optional IgnoreCase As Boolean = False) As Boolean + If Str Is Nothing Then Return False Return Str.EndsWith(Suffix, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)) End Function ''' @@ -1741,6 +1744,7 @@ RetryDir: ''' Public Function ContainsF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Boolean + If Str Is Nothing Then Return False Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)) >= 0 End Function ''' @@ -1777,6 +1781,7 @@ RetryDir: ''' 如果输入有误,返回 0。 ''' Public Function Val(Str As Object) As Double + If Str Is Nothing Then Return 0 Try Return If(TypeOf Str Is String AndAlso Str = "&", 0, Conversion.Val(Str)) Catch @@ -2147,6 +2152,29 @@ RetryDir: Return result End Function + ''' + ''' 为 Task 设置超时,在超时时抛出 TimeoutException。 + ''' + Public Function GetResultWithTimeout(Of T)(TargetTask As Task(Of T), TokenSource As CancellationTokenSource, TimeoutMs As Integer) As T + Dim DelayTask = Task.Delay(TimeoutMs) + If Task.WhenAny(TargetTask, DelayTask).GetAwaiter().GetResult() Is DelayTask Then + TokenSource.Cancel() + Throw New TimeoutException($"任务超时({TimeoutMs} ms)") + End If + Return TargetTask.GetAwaiter().GetResult() + End Function + ''' + ''' 为 Task 设置超时,在超时时抛出 TimeoutException。 + ''' + Public Sub GetResultWithTimeout(TargetTask As Task, TokenSource As CancellationTokenSource, TimeoutMs As Integer) + Dim DelayTask = Task.Delay(TimeoutMs) + If Task.WhenAny(TargetTask, DelayTask).GetAwaiter().GetResult() Is DelayTask Then + TokenSource.Cancel() + Throw New TimeoutException($"任务超时({TimeoutMs} ms)") + End If + TargetTask.GetAwaiter().GetResult() + End Sub + ''' ''' 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。 ''' @@ -2586,7 +2614,7 @@ Retry: RunInUi( Sub() My.Computer.Clipboard.Clear() - My.Computer.Clipboard.SetText(Text) + If Not String.IsNullOrEmpty(Text) Then My.Computer.Clipboard.SetText(Text) End Sub) Catch ex As Exception RetryCount += 1 @@ -2600,6 +2628,21 @@ Retry: If ShowSuccessHint Then Hint("已成功复制!", HintType.Green) End Sub) End Sub + ''' + ''' 获取剪贴板文本。将在 UI 线程运行,且不会抛出异常。 + ''' + Public Function ClipboardGetText() As String + Dim Result As String = Nothing + RunInUiWait( + Sub() + Try + If My.Computer.Clipboard.ContainsText() Then Result = My.Computer.Clipboard.GetText() + Catch ex As Exception + Log(ex, "获取剪贴板文本失败") + End Try + End Sub) + Return Result + End Function ''' ''' 以 Byte() 形式获取程序中的资源。 @@ -3079,6 +3122,7 @@ Retry: Public Sub Telemetry([Event] As String, ParamArray Datas As String()) If Not Setup.Get("SystemSystemTelemetry") Then Return '用户关闭了遥测 If Not ClsBaseUrl.StartsWithF("http") Then Return '开源版没有设置遥测地址 + If VersionBranchName = "Debug" Then Return '开发版本不上传遥测 RunInNewThread( Sub() Try diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb index 238c969..593fd71 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb @@ -344,12 +344,12 @@ LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted End Sub Public Overrides Sub Failed(ex As Exception) + [Error] = ex 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() @@ -464,11 +464,11 @@ End Sub) End Sub Public Overrides Sub Failed(Ex As Exception) + [Error] = Ex '先设置错误再调整状态,防止父加载器获取不到异常 SyncLock LockState If State >= LoadState.Finished Then Return State = LoadState.Failed End SyncLock - [Error] = Ex For Each Loader In Loaders Loader.Abort() Next diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index 0c17c64..fceafa3 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -262,11 +262,11 @@ RequestFinished: }) End If End SyncLock - Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetAwaiter().GetResult() + Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout) Dim ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() Dim ResponseBytes As Byte() Using Stream As New MemoryStream - ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetAwaiter().GetResult() + ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout) ResponseBytes = Stream.ToArray() End Using '输出 @@ -286,7 +286,7 @@ RequestFinished: Throw Catch ex As Exception RecordIPReliability(HostIp, -1) - If TypeOf ex Is OperationCanceledException Then 'CancellationToken 超时 + If TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException Then 'CancellationToken 超时 Throw New WebException($"连接服务器超时,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout) ElseIf ex.IsNetworkRelated Then Throw New WebException($"网络请求失败,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout) @@ -992,7 +992,7 @@ StartThread: HostIp = DNSLookup(HttpRequest, CancelToken) 'DNS 预解析 If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New RangeHeaderValue(Th.DownloadStart, Nothing) Dim ContentLength As Long = 0 - Response = ThreadClient.SendAsync(HttpRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetAwaiter().GetResult() + Response = ThreadClient.SendAsync(HttpRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout) If Not Response.IsSuccessStatusCode Then Throw New Exception($"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Th.Source.Url}") '状态码检查 If State = NetState.Error Then GoTo SourceBreak '快速中断 If ModeDebug AndAlso Response.RequestMessage.RequestUri.ToString <> Th.Source.Url Then Log($"[Download] {LocalName}:重定向至 {Response.RequestMessage.RequestUri}") @@ -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 = ReadWithTimeout(ResponseStream, ResponseBytes, 0, 16384, Timeout) + HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetResultWithTimeout(CancelToken, 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 = ReadWithTimeout(ResponseStream, ResponseBytes, 0, 16384, Timeout) + HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetResultWithTimeout(CancelToken, 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 @@ -1130,7 +1130,8 @@ SourceBreak: RecordIPReliability(HostIp, 0.5) End If Catch ex As Exception - Log($"[Download] {LocalName}:出错,{If(TypeOf ex Is OperationCanceledException, $"已超时({Timeout}ms)", ex.GetDetail())},IP:{HostIp}") + Log($"[Download] {LocalName}:出错,{If(TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException, + $"已超时({Timeout}ms)", ex.GetDetail())},IP:{HostIp}") RecordIPReliability(HostIp, -0.7) SourceFail(Th, ex, False) Finally @@ -1650,15 +1651,15 @@ Retry: 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 + Dim UsefulExs = ExList.Where(Function(e) TypeOf e IsNot OperationCanceledException AndAlso TypeOf e IsNot TimeoutException AndAlso TypeOf e IsNot ThreadInterruptedException).ToList [Error] = If(UsefulExs.FirstOrDefault, ExList.FirstOrDefault) '获取实际失败的文件 For Each File In Files If File.State <> NetState.Error Then Continue For + If File.Sources.All(Function(s) TypeOf s.Ex Is OperationCanceledException OrElse TypeOf s.Ex Is TimeoutException OrElse TypeOf s.Ex Is ThreadInterruptedException) Then Continue For Dim Detail As String = Join(File.Sources.Select(Function(s) $"{If(s.Ex Is Nothing, "无错误信息。", s.Ex.GetBrief())}({s.Url})"), vbCrLf) [Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf & - "各下载源的错误如下:" & vbCrLf & Detail, - [Error]) + "各下载源的错误如下:" & vbCrLf & Detail, [Error]) '上报 Telemetry("文件下载失败", "FileName", File.LocalName, @@ -1951,6 +1952,45 @@ Retry: Return False End Function +#End Region + +#Region "端口" + + ''' + ''' 随机获取单个可用的端口。 + ''' + Public Function FindFreePort() 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 + + ''' + ''' 获取当前已被占用的端口列表。 + ''' + Public Function GetUsedPorts() As List(Of Integer) + Dim IPProperties = NetworkInformation.IPGlobalProperties.GetIPGlobalProperties() + Dim UsedPorts As New List(Of Integer) + UsedPorts.AddRange(IPProperties.GetActiveTcpListeners().Select(Function(ep) ep.Port)) + UsedPorts.AddRange(IPProperties.GetActiveUdpListeners().Select(Function(ep) ep.Port)) + UsedPorts.AddRange(IPProperties.GetActiveTcpConnections().Select(Function(conn) conn.LocalEndPoint.Port)) + Return UsedPorts.Distinct().ToList() + End Function + + ''' + ''' 寻找数个连续编号的可用端口。 + ''' + Public Function FindFreePorts(ConsecutiveCount As Integer, ParamArray ExtraBlackLists As Integer()) As List(Of Integer) + Dim UsedPorts = GetUsedPorts().Concat(ExtraBlackLists) + For port = 12000 To 65000 - ConsecutiveCount + Dim Range = Enumerable.Range(port, ConsecutiveCount) + If Not Range.Any(Function(p) UsedPorts.Contains(p)) Then Return Range.ToList + Next + Throw New Exception("未能找到可用的端口!") + End Function + #End Region ''' @@ -1985,27 +2025,4 @@ 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/Base/ModValidate.vb b/Plain Craft Launcher 2/Modules/Base/ModValidate.vb index d973481..d04b4f2 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModValidate.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModValidate.vb @@ -114,12 +114,7 @@ Public Class ValidateInteger End Sub Public Overrides Function Validate(Str As String) As String Dim Valed As Integer - If Not Integer.TryParse(Str, Valed) Then - Return "请输入一个整数!" - End If - If Not RegexCheck(Str, "^(-?0|-?[1-9]\d*)$") Then - Return "请输入一个不带有前导零的整数!" - End If + If Not Integer.TryParse(Str, Valed) Then Return "请输入一个整数!" If Str.Length > 9 Then Return "请输入一个大小合理的数字!" If Valed > Max Then Return "不可超过 " & Max & "!" If Valed < Min Then Return "不可低于 " & Min & "!" diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb index c2f766b..f444b27 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb @@ -73,8 +73,10 @@ Sub(Task As LoaderTask(Of String, List(Of NetFile))) Try Dim IndexFile = DlClientAssetIndexGet(Version) - Dim IndexFileInfo As New FileInfo(IndexFile.LocalPath) - If AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then + If IndexFile Is Nothing Then + Task.Output = New List(Of NetFile) + Log("[Download] 未找到版本 " & Version.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug) + ElseIf AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then Task.Output = New List(Of NetFile) Else Task.Output = New List(Of NetFile) From {IndexFile} @@ -93,6 +95,11 @@ LoadersAssetsUpdate.Add(New LoaderTask(Of String, List(Of NetFile))("后台分析资源文件索引地址", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Version) + If BackAssetsFile Is Nothing Then + Log("[Download] 未找到版本 " & Version.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug) + Task.Abort() + Throw New ThreadInterruptedException + End If RealAddress = BackAssetsFile.LocalPath TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName BackAssetsFile.LocalPath = TempAddress diff --git a/Plain Craft Launcher 2/Modules/ModEvent.vb b/Plain Craft Launcher 2/Modules/ModEvent.vb index 4dffc4f..06204f9 100644 --- a/Plain Craft Launcher 2/Modules/ModEvent.vb +++ b/Plain Craft Launcher 2/Modules/ModEvent.vb @@ -141,6 +141,7 @@ Public Class CustomEvent 写入设置 修改变量 写入变量 + 加入房间 End Enum ''' @@ -283,6 +284,17 @@ Public Class CustomEvent WriteReg("CustomEvent" & Args(0), Args(1)) If Args.Length = 2 Then Hint($"已写入变量:{Args(0)} → {Args(1)}", HintType.Green) + Case EventType.加入房间 + RunInUi( + Sub() + If String.IsNullOrWhiteSpace(Arg) Then + PageLinkMain.Join() + Else + If Not EventSafetyConfirm("即将加入第三方联机房间。") Then Return + PageLinkMain.Join(Arg) + End If + End Sub) + Case Else MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败") End Select diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index a3e04d0..81b031d 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/Comp/PageComp.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml index 4de9f55..719bbc6 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml @@ -34,7 +34,7 @@ - + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb index 62d5c69..7d2dae1 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb @@ -99,7 +99,8 @@ Public Module ModDownloadLib Sub(Task As LoaderTask(Of String, List(Of NetFile))) Try Dim Version As New McVersion(VersionFolder) - Task.Output = New List(Of NetFile) From {DlClientAssetIndexGet(Version)} + Dim AssetIndex = DlClientAssetIndexGet(Version) + Task.Output = If(AssetIndex Is Nothing, New List(Of NetFile), New List(Of NetFile) From {AssetIndex}) Catch ex As Exception Throw New Exception("分析资源文件索引地址失败", ex) End Try diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb index 374ce8d..84efb45 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb @@ -104,6 +104,9 @@ Public Class PageLaunchRight Case 12 Log("[Page] 主页预设:PCL GitHub 仪表盘") Url = "https://ddf.pcl-community.org/Custom.xaml" + Case 13 + Log("[Page] 主页预设:PCL 更新摘要") + Url = "https://raw.gitcode.com/ENC_Euphony/PCL-AI-Summary-HomePage/raw/master/Custom.xaml" End Select End Select '联网下载 diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb index 7199516..874d53a 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb @@ -121,7 +121,7 @@ Retry: Next Throw New Exception("未知错误(" & Result & ")") Catch ex As Exception - If TypeOf ex Is OperationCanceledException Then + If TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException Then Hint("更改皮肤失败:连接皮肤服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red) Else Log(ex, "更改皮肤失败", LogLevel.Hint) diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml index 9e0c21f..1a978f5 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml @@ -152,7 +152,7 @@ - + @@ -186,12 +186,12 @@ Data="m365,295.3c-15.1-3.6-36.5-1.8-39.6,17.5-1.2,12.4,0,107-.5,119.7.8,16.8-5.2,42.4,16.8,47.1,14.3,3.3,34.5,1.9,38.7-15.4,2.6-1.4.1-142.4,1-148.3-.7-10.9-6.2-17.8-16.4-20.6ZM234.1,311.6c-3.2-16.7-20-18.6-34.4-17.2-9.8.4-17.2,6.2-20,15.4-4.4,47.8-.2,97.3-1.6,144.8,0,16,9.8,26.6,24.6,26.1,14.4,1.4,29.1-2.9,31.7-19,2.2-8.8.8-63.9,1.1-74.1-1-25.3,2.1-50.6-1.5-75.9ZM521.4,300.6c-5.4-6.2-15.9-7.1-24.6-6.4-11.9-1-23,6.6-24.7,18.7-2.2,24.1-2,124-.2,148.1,2.6,19.7,24.1,22.4,40.1,18.4,11.9-2.4,16.6-15.2,16.3-27.1,0-33.6.1-69.3,0-103.1-1.2-15.1,4.2-37-6.9-48.5ZM708.8,194.2c.7-34.5-26.8-63.7-61.5-63.4-22.1-.1-52.4,0-75.6,0-13.4,1.1-8.7-12.1-9.6-20.9-1.6-30.6,7.5-67.2-17.5-90.9-11.7-11.9-27.4-18.4-44.3-18.4-64.8,0-129.7,0-194.5,0-37.7,1.4-76.5-3.1-113.8,2.2-48.1,14.7-46.7,55.3-45.4,97.3,0,8,.1,16.1,0,24.1.2,7-7.3,6.8-12.2,6.6-16,0-32,0-48,0-24.4-.7-51.3-1-68.6,18.4C5.8,161.2,0,176.1,0,194.7c0,0,0,350.9,0,350.9,0,22.9,5.5,41.7,16.8,57.5,19.2,27,45.9,40.7,79.5,40.7,37.4,0,77.4.1,129.9.1,42.5,0,85,0,127.5,0,11.2-1.3,273.6,2.4,278-1.8,46.1-9.1,77.1-46.8,77.1-93.8,0-117.8,0-237.9,0-354.1Zm-71-5.3c7.6.2,9.2,1.9,9.3,9.7,0,0,0,347.3,0,347.3,0,3.2.2,6.5-.3,9.9-2.8,19.3-18.9,32.3-38.5,31.9-4.5.4-508.9-.4-513.5,0-3.2,0-6.5.1-9.9-.4-19-2.9-31.8-17.8-31.8-37.1,0-117.9,0-233.9,0-350.9,0-9,2.3-10.3,10.9-10.3,0,0,131.2,0,131.2,0,1.6,0,3,0,4.5,0,0-1.5,0-2.7,0-4.3,0,0,0-115.7,0-115.7.1-8.3,1.9-10.1,10.3-10.1h280.3c8.3,0,10.1,1.8,10.1,10.3v114.1c0,1.9,0,3.7,0,5.6" /> - + diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb index b0ae907..a4689df 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb @@ -1,4 +1,6 @@ -Public Class PageLinkMain +Imports System.Net.Sockets + +Public Class PageLinkMain '=============================== ' 状态机与前端页面 @@ -6,33 +8,33 @@ #Region "状态管理" - Private Enum States + Public Enum LinkStates Waiting Loading Failed Finished End Enum - Private State As States = States.Waiting + Public Shared LinkState As LinkStates = LinkStates.Waiting ''' ''' 切换到指定的状态。 ''' - Private Sub ChangeState(NewState As States) - If State = NewState Then Return - Dim OldState = State - State = NewState + Private Sub ChangeState(NewState As LinkStates) + If LinkState = NewState Then Return + Dim OldState = LinkState + LinkState = NewState Log($"[Link] 主状态由 {GetStringFromEnum(OldState)} 变更为 {GetStringFromEnum(NewState)}") '触发状态切换 RunInUi( Sub() Select Case NewState - Case States.Waiting + Case LinkStates.Waiting SwitchToWaiting(OldState) - Case States.Loading + Case LinkStates.Loading SwitchToLoading(OldState) - Case States.Failed + Case LinkStates.Failed SwitchToFailed(OldState) - Case States.Finished + Case LinkStates.Finished SwitchToFinished(OldState) End Select End Sub) @@ -45,18 +47,18 @@ Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) Select Case NewState Case LoadState.Finished - ChangeState(States.Finished) + ChangeState(LinkStates.Finished) Case LoadState.Failed Telemetry("联机失败", "Exception", FilterUserName(Loader.Error.GetDetail, "*")) - ChangeState(States.Failed) + ChangeState(LinkStates.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) + FrmLinkMain.PanSelect.Visibility = If(LinkState = LinkStates.Waiting, Visibility.Visible, Visibility.Collapsed) + FrmLinkMain.PanFinish.Visibility = If(LinkState = LinkStates.Finished, Visibility.Visible, Visibility.Collapsed) End Sub #End Region @@ -64,7 +66,7 @@ #Region "初始页面 (Waiting)" '由任意状态切换到 Waiting - Private Sub SwitchToWaiting(OldState As States) + Private Sub SwitchToWaiting(OldState As LinkStates) '加载器与进程状态 LinkLoader.Abort() SyncLock LinkLoader.LockState @@ -72,7 +74,7 @@ End SyncLock ProcessStop() '页面切换 - If OldState <> States.Finished Then + If OldState <> LinkStates.Finished Then '从完成页面退出时,PageOnContentExit 结束会触发 PageEnter 事件,这会调用 UpdatePanelVisibility '如果在现在提前调用了,会丢失完成页面的退出动画 UpdatePanelVisibility() @@ -87,7 +89,7 @@ '创建 Private Sub Create_MouseLeftButtonUp() Handles PanSelectCreate.MouseLeftButtonUp '输入端口号 - Dim Port As String = MyMsgBoxInput("输入端口", $"在单人游戏的暂停菜单选择 {vbLQ}对局域网开放{vbRQ},然后输入端口数字。{vbCrLf}其实也可以输入其他游戏的端口……", + 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 @@ -106,50 +108,31 @@ NetworkSecret = GenerateRandomCode() Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}") '启动 - ChangeState(States.Loading) + ChangeState(LinkStates.Loading) End Sub '加入 - Private LastCode As String = Nothing - Private Sub Join_MouseLeftButtonUp() Handles PanSelectJoin.MouseLeftButtonUp - '输入邀请码 + Private Shared LastCode As String = Nothing + ''' + ''' 输入邀请码,切换到联机页并立即加入房间。 + ''' + Public Shared Sub Join() Handles PanSelectJoin.MouseLeftButtonUp Dim Code As String = MyMsgBoxInput("输入邀请码", "输入房主发给你的邀请码。", - HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码:" & LastCode)) + 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 + Join(Code) + End Sub + Public Sub JoinInternal(Code As String) 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 + Dim ValidateResult = ValidateCodeFormat(Code) + If ValidateResult IsNot Nothing Then + Hint(ValidateResult, HintType.Red) 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) @@ -158,7 +141,45 @@ Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}") '启动 LastCode = Code - ChangeState(States.Loading) + ChangeState(LinkStates.Loading) + End Sub + Public Shared Function ValidateCodeFormat(Code As String) As String + 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 + Return "请让房主使用 PCL 创建房间!" + ElseIf Code.Length = 10 Then 'PCL CE + Return "请让房主使用非社区版的 PCL 创建房间!" + Else + Return "邀请码有误,请让房主使用 PCL 创建房间!" + End If + End If + Return Nothing + End Function + + '自动加入 + + ''' + ''' 切换到联机页并立即加入指定房间。 + ''' + Public Shared Sub Join(Code As String) + If LinkState <> LinkStates.Waiting Then + Hint("你已经在联机房间中了!", HintType.Red) + ElseIf FrmMain.PageCurrent = FormMain.PageType.Link Then + FrmLinkMain.JoinInternal(Code) + Else + AutoJoinCode = Code + FrmMain.PageChange(FormMain.PageType.Link, FormMain.PageSubType.LinkMain) + End If + End Sub + '在进入页面时自动尝试加入房间 + Private Shared AutoJoinCode As String = Nothing + Private Sub PanSelectJoin_Loaded() Handles PanSelectJoin.Loaded + If AutoJoinCode Is Nothing Then Return + If LinkState = LinkStates.Waiting Then JoinInternal(AutoJoinCode) + AutoJoinCode = Nothing End Sub #End Region @@ -166,7 +187,7 @@ #Region "加载页面 (Loading | Failed)" '由 Waiting 或 Failed 状态切换到 Loading - Private Sub SwitchToLoading(OldState As States) + Private Sub SwitchToLoading(OldState As LinkStates) '清理变量 Peers = Nothing FailCount = 0 @@ -180,7 +201,7 @@ End Sub '由 Loading 状态切换到 Failed - Private Sub SwitchToFailed(OldState As States) + Private Sub SwitchToFailed(OldState As LinkStates) '加载器与进程状态 LinkLoader.Abort() ProcessStop() @@ -217,12 +238,12 @@ '取消加载 Private Sub BtnLoadCancel_Click() Handles BtnLoadCancel.Click - ChangeState(States.Waiting) + ChangeState(LinkStates.Waiting) End Sub '点击重试 Private Sub CardLoad_MouseLeftButtonUp() Handles CardLoad.MouseLeftButtonUp - ChangeState(States.Loading) + ChangeState(LinkStates.Loading) End Sub '进度条 @@ -252,7 +273,7 @@ #Region "完成页面 (Finished)" '由 Loading 状态切换到 Finished - Private Sub SwitchToFinished(OldState As States) + Private Sub SwitchToFinished(OldState As LinkStates) '由于只可能从加载器的完成事件触发,不需要管加载器 UpdatePanelVisibility() '页面的实际切换会由 Loader 调用 MyPageRight 来触发 @@ -260,26 +281,27 @@ If IsServerSide Then LabFinishTitle.Text = "已创建房间" LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{NetworkName}-{NetworkSecret}" - BtnFinishCopy.Text = "复制邀请码" BtnFinishExit.Text = "关闭" + BtnFinishCopy.Visibility = Visibility.Visible + Copy() '立即复制邀请码 '下边栏 - BtnFinishPing.ToolTip = "网络延迟" - LineFinishPort.Visibility = Visibility.Visible BtnFinishPort.Visibility = Visibility.Visible LabFinishPort.Visibility = Visibility.Visible + BtnFinishIp.Visibility = Visibility.Collapsed + LabFinishIp.Visibility = Visibility.Collapsed LabFinishPort.Text = ServerPort Else LabFinishTitle.Text = "已加入房间" - LabFinishDesc.Text = $"在多人游戏中选择直接连接,粘贴地址即可加入游戏!{vbCrLf}服务器地址:{ClientAddress}" '"如果提示无效会话,不要退出联机房间,重新启动游戏即可!" - BtnFinishCopy.Text = "复制地址" + LabFinishDesc.Text = $"在多人游戏页面的最下方就能找到联机房间!{vbCrLf}注意:使用离线登录时不要手动输入 IP!" BtnFinishExit.Text = "离开" + BtnFinishCopy.Visibility = Visibility.Collapsed '下边栏 - BtnFinishPing.ToolTip = "与房主的延迟" - LineFinishPort.Visibility = Visibility.Collapsed BtnFinishPort.Visibility = Visibility.Collapsed LabFinishPort.Visibility = Visibility.Collapsed + LabFinishIp.Text = ClientAddress + BtnFinishIp.Visibility = Visibility.Visible + LabFinishIp.Visibility = Visibility.Visible End If - Copy() '复制信息 Update() '立即刷新 End Sub @@ -289,30 +311,37 @@ '退出 Private Sub BtnFinishExit_Click() Handles BtnFinishExit.Click - TryExit(True) + TryExit(False, False) 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 + Public Function TryExit(Slient As Boolean, Closing As Boolean) As Boolean + If LinkState = LinkStates.Waiting OrElse LinkState = LinkStates.Failed Then Return False + If Not Slient Then + If IsServerSide AndAlso PeopleCount > 1 Then + If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "退出联机", "确定", "取消", IsWarn:=True) = 2 Then Return True + ElseIf Closing Then + If MyMsgBox(If(IsServerSide, "你确定要关闭联机房间吗?", "你确定要离开联机房间吗?"), "退出联机", "确定", "取消") = 2 Then Return True + End If End If - ChangeState(States.Waiting) + ChangeState(LinkStates.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 + Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{NetworkName}-{NetworkSecret}】,即可加入联机房间!" + ClipboardSet(CodeText, False) + Setup.Set("LinkLastAutoJoinInviteCode", CodeText) + Hint("已复制邀请码!", HintType.Green) + End Sub + + '复制 IP + Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp + ClipboardSet(ClientAddress, False) + Hint("已复制服务器地址!", HintType.Green) End Sub #End Region @@ -334,6 +363,14 @@ ''' Private ServerPort As Integer ''' + ''' 客户端映射后的端口号。 + ''' + Private ClientPort As Integer + ''' + ''' RPC 端口号。 + ''' + Private RPCPort As Integer + ''' ''' 在客户端映射后的地址。 ''' 除非已作为客户端启动 EasyTier,否则一直为 Nothing。 ''' @@ -414,7 +451,7 @@ '解压 Dim ExtractPath As String = RequestTaskTempFolder() ExtractFile(PathEasyTier & "EasyTier.zip", ExtractPath, - ProgressIncrementHandler:=Sub(Progress) Task.Progress += Progress * 0.06) + ProgressIncrementHandler:=Sub(Progress) Task.Progress += Progress * 0.05) Dim ExtractedPath As String = New DirectoryInfo(ExtractPath).EnumerateDirectories.FirstOrDefault?.FullName Log("[Link] 联机模块解压时的临时路径:" & ExtractedPath) CopyDirectory(ExtractedPath, PathEasyTier) @@ -427,28 +464,42 @@ File.Delete(PathEasyTier & "EasyTier.zip") Setup.Set("LinkEasyTierVersion", ServerVersion) End If - Task.Progress = 0.1 - '获取启动参数 + Task.Progress = 0.07 + '获取节点列表 + Dim Peers As List(Of String) + Dim CustomPeers As String = Setup.Get("LinkCustomPeer") + If String.IsNullOrWhiteSpace(CustomPeers) Then + Peers = GetOnlinePeers() + Else + Peers = CustomPeers.Split(",,".ToCharArray).Select(Function(p) p.Trim).Where(Function(p) Not String.IsNullOrEmpty(p)).ToList() + Log("[Link] 使用自定义节点") + End If + Task.Progress = 0.13 + '获取空闲端口 UpdateLoadingPage("正在启动联机模块……", "启动联机模块") + Dim FreePorts = FindFreePorts(5, ServerPort) + ClientPort = FreePorts(0) + RPCPort = FreePorts(1) + Dim ListenersPort As Integer = FreePorts(2) '它会占用【当前、当前 +1、当前 +2】共三个端口 + '获取启动参数 Dim Arguments As String = ServerConfig("Link")("Argument") - Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret}" + Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort}" 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}" + ClientAddress = $"localhost:{ClientPort}" + Arguments += $" --port-forward tcp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}" + Arguments += $" --port-forward udp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}" + Arguments += $" --port-forward tcp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}" + Arguments += $" --port-forward udp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}" End If - For Each Peer As String In ServerConfig("Link")("Peers") + For Each Peer As String In Peers Arguments += $" -p=""{Peer}""" Next - If Not Setup.Get("LinkShareMode") Then Arguments += " --private-mode true" + Arguments += " --private-mode true" '老好人模式现在莫得用:If Not Setup.Get("LinkShareMode") Then '启动进程 ProcessStart(Arguments) Task.Progress = 0.15 @@ -482,9 +533,9 @@ '超时判定 If LastProgress <> Task.Progress Then TimeoutCounter = 0 - ElseIf TimeoutCounter < 20 Then + ElseIf TimeoutCounter < 30 * 2 Then TimeoutCounter += 1 - Else '进度停滞超过 10s + Else '进度停滞超过 30s Select Case PeerCount Case -1 'CLI 无返回 Panic("无法启动联机模块", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf)) @@ -497,6 +548,49 @@ Loop Until Task.IsAborted If Task.IsAborted Then Throw New ThreadInterruptedException End Sub + ''' + ''' 从在线配置和 API 获取节点列表。 + ''' + Private Function GetOnlinePeers() As List(Of String) + Dim Peers As List(Of String) = ServerConfig("Link")("Peers").Select(Function(p) p.ToString).ToList() + Try + '从 API 获取节点列表 + Dim BlackList As List(Of String) = ServerConfig("Link")("PeersBlackList").Select(Function(p) p.ToString).ToList() '黑名单 + Dim CentralNodes As New List(Of String) + Dim RandomNodes As New List(Of Tuple(Of String, Double)) + For Each Node As JObject In CType(GetJson(NetRequestByClientRetry("https://uptime.easytier.cn/api/nodes?page=1&per_page=200")), JObject)("data")("items") + '状态检查 + If Node("protocol").ToString <> "tcp" Then Continue For + If Node("current_health_status").ToString <> "healthy" Then Continue For + If Not Node("is_active").ToObject(Of Boolean) Then Continue For + If Not Node("is_approved").ToObject(Of Boolean) Then Continue For + Dim Tags = Node("tags").Select(Function(t) t.ToString).ToList + If Not Tags.Contains("国内") Then Continue For + If Tags.Contains("即将下线") Then Continue For + Dim Address As String = Node("address").ToString + If BlackList.Contains(Address) Then Continue For + '添加节点 + If Tags.Contains("MC") Then + CentralNodes.Add(Address) + Else + If Not Node("allow_relay").ToObject(Of Boolean) Then Continue For + If Node("usage_percentage").ToObject(Of Double) = 0 Then Continue For '或许节点有问题才导致是 0 负载 + RandomNodes.Add(New Tuple(Of String, Double)( + Address, + Node("usage_percentage").ToObject(Of Double) * (103 - Node("health_percentage_24h").ToObject(Of Double)))) '负载,越低越好 + End If + Next + RandomNodes = RandomNodes.OrderBy(Function(n) n.Item2).ToList() + Log($"[Link] 获取到 {CentralNodes.Count} 个中心节点,{RandomNodes.Count} 个随机节点") + '选择节点 + Dim RandomCount As Integer = ServerConfig("Link")("RandomPeer").ToObject(Of Integer) + If RandomNodes.Count < RandomCount Then Throw New Exception($"可用的随机节点数量不足,需要 {RandomCount} 个,实际 {RandomNodes.Count} 个") + Peers = CentralNodes.Concat(RandomNodes.Take(RandomCount).Select(Function(n) n.Item1)).ToList() + Catch ex As Exception + Log(ex, "获取节点列表失败,联机质量可能受到影响", LogLevel.Hint) + End Try + Return Peers + End Function #End Region @@ -544,26 +638,14 @@ ''' 出现意外错误,给出错误信息并结束联机。 ''' Private Sub Panic(Brief As String, Detail As String) - If State = States.Loading Then + If LinkState = LinkStates.Loading Then FailReason = Brief LinkLoader.Failed(New Exception(Detail)) Else MyMsgBox(Detail, "联机出错:" & Brief, IsWarn:=True) - ChangeState(States.Waiting) + ChangeState(LinkStates.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 ''' @@ -656,32 +738,33 @@ ''' 名称。 ''' Public ReadOnly Name As String + ''' + ''' 连接方式。 + ''' + Public ReadOnly Cost As String ''' - ''' 从 CLI 给出的信息行分析对应的数据。 + ''' 从 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)) + Public Sub New(Info As JObject) '类别 - Dim PeerName = Parts(1) + Dim PeerName = Info("hostname").ToString 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 作为房主,避免网络被多个房主搞炸的情况 + ElseIf Info("ipv4") = "10.114.114.114" Then '只允许这个 IP 作为房主,避免网络被多个房主搞炸的情况 Type = Types.Server Else Type = Types.Misc End If - '主机名 + '基础信息 + Ping = If(Info("lat_ms").ToString = "-", 0, Info("lat_ms").ToString) Name = PeerName + Cost = Info("cost") End Sub Public Overrides Function ToString() As String - Return $"{Type} - {Name} - Ping {Ping:0.0}ms" + Return $"{Type} - {Name} - Ping {Ping:0.0}ms [{Cost}]" End Function End Class @@ -705,12 +788,12 @@ '| 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) + Dim CliResult = StartProcessAndGetOutput(PathEasyTier & "联机模块 CLI.exe", $"-o json -p 127.0.0.1:{RPCPort} 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) + If Not CliResult.Contains("lat_ms") Then Throw New Exception("CLI 调用失败:" & vbCrLf & CliResult) + If GetUuid() Mod If(ModeDebug, 23, 103) = 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) + For Each Line As JObject In CType(GetJson(CliResult), JArray).Skip(1) Try Dim Peer = New Peer(Line) If Peer.Type = Peer.Types.Self Then Continue For '自己 @@ -724,7 +807,7 @@ FailCount = 0 Catch ex As Exception Log(ex, "获取节点信息失败") - If State = States.Finished Then + If LinkState = LinkStates.Finished Then FailCount += 1 If FailCount >= 4 Then Panic("获取节点信息失败", ex.Message) End If @@ -741,18 +824,26 @@ ''' 0 代表尚不可用。 ''' Private Function GetPeerPing() As Double - If Peers Is Nothing Then Return 0 - Dim PingTargets As IEnumerable(Of Peer) + Dim Peer = GetTargetPeer() + If Peer Is Nothing Then Return 0 + Return Peer.Ping + End Function + ''' + ''' 服务端会返回所有节点中 Ping 的那一个,客户端会返回服务端。 + ''' 若没有则为 Nothing。 + ''' + Private Function GetTargetPeer() As Peer + If Peers Is Nothing Then Return Nothing + Dim Targets As IEnumerable(Of Peer) If IsServerSide Then - PingTargets = Peers + Targets = Peers.Where(Function(p) p.Ping > 0) 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()) + Targets = Peers.Where(Function(p) p.Type = Peer.Types.Server AndAlso p.Ping > 0) End If + If Not Targets.Any Then Return Nothing + '返回 Ping 大于 0 且最低的那个 + Dim MinPing = Targets.Min(Function(p) p.Ping) + Return Targets.First(Function(p) p.Ping = MinPing) End Function ''' @@ -768,7 +859,7 @@ #End Region -#Region "定时状态更新" +#Region "定时任务" '启动 Private IsTimerStarted As Boolean = False @@ -781,13 +872,13 @@ Try Thread.Sleep(200) Counter += 200 - '每 4 秒执行一次 - If Counter >= 4000 Then + '每秒执行一次 + If Counter >= 1000 Then Counter = 0 Update() End If '每 200ms 更新进度条 - If State = States.Loading Then RunInUi(AddressOf UpdateProgressBar) + If LinkState = LinkStates.Loading Then RunInUi(AddressOf UpdateProgressBar) Catch ex As Exception Log(ex, "联机模块主时钟出错", LogLevel.Feedback) Thread.Sleep(10000) @@ -796,9 +887,10 @@ End Sub, "Link Timer") End Sub - '每 4 秒或进入页面时更新一次信息 + '每秒或进入页面时触发 + Private BroadcastSocket As New Socket(SocketType.Dgram, ProtocolType.Udp) Private Sub Update() Handles Me.Loaded - If State <> States.Finished Then Return + If LinkState <> LinkStates.Finished Then Return '重新获取信息 SyncLock RefreshPeerLoader.LockState If RefreshPeerLoader.State <> LoadState.Loading Then RefreshPeerLoader.Start(IsForceRestart:=True) @@ -808,13 +900,25 @@ RunInUi( Sub() Dim Ping As Double = GetPeerPing() + Dim RelayLayer As Integer = If(GetTargetPeer().Cost.RegexSeek("(?<=relay\()\d+") Is Nothing, + 0, Val(GetTargetPeer().Cost.RegexSeek("(?<=relay\()\d+")) - 1) + '更新 Ping 显示 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") + LabFinishPing.Text = + If(RelayLayer > 0, If(RelayLayer > 1, $"中继 {RelayLayer} · ", "中继 · "), "") & '使用中继连接时显示 “中继” 前缀 + If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms") AniStart(AaRotateTransform(ImgFinishLogo, 500, 5000), "Link Logo Rotation") 'Logo 旋转动画 End If + '更新 Ping 的 Tooltip 显示 + Dim Tooltip As String = If(IsServerSide, "网络延迟", "与房主的延迟") + If RelayLayer Then Tooltip &= If(IsServerSide, + $"(你的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)", + $"(你或者房主的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)") + BtnFinishPing.ToolTip = Tooltip + '更新人数显示 LabFinishPlayer.Text = PeopleCount & "  人" End Sub) End If @@ -827,9 +931,20 @@ End If If Not IsServerSide AndAlso Not Peers.Any(Function(p) p.Type = Peer.Types.Server) Then MyMsgBox("房主已离开房间!", "联机结束") - ChangeState(States.Waiting) + ChangeState(LinkStates.Waiting) Return End If + '广播联机房间端口 + If Not IsServerSide Then + Try + BroadcastSocket.SendTo( + Encoding.UTF8.GetBytes($"[MOTD]PCL 联机房间[/MOTD][AD]{ClientPort}[/AD]"), + SocketFlags.None, + New IPEndPoint(IPAddress.Loopback, 4445)) + Catch ex As Exception + Log(ex, "广播联机房间端口失败") + End Try + End If End Sub ''' ''' 检查联机模块是否崩溃。 diff --git a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml index 10f5535..8b75d08 100644 --- a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml +++ b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml @@ -46,6 +46,7 @@ + @@ -58,11 +59,13 @@ - - - - - + + + + + + + @@ -78,27 +81,32 @@ - + - + - + - + - + + + + + + @@ -107,217 +115,566 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb b/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb index 724fcb8..659d3ac 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb @@ -111,8 +111,9 @@ {"LaunchAdvanceGraphicCard", New SetupEntry(True, Source:=SetupSource.Registry)}, {"LaunchRamType", New SetupEntry(0)}, {"LaunchRamCustom", New SetupEntry(15)}, - {"LinkOfflineHint", New SetupEntry(False, Source:=SetupSource.Registry)}, + {"LinkLastAutoJoinInviteCode", New SetupEntry("", Source:=SetupSource.Registry)}, {"LinkShareMode", New SetupEntry(True, Source:=SetupSource.Registry)}, + {"LinkCustomPeer", New SetupEntry("")}, {"LinkEasyTierVersion", New SetupEntry(-1, Source:=SetupSource.Registry)}, {"ToolHelpChinese", New SetupEntry(True, Source:=SetupSource.Registry)}, {"ToolDownloadThread", New SetupEntry(63, Source:=SetupSource.Registry)}, diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml index 8adb4fa..9345f80 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml @@ -86,14 +86,14 @@ - + - + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml index daf7f96..f6acdf9 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml @@ -6,11 +6,32 @@ 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 0dc33c1..2db61b6 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb @@ -1,4 +1,4 @@ -Class PageSetupLink +Public Class PageSetupLink Private Shadows IsLoaded As Boolean = False @@ -18,12 +18,14 @@ End Sub Public Sub Reload() CheckShareMode.Checked = Setup.Get("LinkShareMode") + TextCustomPeer.Text = Setup.Get("LinkCustomPeer") End Sub '初始化 Public Sub Reset() Try Setup.Reset("LinkShareMode") + Setup.Reset("LinkCustomPeer") Log("[Setup] 已初始化联机页设置") Hint("已初始化联机页设置!", HintType.Green, False) @@ -35,7 +37,7 @@ 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 TextCustomPeer.ValidatedTextChanged If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Text) End Sub Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckShareMode.Change diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml index a32d1a6..7767763 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml @@ -253,6 +253,7 @@ + diff --git a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb index 785b928..d7df960 100644 --- a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb @@ -1,6 +1,4 @@ -Imports System.Threading.Tasks - -Public Class PageSpeedLeft +Public Class PageSpeedLeft Private Const WatcherInterval As Integer = 300 '初始化 diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj index ffeaa15..d77c95b 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj @@ -933,6 +933,8 @@ - + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Resources/Help.zip b/Plain Craft Launcher 2/Resources/Help.zip index d1f20d1..0846a34 100644 Binary files a/Plain Craft Launcher 2/Resources/Help.zip and b/Plain Craft Launcher 2/Resources/Help.zip differ