From ad757f54b7baed798996bbd017617ce61d25f0fd 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: Thu, 6 Nov 2025 23:10:47 +0800 Subject: [PATCH] 2.11.2 --- Plain Craft Launcher 2/FormMain.xaml.vb | 11 +- .../Modules/Base/ModBase.vb | 18 +- .../Modules/Base/ModLoader.vb | 7 +- Plain Craft Launcher 2/Modules/Base/ModNet.vb | 2 +- .../Modules/Minecraft/ModLaunch.vb | 4 +- Plain Craft Launcher 2/Modules/ModMusic.vb | 2 +- .../My Project/AssemblyInfo.vb | 4 +- .../Pages/PageLink/PageLinkMain.xaml | 9 +- .../Pages/PageLink/PageLinkMain.xaml.vb | 349 +++++++++++++----- .../Pages/PageSetup/ModSetup.vb | 1 + .../Pages/PageSetup/PageSetupLink.xaml | 25 +- .../Pages/PageSetup/PageSetupLink.xaml.vb | 5 + Plain Craft Launcher 2/Resources/Help.zip | Bin 86816 -> 86888 bytes 13 files changed, 322 insertions(+), 115 deletions(-) diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 3aead7d..cb0da8d 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -113,6 +113,14 @@ Public Class FormMain '3:BUG+ IMP* FEAT- '2:BUG* IMP- '1:BUG- + If LastVersion < 375 Then 'Snapshot 2.11.2 + If LastVersion >= 373 Then + FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:对联机进行了各种各样的优化,以改善稳定性")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若有加入者的网络环境比房主更好,会提示可以让那位加入者担任房主")) + End If + FeatureCount += 16 + BugCount += 4 + End If If LastVersion < 374 Then 'Snapshot 2.11.1 If LastVersion >= 373 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:使用离线登录也可以直接加入联机房间了")) @@ -452,7 +460,7 @@ Public Class FormMain Thread.Sleep(100) DlClientListMojangLoader.Start(1) 'PCL 会同时根据这里的加载结果决定是否使用官方源进行下载 RunCountSub() - ServerLoader.Start(1) + ServerLoader.Start() RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal) Catch ex As Exception Log(ex, "初始化加载池运行失败", LogLevel.Feedback) @@ -776,6 +784,7 @@ Public Class FormMain End Try '读取剪贴板,自动加入联机房间 If PageLinkMain.LinkState <> PageLinkMain.LinkStates.Waiting Then Return '已启动联机 + If PageCurrent = PageType.Link 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 '不是邀请码 diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index aa66d98..c54156d 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -14,13 +14,13 @@ Public Module ModBase #Region "声明" '下列版本信息由更新器自动修改 - Public Const VersionBaseName As String = "2.11.1" '不含分支前缀的显示用版本名 - Public Const VersionStandardCode As String = "2.11.1." & VersionBranchCode '标准格式的四段式版本号 + Public Const VersionBaseName As String = "2.11.2" '不含分支前缀的显示用版本名 + Public Const VersionStandardCode As String = "2.11.2." & 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 = 374 'Snapshot + Public Const VersionCode As Integer = 375 'Snapshot #End If '自动生成的版本信息 Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName @@ -3169,12 +3169,12 @@ Retry: ''' ''' 将数组随机打乱。 ''' - Public Function Shuffle(Of T)(array As IList(Of T)) As IList(Of T) - Shuffle = New List(Of T) - Do While array.Any - Dim i As Integer = RandomInteger(0, array.Count - 1) - Shuffle.Add(array(i)) - array.RemoveAt(i) + Public Iterator Function Shuffle(Of T)(Raw As IEnumerable(Of T)) As IEnumerable(Of T) + Dim RawCopy As New List(Of T)(Raw) + Do While RawCopy.Any + Dim i As Integer = RandomInteger(0, RawCopy.Count - 1) + Yield RawCopy(i) + RawCopy.RemoveAt(i) Loop End Function diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb index 593fd71..75b8b4f 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb @@ -341,7 +341,12 @@ Failed(ex) End Try End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority} - LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted + Try + LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted + Catch ex As ThreadStateException '若遇到偶发的 “线程正在运行或被终止”,则等待后重试 + Thread.Sleep(500) + LastRunningThread.Start() + End Try End Sub Public Overrides Sub Failed(ex As Exception) [Error] = ex diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index fceafa3..401e8fa 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -1984,7 +1984,7 @@ Retry: ''' 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 + For port = 12000 + RandomInteger(0, 1000) To 65000 - ConsecutiveCount Dim Range = Enumerable.Range(port, ConsecutiveCount) If Not Range.Any(Function(p) UsedPorts.Contains(p)) Then Return Range.ToList Next diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb index c4bde9b..531f034 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb @@ -2220,7 +2220,9 @@ IgnoreCustomSkin: Dim StartInfo As New ProcessStartInfo(McLaunchJavaSelected.PathJava) '使用 javaw.exe 会导致 #6263 '设置环境变量 - Dim Paths As New List(Of String)(StartInfo.EnvironmentVariables("Path").Split(";")) + Dim PathEnv As String = StartInfo.EnvironmentVariables("Path") + Dim Paths As New List(Of String) + If Not String.IsNullOrEmpty(PathEnv) Then Paths.AddRange(PathEnv.Split(";")) Paths.Add(ShortenPath(McLaunchJavaSelected.PathFolder)) StartInfo.EnvironmentVariables("Path") = Join(Paths.Distinct.ToList, ";") StartInfo.EnvironmentVariables("appdata") = ShortenPath(PathMcFolder) diff --git a/Plain Craft Launcher 2/Modules/ModMusic.vb b/Plain Craft Launcher 2/Modules/ModMusic.vb index 403ceee..b0c36fc 100644 --- a/Plain Craft Launcher 2/Modules/ModMusic.vb +++ b/Plain Craft Launcher 2/Modules/ModMusic.vb @@ -30,7 +30,7 @@ Next End If '打乱顺序播放 - MusicWaitingList = If(Setup.Get("UiMusicRandom"), Shuffle(New List(Of String)(MusicAllList)), New List(Of String)(MusicAllList)) + MusicWaitingList = If(Setup.Get("UiMusicRandom"), New List(Of String)(MusicAllList).Shuffle().ToList, New List(Of String)(MusicAllList)) If PreventFirst IsNot Nothing AndAlso MusicWaitingList.FirstOrDefault = PreventFirst Then '若需要避免成为第一项的为第一项,则将它放在最后 MusicWaitingList.RemoveAt(0) diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index 81b031d..8beb5b3 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/PageLink/PageLinkMain.xaml b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml index 1a978f5..6e5496f 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml @@ -122,11 +122,16 @@ + + + - + + @@ -156,7 +161,7 @@ - + diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb index a4689df..643d06b 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb @@ -1,6 +1,8 @@ -Imports System.Net.Sockets +Imports System.Globalization +Imports System.Net.Sockets Public Class PageLinkMain + Private Const INVITE_CODE_VERSION As Integer = 2 '=============================== ' 状态机与前端页面 @@ -106,6 +108,7 @@ Public Class PageLinkMain End Function NetworkName = $"P{RadixConvert(ServerPort, 10, 16).PadLeft(4, "0"c)}-{GenerateRandomCode()}" NetworkSecret = GenerateRandomCode() + DiscoverNodeID = -1 Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}") '启动 ChangeState(LinkStates.Loading) @@ -118,7 +121,7 @@ Public Class PageLinkMain ''' Public Shared Sub Join() Handles PanSelectJoin.MouseLeftButtonUp Dim Code As String = MyMsgBoxInput("输入邀请码", "输入房主发给你的邀请码。", - HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码:" & LastCode)) + HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码")) If Not String.IsNullOrEmpty(LastCode) AndAlso Code IsNot Nothing AndAlso Code = "" Then Code = LastCode If Code Is Nothing Then Return Join(Code) @@ -126,27 +129,32 @@ Public Class PageLinkMain 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") '输入修正 Dim ValidateResult = ValidateCodeFormat(Code) If ValidateResult IsNot Nothing Then Hint(ValidateResult, HintType.Red) Return End If + Code = FixCodeFormat(Code) + Log($"[Link] 实际使用的邀请码:{Code}") '基础信息 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}") + If Code.Substring(20, 3) = "000" Then + DiscoverNodeID = -2 + Else + DiscoverNodeID = RadixConvert(Code.Substring(20, 3), 16, 10) + End If + Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort},发现节点 {DiscoverNodeID}") '启动 LastCode = Code 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") '输入修正 + Code = FixCodeFormat(Code) + '判断类型 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 创建房间!" @@ -156,8 +164,20 @@ Public Class PageLinkMain Return "邀请码有误,请让房主使用 PCL 创建房间!" End If End If + '校验版本 + If Code.Length >= 23 AndAlso Code(17) = "-"c AndAlso + Val(Code.Substring(18, 2)) > INVITE_CODE_VERSION Then Return "你的 PCL 版本太老了,请在更新 PCL 之后再联机!" Return Nothing End Function + Private Shared Function FixCodeFormat(Code As String) As String + Code = Code.Between("【", "】").Between("[", "]") '从完整消息中提取 + Code = Code.ToUpper.Replace("O", "0").Replace("I", "1") '输入修正 + '版本 1 兼容 + If Code.Length >= 17 AndAlso (Code.Length < 23 OrElse (Code.Length >= 18 AndAlso Code(17) <> "-"c)) Then + Code = Code.Substring(0, 17) & "-0105E" + End If + Return Code + End Function '自动加入 @@ -280,8 +300,9 @@ Public Class PageLinkMain 'UI 更新 If IsServerSide Then LabFinishTitle.Text = "已创建房间" - LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{NetworkName}-{NetworkSecret}" + LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{GetInviteCode()}" BtnFinishExit.Text = "关闭" + BtnFinishPing.ToolTip = "网络延迟" BtnFinishCopy.Visibility = Visibility.Visible Copy() '立即复制邀请码 '下边栏 @@ -294,6 +315,7 @@ Public Class PageLinkMain LabFinishTitle.Text = "已加入房间" LabFinishDesc.Text = $"在多人游戏页面的最下方就能找到联机房间!{vbCrLf}注意:使用离线登录时不要手动输入 IP!" BtnFinishExit.Text = "离开" + BtnFinishPing.ToolTip = "与房主的延迟" BtnFinishCopy.Visibility = Visibility.Collapsed '下边栏 BtnFinishPort.Visibility = Visibility.Collapsed @@ -332,11 +354,15 @@ Public Class PageLinkMain '复制邀请码 Private Sub Copy() Handles BtnFinishCopy.Click - Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{NetworkName}-{NetworkSecret}】,即可加入联机房间!" + Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{GetInviteCode()}】,即可加入联机房间!" ClipboardSet(CodeText, False) Setup.Set("LinkLastAutoJoinInviteCode", CodeText) Hint("已复制邀请码!", HintType.Green) End Sub + Private Function GetInviteCode() As String + Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _ + RadixConvert(If(DiscoverNodeID = -1, 0, DiscoverNodeID), 10, 16).PadLeft(3, "0"c)}" + End Function '复制 IP Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp @@ -376,6 +402,11 @@ Public Class PageLinkMain ''' Private ClientAddress As String = Nothing ''' + ''' 发现节点的 ID。 + ''' 若必须设定自定义节点则为 -2;若等待选择则为 -1;选择回退节点则为 0;否则为对应节点的 ID。 + ''' + Private DiscoverNodeID As Integer = -1 + ''' ''' 网络信息。 ''' Private NetworkName As String, NetworkSecret As String @@ -383,7 +414,7 @@ Public Class PageLinkMain #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, Integer)("获取配置", AddressOf InitConfig) With {.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} @@ -397,8 +428,12 @@ Public Class PageLinkMain 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) + '检查是否已禁用联机功能 + Dim DisableReason = ServerConfig("Link")?("DisableReason2")?.ToString + If Not String.IsNullOrEmpty(DisableReason) Then Throw New Exception("$" & DisableReason) + If CType(ServerConfig("Link"), JObject).ContainsKey("MinVersionCode") AndAlso + VersionCode < ServerConfig("Link")("MinVersionCode").ToObject(Of Integer) Then + Throw New Exception("$你的 PCL 版本太老了,请在更新 PCL 之后再联机!") End If End Sub @@ -443,7 +478,6 @@ Public Class PageLinkMain End Sub '3. 启动联机模块 - Private Shared HostName As String Private Sub InitLaunch(Task As LoaderTask(Of Integer, Integer)) '解压文件 UpdateLoadingPage("正在解压联机模块……", "解压联机模块") @@ -466,13 +500,19 @@ Public Class PageLinkMain End If Task.Progress = 0.07 '获取节点列表 - Dim Peers As List(Of String) + UpdateLoadingPage("正在获取节点列表……", "获取节点列表") + Dim RawPeers As List(Of String) Dim CustomPeers As String = Setup.Get("LinkCustomPeer") If String.IsNullOrWhiteSpace(CustomPeers) Then - Peers = GetOnlinePeers() + If DiscoverNodeID = -2 AndAlso Not IsServerSide Then + Panic("未填写自定义节点设置", $"$你必须在 {vbLQ}自定义节点{vbRQ} 设置中填写与房主相同的内容,{vbCrLf}才能进入该房间!") + Return + End If + RawPeers = GetTargetPeers() Else - Peers = CustomPeers.Split(",,".ToCharArray).Select(Function(p) p.Trim).Where(Function(p) Not String.IsNullOrEmpty(p)).ToList() - Log("[Link] 使用自定义节点") + If DiscoverNodeID <> -2 AndAlso Not IsServerSide Then Hint("房主可能没有使用自定义节点设置,请确认你们的自定义节点设置是否一致!") + RawPeers = CustomPeers.Split(",,".ToCharArray).Select(Function(p) p.Trim).Where(Function(p) Not String.IsNullOrEmpty(p)).ToList() + Log("[Link] 使用自定义节点:" & CustomPeers) End If Task.Progress = 0.13 '获取空闲端口 @@ -484,7 +524,7 @@ Public Class PageLinkMain '获取启动参数 Dim Arguments As String = ServerConfig("Link")("Argument") Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort}" - HostName = If(IsServerSide, "Server-", "Client-") & RadixConvert(Math.Abs(Identify.GetHashCode), 10, 36) + Dim 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 @@ -496,10 +536,11 @@ Public Class PageLinkMain 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 Peers + For Each Peer As String In RawPeers Arguments += $" -p=""{Peer}""" Next Arguments += " --private-mode true" '老好人模式现在莫得用:If Not Setup.Get("LinkShareMode") Then + If Setup.Get("LinkLatencyMode") = 1 Then Arguments += " --latency-first" '启动进程 ProcessStart(Arguments) Task.Progress = 0.15 @@ -512,23 +553,23 @@ Public Class PageLinkMain RefreshPeerLoader.WaitForExit(IsForceRestart:=True) '查找目标节点 Dim Ping = GetPeerPing() - If Ping <> 0 Then + If Ping > 0 Then Log($"[Link] 已与目标建立连接,当前 Ping 为 {Ping:0.0}ms") - Telemetry("联机成功", "Server", IsServerSide, "Ping", Ping) + Telemetry("联机成功", "Server", IsServerSide, "NAT", NATType) Exit Do '退出循环 End If '更新进度 Dim LastProgress = Task.Progress - Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Count) + Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Where(Function(p) p.Ping > 0).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)) + Task.Progress = MathClamp(Task.Progress + 0.02, If(IsServerSide, 0.5, 0.3), If(IsServerSide, 0.95, 0.5)) Case Else '已连接到节点,但未连接到房主 UpdateLoadingPage("正在连接到房主……", "连接房主") - Task.Progress = MathClamp(Task.Progress + 0.02, Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.99) + Task.Progress = MathClamp(Task.Progress + 0.02, Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.95) End Select '超时判定 If LastProgress <> Task.Progress Then @@ -547,49 +588,112 @@ Public Class PageLinkMain End If Loop Until Task.IsAborted If Task.IsAborted Then Throw New ThreadInterruptedException + '等待连接稳定,最多 5s + If IsServerSide Then Return + UpdateLoadingPage("连接优化中……", "优化连接") + Task.Progress = 0.999 + For i = 1 To 50 + Dim Server = GetTargetPeer() + If Server IsNot Nothing AndAlso Not Server.Relay AndAlso Server.Ping < 100 Then Return '结束 + If Task.IsAborted Then Throw New ThreadInterruptedException + Thread.Sleep(100) + Next End Sub ''' - ''' 从在线配置和 API 获取节点列表。 + ''' 从在线配置和 API 获取需要连接的节点列表,并更新发现节点 ID。 + ''' 根据发现节点 ID,会有以下行为变化: + ''' -1:作为房主,根据负载均衡选择一个发现节点。 + ''' 0:作为加入者,但邀请码未提供发现节点 ID,使用回退发现节点。 + ''' >0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点。 ''' - Private Function GetOnlinePeers() As List(Of String) - Dim Peers As List(Of String) = ServerConfig("Link")("Peers").Select(Function(p) p.ToString).ToList() + Private Function GetTargetPeers() As List(Of String) + Dim FinalPeers As New List(Of String), FinalDiscoverID As Integer = -1 + Dim FallbackDiscoverID = ServerConfig("Link")("DiscoverPeerId").ToObject(Of Integer) + Dim FallbackDiscoverAddress As String = ServerConfig("Link")("DiscoverPeer").ToString() 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") + Dim RawNodes As JObject + Dim IsFallbackRawList As Boolean = False + Try + RawNodes = GetJson(NetRequestByClient("https://uptime.easytier.cn/api/nodes?page=1&per_page=1000", RequireJson:=True)) + Catch exx As Exception + Log(exx, "从源站获取节点列表失败,将使用 CDN 缓存") + RawNodes = GetJson(NetRequestByClientRetry("https://easytier.meloong.com/?page=1&per_page=1000", RequireJson:=True)) + IsFallbackRawList = True + End Try + '分析节点列表 + Dim Nodes As New List(Of JObject) '负载会添加在 load 字段上 + Dim BlackList As List(Of String) = ServerConfig("Link")("PeersBlackList").Select(Function(p) p.ToString).ToList() + For Each Node As JObject In RawNodes("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 ID = Node("id").ToObject(Of Integer) + If Not Node("is_active").ToObject(Of Boolean) OrElse Not Node("is_approved").ToObject(Of Boolean) Then Continue For + If ID = DiscoverNodeID Then GoTo ForcedPass '若为指定的发现节点,忽略后续检查 + If ID = FallbackDiscoverID Then Continue For '不主动选取回退发现节点 + If BlackList.Contains(Node("address").ToString) Then Continue For 'ServerConfig 黑名单 + If Node("usage_percentage").ToObject(Of Double) = 0 AndAlso RandomInteger(1, 100) <> 1 Then Continue For '或许节点有问题才导致是 0 负载,让它只有 1% 概率被选中 + '标签检查 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 + If Not Tags.Contains("国内") OrElse Not Tags.Contains("MC中继") Then Continue For + '计算负载并加入列表 +ForcedPass: + Dim Load As Double = Node("usage_percentage").ToObject(Of Double) '负载 + Load *= 110 - Node("health_percentage_24h").ToObject(Of Double) '可用率 + Node("load") = Load + Nodes.Add(Node) 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() + '排序 + If Not IsFallbackRawList Then + Nodes = Nodes.OrderBy(Function(n) n("load").ToObject(Of Double)).ToList() '按负载从低到高排序 + Else + Nodes = Nodes.Shuffle().ToList() '回退到 CDN 缓存时,由于负载数据可能过期,直接随机选择 + End If + '选取发现节点 + Dim SelectedDiscoverNode As JObject = Nothing + If DiscoverNodeID = -1 Then '-1:作为房主,根据负载均衡选择一个发现节点 + SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) Not n("allow_relay").ToObject(Of Boolean)) + ElseIf DiscoverNodeID > 0 Then '>0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点 + SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) n("id").ToObject(Of Integer) = DiscoverNodeID) + If SelectedDiscoverNode Is Nothing Then + Log($"[Link] 未找到 ID {DiscoverNodeID} 的发现节点", LogLevel.Debug) + Panic("房间已过期", "请让房主重新创建房间!") + Throw New ThreadInterruptedException + End If + End If + If SelectedDiscoverNode Is Nothing Then '使用回退发现节点 + SelectedDiscoverNode = New JObject From {{"address", FallbackDiscoverAddress}, {"id", FallbackDiscoverID}} + Log("[Link] 将使用回退发现节点", LogLevel.Debug) + End If + FinalPeers.Add(SelectedDiscoverNode("address").ToString()) + FinalDiscoverID = SelectedDiscoverNode("id").ToObject(Of Integer) + Log($"[Link] 发现节点:{SelectedDiscoverNode("address")} (ID: {FinalDiscoverID})") + '选取中继节点 + If ModeDebug OrElse Not IsServerSide Then '房主只连接发现节点,不连接中继节点 + Dim RelayCount As Integer = ServerConfig("Link")("RandomPeer").ToObject(Of Integer) + Dim RelayNodes = Nodes.Where(Function(n) n("allow_relay").ToObject(Of Boolean)).ToList() + If RelayNodes.Count < RelayCount Then Throw New Exception($"可用的中继节点数量不足,需要 {RelayCount} 个,实际 {RelayNodes.Count} 个") + FinalPeers.AddRange(RelayNodes.Take(RelayCount).Select(Function(n) n("address").ToString())) + End If + Catch ex As ThreadInterruptedException + Throw Catch ex As Exception Log(ex, "获取节点列表失败,联机质量可能受到影响", LogLevel.Hint) + FinalPeers.AddRange(ServerConfig("Link")("Peers").Select(Function(p) p.ToString)) + If FinalDiscoverID <= 0 Then + FinalPeers.Add(FallbackDiscoverAddress) + FinalDiscoverID = FallbackDiscoverID + End If End Try - Return Peers + '版本 1 兼容 + If CType(ServerConfig("Link"), JObject).ContainsKey("EnableForcedPeer") Then + FinalPeers.Add("tcp://mc1.easytier.cn:55558") + If FinalDiscoverID <= 0 Then FinalDiscoverID = 94 + End If + '强制添加的节点 + FinalPeers.AddRange(ServerConfig("Link")("MandatoryPeers").Select(Function(p) p.ToString)) + '结束 + DiscoverNodeID = FinalDiscoverID + Return FinalPeers.Distinct.ToList End Function #End Region @@ -638,6 +742,11 @@ Public Class PageLinkMain ''' 出现意外错误,给出错误信息并结束联机。 ''' Private Sub Panic(Brief As String, Detail As String) + '常见原因分析 + If Detail.Contains("failed to listen on ") Then + Detail = $"监听端口失败。{vbCrLf}请点击重试,如果还是出现此错误,可以重启电脑解决。{vbCrLf}{vbCrLf}{Detail}" + End If + '显示信息 If LinkState = LinkStates.Loading Then FailReason = Brief LinkLoader.Failed(New Exception(Detail)) @@ -739,9 +848,13 @@ Public Class PageLinkMain ''' Public ReadOnly Name As String ''' - ''' 连接方式。 + ''' 是否通过中继连接。 ''' - Public ReadOnly Cost As String + Public ReadOnly Relay As Boolean + ''' + ''' NAT 类型。 + ''' + Public ReadOnly NATType As NATTypes ''' ''' 从 CLI 给出的信息分析对应的数据。 @@ -749,7 +862,7 @@ Public Class PageLinkMain Public Sub New(Info As JObject) '类别 Dim PeerName = Info("hostname").ToString - If PeerName = HostName Then + If Info("cost").ToString = "Local" Then Type = Types.Self ElseIf PeerName.StartsWithF("Client") Then Type = Types.Client @@ -759,20 +872,42 @@ Public Class PageLinkMain Type = Types.Misc End If '基础信息 - Ping = If(Info("lat_ms").ToString = "-", 0, Info("lat_ms").ToString) + Double.TryParse(Info("lat_ms"), NumberStyles.Any, CultureInfo.InvariantCulture, Ping) Name = PeerName - Cost = Info("cost") + Relay = Info("cost").ToString.ContainsF("relay", True) + NATType = Info("nat_type").ToString.ParseToEnum(Of NATTypes) End Sub Public Overrides Function ToString() As String - Return $"{Type} - {Name} - Ping {Ping:0.0}ms [{Cost}]" + Return $"{Type} - {Name} - Ping {Ping:0.0}ms [中继? {Relay}] - NAT {NATType}" End Function End Class + Private Enum NATTypes + 'https://github.com/EasyTier/EasyTier/blob/6bb2fd9a15ab2499bdeabdcc3a925e9bd9aebf50/easytier/src/proto/common.proto#L129 + Unknown = 0 + OpenInternet = 1 + NoPAT = 2 + FullCone = 3 + Restricted = 4 + PortRestricted = 5 + Symmetric = 6 + SymUdpFirewall = 7 + SymmetricEasyInc = 8 + SymmetricEasyDec = 9 + ''' + ''' 尚未获取。 + ''' + Pending = 10 + End Enum ''' ''' 当前的节点列表,使用 RefreshPeerLoader 来刷新。 ''' 若尚未成功获取过则为 Nothing,但保证在加载完成后至少是一个列表。 ''' Private Peers As List(Of Peer) = Nothing + ''' + ''' 自己的 NAT 类型。 + ''' + Private NATType As NATTypes = NATTypes.Pending ''' ''' 调用 EasyTier CLI 获取已连接节点信息。 @@ -781,25 +916,22 @@ Public Class PageLinkMain ''' 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", $"-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, 23, 103) = 0 Then Log("[EasyTier] CLI 输出抽样:" & vbCrLf & CliResult) Dim NewPeers As New List(Of Peer) - For Each Line As JObject In CType(GetJson(CliResult), JArray).Skip(1) + For Each Line As JObject In CType(GetJson(CliResult), JArray) Try Dim Peer = New Peer(Line) - If Peer.Type = Peer.Types.Self Then Continue For '自己 - NewPeers.Add(Peer) + If Peer.Type = Peer.Types.Self Then + NATType = Peer.NATType '记录自己的 NAT + Else + NewPeers.Add(Peer) + End If Catch exx As Exception - Log(exx, $"错误的信息行({Line})") + Log(exx, $"错误的信息({Line})") End Try Next '完成 @@ -829,7 +961,7 @@ Public Class PageLinkMain Return Peer.Ping End Function ''' - ''' 服务端会返回所有节点中 Ping 的那一个,客户端会返回服务端。 + ''' 服务端会返回所有节点中 Ping 大于 0 最低的那一个,客户端会返回服务端。 ''' 若没有则为 Nothing。 ''' Private Function GetTargetPeer() As Peer @@ -841,7 +973,7 @@ Public Class PageLinkMain 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 且最低的那个 + '返回 Ping 且最低的那个 Dim MinPing = Targets.Min(Function(p) p.Ping) Return Targets.First(Function(p) p.Ping = MinPing) End Function @@ -862,8 +994,8 @@ Public Class PageLinkMain #Region "定时任务" '启动 - Private IsTimerStarted As Boolean = False Private Sub StartTimerThread() Handles Me.Loaded + Static IsTimerStarted As Boolean = False If IsTimerStarted Then Return RunInNewThread( Sub() @@ -888,38 +1020,66 @@ Public Class PageLinkMain End Sub '每秒或进入页面时触发 - Private BroadcastSocket As New Socket(SocketType.Dgram, ProtocolType.Udp) Private Sub Update() Handles Me.Loaded If LinkState <> LinkStates.Finished Then Return '重新获取信息 SyncLock RefreshPeerLoader.LockState If RefreshPeerLoader.State <> LoadState.Loading Then RefreshPeerLoader.Start(IsForceRestart:=True) End SyncLock - '更新 Ping 与人数显示 + '更新 UI If FrmMain.PageCurrent = FormMain.PageType.Link Then RunInUi( Sub() + 'Logo 旋转动画 + AniStart(AaRotateTransform(ImgFinishLogo, 500, 5000), "Link Logo Rotation") + 'Ping 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") + Dim Connecting As Boolean = Ping Mod 500 = 0 OrElse $"{Ping:0.0}" = "1.0" OrElse FailCount > 0 + If Connecting Then + LabFinishPing.Text = "连接优化中" Else - 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 旋转动画 + LabFinishPing.Text = If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms") End If - '更新 Ping 的 Tooltip 显示 - Dim Tooltip As String = If(IsServerSide, "网络延迟", "与房主的延迟") - If RelayLayer Then Tooltip &= If(IsServerSide, - $"(你的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)", - $"(你或者房主的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)") - BtnFinishPing.ToolTip = Tooltip - '更新人数显示 + '人数显示 LabFinishPlayer.Text = PeopleCount & "  人" + '------------------------ + ' 提示条 + '------------------------ + HintFinish.Visibility = Visibility.Collapsed + If IsServerSide Then Return '服务端没有需要显示的提示 + If Connecting Then Return '连接稳定过程中不显示提示 + '中继提示 + Dim Server As Peer = GetTargetPeer() + If Server Is Nothing Then Return + If Server.Relay Then + If NATType >= NATTypes.Symmetric AndAlso Server.NATType >= NATTypes.Symmetric Then + HintFinish.Text = "你和房主的网络环境都不太好," + ElseIf NATType >= NATTypes.Symmetric Then + HintFinish.Text = "你的网络环境不太好," + ElseIf Server.NATType >= NATTypes.Symmetric Then + HintFinish.Text = "房主的网络环境不太好," + Else + HintFinish.Text = "你或者房主的网络环境不太好," + End If + If String.IsNullOrWhiteSpace(Setup.Get("LinkCustomPeer")) Then + HintFinish.Text &= "正使用社区节点进行中继。" + Else + HintFinish.Text &= "正通过自定义节点进行中继。" + End If + HintFinish.Visibility = Visibility.Visible + HintFinish.Theme = MyHint.Themes.Yellow + Return + End If + '环境比房主更好的提示 + If NATType >= NATTypes.PortRestricted Then Return '自身的 NAT 为 2 或更好 + If Server.NATType < NATTypes.PortRestricted Then Return '房主的 NAT 为 3 或更差 + Dim OtherPlayers = Peers.Where(Function(p) p.Type = Peer.Types.Client).ToList() + If Not OtherPlayers.Any() Then Return '房间里还有其他玩家 + If OtherPlayers.Any(Function(p) p.Relay) Then Return '自己可以与所有玩家打洞 + If OtherPlayers.All(Function(p) p.NATType < NATTypes.PortRestricted) Then Return '任意其他玩家的 NAT 为 3 或更差 + HintFinish.Visibility = Visibility.Visible + HintFinish.Theme = MyHint.Themes.Blue + HintFinish.Text = "你的网络环境比房主更好!如果你来当房主,其他玩家或许能更加流畅!" End Sub) End If '检查核心状态 @@ -937,6 +1097,7 @@ Public Class PageLinkMain '广播联机房间端口 If Not IsServerSide Then Try + Static BroadcastSocket As New Socket(SocketType.Dgram, ProtocolType.Udp) BroadcastSocket.SendTo( Encoding.UTF8.GetBytes($"[MOTD]PCL 联机房间[/MOTD][AD]{ClientPort}[/AD]"), SocketFlags.None, diff --git a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb b/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb index 659d3ac..a4f29ab 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb @@ -113,6 +113,7 @@ {"LaunchRamCustom", New SetupEntry(15)}, {"LinkLastAutoJoinInviteCode", New SetupEntry("", Source:=SetupSource.Registry)}, {"LinkShareMode", New SetupEntry(True, Source:=SetupSource.Registry)}, + {"LinkLatencyMode", New SetupEntry(0, Source:=SetupSource.Registry)}, {"LinkCustomPeer", New SetupEntry("")}, {"LinkEasyTierVersion", New SetupEntry(-1, Source:=SetupSource.Registry)}, {"ToolHelpChinese", New SetupEntry(True, Source:=SetupSource.Registry)}, diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml index f6acdf9..fdd2289 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml @@ -6,7 +6,26 @@ PanScroll="PanBack"> - + + + + + + + + + + + + + + + + + + + + @@ -25,8 +44,8 @@ + ToolTip="联机功能全靠社区的各位无私贡献的节点才能存在! 如果你有空闲的高带宽服务器,欢迎搭建一个共享节点,人人为我,我为人人嘛……" + local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://shimo.im/docs/qKPttVvXKqPD8YDC#anchor-Dupo" /> diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb index 2db61b6..abd8d53 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml.vb @@ -17,6 +17,7 @@ End Sub Public Sub Reload() + ComboLatencyMode.SelectedIndex = Setup.Get("LinkLatencyMode") CheckShareMode.Checked = Setup.Get("LinkShareMode") TextCustomPeer.Text = Setup.Get("LinkCustomPeer") End Sub @@ -24,6 +25,7 @@ '初始化 Public Sub Reset() Try + Setup.Reset("LinkLatencyMode") Setup.Reset("LinkShareMode") Setup.Reset("LinkCustomPeer") @@ -43,5 +45,8 @@ 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 Shared Sub ComboChange(sender As MyComboBox, e As Object) Handles ComboLatencyMode.SelectionChanged + If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.SelectedIndex) + End Sub End Class diff --git a/Plain Craft Launcher 2/Resources/Help.zip b/Plain Craft Launcher 2/Resources/Help.zip index 0846a3453411edb799639f2f5474c6dd686641c5..3188d53e17644726404d0fb8a822522d938eb41c 100644 GIT binary patch delta 10746 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 U1Qm`zON+t;K$6@z3P~aU4@?eE=Kufz delta 10634 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 VUq0`413^gC5|lrn%#Vxr{{aCc#FhX6