From efd480f06cc306ec0ae557c2286f1b2b119d4354 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: Sat, 1 Nov 2025 11:51:54 +0800 Subject: [PATCH] 2.11.1 --- Plain Craft Launcher 2/FormMain.xaml.vb | 30 +- .../Images/Heads/EasyTier.png | Bin 0 -> 1131 bytes .../Modules/Base/ModBase.vb | 52 +- .../Modules/Base/ModLoader.vb | 4 +- Plain Craft Launcher 2/Modules/Base/ModNet.vb | 83 +- .../Modules/Base/ModValidate.vb | 7 +- .../Modules/Minecraft/ModDownload.vb | 11 +- Plain Craft Launcher 2/Modules/ModEvent.vb | 12 + .../My Project/AssemblyInfo.vb | 4 +- .../Pages/PageDownload/Comp/PageComp.xaml | 2 +- .../Pages/PageDownload/ModDownloadLib.vb | 3 +- .../Pages/PageLaunch/PageLaunchRight.xaml.vb | 3 + .../Pages/PageLaunch/PageLoginMsSkin.xaml.vb | 2 +- .../Pages/PageLink/PageLinkMain.xaml | 8 +- .../Pages/PageLink/PageLinkMain.xaml.vb | 395 ++++++--- .../Pages/PageOther/PageOtherAbout.xaml | 795 +++++++++++++----- .../Pages/PageSetup/ModSetup.vb | 3 +- .../Pages/PageSetup/PageSetupLaunch.xaml | 4 +- .../Pages/PageSetup/PageSetupLink.xaml | 29 +- .../Pages/PageSetup/PageSetupLink.xaml.vb | 6 +- .../Pages/PageSetup/PageSetupUI.xaml | 1 + .../Pages/PageSpeedLeft.xaml.vb | 4 +- .../Plain Craft Launcher 2.vbproj | 4 +- Plain Craft Launcher 2/Resources/Help.zip | Bin 84738 -> 86816 bytes 24 files changed, 1031 insertions(+), 431 deletions(-) create mode 100644 Plain Craft Launcher 2/Images/Heads/EasyTier.png 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 0000000000000000000000000000000000000000..34602517b2ac11784bd773cfb521f95747819d33 GIT binary patch literal 1131 zcmV-x1eE)UP)004&%004{+008|`004nN004b?008NW002DY000@xb3BE200009 za7bBm000XS000XS0e@s)kpKV#;Ymb6RCr$Hm|?CHF${zY2jVB(2MLKIAt7-e_{oLf z4b;`FJ5J)n*;3voRd4Agj%Vy-m%jP+!sQk75CcAcy#ryHq@kW_4 zfEOQGI5U1{Y(?6D*#vRuef;qEy2rQg@u8?#F(LIL#A31wjfw}Eu>iT)Q-#eKVb}l; zJer`*$(jXh&Zr>HDPzLW7T|yxF+2|yLR(vNj# zfNTNL!2nxuBvFMwKUhE}CdR55V73cveJ%(n-vSE12O&azn3Xdq!!`p{ zHz}cvzaLetl5zv0{py8d9DkU|q$u_ZWd`&rYp`HcmiIUd9F4ciDQAGe0=cdEn#Y_$ znE@k-`-&TO&G%bWnp9?ho!UwO4l_Osy-@#eKi;r0l~0ImTfnNs`&EhZ@EDt0S#i&) zX8^G3vQYKiSx5s`Ux}?%RTnTV(Z2e;MP`UJ9SpE!W*2K!F!ShYjh*ch)8yxG+XB7| z0ED%~`TJGXZ*ljFkR>qdG^h~Knm3bMq$}(I7LkP);ZERgh~RN7o?~WnYmMLfqbM&t zBA)S;w_KE%UY;#IO#_g0l*lM5;m8ct?wkXz@ee~Z1gm2dlA}! zRggP$m8UWQun84LNCRvUfARwL-P!P7W)oWZs{Id6dkpR%(*aSwPqzh_iXz-~fO6GN zQq^LQ?gW)Da#Al(z~_4~fP?jfWybEJBH!B<;9pEe1o0SEh23Nez++hJtphCJNHE5W z&(6=Xx}Mj$d^~jgUCCneaadL!l*&k6V>K@39}HN96RL`7?1+6c xAbMnEi7GM^KZ*q^3_vI0zto4v;OpaZ`3=ZKO&P^Kla&Af002ovPDHLkV1g643H<;7 literal 0 HcmV?d00001 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 d1f20d1f8e371c220e051577a0612e7d8f2ba89a..0846a3453411edb799639f2f5474c6dd686641c5 100644 GIT binary patch delta 30754 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 CJ+6-c delta 28631 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 GiugY$e~_{O