diff --git a/Plain Craft Launcher 2/App.config b/Plain Craft Launcher 2/App.config index d464350..0e1a473 100644 --- a/Plain Craft Launcher 2/App.config +++ b/Plain Craft Launcher 2/App.config @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/Plain Craft Launcher 2/Application.xaml.vb b/Plain Craft Launcher 2/Application.xaml.vb index 5997359..5ef002b 100644 --- a/Plain Craft Launcher 2/Application.xaml.vb +++ b/Plain Craft Launcher 2/Application.xaml.vb @@ -181,53 +181,29 @@ WaitRetry: End Sub '动态 DLL 调用 - Private Shared AssemblyNAudio As Assembly - Private Shared AssemblyJson As Assembly - Private Shared AssemblyDialog As Assembly - Private Shared AssemblyImazenWebp As Assembly - Private Shared ReadOnly AssemblyNAudioLock As New Object - Private Shared ReadOnly AssemblyJsonLock As New Object - Private Shared ReadOnly AssemblyDialogLock As New Object - Private Shared ReadOnly AssemblyImazenWebpLock As New Object Private Declare Function SetDllDirectory Lib "kernel32" Alias "SetDllDirectoryA" (lpPathName As String) As Boolean - Public Shared Function AssemblyResolve(sender As Object, args As ResolveEventArgs) As Assembly - If args.Name.StartsWithF("NAudio") Then - SyncLock AssemblyNAudioLock - If AssemblyNAudio Is Nothing Then - Log("[Start] 加载 DLL:NAudio") - AssemblyNAudio = Assembly.Load(GetResources("NAudio")) - End If - Return AssemblyNAudio - End SyncLock - ElseIf args.Name.StartsWithF("Newtonsoft.Json") Then - SyncLock AssemblyJsonLock - If AssemblyJson Is Nothing Then - Log("[Start] 加载 DLL:Json") - AssemblyJson = Assembly.Load(GetResources("Json")) - End If - Return AssemblyJson - End SyncLock - ElseIf args.Name.StartsWithF("Ookii.Dialogs.Wpf") Then - SyncLock AssemblyDialogLock - If AssemblyDialog Is Nothing Then - Log("[Start] 加载 DLL:Dialogs") - AssemblyDialog = Assembly.Load(GetResources("Dialogs")) - End If - Return AssemblyDialog - End SyncLock - ElseIf args.Name.StartsWithF("Imazen.WebP") Then - SyncLock AssemblyImazenWebpLock - If AssemblyImazenWebp Is Nothing Then - Log("[Start] 加载 DLL:Imazen.WebP") - AssemblyImazenWebp = Assembly.Load(GetResources("Imazen_WebP")) - SetDllDirectory(PathPure.TrimEnd("\")) + Public Shared Function AssemblyResolve(sender As Object, Args As ResolveEventArgs) As Assembly + '缓存 + Static Prefixes As String() = {"NAudio", "Newtonsoft.Json", "Ookii.Dialogs.Wpf", "Imazen.WebP", "CacheCow.Common", "CacheCow.Client.FileStore", "CacheCow.Client", "System.Net.Http.Formatting"} + Static Locks As New Dictionary(Of String, Object)(StringComparer.Ordinal) + Static LoadedAssembly As New Dictionary(Of String, Assembly)(StringComparer.Ordinal) + '查找对应的 DLL + Dim Prefix As String = Prefixes.FirstOrDefault(Function(p) Args.Name.StartsWithF(p)) + If Prefix Is Nothing Then Return Nothing + '加载 DLL + If Not Locks.ContainsKey(Prefix) Then Locks(Prefix) = New Object() + SyncLock Locks(Prefix) + If Not LoadedAssembly.ContainsKey(Prefix) Then + Log($"[Start] 加载 DLL:{Prefix}") + LoadedAssembly(Prefix) = Assembly.Load(GetResources(Prefix)) + 'WebP 特判 + If Prefix = "Imazen.WebP" Then + SetDllDirectory(PathPure.TrimEnd("\"c)) WriteFile(PathPure & "libwebp.dll", GetResources("libwebp64")) End If - Return AssemblyImazenWebp - End SyncLock - Else - Return Nothing - End If + End If + Return LoadedAssembly(Prefix) + End SyncLock End Function '切换窗口 diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 14a2c12..554bb8a 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -83,6 +83,13 @@ Public Class FormMain '3:BUG+ IMP* FEAT- '2:BUG* IMP- '1:BUG- + If LastVersion < 363 Then 'Snapshot 2.10.4 + FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源时,会单独记忆每种资源上次下载到的文件夹,以防混淆")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络底层框架与下载稳定性")) + FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法启动一部分 LabyMod 和 GTNH 客户端")) + FeatureCount += 22 + BugCount += 26 + End If If LastVersion < 362 Then 'Snapshot 2.10.3 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法安装部分使用老版本 PCL 导出的整合包")) End If @@ -204,8 +211,6 @@ Public Class FormMain End Sub, "UpdateLog Output") End Sub - Private ReadOnly Helper As New DragFileHelper() - '窗口加载 Private IsWindowLoadFinished As Boolean = False Public Sub New() @@ -228,7 +233,7 @@ Public Class FormMain If Not Setup.IsUnset("LaunchArgumentIndie") Then Log("[Start] 从老 PCL 迁移版本隔离") Setup.Set("LaunchArgumentIndieV2", Setup.Get("LaunchArgumentIndie")) - ElseIf Not Setup.IsUnset("WindowHeight") Then + ElseIf Not Setup.IsUnset("LaunchVersionSelect") Then Log("[Start] 从老 PCL 升级,但此前未调整版本隔离,使用老的版本隔离默认值") Setup.Set("LaunchArgumentIndieV2", Setup.GetDefault("LaunchArgumentIndie")) Else @@ -244,36 +249,24 @@ Public Class FormMain [AddHandler](DragDrop.DragOverEvent, New DragEventHandler(AddressOf HandleDrag), handledEventsToo:=True) '加载 UI InitializeComponent() - - - Opacity = 0 - '旧代码 - ''开启管理员权限下的文件拖拽,但下列代码也没用(#2531) - 'If IsAdmin() Then - ' Log("[Start] PCL 正以管理员权限运行") + '开启管理员权限下的文件拖拽 + If IsAdmin() Then + Static Helper As New DragHelper + AddHandler SourceInitialized, + Sub() + Dim WpfHelper As New WindowInteropHelper(Me) + Helper.HwndIntPtrSource = HwndSource.FromHwnd(WpfHelper.Handle) + Helper.AddHook() + End Sub + AddHandler Closing, Sub() Helper.RemoveDragHook() + AddHandler Helper.DragDrop, Sub() FileDrag(Helper.DropFilePaths) + End If + 'If IsAdmin() Then '下列代码没用(#2531) ' ChangeWindowMessageFilter(&H233, 1) ' ChangeWindowMessageFilter(&H4A, 1) ' ChangeWindowMessageFilter(&H49, 1) 'End If - - '开启管理员权限下的文件拖拽 - If IsAdmin() Then - AddHandler Me.SourceInitialized, - Sub(sender, e) - Dim wpfHelper As New WindowInteropHelper(Me) - Helper.HwndIntPtrSource = HwndSource.FromHwnd(wpfHelper.Handle) - Helper.AddHook() - End Sub - AddHandler Me.Closing, - Sub(sender, e) - Helper.RemoveDragHook() - End Sub - AddHandler Helper.DragDrop, - Sub(sender, e) - Me.FileDrag(Helper.DropFilePaths) - End Sub - End If '切换到首页 If Not IsNothing(FrmLaunchLeft.Parent) Then FrmLaunchLeft.SetValue(ContentPresenter.ContentProperty, Nothing) If Not IsNothing(FrmLaunchRight.Parent) Then FrmLaunchRight.SetValue(ContentPresenter.ContentProperty, Nothing) @@ -390,7 +383,7 @@ Public Class FormMain Catch ex As Exception Log(ex, "清理自动更新文件失败") End Try - End Sub, "Start Loader", ThreadPriority.Lowest) + End Sub, "初始化", ThreadPriority.Lowest) Log("[Start] 第三阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms") End Sub @@ -616,11 +609,12 @@ Public Class FormMain '按键事件 Private Sub FormMain_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown If e.IsRepeat Then Return - '调用弹窗:回车选择第一个,Esc 选择最后一个 - If PanMsg.Children.Count > 0 Then + '修复按下 Alt 后误认为弹出系统菜单导致的冻结 + If e.SystemKey = Key.LeftAlt OrElse e.SystemKey = Key.RightAlt Then e.Handled = True + '在有弹窗时:回车选择第一个,Esc 选择最后一个 + If PanMsg.Children.Any Then If e.Key = Key.Enter Then CType(PanMsg.Children(0), Object).Btn1_Click() - Return ElseIf e.Key = Key.Escape Then Dim Msg As Object = PanMsg.Children(0) If TypeOf Msg IsNot MyMsgInput AndAlso TypeOf Msg IsNot MyMsgSelect AndAlso Msg.Btn3.Visibility = Visibility.Visible Then @@ -630,9 +624,14 @@ Public Class FormMain Else Msg.Btn1_Click() End If - Return End If + Return End If + + '========================== + ' 在没有弹窗时:继续检查…… + '========================== + '按 ESC 返回上一级 If e.Key = Key.Escape Then TriggerPageBack() '更改隐藏版本可见性 @@ -666,8 +665,6 @@ Public Class FormMain FrmLaunchLeft.LaunchButtonClick() End If End If - '修复按下 Alt 后误认为弹出系统菜单导致的冻结 - If e.SystemKey = Key.LeftAlt OrElse e.SystemKey = Key.RightAlt Then e.Handled = True End Sub Private Sub FormMain_MouseDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseDown '鼠标侧键返回上一级 @@ -732,6 +729,7 @@ Public Class FormMain End Sub Private Sub FrmMain_Drop(sender As Object, e As DragEventArgs) Handles Me.Drop Try + ShowWindowToTop() If e.Data.GetDataPresent(DataFormats.Text) Then '获取文本 Try @@ -1505,4 +1503,4 @@ Public Class FormMain lastMouseArg = e End Sub -End Class \ No newline at end of file +End Class diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index 407855a..86eb2a3 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -13,13 +13,13 @@ Public Module ModBase #Region "声明" '下列版本信息由更新器自动修改 - Public Const VersionBaseName As String = "2.10.3" '不含分支前缀的显示用版本名 - Public Const VersionStandardCode As String = "2.10.3." & VersionBranchCode '标准格式的四段式版本号 + Public Const VersionBaseName As String = "2.10.4" '不含分支前缀的显示用版本名 + Public Const VersionStandardCode As String = "2.10.4." & VersionBranchCode '标准格式的四段式版本号 Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换 #If BETA Then Public Const VersionCode As Integer = 361 'Release #Else - Public Const VersionCode As Integer = 362 'Snapshot + Public Const VersionCode As Integer = 363 'Snapshot #End If '自动生成的版本信息 Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName @@ -1551,7 +1551,7 @@ RetryDir: ''' ''' 获取 JSON 对象。 ''' - Public Function GetJson(Data As String) + Public Function GetJson(Data As String) As JToken Try Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local}) Catch ex As Exception @@ -2131,6 +2131,19 @@ RetryDir: End Function End Class + ''' + ''' 将二维数组转换为字典。 + ''' + Public Function ToDictionary(Of T)(arr As T(,)) As Dictionary(Of T, T) + Dim result As New Dictionary(Of T, T) + If arr.Length = 0 Then Return result + If arr.GetLength(1) <> 2 Then Throw New ArgumentException("数组必须为两列,第一列为 Key,第二列为 Value。") + For i As Integer = 0 To arr.GetLength(0) - 1 + result(arr(i, 0)) = arr(i, 1) + Next + Return result + End Function + ''' ''' 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。 ''' @@ -2179,10 +2192,10 @@ RetryDir: End Function ''' - ''' 判断当前系统语言是否为 zh-CN。 + ''' 判断当前系统语言是否为中文。 ''' Public Function IsSystemLanguageChinese() As Boolean - Return CultureInfo.CurrentCulture.Name = "zh-CN" OrElse CultureInfo.CurrentUICulture.Name = "zh-CN" + Return CultureInfo.CurrentCulture.Name.StartsWithF("zh-") OrElse CultureInfo.CurrentUICulture.Name.StartsWithF("zh-") End Function Private Uuid As Integer = 1 @@ -2225,6 +2238,9 @@ NextElement: Next i Return ResultArray End Function + Public Function Any(Arr As UIElementCollection) As Boolean + Return Arr?.Count > 0 + End Function ''' ''' 获取格式类似于“11:08:52.037”的当前时间的字符串。 @@ -2462,7 +2478,7 @@ NextElement: ''' Public Sub RunInThread(Action As Action) If RunInUi() Then - RunInNewThread(Action, "Runtime Invoke " & GetUuid() & "#") + RunInNewThread(Action, "Invoke " & GetUuid()) Else Action() End If @@ -2760,11 +2776,11 @@ Retry: Using Reader As New XamlXmlReader(Stream) While Reader.Read() For Each BlackListType In {GetType(WebBrowser), GetType(Frame), GetType(MediaElement), GetType(ObjectDataProvider), GetType(XamlReader), GetType(Window), GetType(XmlDataProvider)} - If Reader.Type IsNot Nothing AndAlso BlackListType.IsAssignableFrom(Reader.Type.UnderlyingType) Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 类型。") - If Reader.Value IsNot Nothing AndAlso Reader.Value = BlackListType.Name Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 值。") + If Reader.Type IsNot Nothing AndAlso BlackListType.IsAssignableFrom(Reader.Type.UnderlyingType) Then Throw New UnauthorizedAccessException($"基于安全考虑,不允许使用 {BlackListType.Name} 类型。") + If Reader.Value IsNot Nothing AndAlso Reader.Value = BlackListType.Name Then Throw New UnauthorizedAccessException($"基于安全考虑,不允许使用 {BlackListType.Name} 值。") Next For Each BlackListMember In {"Code", "FactoryMethod", "Static"} - If Reader.Member IsNot Nothing AndAlso Reader.Member.Name = BlackListMember Then Throw New UnauthorizedAccessException($"不允许使用 {BlackListMember} 成员。") + If Reader.Member IsNot Nothing AndAlso Reader.Member.Name = BlackListMember Then Throw New UnauthorizedAccessException($"基于安全考虑,不允许使用 {BlackListMember} 成员。") Next End While End Using @@ -2922,7 +2938,7 @@ Retry: '处理错误会导致再次调用 Log() 导致无限循环 '输出日志 - Dim AppendText As String = "[" & GetTimeNow() & "] " & Text & vbCrLf '减轻同步锁占用 + Dim AppendText As String = $"[{GetTimeNow()}] <{If(Thread.CurrentThread.Name = "", "主线程", Thread.CurrentThread.Name)}> {Text}{vbCrLf}" '减轻同步锁占用 If ModeDebug Then SyncLock LogListLock LogList.Append(AppendText) @@ -2986,7 +3002,7 @@ Retry: Dim ExFull As String = Desc & ":" & GetExceptionDetail(Ex) '输出日志 - Dim AppendText As String = "[" & GetTimeNow() & "] " & Desc & ":" & GetExceptionDetail(Ex, True) & vbCrLf '减轻同步锁占用 + Dim AppendText As String = $"[{GetTimeNow()}] <{If(Thread.CurrentThread.Name = "", "主线程", Thread.CurrentThread.Name)}> {Desc}:{GetExceptionDetail(Ex, True)}{vbCrLf}" '减轻同步锁占用 If ModeDebug Then SyncLock LogListLock LogList.Append(AppendText) @@ -3048,7 +3064,7 @@ Retry: If ForceOpenLog OrElse (ShowMsgbox AndAlso MyMsgBox("若你在汇报一个 Bug,请点击 打开文件夹 按钮,并上传 Log(1~5).txt 中包含错误信息的文件。" & vbCrLf & "游戏崩溃一般与启动器无关,请不要因为游戏崩溃而提交反馈。", "反馈提交提醒", "打开文件夹", "不需要") = 1) Then OpenExplorer(Path & "PCL\Log1.txt") End If - OpenWebsite("https://github.com/Hex-Dragon/PCL2/issues/") + OpenWebsite("https://github.com/Meloong-Git/PCL/issues/") End Sub Public Function CanFeedback(ShowHint As Boolean) As Boolean If False.Equals(PageSetupSystem.IsLauncherNewest) Then diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb index c0bb4c8..4719587 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb @@ -339,7 +339,7 @@ [Error] = ex State = LoadState.Failed End Try - End Sub) With {.Name = Name, .Priority = ThreadPriority} + End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority} LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted End Sub Public Overrides Sub Abort() diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index f7d1b3c..37c8c2e 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -1,301 +1,41 @@ -Imports System.Drawing -Imports System.Net.Http -Imports System.Net.Http.Headers +Imports System.Net.Http.Headers Imports System.Threading.Tasks -Imports Newtonsoft.Json +Imports CacheCow.Client Public Module ModNet - Public Const NetDownloadEnd As String = ".PCLDownloading" - Public ReadOnly HttpProxy As New HttpWebProxyFactory() - Private ClientFactory As New HttpClientFactory() - Private Request As New HttpRequest() - Public Class HttpRequest - Public Sub New() - End Sub - Public Function GetRequestMessage(RequestUrl As String, - Optional Method As String = "GET", - Optional Headers As Dictionary(Of String, String) = Nothing, - Optional Data As Byte() = Nothing, - Optional UseBrowserUA As Boolean = False, - Optional RequireHeaderSign As Boolean = False, - Optional RequireCdnSign As Boolean = False) As HttpRequestMessage - Dim RequestMethod As HttpMethod = GetHttpMethod(Method) - If RequireCdnSign Then RequestUrl = SecretCdnSign(RequestUrl) - - Dim RequestMessage As New HttpRequestMessage(RequestMethod, RequestUrl) - '基本参数验证 - If Data IsNot Nothing AndAlso Not {HttpMethod.Get, HttpMethod.Head}.Contains(RequestMethod) Then - RequestMessage.Content = New ByteArrayContent(Data) - RequestMessage.Content.Headers.ContentLength = Data.Length - End If - - If Headers IsNot Nothing Then - For Each Header In Headers - If Header.Key.ContainsF("Content") AndAlso RequestMessage.Content IsNot Nothing Then - 'FixContentHeader 往下挪一行就 NullReference 了 -FixContentHeader: - If Data Is Nothing OrElse RequestMessage.Content Is Nothing Then Continue For - If RequestMessage.Content.Headers.Contains(Header.Key) Then Continue For - If Not RequestMessage.Content.Headers.Contains(Header.Key) Then - If Header.Key.ToLower() = "Content-Type" Then RequestMessage.Content.Headers.ContentType = New MediaTypeHeaderValue(Header.Value) - RequestMessage.Content.Headers.TryAddWithoutValidation(Header.Key, Header.Value) - End If - Else - '不知道为什么 Content-Type 标头会通过字符串验证然后加入不应该被加入的 HttpRequestMessage Headers - '这会导致 HttpRequestMessage 内部的标头验证失败,并抛出一个 Exception 导致网络请求失败 - If Header.Key.ContainsF("Content") Then GoTo FixContentHeader - If RequestMessage.Headers.Contains(Header.Key) Then Continue For - RequestMessage.Headers.Add(Header.Key, Header.Value) - End If - Next - End If - - If RequireHeaderSign Then - SecretHeadersSign(RequestUrl, requestMessage, UseBrowserUA) - End If - - Return RequestMessage - - End Function - - ' 发送请求 - Public Function SendRequest(Req As HttpRequestMessage, - Optional RequireDecode As Boolean = True, - Optional Encode As Encoding = Nothing, - Optional Timeout As Integer = 25000, - Optional RequireReturnRespObj As Boolean = False) - Using CTS As New CancellationTokenSource(Timeout) - Dim Client = ClientFactory.GetHttpClient() - Dim Resp As HttpResponseMessage = Client.SendAsync(Req, HttpCompletionOption.ResponseHeadersRead, CTS.Token).GetAwaiter().GetResult() - - If RequireReturnRespObj Then Return Resp - - Resp.EnsureSuccessStatusCode() - CTS.CancelAfter(Timeout) - Dim ResultStream = Resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult() - Using DataStream As New MemoryStream() - ResultStream.CopyTo(DataStream) - Dim Bytes = DataStream.ToArray() - Resp.Dispose() - Req.Dispose() - - If RequireDecode Then - If Encode Is Nothing Then Encode = Encoding.UTF8 - Return Encode.GetString(Bytes) - Else - Return Bytes - End If - End Using - End Using - - - End Function - - ' 获取HttpMethod - Private Function GetHttpMethod(Method As String) As HttpMethod - Select Case Method?.Trim().ToUpperInvariant() - Case "HEAD" - Return HttpMethod.Head - Case "POST" - Return HttpMethod.Post - Case "PUT" - Return HttpMethod.Put - Case "DELETE" - Return HttpMethod.Delete - Case Else - Return HttpMethod.Get - End Select - End Function - - - End Class - - Public Class HttpWebProxyFactory - Implements IWebProxy - Public DisableHttpProxy As Boolean = True - Public ProxyAddress As String - Private CurrentProxy As WebProxy - Public RequiredReloadProxy As Boolean - Public RefreshSyncLock As New Object - Public SystemProxy As New WebProxy() - Public Sub New() - End Sub - - Public Property Credentials As ICredentials Implements IWebProxy.Credentials - Get - Return AuthCredentials - End Get - Set(value As ICredentials) - AuthCredentials = value - End Set - End Property - Private AuthCredentials As ICredentials - - Public Function GetProxy(destination As Uri) As Uri Implements IWebProxy.GetProxy - Return GetProxy(destination.AbsoluteUri).Address - End Function - Public Function GetProxy(destination As String) As WebProxy - If CurrentProxy IsNot Nothing AndAlso Not RequiredReloadProxy Then Return CurrentProxy - If RequiredReloadProxy OrElse CurrentProxy Is Nothing Then - If ProxyAddress IsNot Nothing OrElse - Not String.IsNullOrWhiteSpace(ProxyAddress) Then - SyncLock RefreshSyncLock - CurrentProxy = New WebProxy(ProxyAddress, True) - If AuthCredentials IsNot Nothing Then CurrentProxy.Credentials = AuthCredentials - Return CurrentProxy - End SyncLock - Else - SyncLock RefreshSyncLock - CurrentProxy = New WebProxy() - If AuthCredentials IsNot Nothing Then CurrentProxy.Credentials = AuthCredentials - Return CurrentProxy - End SyncLock - End If - End If - Return Nothing - End Function - Public Function IsBypassed(host As Uri) As Boolean Implements IWebProxy.IsBypassed - If DisableHttpProxy Then Return True - If CurrentProxy Is Nothing Then - SyncLock RefreshSyncLock - CurrentProxy = GetProxy(host.AbsoluteUri) - End SyncLock - End If - If CurrentProxy.IsBypassed(host) Then Return True - Return False - End Function - End Class - - Public Class HttpClientFactory - Private PreviousClient As HttpClient - Private CurrentClient As HttpClient - Private HttpClientLock As New Object - Public ClientHandler As New HttpClientHandler() With { - .UseProxy = True, - .Proxy = HttpProxy, - .AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None - } - Public Sub New() - HttpClientReload() - End Sub - Public Function GetHttpClient(Optional Handler As HttpClientHandler = Nothing) As HttpClient - SyncLock HttpClientLock - If Handler IsNot Nothing Then - ClientHandler = Handler - CurrentClient.Dispose() - CurrentClient = New HttpClient(Handler) - Return CurrentClient - End If - If CurrentClient IsNot Nothing Then Return CurrentClient - CurrentClient = New HttpClient(ClientHandler) With {.Timeout = Timeout.InfiniteTimeSpan} - Return CurrentClient - End SyncLock - End Function - Public Sub HttpClientReload() - RunInNewThread( - Sub() - While True - Thread.Sleep(TimeSpan.FromHours(6)) - If PreviousClient IsNot Nothing Then PreviousClient.Dispose() - PreviousClient = CurrentClient - CurrentClient = GetHttpClient() - End While - End Sub - ) - End Sub - - End Class +#Region "网络请求" ''' - ''' 测试 Ping。失败则返回 -1。 + ''' 发送一次网络请求并获取返回内容。 ''' - ''' - Public Function Ping(Ip As String, Optional Timeout As Integer = 10000, Optional MakeLog As Boolean = True) As Integer - Dim PingResult As NetworkInformation.PingReply + Public Function NetRequestByClient(Url As String, Optional Method As HttpMethod = Nothing, + Optional Content As Object = Nothing, Optional ContentType As String = Nothing, Optional Accept As String = "*/*", + Optional Timeout As Integer = 25000, Optional Headers As String(,) = Nothing, + Optional Encoding As Encoding = Nothing, Optional UseBrowserUserAgent As Boolean = False) As String + If Method Is Nothing Then Method = HttpMethod.Get + Log("[Net] 发起网络请求(" & Method.Method & "," & Url & "),最大超时 " & Timeout) Try - PingResult = (New NetworkInformation.Ping).Send(Ip) + Dim HeaderDictionary = If(Headers, {}).ToDictionary + If ContentType IsNot Nothing Then HeaderDictionary("Content-Type") = ContentType + If Accept IsNot Nothing Then HeaderDictionary("Accept") = Accept + Return If(Encoding, Encoding.UTF8).GetString(SendRequest(Url, Method, Content, HeaderDictionary, Timeout:=Timeout, Encoding:=Encoding, UseBrowserUserAgent:=UseBrowserUserAgent)) + Catch ex As ThreadInterruptedException + Throw Catch ex As Exception - If MakeLog Then Log("[Net] Ping " & Ip & " 失败:" & ex.Message) - Return -1 + Log(ex, "网络请求失败", LogLevel.Developer) + Throw End Try - If PingResult.Status = NetworkInformation.IPStatus.Success Then - If MakeLog Then Log("[Net] Ping " & Ip & " 结束:" & PingResult.RoundtripTime & "ms") - Return PingResult.RoundtripTime - Else - If MakeLog Then Log("[Net] Ping " & Ip & " 失败") - Return -1 - End If End Function - ''' - ''' 以 WebClient 获取网页源代码。会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。 + ''' 发送网络请求并获取返回内容。 + ''' 会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。 ''' - ''' 网页的 Url。 - ''' 网页的编码,通常为 UTF-8。 - Public Function NetGetCodeByClient(Url As String, Encoding As Encoding, Optional Accept As String = "application/json, text/javascript, */*; q=0.01", Optional UseBrowserUserAgent As Boolean = False) As String - Dim RetryCount As Integer = 0 - Dim RetryException As Exception = Nothing - Dim StartTime As Long = GetTimeTick() - Try -Retry: - Select Case RetryCount - Case 0 '正常尝试 - Return NetGetCodeByClient(Url, Encoding, 10000, Accept, UseBrowserUserAgent) - Case 1 '慢速重试 - Thread.Sleep(500) - Return NetGetCodeByClient(Url, Encoding, 30000, Accept, UseBrowserUserAgent) - Case Else '快速重试 - If GetTimeTick() - StartTime > 5500 Then - '若前两次加载耗费 5 秒以上,才进行重试 - Thread.Sleep(500) - Return NetGetCodeByClient(Url, Encoding, 4000, Accept, UseBrowserUserAgent) - Else - Throw RetryException - End If - End Select - Catch ex As Exception - Select Case RetryCount - Case 0 - RetryException = ex - RetryCount += 1 - GoTo Retry - Case 1 - RetryCount += 1 - GoTo Retry - Case Else - Throw - End Select - End Try - End Function - Public Function NetGetCodeByClient(Url As String, Encoding As Encoding, Timeout As Integer, Accept As String, Optional UseBrowserUserAgent As Boolean = False) As String - Log("[Net] 获取客户端网络结果:" & Url & ",最大超时 " & Timeout) - Try - Dim Headers As New Dictionary(Of String, String) From { - {"Accept", Accept} - } - Using Req As HttpRequestMessage = Request.GetRequestMessage(Url, Headers:=Headers, RequireHeaderSign:=True, RequireCdnSign:=True, UseBrowserUA:=UseBrowserUserAgent) - Req.Headers.AcceptLanguage.Add(New StringWithQualityHeaderValue("en-US,en;q=0.5")) - Req.Headers.TryAddWithoutValidation("X-Request-With","XMLHttpRequest") - Dim Resp As String = Request.SendRequest(Req, Encode:=Encoding, Timeout:=Timeout) - - Return Resp - End Using - Catch ex As TaskCanceledException - Throw New TaskCanceledException("连接服务器超时(" & Url & ")", ex) - Catch ex As Exception - Throw New WebException("获取结果失败," & ex.Message & "(" & Url & ")", ex) - End Try - End Function - - ''' - ''' 以 WebRequest 获取网页源代码或 Json。会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。 - ''' - ''' 网页的 Url。 - ''' 网页的编码,通常为 UTF-8。 ''' 如果第一次尝试失败,换用的备用 URL。 - Public Function NetGetCodeByRequestRetry(Url As String, Optional Encode As Encoding = Nothing, Optional Accept As String = "", - Optional IsJson As Boolean = False, Optional BackupUrl As String = Nothing, Optional UseBrowserUserAgent As Boolean = False) + Public Function NetRequestByClientRetry(Url As String, Optional Method As HttpMethod = Nothing, Optional BackupUrl As String = Nothing, + Optional Content As Object = Nothing, Optional ContentType As String = Nothing, Optional Accept As String = "*/*", + Optional Headers As String(,) = Nothing, + Optional Encoding As Encoding = Nothing, Optional UseBrowserUserAgent As Boolean = False) As String Dim RetryCount As Integer = 0 Dim RetryException As Exception = Nothing Dim StartTime As Long = GetTimeTick() @@ -303,15 +43,15 @@ Retry: Retry: Select Case RetryCount Case 0 '正常尝试 - Return NetGetCodeByRequestOnce(Url, Encode, 10000, IsJson, Accept, UseBrowserUserAgent) + Return NetRequestByClient(Url, Method, Content, ContentType, Accept, 10000, Headers, Encoding, UseBrowserUserAgent) Case 1 '慢速重试 Thread.Sleep(500) - Return NetGetCodeByRequestOnce(If(BackupUrl, Url), Encode, 30000, IsJson, Accept, UseBrowserUserAgent) + Return NetRequestByClient(Url, Method, Content, ContentType, Accept, 30000, Headers, Encoding, UseBrowserUserAgent) Case Else '快速重试 If GetTimeTick() - StartTime > 5500 Then '若前两次加载耗费 5 秒以上,才进行重试 Thread.Sleep(500) - Return NetGetCodeByRequestOnce(If(BackupUrl, Url), Encode, 4000, IsJson, Accept, UseBrowserUserAgent) + Return NetRequestByClient(Url, Method, Content, ContentType, Accept, 4000, Headers, Encoding, UseBrowserUserAgent) Else Throw RetryException End If @@ -333,20 +73,22 @@ Retry: End Try End Function ''' - ''' 以 WebRequest 获取网页源代码或 Json。会逐渐生成 4 个尝试线程,并在 60s 后超时。 + ''' 生成数个线程,同时发送网络请求并获取返回内容。 + ''' 每个线程开始之前会略有延迟。 ''' - ''' 网页的 Url。 - ''' 网页的编码,通常为 UTF-8。 - Public Function NetGetCodeByRequestMultiple(Url As String, Optional Encode As Encoding = Nothing, Optional Accept As String = "", Optional IsJson As Boolean = False) + Public Function NetRequestByClientMultiple(Url As String, Optional Method As HttpMethod = Nothing, Optional ThreadCount As Integer = 3, + Optional Content As Object = Nothing, Optional ContentType As String = Nothing, Optional Accept As String = "*/*", + Optional Headers As String(,) = Nothing, Optional Timeout As Integer = 25000, + Optional Encoding As Encoding = Nothing, Optional UseBrowserUserAgent As Boolean = False) As String Dim Threads As New List(Of Thread) Dim RequestResult = Nothing Dim RequestEx As Exception = Nothing Dim FailCount As Integer = 0 - For i = 1 To 4 + For i = 1 To ThreadCount Dim th As New Thread( Sub() Try - RequestResult = NetGetCodeByRequestOnce(Url, Encode, 30000, IsJson, Accept) + RequestResult = NetRequestByClient(Url, Method, Content, ContentType, Accept, Timeout, Headers, Encoding, UseBrowserUserAgent) Catch ex As Exception FailCount += 1 RequestEx = ex @@ -367,7 +109,7 @@ RequestFinished: Catch End Try Return RequestResult - ElseIf FailCount = 4 Then + ElseIf FailCount = ThreadCount Then Try For Each th In Threads If th.IsAlive Then th.Interrupt() @@ -378,112 +120,71 @@ RequestFinished: End If Thread.Sleep(20) Loop - Throw New Exception("未知错误") - End Function - Public Function NetGetCodeByRequestOnce(Url As String, Optional Encode As Encoding = Nothing, Optional Timeout As Integer = 30000, Optional IsJson As Boolean = False, Optional Accept As String = "", Optional UseBrowserUserAgent As Boolean = False) - If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求") - Log($"[Net] 获取网络结果:{Url},超时 {Timeout}ms{If(IsJson, ",要求 json", "")}") - Try - Dim Headers As New Dictionary(Of String, String) From { - {"Accept", If(Accept = "", "*/*", Accept)} - } - Using Req As HttpRequestMessage = Request.GetRequestMessage(Url, Headers:=Headers, UseBrowserUA:=UseBrowserUserAgent, RequireHeaderSign:=True, RequireCdnSign:=True) - Dim Resp As String = Request.SendRequest(Req, Encode:=Encode, Timeout:=Timeout) - Return If(IsJson, GetJson(Resp), Resp) - End Using - - Catch ex As ThreadInterruptedException - Throw - - Catch ex As TaskCanceledException - Throw New TimeoutException($"获取结果失败({ex.Message},{Url})", ex) - Catch ex As HttpRequestException - Throw New WebException($"获取结果失败({ex.Message},{Url})", ex) - End Try + Return Nothing End Function ''' - ''' 以多线程下载网页文件的方式获取网页源代码。 + ''' 以多线程下载网页文件的方式获取内容。 + ''' 不支持缓存协商;若需缓存协商,需要换用 NetRequestByClient。 ''' - ''' 网页的 Url。 - Public Function NetGetCodeByLoader(Url As String, Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String - Dim Temp As String = RequestTaskTempFolder() & "download.txt" - Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile({Url}, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)}) - Try - NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(" & Url & ")") - NetGetCodeByLoader = ReadFile(Temp) - File.Delete(Temp) - Finally - NewTask.Abort() - End Try + Public Function NetRequestByLoader(Url As String, Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String + Return NetRequestByLoader({Url}, Timeout, IsJson, UseBrowserUserAgent) End Function ''' - ''' 以多线程下载网页文件的方式获取网页源代码。 + ''' 以多线程下载网页文件的方式获取内容。 + ''' 不支持缓存协商;若需缓存协商,需要换用 NetRequestByClient。 ''' - ''' 网页的 Url 列表。 - Public Function NetGetCodeByLoader(Urls As IEnumerable(Of String), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String + Public Function NetRequestByLoader(Urls As IEnumerable(Of String), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String Dim Temp As String = RequestTaskTempFolder() & "download.txt" Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)}) Try NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls.First & ")") - NetGetCodeByLoader = ReadFile(Temp) + NetRequestByLoader = ReadFile(Temp) File.Delete(Temp) Finally NewTask.Abort() End Try End Function +#End Region + +#Region "文件下载" + ''' - ''' 使用 WebClient 从网络中下载文件。这不能下载 CDN 中的文件。 + ''' 通过网络请求直接下载小文件,若文件已存在将被覆盖。 + ''' 不建议用于下载大文件。 ''' - ''' 网络 Url。 - ''' 下载的本地地址。 - Public Sub NetDownloadByClient(Url As String, LocalFile As String, Optional UseBrowserUserAgent As Boolean = False) - Log("[Net] 直接下载文件:" & Url) - '初始化 + Public Sub NetDownloadByClient(Url As String, LocalPath As String) + Log($"[Net] 通过网络请求直接下载小文件:{Url} → {LocalPath}") + '初始化文件路径 Try - '建立目录 - Directory.CreateDirectory(GetPathFromFullPath(LocalFile)) - '尝试删除原文件 - File.Delete(LocalFile) + Directory.CreateDirectory(GetPathFromFullPath(LocalPath)) + File.Delete(LocalPath) Catch ex As Exception - Throw New WebException($"预处理下载文件路径失败({LocalFile})", ex) + Throw New Exception($"预处理下载文件路径失败({LocalPath})", ex) End Try '下载 - Using Client As New WebClient - Try - 'SecretHeadersSign(Url, Client, UseBrowserUserAgent) - Client.DownloadFile(Url, LocalFile) - Catch ex As Exception - File.Delete(LocalFile) - Throw New WebException($"直接下载文件失败({Url})", ex) - End Try - End Using - End Sub - - ''' - ''' 简单的多线程下载文件。可以下载 CDN 中的文件。 - ''' - ''' 文件的 Url。 - ''' 下载的本地地址。 - Public Sub NetDownloadByLoader(Url As String, LocalFile As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False) - Dim NewTask As New LoaderDownload("文件下载 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile({Url}, LocalFile, Check, UseBrowserUserAgent)}) Try - NewTask.WaitForExit(LoaderToSyncProgress:=LoaderToSyncProgress) + File.WriteAllBytes(LocalPath, SendRequest(Url, HttpMethod.Get)) Catch ex As Exception - Throw New WebException($"多线程直接下载文件失败({Url})", ex) - Finally - NewTask.Abort() + File.Delete(LocalPath) + Throw New WebException($"通过网络请求直接下载小文件失败({LocalPath})", ex) End Try End Sub ''' - ''' 简单的多线程下载文件。可以下载 CDN 中的文件。 + ''' 通过多线程下载引擎下载文件。 + ''' + Public Sub NetDownloadByLoader(Url As String, LocalPath As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False) + NetDownloadByLoader({Url}, LocalPath, LoaderToSyncProgress, Check, UseBrowserUserAgent) + End Sub + ''' + ''' 通过多线程下载引擎下载文件。 ''' ''' 文件的 Url 列表。 - ''' 下载的本地地址。 - Public Sub NetDownloadByLoader(Urls As IEnumerable(Of String), LocalFile As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False) - Dim NewTask As New LoaderDownload("文件下载 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, LocalFile, Check, UseBrowserUserAgent)}) + ''' 下载的本地地址。 + Public Sub NetDownloadByLoader(Urls As IEnumerable(Of String), LocalPath As String, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False) + Dim NewTask As New LoaderDownload($"文件下载 {GetUuid()}#", New List(Of NetFile) From {New NetFile(Urls, LocalPath, Check, UseBrowserUserAgent)}) Try NewTask.WaitForExit(LoaderToSyncProgress:=LoaderToSyncProgress) Catch ex As Exception @@ -493,140 +194,86 @@ RequestFinished: End Try End Sub - ''' - ''' 发送一个网络请求并获取返回内容,会重试三次并在最长 45s 后超时。 - ''' - ''' 请求的服务器地址。 - ''' 请求方式(POST 或 GET)。 - ''' 请求的内容。 - ''' 请求的套接字类型。 - ''' 当返回 40x 时不重试。 - Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing, Optional RequireReturnRespObj As Boolean = False) As String - Dim RetryCount As Integer = 0 - Dim RetryException As Exception = Nothing - Dim StartTime As Long = GetTimeTick() +#End Region + +#Region "基础请求引擎" + + '基础请求函数 + Private Function SendRequest(Url As String, Method As HttpMethod, + Optional Content As Object = Nothing, Optional Headers As Dictionary(Of String, String) = Nothing, + Optional UseBrowserUserAgent As Boolean = False, Optional Timeout As Integer = 25000, Optional Encoding As Encoding = Nothing) As Byte() + If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求") + Dim Request As HttpRequestMessage = Nothing, CancelToken As CancellationTokenSource = Nothing, Response As HttpResponseMessage = Nothing Try -Retry: - Select Case RetryCount - Case 0 '正常尝试 - Return NetRequestOnce(Url, Method, Data, ContentType, 15000, Headers, RequiredReturnRespObj:=RequireReturnRespObj) - Case 1 '慢速重试 - Thread.Sleep(500) - Return NetRequestOnce(Url, Method, Data, ContentType, 25000, Headers, RequiredReturnRespObj:=RequireReturnRespObj) - Case Else '快速重试 - If GetTimeTick() - StartTime > 5500 Then - '若前两次加载耗费 5 秒以上,才进行重试 - Thread.Sleep(500) - Return NetRequestOnce(Url, Method, Data, ContentType, 4000, Headers,RequiredReturnRespObj:=RequireReturnRespObj) + Static Client As HttpClient = ClientExtensions.CreateClient(New FileCacheStore.FileStore(PathTemp & "Cache/Http/"), New HttpClientHandler With { + .AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None, + .UseCookies = False '不设为 False 就不能从 Header 手动传入 Cookies + }) + Url = SecretCdnSign(Url) + Request = New HttpRequestMessage(Method, Url) + '写入 Content + If Content IsNot Nothing AndAlso Request.Method <> HttpMethod.Get AndAlso Request.Method <> HttpMethod.Head Then + Dim Bytes As Byte() = If(TypeOf Content Is Byte(), Content, If(Encoding, Encoding.UTF8).GetBytes(Content.ToString)) + Request.Content = New ByteArrayContent(Bytes) + Request.Content.Headers.ContentLength = Bytes.Length + End If + '写入 Headers + If Headers IsNot Nothing Then + For Each Header In Headers + If Header.Key.ToLower = "content-type" Then + Request.Content?.Headers.TryAddWithoutValidation(Header.Key, Header.Value) Else - Throw RetryException + Request.Headers.Add(Header.Key, Header.Value) End If - End Select + Next + End If + SecretHeadersSign(Url, Request, UseBrowserUserAgent) + '发送请求 + CancelToken = New CancellationTokenSource(Timeout) + Response = Client.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetAwaiter().GetResult() + Dim ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() + Dim ResponseBytes As Byte() + Using Stream As New MemoryStream + ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetAwaiter().GetResult() + ResponseBytes = Stream.ToArray() + End Using + '输出 + If Response.IsSuccessStatusCode Then + Return ResponseBytes + Else + Throw New ResponsedWebException($"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Method},{Url}", If(Encoding, Encoding.UTF8).GetString(ResponseBytes)) + End If Catch ex As ThreadInterruptedException Throw + Catch ex As ResponsedWebException + Throw + Catch ex As TaskCanceledException 'CancellationTokenSource 超时 + Throw New WebException($"连接服务器超时,请检查你的网络环境是否良好({Method}, {Url})", WebExceptionStatus.Timeout) Catch ex As Exception - If ex.InnerException IsNot Nothing AndAlso ex.InnerException.Message.Contains("(40") AndAlso DontRetryOnRefused Then Throw - Select Case RetryCount - Case 0 - If ModeDebug Then Log(ex, "[Net] 网络请求第一次失败(" & Url & ")") - RetryException = ex - RetryCount += 1 - GoTo Retry - Case 1 - If ModeDebug Then Log(ex, "[Net] 网络请求第二次失败(" & Url & ")") - RetryCount += 1 - GoTo Retry - Case Else - Throw - End Select + Throw New Exception($"网络请求出现意外异常({Method}, {Url})", ex) + Finally + Request?.Dispose() + CancelToken?.Dispose() + Response?.Dispose() End Try End Function - ''' - ''' 同时发送多个网络请求并要求返回内容。 - ''' - Public Function NetRequestMultiple(Url As String, Method As String, Data As Object, ContentType As String, Optional RequestCount As Integer = 4, Optional Headers As Dictionary(Of String, String) = Nothing, Optional MakeLog As Boolean = True) - Dim Threads As New List(Of Thread) - Dim RequestResult = Nothing - Dim RequestEx As Exception = Nothing - Dim FailCount As Integer = 0 - For i = 1 To RequestCount - Dim th As New Thread( - Sub() - Try - RequestResult = NetRequestOnce(Url, Method, Data, ContentType, 30000, Headers, MakeLog) - Catch ex As Exception - FailCount += 1 - RequestEx = ex - End Try - End Sub) - th.Start() - Threads.Add(th) - Thread.Sleep(i * 250) - If RequestResult IsNot Nothing Then GoTo RequestFinished - Next - Do While True - If RequestResult IsNot Nothing Then -RequestFinished: - For Each th In Threads - If th.IsAlive Then th.Interrupt() - Next - Return RequestResult - ElseIf FailCount = RequestCount Then - For Each th In Threads - If th.IsAlive Then th.Interrupt() - Next - Throw RequestEx - End If - Thread.Sleep(20) - Loop - Throw New Exception("未知错误") - End Function - ''' - ''' 发送一次网络请求并获取返回内容。 - ''' - Public Function NetRequestOnce(Url As String, Method As String, Data As Object, ContentType As String, Optional Timeout As Integer = 25000, Optional Headers As Dictionary(Of String, String) = Nothing, Optional MakeLog As Boolean = True, Optional UseBrowserUserAgent As Boolean = False, Optional RequiredReturnRespObj As Boolean = False) As Object - If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求") - If MakeLog Then Log("[Net] 发起网络请求(" & Method & "," & Url & "),最大超时 " & Timeout) - Try - Dim ReqHeaders As New Dictionary(Of String, String) - If ContentType IsNot Nothing AndAlso Data IsNot Nothing Then - ' 可能指定了 Content-Type 但是 Headers 是 Nothing - If Headers IsNot Nothing Then - If Not Headers.ContainsKey("Content-Type") Then Headers.Add("Content-Type", ContentType) - Else - If Not ReqHeaders.ContainsKey("Content-Type") Then ReqHeaders.Add("Content-Type", ContentType) - End If - End If - Dim ReqData As Byte() = If(TypeOf Data Is String, Encoding.UTF8.GetBytes(Data), Data) - - Dim Req As HttpRequestMessage = Request.GetRequestMessage(Url, Method, If(Headers, ReqHeaders), If(Data IsNot Nothing, ReqData, Nothing), UseBrowserUserAgent, True, True) - Return Request.SendRequest(Req, If(RequiredReturnRespObj, False, True), Timeout:=Timeout, RequireReturnRespObj:=RequiredReturnRespObj) - Catch ex As ThreadInterruptedException - Throw - Catch ex As TaskCanceledException - Return New WebException($"连接服务器超时,请检查你的网络环境是否良好({ex.Message},{Url})", ex) - Catch ex As HttpRequestException - If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer) - Throw New WebException(ex.Message, ex) - Catch ex As Exception - ex = New WebException("网络请求失败(" & Url & ")", ex) - If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer) - Throw ex - End Try - End Function Public Class ResponsedWebException Inherits WebException ''' ''' 远程服务器给予的回复。 ''' Public Overloads Property Response As String - Public Sub New(Message As String, Response As String, InnerException As Exception) - MyBase.New(Message, InnerException) + Public Sub New(Message As String, Response As String) + MyBase.New(Message) Me.Response = Response End Sub End Class +#End Region + +#Region "多线程下载引擎" + ''' ''' 最大线程数。 ''' @@ -659,7 +306,10 @@ RequestFinished: Public Url As String Public FailCount As Integer Public Ex As Exception - Public Thread As NetThread + ''' + ''' 若该下载源正在进行强制单线程下载,标记这个唯一的线程。 + ''' + Public SingleThread As NetThread Public IsFailed As Boolean Public Overrides Function ToString() As String Return Url @@ -728,7 +378,7 @@ RequestFinished: ''' 下载线程。 ''' Public Class NetThread - Implements IEnumerable(Of NetThread) + Implements IEnumerable(Of NetThread), IEquatable(Of NetThread) ''' ''' 对应的下载任务。 @@ -787,7 +437,7 @@ RequestFinished: SyncLock Task.LockChain If NextThread Is Nothing Then If Task.IsUnknownSize Then - Return 5 * 1024 * 1024 * 1024L '5G + Return 1000 * 1000 * 1000 * 1000L '约 1T Else Return Task.FileSize - 1 End If @@ -861,6 +511,19 @@ RequestFinished: ''' Public Source As NetSource + '允许进行 UUID 比较 + Public Overloads Function Equals(other As NetThread) As Boolean Implements IEquatable(Of NetThread).Equals + Return other IsNot Nothing AndAlso Uuid = other.Uuid + End Function + Public Overrides Function Equals(obj As Object) As Boolean + Return Equals(TryCast(obj, NetThread)) + End Function + Public Shared Operator =(left As NetThread, right As NetThread) As Boolean + Return EqualityComparer(Of NetThread).Default.Equals(left, right) + End Operator + Public Shared Operator <>(left As NetThread, right As NetThread) As Boolean + Return Not left = right + End Operator End Class ''' ''' 下载单个文件。 @@ -886,12 +549,17 @@ RequestFinished: ''' Public SourcesOnce As New SafeList(Of NetSource) ''' + ''' 仅当合并失败或首次下载失败时,会将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载。 + ''' 这一策略可以兼容多个下载源中的一部分返回错误的文件的情况,以及部分在多线程下载时会抽风的源。 + ''' + Private Retried As Boolean = False + ''' ''' 获取从某个源开始,第一个可用的源。 ''' Private Function GetSource(Optional Id As Integer = 0) As NetSource If Id >= Sources.Count OrElse Id < 0 Then Id = 0 SyncLock LockSource - If Not IsSourceFailed(False) Then + If HasAvailableSource(False) Then '存在多线程可用源 Dim CurrentSource As NetSource = Sources(Id) While CurrentSource.IsFailed @@ -902,7 +570,7 @@ RequestFinished: Return CurrentSource ElseIf SourcesOnce.Any Then '仅存在单线程可用源 - Return SourcesOnce(0) + Return SourcesOnce.First Else '没有可用源 Return Nothing @@ -912,14 +580,12 @@ RequestFinished: ''' ''' 是否已经没有可用源了。 ''' - Public Function IsSourceFailed(Optional AllowOnceSource As Boolean = True) As Boolean - If AllowOnceSource AndAlso SourcesOnce.Any Then Return False + Public Function HasAvailableSource(Optional AllowOnceSource As Boolean = True) As Boolean SyncLock LockSource - For Each Source As NetSource In Sources - If Not Source.IsFailed Then Return False - Next + If AllowOnceSource AndAlso SourcesOnce.Any Then Return True + If Sources.Any(Function(s) Not s.IsFailed) Then Return True End SyncLock - Return True + Return False End Function ''' @@ -961,12 +627,11 @@ RequestFinished: Get Return IsUnknownSize OrElse FileSize < FilePieceLimit End Get - End Property ''' - ''' 为不需要分割的小文件进行临时存储。 + ''' 为不需要分割的小文件在内存中提供临时存储。 ''' - Private SmailFileCache As Queue(Of Byte) + Private Cache As MemoryStream ''' ''' 文件的已下载大小。 @@ -1101,7 +766,7 @@ RequestFinished: Try '条件检测 - If NetTaskThreadCount >= NetTaskThreadLimit OrElse IsSourceFailed() OrElse + If NetTaskThreadCount >= NetTaskThreadLimit OrElse Not HasAvailableSource() OrElse (IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Error) Then Return Nothing If State >= NetState.Merge OrElse State = NetState.WaitForCheck Then Return Nothing SyncLock LockState @@ -1113,20 +778,12 @@ RequestFinished: SyncLock LockChain '获取线程起点与下载源 '不分割 - If IsNoSplit Then GoTo Capture - '单线程 - If IsSourceFailed(False) Then - '确认没有其他线程正使用此点 - If SourcesOnce(0).Thread IsNot Nothing AndAlso SourcesOnce(0).Thread.State <> NetState.Error Then Return Nothing - '占用此点 -Capture: - SmailFileCache = Nothing - Threads = Nothing - NetManager.DownloadDone -= DownloadDone - SyncLock LockDone - DownloadDone = 0 - End SyncLock - SpeedLastDone = 0 + If IsNoSplit Then GoTo StartSingleThreadDownload + '只剩下单线程可用点 + If Not HasAvailableSource(False) Then + If SourcesOnce.First.SingleThread IsNot Nothing AndAlso SourcesOnce.First.SingleThread.State <> NetState.Error Then Return Nothing +StartSingleThreadDownload: + Reset() State = NetState.Get End If '首个开始点 @@ -1164,7 +821,7 @@ StartThread: '构建线程 Dim ThreadUuid As Integer = GetUuid() If Not Tasks.Any() Then Return Nothing '由于中断,已没有可用任务 - Th = New Thread(AddressOf Thread) With {.Name = $"NetTask {Tasks(0).Uuid}/{Uuid} Download {ThreadUuid}#", .Priority = ThreadPriority.BelowNormal} + Th = New Thread(AddressOf Thread) With {.Name = $"下载 {Tasks(0).Uuid}/{Uuid}/{ThreadUuid}#", .Priority = ThreadPriority.BelowNormal} ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload} '链表处理 If ThreadInfo.IsFirstThread OrElse Threads Is Nothing Then @@ -1184,7 +841,7 @@ StartThread: NetTaskThreadCount += 1 End SyncLock SyncLock LockSource - If IsSourceFailed(False) Then SourcesOnce(0).Thread = ThreadInfo + If Not HasAvailableSource(False) Then SourcesOnce.First.SingleThread = ThreadInfo End SyncLock Th.Start(ThreadInfo) Return ThreadInfo @@ -1197,319 +854,297 @@ StartThread: ''' ''' 每个下载线程执行的代码。 ''' - Private Sub Thread(Info As NetThread) - If ModeDebug OrElse Info.DownloadStart = 0 Then Log("[Download] " & LocalName & " " & Info.Uuid & "#:开始,起始点 " & Info.DownloadStart & "," & Info.Source.Url) - Dim HttpRequest As HttpRequestMessage + Private Sub Thread(Th As NetThread) + If ModeDebug OrElse Th.DownloadStart = 0 Then Log($"[Download] {LocalName}:开始,起始点 {Th.DownloadStart},{Th.Source.Url}") Dim ResultStream As Stream = Nothing - Dim HttpResponse As HttpResponseMessage '部分下载源真的特别慢,并且只需要一个请求,例如 Ping 为 20s,如果增长太慢,就会造成类似 2.5s 5s 7.5s 10s 12.5s... 的极大延迟 '延迟过长会导致某些特别慢的链接迟迟不被掐死 - Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + Info.Source.FailCount), 30000) - Info.State = NetState.Connect + Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + Th.Source.FailCount), 30000) + Th.State = NetState.Connect Try Dim HttpDataCount As Integer = 0 - If SourcesOnce.Contains(Info.Source) AndAlso Not Info.Equals(Info.Source.Thread) Then GoTo SourceBreak - HttpRequest = Request.GetRequestMessage(Info.Source.Url, - RequireHeaderSign:=True) - '可能出现结束减去起始等于或者小于 0 的情况,导致下载抛出 ArgumentOutOfRangeException - If Not Info.IsFirstThread Then HttpRequest.Headers.Range = New RangeHeaderValue(Info.DownloadStart, Nothing) - HttpResponse = Request.SendRequest(HttpRequest, False, Timeout:=Timeout, RequireReturnRespObj:=True) + If SourcesOnce.Contains(Th.Source) AndAlso Th <> Th.Source.SingleThread Then GoTo SourceBreak + Dim HttpRequest As New HttpRequestMessage(HttpMethod.Get, Th.Source.Url) + SecretHeadersSign(Th.Source.Url, HttpRequest, UseBrowserUserAgent) + If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New RangeHeaderValue(Th.DownloadStart, Nothing) Dim ContentLength As Long = 0 - Using HttpResponse - If State = NetState.Error Then GoTo SourceBreak '快速中断 - If ModeDebug AndAlso HttpResponse.RequestMessage.RequestUri.ToString <> Info.Source.Url Then - Log($"[Download] {LocalName} {Info.Uuid}#:重定向至 {HttpResponse.RequestMessage.RequestUri.ToString}") - End If - ''从响应头获取文件名 - 'If Info.IsFirstThread Then - ' Dim FileName As String = GetFileNameFromResponse(HttpResponse) - ' If ModeDebug Then Log($"[Download] {LocalName} {Info.Uuid}#:远程文件名:{If(FileName, "未提供")}") - ' If FileName IsNot Nothing AndAlso LocalName = "待定" Then - ' LocalName = FileName - ' Log($"[Download] {LocalName} {Info.Uuid}#:从响应头获取到文件名") - ' End If - 'End If - '文件大小校验 - ContentLength = HttpResponse.Content.Headers.ContentLength.GetValueOrDefault(-1) - - If ContentLength = -1 Then - If FileSize > 1 Then - If Info.DownloadStart = 0 Then - Log($"[Download] {LocalName} {Info.Uuid}#:文件大小未知,但已从其他下载源获取,不作处理") - Else - Log($"[Download] {LocalName} {Info.Uuid}#:ContentLength 返回了 -1,无法确定是否支持分段下载,视作不支持") - GoTo NotSupportRange - End If - Else - FileSize = -1 : IsUnknownSize = True - Log($"[Download] {LocalName} {Info.Uuid}#:文件大小未知") - End If - ElseIf ContentLength < 0 Then - Throw New Exception("获取片大小失败,结果为 " & ContentLength & "。") - ElseIf Info.IsFirstThread Then - If Check IsNot Nothing Then - If ContentLength < Check.MinSize AndAlso Check.MinSize > 0 Then - Throw New Exception($"文件大小不足,获取结果为 {ContentLength},要求至少为 {Check.MinSize}。") - End If - If ContentLength <> Check.ActualSize AndAlso Check.ActualSize > 0 Then - Throw New Exception($"文件大小不一致,获取结果为 {ContentLength},要求必须为 {Check.ActualSize}。") - End If - End If - FileSize = ContentLength : IsUnknownSize = False - Log($"[Download] {LocalName} {Info.Uuid}#:文件大小 {ContentLength}({GetString(ContentLength)})") - '若文件大小大于 50 M,进行剩余磁盘空间校验 - If ContentLength > 50 * 1024 * 1024 Then - For Each Drive As DriveInfo In DriveInfo.GetDrives - Dim DriveName As String = Drive.Name.First.ToString - Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) + - If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0) - If Drive.TotalFreeSpace < RequiredSpace Then - Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" & - If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", "")) - End If - Next - End If - ElseIf FileSize < 0 Then - Throw New Exception("非首线程运行时,尚未获取文件大小") - ElseIf Info.DownloadStart > 0 AndAlso ContentLength = FileSize Then -NotSupportRange: - SyncLock LockSource - If SourcesOnce.Contains(Info.Source) Then - GoTo SourceBreak - Else - SourcesOnce.Add(Info.Source) - End If - End SyncLock - - Throw New WebException($"该下载源不支持分段下载:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") - ElseIf Not FileSize - Info.DownloadStart = ContentLength Then - Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") - End If - Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") - Info.State = NetState.Get - SyncLock LockState - If State < NetState.Get Then State = NetState.Get - End SyncLock - '创建缓存文件 - If IsNoSplit Then - Info.Temp = Nothing - SmailFileCache = New Queue(Of Byte) - Else - Info.Temp = $"{PathTemp}Download\{Uuid}_{Info.Uuid}_{RandomInteger(0, 999999)}.tmp" - ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read) - End If - '开始下载 - Using HttpStream = HttpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult() - If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000)) - Dim HttpData As Byte() = New Byte(16384) {} - HttpDataCount = HttpStream.Read(HttpData, 0, 16384) - While (IsUnknownSize OrElse Info.DownloadUndone > 0) AndAlso '判断是否下载完成 - HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Info.Source.IsFailed OrElse Info.Equals(Info.Source.Thread)) - '限速 - While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0 - Threading.Thread.Sleep(16) - End While - Dim RealDataCount As Integer = If(IsUnknownSize, HttpDataCount, Math.Min(HttpDataCount, Info.DownloadUndone)) - SyncLock NetTaskSpeedLimitLeftLock - If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft -= RealDataCount - End SyncLock - Dim DeltaTime = GetTimeTick() - Info.LastReceiveTime - If DeltaTime > 1000000 Then DeltaTime = 1 '时间刻反转导致出现极大值 - If RealDataCount > 0 Then - '有数据 - If Info.DownloadDone = 0 Then - '第一次接受到数据 - Info.State = NetState.Download - SyncLock LockState - If State < NetState.Download Then State = NetState.Download - End SyncLock - SyncLock LockCount - ConnectCount += 1 - ConnectTime += GetTimeTick() - Info.InitTime - End SyncLock - End If - SyncLock LockCount - Info.Source.FailCount = 0 - For Each Task In Tasks - Task.FailCount = 0 - Next - End SyncLock - NetManager.DownloadDone += RealDataCount - SyncLock LockDone - DownloadDone += RealDataCount - End SyncLock - Info.DownloadDone += RealDataCount - If IsNoSplit Then - If HttpData.Count = RealDataCount Then - 'SmailFileCache.AddRange(HttpData) - For Each B In HttpData - SmailFileCache.Enqueue(B) - Next - Else - 'SmailFileCache.AddRange(HttpData.ToList.GetRange(0, RealDataCount)) - For i = 0 To RealDataCount - 1 - SmailFileCache.Enqueue(HttpData(i)) - Next - End If + Using CancelToken As New CancellationTokenSource(Timeout) + Static Client As New HttpClient(New HttpClientHandler With { + .AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None + }) + Using Response As HttpResponseMessage = Client.SendAsync(HttpRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetAwaiter().GetResult() + 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}") + '文件大小校验 + ContentLength = Response.Content.Headers.ContentLength.GetValueOrDefault(-1) + If ContentLength = -1 Then + If FileSize > 1 Then + If Th.DownloadStart = 0 Then + Log($"[Download] {LocalName}:文件大小未知,但已从其他下载源获取,不作处理") Else - ResultStream.Write(HttpData, 0, RealDataCount) + Log($"[Download] {LocalName}:ContentLength 返回了 -1,无法确定是否支持分段下载,视作不支持") + GoTo NotSupportRange End If - '检查速度是否过慢 - If DeltaTime > 1500 AndAlso DeltaTime > RealDataCount Then '数据包间隔大于 1.5s,且速度小于 1.5K/s - Throw New TimeoutException("由于速度过慢断开链接,下载 " & RealDataCount & " B,消耗 " & DeltaTime & " ms。") - End If - Info.LastReceiveTime = GetTimeTick() - '已完成 - If Info.DownloadUndone = 0 AndAlso Not IsUnknownSize Then Exit While - ElseIf Info.LastReceiveTime > 0 AndAlso DeltaTime > Timeout Then - '无数据,且已超时 - Throw New TimeoutException("操作超时,无数据。") + Else + FileSize = -1 : IsUnknownSize = True + Log($"[Download] {LocalName}:文件大小未知") End If - HttpDataCount = HttpStream.Read(HttpData, 0, 16384) - End While + ElseIf ContentLength < 0 Then + Throw New Exception("获取片大小失败,结果为 " & ContentLength & "。") + ElseIf Th.IsFirstThread Then + If Check IsNot Nothing Then + If ContentLength < Check.MinSize AndAlso Check.MinSize > 0 Then + Throw New Exception($"文件大小不足,获取结果为 {ContentLength},要求至少为 {Check.MinSize}。") + End If + If ContentLength <> Check.ActualSize AndAlso Check.ActualSize > 0 Then + Throw New Exception($"文件大小不一致,获取结果为 {ContentLength},要求必须为 {Check.ActualSize}。") + End If + End If + FileSize = ContentLength : IsUnknownSize = False + Log($"[Download] {LocalName}:文件大小 {ContentLength}({GetString(ContentLength)})") + '若文件大小大于 50 M,进行剩余磁盘空间校验 + If ContentLength > 50 * 1024 * 1024 Then + For Each Drive As DriveInfo In DriveInfo.GetDrives + Dim DriveName As String = Drive.Name.First.ToString + Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) + + If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0) + If Drive.TotalFreeSpace < RequiredSpace Then + Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" & + If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", "")) + End If + Next + End If + ElseIf FileSize < 0 Then + Throw New Exception("非首线程运行时,尚未获取文件大小") + ElseIf Th.DownloadStart > 0 AndAlso ContentLength = FileSize Then +NotSupportRange: + SyncLock LockSource + If SourcesOnce.Contains(Th.Source) Then + GoTo SourceBreak + Else + SourcesOnce.Add(Th.Source) + End If + End SyncLock + Throw New WebException($"该下载源不支持分段下载:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") + ElseIf Not FileSize - Th.DownloadStart = ContentLength Then + Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") + End If + 'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") + Th.State = NetState.Get + SyncLock LockState + If State < NetState.Get Then State = NetState.Get + End SyncLock + '创建缓存文件 + If IsNoSplit Then + Th.Temp = Nothing + Cache = New MemoryStream + ResultStream = Cache + Else + Th.Temp = $"{PathTemp}Download\{Uuid}_{Th.Uuid}_{RandomInteger(0, 999999)}.tmp" + ResultStream = New FileStream(Th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read) + End If + '开始下载 + Using ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() + If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000)) + Dim ResponseBytes As Byte() = New Byte(16384) {} + HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetAwaiter().GetResult() + 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) + '限速 + While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0 + Threading.Thread.Sleep(16) + End While + Dim RealDataCount As Integer = If(IsUnknownSize, HttpDataCount, Math.Min(HttpDataCount, Th.DownloadUndone)) + SyncLock NetTaskSpeedLimitLeftLock + If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft -= RealDataCount + End SyncLock + Dim DeltaTime = GetTimeTick() - Th.LastReceiveTime + If DeltaTime > 1000000 Then DeltaTime = 1 '时间刻反转导致出现极大值 + If RealDataCount > 0 Then + '有数据 + If Th.DownloadDone = 0 Then + '第一次接受到数据 + Th.State = NetState.Download + SyncLock LockState + If State < NetState.Download Then State = NetState.Download + End SyncLock + SyncLock LockCount + ConnectCount += 1 + ConnectTime += GetTimeTick() - Th.InitTime + End SyncLock + End If + SyncLock LockCount + Th.Source.FailCount = 0 + For Each Task In Tasks + Task.FailCount = 0 + Next + End SyncLock + NetManager.DownloadDone += RealDataCount + SyncLock LockDone + DownloadDone += RealDataCount + End SyncLock + Th.DownloadDone += RealDataCount + ResultStream.Write(ResponseBytes, 0, RealDataCount) + '检查速度是否过慢 + If DeltaTime > 1500 AndAlso DeltaTime > RealDataCount Then '数据包间隔大于 1.5s,且速度小于 1.5K/s + Throw New TimeoutException("由于速度过慢断开链接,下载 " & RealDataCount & " B,消耗 " & DeltaTime & " ms。") + End If + Th.LastReceiveTime = GetTimeTick() + '已完成 + If Th.DownloadUndone = 0 AndAlso Not IsUnknownSize Then Exit While + ElseIf Th.LastReceiveTime > 0 AndAlso DeltaTime > Timeout Then + '无数据,且已超时 + Throw New TimeoutException("操作超时,无数据。") + End If + HttpDataCount = ResponseStream.Read(ResponseBytes, 0, 16384) + End While + End Using End Using End Using SourceBreak: - If State = NetState.Error OrElse Info.Source.IsFailed OrElse (Info.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then + If State = NetState.Error OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then '被外部中断 - Info.State = NetState.Error - Log($"[Download] {LocalName} {Info.Uuid}#:中断") - ElseIf HttpDataCount = 0 AndAlso Info.DownloadUndone > 0 AndAlso Not IsUnknownSize Then + Th.State = NetState.Error + Log($"[Download] {LocalName}:中断") + ElseIf HttpDataCount = 0 AndAlso Th.DownloadUndone > 0 AndAlso Not IsUnknownSize Then '服务器无返回数据 - Throw New Exception($"返回的 ContentLength 过多:ContentLength 为 {ContentLength},但获取到的总数据量仅为 {Info.DownloadDone}(全文件总数据量 {DownloadDone})") + Throw New Exception($"返回的 ContentLength 过多:ContentLength 为 {ContentLength},但获取到的总数据量仅为 {Th.DownloadDone}(全文件总数据量 {DownloadDone})") Else '本线程完成 - Info.State = NetState.Finish - If ModeDebug Then Log($"[Download] {LocalName} {Info.Uuid}#:完成,已下载 {Info.DownloadDone}") + Th.State = NetState.Finish + If ModeDebug Then Log($"[Download] {LocalName}:完成,已下载 {Th.DownloadDone}({Th.DownloadStart}~{Th.DownloadEnd})") End If Catch ex As Exception - '状态变更 - SyncLock LockCount - Info.Source.FailCount += 1 - For Each Task In Tasks - Task.FailCount += 1 - Next - End SyncLock - Dim IsTimeoutString As String = GetExceptionSummary(ex).ToLower.Replace(" ", "") - Dim IsTimeout As Boolean = IsTimeoutString.Contains("由于连接方在一段时间后没有正确答复或连接的主机没有反应") OrElse - IsTimeoutString.Contains("超时") OrElse IsTimeoutString.Contains("timeout") OrElse IsTimeoutString.Contains("timedout") - Log("[Download] " & LocalName & " " & Info.Uuid & If(IsTimeout, "#:超时(" & (Timeout * 0.001) & "s)", "#:出错," & GetExceptionDetail(ex))) - Info.State = NetState.Error - ''使用该下载源的线程是否没有速度 - ''下载超时也会导致没有速度,容易误判下载失败,所以已弃用此方法 - 'Dim IsNoSpeed As Boolean = True - 'SyncLock LockChain - ' If Threads IsNot Nothing Then - ' For Each Thread As NetThread In Threads - ' If Thread.Source.Id = Info.Source.Id AndAlso Thread.Speed > 0 Then - ' IsNoSpeed = False - ' Exit For - ' End If - ' Next - ' End If - 'End SyncLock - Info.Source.Ex = ex - '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传) - If ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("未能解析") OrElse ex.Message.Contains("(404)") OrElse - ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse ex.Message.Contains("获取到的分段大小不一致") OrElse - (ex.Message.Contains("(403)") AndAlso Not Info.Source.Url.ContainsF("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403,所以不应因此禁用下载源 - (Info.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse - Info.Source.FailCount > NetTaskThreadLimit + 2 Then - Dim IsThisFail As Boolean = False - SyncLock LockSource - If Info.Source.Thread IsNot Nothing AndAlso Info.Source.Thread.Equals(Info) Then - '单线程下,本线程出错 - SourcesOnce.RemoveAt(0) - GoTo Wrong - ElseIf Not Info.Source.IsFailed Then - '多线程下,本线程出错 -Wrong: - Info.Source.IsFailed = True - IsThisFail = True - End If - End SyncLock - '本线程引发下载源被禁用 - If IsThisFail Then - Log($"[Download] {LocalName} {Info.Uuid}#:下载源被禁用({Info.Source.Id}):{Info.Source.Url}") - Log(ex, "下载源 " & Info.Source.Id & " 已被禁用", If(ex.Message.Contains("不支持分段下载") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(416)"), LogLevel.Developer, LogLevel.Debug)) - If IsSourceFailed() Then - '没有可用源 - Log("[Download] 文件 " & LocalName & " 已无可用下载源") - Dim ExampleEx As Exception = Nothing - SyncLock LockSource - For Each Source As NetSource In Sources - Log("[Download] 已禁用的下载源:" & Source.Url) - If Source.Ex IsNot Nothing Then - ExampleEx = Source.Ex - Log(Source.Ex, "下载源禁用原因", LogLevel.Developer) - End If - Next - End SyncLock - Fail(ExampleEx) - ElseIf ex.Message.Contains("空间不足") Then - '没有空间 - Fail(ex) - End If - End If - End If - '首线程错误 - If FileSize = -2 Then - SyncLock LockChain - Threads = Nothing - End SyncLock - End If + Log($"[Download] {LocalName}:出错,{If(TypeOf ex Is TaskCanceledException, $"已超时({Timeout}ms)", GetExceptionDetail(ex))}") + SourceFail(Th, ex, False) Finally - If ResultStream IsNot Nothing Then ResultStream.Dispose() SyncLock NetTaskThreadCountLock NetTaskThreadCount -= 1 End SyncLock - '可能在没有下载完的时候开始合并文件了,这造成了大多数合并失败 - If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge() + '合并 + If Not IsNoSplit Then ResultStream?.Dispose() + If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge(Th) End Try End Sub - ''' - ''' 从 HTTP 响应头中获取文件名。 - ''' 如果没有,返回 Nothing。 - ''' - Private Function GetFileNameFromResponse(response As HttpWebResponse) As String - Dim header As String = response.Headers("Content-Disposition") - If String.IsNullOrEmpty(header) Then Return Nothing - 'attachment; filename="filename.ext" - If Not header.Contains("filename=") Then Return Nothing - Return header.AfterLast("filename=").Trim(""""c, " "c).BeforeFirst(";") - End Function + Private Sub SourceFail(Th As NetThread, ex As Exception, IsMergeFailure As Boolean) + '状态变更 + SyncLock LockCount + Th.Source.FailCount += 1 + For Each Task In Tasks + Task.FailCount += 1 + Next + End SyncLock + Th.State = NetState.Error + Th.Source.Ex = ex + '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传) + If IsMergeFailure OrElse + ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("未能解析") OrElse ex.Message.Contains("(404)") OrElse + ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse ex.Message.Contains("获取到的分段大小不一致") OrElse + (ex.Message.Contains("(403)") AndAlso Not Th.Source.Url.ContainsF("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403,所以不应因此禁用下载源 + (Th.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then + '当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理 + Dim IsThisFail As Boolean = False + SyncLock LockSource + If Not Th.Source.IsFailed OrElse Th.Source.SingleThread = Th Then + IsThisFail = True + Th.Source.IsFailed = True + End If + End SyncLock + '……后续处理 + If IsThisFail Then + Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id}):{Th.Source.Url}") + Log(ex, $"{If(SourcesOnce.FirstOrDefault?.SingleThread Is Nothing, "", "单线程")}下载源 {Th.Source.Id} 已被禁用", + If(ex.Message.Contains("不支持分段下载") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(416)"), LogLevel.Developer, LogLevel.Debug)) + SyncLock LockSource + SourcesOnce.Remove(Th.Source) + End SyncLock + If ex.Message.Contains("空间不足") Then + '硬盘空间不足:强制失败 + Fail(ex) + ElseIf HasAvailableSource() AndAlso Not IsMergeFailure Then + '当前源失败,但还有下载源:正常地继续执行 + ElseIf Not Retried AndAlso Sources.Count > 1 Then + '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载 + Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", If(ModeDebug, LogLevel.Hint, LogLevel.Debug)) '在调试模式下给出红色提示 + Retried = True + SyncLock LockSource + SourcesOnce.Clear() + For Each Source In Sources + SourcesOnce.Add(Source) + Source.IsFailed = True + Next + End SyncLock + Reset() + SyncLock LockState + State = NetState.WaitForDownload + End SyncLock + ElseIf HasAvailableSource() AndAlso IsMergeFailure Then + '合并失败且单个源失败:继续下一个源 + Reset() + SyncLock LockState + State = NetState.WaitForDownload + End SyncLock + Else + '失败 + Log($"[Download] {LocalName}:已无可用下载源,下载失败") + Dim ExampleEx As Exception = Nothing + SyncLock LockSource + For Each Source As NetSource In Sources + Log("[Download] 已禁用的下载源:" & Source.Url) + If Source.Ex IsNot Nothing Then + ExampleEx = Source.Ex + Log(Source.Ex, "下载源禁用原因", LogLevel.Developer) + End If + Next + End SyncLock + Fail(ExampleEx) + End If + End If + End If + '清理当前已下载的内容 + If FileSize = -2 Then Reset() + End Sub + Private Sub Reset() + FileSize = -2 + Cache?.Dispose() : Cache = Nothing + SyncLock LockChain + Threads = Nothing + End SyncLock + NetManager.DownloadDone -= DownloadDone + SyncLock LockDone + DownloadDone = 0 + End SyncLock + SpeedLastDone = 0 + End Sub + + '最终收束事件 - '下载文件的最终收束事件 ''' ''' 下载完成。合并文件。 ''' - Private Sub Merge() + Private Sub Merge(Th As NetThread) '状态判断 SyncLock LockState If State < NetState.Merge Then State = NetState.Merge Else - Exit Sub + Return End If End SyncLock Dim RetryCount As Integer = 0 - Dim MergeFile As Stream = Nothing, AddWriter As BinaryWriter = Nothing Try Retry: SyncLock LockChain '创建文件夹 If File.Exists(LocalPath) Then File.Delete(LocalPath) - Dim Info As New FileInfo(LocalPath) - Info.Directory.Create() + Directory.CreateDirectory(GetPathFromFullPath(LocalPath)) '合并文件 If IsNoSplit Then '仅有一个线程,从缓存中输出 - If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从缓存输出文件,长度:" & SmailFileCache.Count) - MergeFile = New FileStream(LocalPath, FileMode.Create) - AddWriter = New BinaryWriter(MergeFile) - AddWriter.Write(SmailFileCache.ToArray) - AddWriter.Dispose() : AddWriter = Nothing - MergeFile.Dispose() : MergeFile = Nothing + If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从内存输出文件") + Cache.Position = 0 + WriteFile(LocalPath, Cache) ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then '仅有一个文件,直接复制 If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并") @@ -1517,40 +1152,37 @@ Retry: Else '有多个线程,合并 If ModeDebug Then Log($"[Download] {LocalName}:下载结束,开始合并文件") - MergeFile = New FileStream(LocalPath, FileMode.Create) - AddWriter = New BinaryWriter(MergeFile) - For Each Thread As NetThread In Threads - If Thread.DownloadDone = 0 OrElse Thread.Temp Is Nothing Then Continue For - Using fs As New FileStream(Thread.Temp, FileMode.Open, FileAccess.Read, FileShare.Read) - Using TempReader As New BinaryReader(fs) - AddWriter.Write(TempReader.ReadBytes(Thread.DownloadDone)) - End Using + Using MergeFile As New FileStream(LocalPath, FileMode.Create) + Using AddWriter As New BinaryWriter(MergeFile) + For Each Thread As NetThread In Threads + If Thread.DownloadDone = 0 OrElse Thread.Temp Is Nothing Then Continue For + Using fs As New FileStream(Thread.Temp, FileMode.Open, FileAccess.Read, FileShare.Read) + Using TempReader As New BinaryReader(fs) + AddWriter.Write(TempReader.ReadBytes(Thread.DownloadDone)) + End Using + End Using + Next End Using - Next - AddWriter.Dispose() : AddWriter = Nothing - MergeFile.Dispose() : MergeFile = Nothing - End If - '写入大小要求 - If Not IsUnknownSize AndAlso Check IsNot Nothing Then - If Check.ActualSize = -1 Then - Check.ActualSize = FileSize - ElseIf Check.ActualSize <> FileSize Then - Throw New Exception($"文件大小不一致:任务要求为 {Check.ActualSize} B,网络获取结果为 {FileSize}B") - End If + End Using End If '检查文件 Dim CheckResult As String = Check?.Check(LocalPath) + If CheckResult Is Nothing AndAlso Not IsUnknownSize AndAlso Check IsNot Nothing Then + If Check.ActualSize = -1 Then + CheckResult = (New FileChecker With {.ActualSize = FileSize}).Check(LocalPath) '不修改原始的 Checker,以免污染原始实例 + ElseIf Check.ActualSize <> FileSize Then + CheckResult = $"文件大小不一致:任务校验要求为 {Check.ActualSize},请求结果为 {FileSize}" + End If + End If If CheckResult IsNot Nothing Then Log($"[Download] {LocalName} 文件校验失败,下载线程细节:") - For Each Th As NetThread In Threads - Log($"[Download] {Th.Uuid}#,状态 {GetStringFromEnum(Th.State)},范围 {Th.DownloadStart}~{Th.DownloadStart + Th.DownloadDone},完成 {Th.DownloadDone},剩余 {Th.DownloadUndone}") + For Each T As NetThread In Threads + Log($"[Download] - {T.Uuid}#,状态 {GetStringFromEnum(T.State)},范围 {T.DownloadStart}~{T.DownloadEnd},已下载 {T.DownloadDone},未下载 {T.DownloadUndone}") Next Throw New Exception(CheckResult) End If '后处理 - If IsNoSplit Then - SmailFileCache = Nothing - Else + If Not IsNoSplit Then For Each Thread As NetThread In Threads If Thread.Temp IsNot Nothing Then File.Delete(Thread.Temp) Next @@ -1558,20 +1190,18 @@ Retry: Finish() End SyncLock Catch ex As Exception - Log(ex, "合并文件出错(" & LocalName & ")") - If MergeFile IsNot Nothing Then - MergeFile.Dispose() : MergeFile = Nothing - End If - If AddWriter IsNot Nothing Then - AddWriter.Dispose() : AddWriter = Nothing - End If - '重试 - If RetryCount <= 3 Then - Threading.Thread.Sleep(RandomInteger(500, 1000)) - RetryCount += 1 + RetryCount += 1 + Log(ex, $"合并文件出错,第 {RetryCount} 次尝试({LocalName})") + '重新尝试合并 + If RetryCount < 3 Then + Threading.Thread.Sleep(500 * RetryCount) GoTo Retry End If - Fail(ex) + '失败,禁用当前下载源并重启下载 + If File.Exists(LocalPath) Then File.Delete(LocalPath) + SourceFail(Th, ex, True) + Finally + Cache?.Dispose() End Try End Sub ''' @@ -1579,12 +1209,12 @@ Retry: ''' Private Sub Fail(Optional RaiseEx As Exception = Nothing) SyncLock LockState - If State >= NetState.Finish Then Exit Sub + If State >= NetState.Finish Then Return If RaiseEx IsNot Nothing Then Ex.Add(RaiseEx) '凉凉 State = NetState.Error End SyncLock - InterruptAndDelete() + AbortInternal() For Each Task In Tasks Task.OnFileFail(Me) Next @@ -1592,24 +1222,24 @@ Retry: ''' ''' 下载中断。 ''' - Public Sub Abort(CausedByTask As LoaderDownload) + Public Sub Abort(AbortedTask As LoaderDownload) '从特定任务中移除,如果它还属于其他任务,则继续下载 - Tasks.Remove(CausedByTask) - If Tasks.Any Then Exit Sub + Tasks.Remove(AbortedTask) + If Tasks.Any Then Return '确认中断 SyncLock LockState - If State >= NetState.Finish Then Exit Sub + If State >= NetState.Finish Then Return State = NetState.Error End SyncLock - InterruptAndDelete() + AbortInternal() End Sub - Private Sub InterruptAndDelete() + Private Sub AbortInternal() On Error Resume Next - If File.Exists(LocalPath) Then File.Delete(LocalPath) + Reset() SyncLock NetManager.LockRemain NetManager.FileRemain -= 1 - Log($"[Download] {LocalName}:状态 {State},剩余文件 {NetManager.FileRemain}") End SyncLock + Log($"[Download] {LocalName}:已终止,当前状态 {State}") End Sub '状态改变接口 @@ -1618,13 +1248,13 @@ Retry: ''' Public Sub Finish(Optional PrintLog As Boolean = True) SyncLock LockState - If State >= NetState.Finish Then Exit Sub + If State >= NetState.Finish Then Return State = NetState.Finish End SyncLock SyncLock NetManager.LockRemain NetManager.FileRemain -= 1 - If PrintLog Then Log("[Download] " & LocalName & ":已完成,剩余文件 " & NetManager.FileRemain) End SyncLock + If PrintLog Then Log($"[Download] {LocalName}:已完成") For Each Task In Tasks Task.OnFileFinish(Me) Next @@ -1745,7 +1375,7 @@ NextElement: '输入检测 If Not Files.Any() Then OnFinish() - Exit Sub + Return End If For Each File As NetFile In Files If File Is Nothing Then Throw New ArgumentException("存在空文件请求!") @@ -1755,7 +1385,7 @@ NextElement: Source.IsFailed = True End If Next - If File.IsSourceFailed() Then Throw New ArgumentException("输入的下载链接不正确!") + If Not File.HasAvailableSource() Then Throw New ArgumentException("输入的下载链接不正确!") If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确!") If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!") '文件夹检测 @@ -1785,30 +1415,31 @@ NextElement: If FilesInThread.Count = FilesPerThread Then Dim FilesToRun As New List(Of NetFile) FilesToRun.AddRange(FilesInThread) - RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid) + RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "下载 文件复制 " & Uuid) FilesInThread.Clear() End If Next If FilesInThread.Any Then Dim FilesToRun As New List(Of NetFile) FilesToRun.AddRange(FilesInThread) - RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid) + RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "下载 文件复制 " & Uuid) FilesInThread.Clear() End If Catch ex As Exception OnFail(New List(Of Exception) From {ex}) End Try - End Sub, "NetTask " & Uuid & " Main") + End Sub, "L/下载 " & Uuid) End Sub Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String)) Try - If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count & ",线程名:" & Thread.CurrentThread.Name) + If ModeDebug Then Log($"[Download] 检查线程分配文件数:{Files.Count},线程名:{Thread.CurrentThread.Name}") '试图从已存在的 Minecraft 文件夹中寻找目标文件 Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String} For Each File As NetFile In Files Dim ExistFilePath As String = Nothing '判断是否有已存在的文件 - If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then + If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso + File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then Dim Relative = File.LocalPath.Replace(PathMcFolder, "") For Each Folder In FolderList Dim Target = Folder & Relative @@ -1834,7 +1465,7 @@ NextElement: For Each FileToken In ExistFiles Dim File As NetFile = FileToken.Key SyncLock LockState - If File.State > NetState.WaitForCopy Then Exit Sub + If File.State > NetState.WaitForCopy Then Return End SyncLock Dim LocalPath As String = FileToken.Value Dim RetryCount As Integer = 0 @@ -1845,7 +1476,7 @@ Retry: File.Finish(False) Catch ex As Exception RetryCount += 1 - Log(ex, String.Format("复制已存在的文件失败,重试第 {2} 次({0} -> {1})", LocalPath, File.LocalPath, RetryCount)) + Log(ex, $"复制已存在的文件失败,重试第 {RetryCount} 次({LocalPath} -> {File.LocalPath})") If RetryCount < 3 Then Thread.Sleep(200) GoTo Retry @@ -1863,14 +1494,14 @@ Retry: '要求全部文件完成 SyncLock FileRemainLock FileRemain -= 1 - If FileRemain > 0 Then Exit Sub + If FileRemain > 0 Then Return End SyncLock OnFinish() End Sub Public Sub OnFinish() RaisePreviewFinish() SyncLock LockState - If State > LoadState.Loading Then Exit Sub + If State > LoadState.Loading Then Return State = LoadState.Finished End SyncLock End Sub @@ -1883,25 +1514,26 @@ Retry: End Sub Public Sub OnFail(ExList As List(Of Exception)) SyncLock LockState - If State > LoadState.Loading Then Exit Sub + If State > LoadState.Loading Then Return If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")} - '寻找第一个不是 404 的下载源 - Dim UsefulExs = ExList.Where(Function(e) Not e.Message.Contains("(404)")).ToList - [Error] = If(UsefulExs.Any, UsefulExs(0), ExList(0)) + '寻找有效的错误信息 + Dim UsefulExs = ExList.Where(Function(e) TypeOf e IsNot TaskCanceledException AndAlso TypeOf e IsNot ThreadInterruptedException).ToList + [Error] = If(UsefulExs.FirstOrDefault, ExList.FirstOrDefault) '获取实际失败的文件 For Each File In Files - If File.State = NetState.Error Then - [Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf & Join( - File.Sources.Select(Function(s) If(s.Ex Is Nothing, s.Url, s.Ex.Message & "(" & s.Url & ")")), vbCrLf), [Error]) - Exit For - End If + If File.State <> NetState.Error Then Continue For + [Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf & + "各下载源的错误如下:" & vbCrLf & + Join(File.Sources.Select(Function(s) $"{If(s.Ex?.Message, "无错误信息。")}({s.Url})"), vbCrLf), + [Error]) + Exit For Next '在设置 Error 对象后再更改为失败,避免 WaitForExit 无法捕获错误 State = LoadState.Failed End SyncLock '中断所有文件 For Each TaskFile In Files - If TaskFile.State < NetState.Merge Then TaskFile.State = NetState.Error + TaskFile.Abort(Me) Next '在退出同步锁后再进行日志输出 Dim ErrOutput As New List(Of String) @@ -1912,7 +1544,7 @@ Retry: End Sub Public Overrides Sub Abort() SyncLock LockState - If State >= LoadState.Finished Then Exit Sub + If State >= LoadState.Finished Then Return State = LoadState.Aborted End SyncLock Log("[Download] " & Name & " 已取消!") @@ -2029,7 +1661,7 @@ Retry: ''' 启动监控线程,用于新增下载线程。 ''' Private Sub StartManager() - If IsManagerStarted Then Exit Sub + If IsManagerStarted Then Return IsManagerStarted = True Dim ThreadStarter = Sub(Id As Integer) '0 或 1 @@ -2141,9 +1773,8 @@ Retry: Files(File.LocalPath) = File SyncLock LockRemain FileRemain += 1 - If ModeDebug Then Log("[Download] " & File.LocalName & ":已替换列表,剩余文件 " & FileRemain) - 'FileRemainList.Add(File.LocalPath) End SyncLock + If ModeDebug Then Log($"[Download] {File.LocalName}:重新加入下载列表") Else '该文件正在下载中 '将当前文件替换成下载中的文件,即两个任务指向同一个文件 @@ -2156,9 +1787,8 @@ Retry: Files.Add(File.LocalPath, File) SyncLock LockRemain FileRemain += 1 - If ModeDebug Then Log("[Download] " & File.LocalName & ":已加入列表,剩余文件 " & FileRemain) - 'FileRemainList.Add(File.LocalPath) End SyncLock + If ModeDebug Then Log($"[Download] {File.LocalName}:已加入下载列表") End If Task.Files(i) = File '回设 Next @@ -2181,4 +1811,26 @@ Retry: Return False End Function +#End Region + + ''' + ''' 测试 Ping。失败则返回 -1。 + ''' + Public Function Ping(Ip As String, Optional Timeout As Integer = 10000, Optional MakeLog As Boolean = True) As Integer + Dim PingResult As NetworkInformation.PingReply + Try + PingResult = (New NetworkInformation.Ping).Send(Ip) + Catch ex As Exception + If MakeLog Then Log("[Net] Ping " & Ip & " 失败:" & ex.Message) + Return -1 + End Try + If PingResult.Status = NetworkInformation.IPStatus.Success Then + If MakeLog Then Log("[Net] Ping " & Ip & " 结束:" & PingResult.RoundtripTime & "ms") + Return PingResult.RoundtripTime + Else + If MakeLog Then Log("[Net] Ping " & Ip & " 失败") + Return -1 + End If + End Function + End Module diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb index 727013a..c62beb9 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb @@ -1076,7 +1076,7 @@ Retry: Try '获取工程列表 Log("[Comp] 开始从 CurseForge 获取工程列表:" & CurseForgeUrl) - Dim RequestResult As JObject = DlModRequest(CurseForgeUrl, IsJson:=True) + Dim RequestResult As JObject = DlModRequest(CurseForgeUrl) Task.Progress += 0.2 Dim ProjectList As New List(Of CompProject) For Each JsonEntry As JObject In RequestResult("data") @@ -1107,7 +1107,7 @@ Retry: Sub() Try Log("[Comp] 开始从 Modrinth 获取工程列表:" & ModrinthUrl) - Dim RequestResult As JObject = DlModRequest(ModrinthUrl, IsJson:=True) + Dim RequestResult As JObject = DlModRequest(ModrinthUrl) Task.Progress += 0.2 Dim ProjectList As New List(Of CompProject) For Each JsonEntry As JObject In RequestResult("hits") @@ -1396,7 +1396,7 @@ Retry: 'DownloadAddress Dim Url = Data("downloadUrl").ToString If Url = "" Then Url = $"https://media.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}" - Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码 + Url = Url.Replace(FileName, WebUtility.UrlEncode(FileName)) '对文件名进行编码 DownloadUrls = HandleCurseForgeDownloadUrls(Url) '对脑残 CurseForge 的下载地址进行多种修正 DownloadUrls.AddRange(DownloadUrls.Select(Function(u) DlSourceModGet(u)).ToList) '添加镜像源,这个写法是为了让镜像源排在后面 DownloadUrls = DownloadUrls.Distinct.ToList '最终去重 @@ -1492,9 +1492,8 @@ Retry: Return { Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."), Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."), - Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://media."), Url.Replace("-service.overwolf.wtf", ".forgecdn.net"), - Url.Replace("://edge.", "://media."), + Url.Replace("://media.", "://edge."), Url }.Distinct.ToList End Function @@ -1591,27 +1590,40 @@ Retry: If CompProjectCache.ContainsKey(ProjectId) Then '存在缓存 TargetProject = CompProjectCache(ProjectId) ElseIf FromCurseForge Then 'CurseForge - TargetProject = New CompProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId, IsJson:=True)("data")) + TargetProject = New CompProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data")) Else 'Modrinth - TargetProject = New CompProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId, IsJson:=True)) + TargetProject = New CompProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId)) End If '获取工程对象的文件列表 If Not CompFilesCache.ContainsKey(ProjectId) Then '有缓存也不能直接返回,这时候前置可能没获取(#5173) Log("[Comp] 开始获取文件列表:" & ProjectId) - Dim ResultJsonArray As JArray + Dim ResultJsonArray As JArray = Nothing If FromCurseForge Then 'CurseForge - 'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522) - ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=10000", IsJson:=True)("data") - '之前只请求一部分文件的方法备份如下: - 'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件 - ' ResultJsonArray = GetJson(DlModRequest("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json"))("data") - 'Else '否则使用全部文件 - ' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999", IsJson:=True)("data") - 'End If + For RetryCount As Integer = 0 To 2 + Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" & + (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存 + 'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522) + '之前只请求一部分文件的方法备份如下: + 'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件 + ' ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data") + 'Else '否则使用全部文件 + ' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data") + 'End If + If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then + ResultJsonArray = ResultJson("data") + Exit For + ElseIf RetryCount < 2 Then + Log($"[Comp] CurseForge 返回的文件列表存在缺失,即将进行第 {RetryCount + 1} 次重试", LogLevel.Debug) '#6224 + Log($"[Comp] 返回的原始内容如下:{vbCrLf}{ResultJson}") + Else + Log($"[Comp] CurseForge 返回的文件列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}") + Throw New Exception("CurseForge 返回的文件列表存在缺失") + End If + Next Else 'Modrinth - ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version", IsJson:=True) + ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version") End If CompFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Type)). Where(Function(a) a.Available).ToList. @@ -1625,10 +1637,10 @@ Retry: Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置:{Join(UndoneDeps, ",")}") Dim Projects As JArray If TargetProject.FromCurseForge Then - Projects = GetJson(DlModRequest("https://api.curseforge.com/v1/mods", - "POST", "{""modIds"": [" & Join(UndoneDeps, ",") & "]}", "application/json"))("data") + Projects = DlModRequest("https://api.curseforge.com/v1/mods", + HttpMethod.Post, "{""modIds"": [" & Join(UndoneDeps, ",") & "]}", "application/json")("data") Else - Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{Join(UndoneDeps, """,""")}""]", IsJson:=True) + Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{Join(UndoneDeps, """,""")}""]") End If For Each Project In Projects Dim Unused As New CompProject(Project) '在 New 的时候会添加缓存以便之后读取 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb index 33fe597..2ed1744 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb @@ -1018,9 +1018,9 @@ NextStack: End If Case CrashReason.特定实体导致崩溃 If Additional.Count = 1 Then - Results.Add("游戏似乎因为实体 " & Additional.First & " 出现了问题。\n\n你可以创建一个新世界,并生成一个该实体,然后观察游戏的运行情况:\n - 若正常运行,则是该实体导致出错,你或许需要使用一些方式删除此实体。\n - 若仍然出错,问题就可能来自其他原因……\h") + Results.Add("游戏似乎因为实体 " & Additional.First & " 出现了问题。\n\n你可以创建一个新世界,并生成一个该实体,然后观察游戏的运行情况。\h") Else - Results.Add("游戏似乎因为世界中的某些实体出现了问题。\n\n你可以创建一个新世界,并生成各种实体,观察游戏的运行情况:\n - 若正常运行,则是某些实体导致出错,你或许需要删除该世界。\n - 若仍然出错,问题就可能来自其他原因……\h") + Results.Add("游戏似乎因为世界中的某些实体出现了问题。\n\n你可以创建一个新世界,并生成各种实体,然后观察游戏的运行情况。\h") End If Case CrashReason.OptiFine与Forge不兼容 Results.Add("由于 OptiFine 与当前版本的 Forge 不兼容,导致了游戏崩溃。\n\n请前往 OptiFine 官网(https://optifine.net/downloads)查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏。") diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb index ef7e020..683adee 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb @@ -198,7 +198,7 @@ Private IsNewClientVersionHinted As Boolean = False Private Sub DlClientListMojangMain(Loader As LoaderTask(Of String, DlClientListResult)) Dim StartTime As Long = GetTimeTick() - Dim Json As JObject = NetGetCodeByRequestRetry("https://launchermeta.mojang.com/mc/game/version_manifest.json", IsJson:=True) + Dim Json As JObject = GetJson(NetRequestByClientRetry("https://launchermeta.mojang.com/mc/game/version_manifest.json")) Try Dim Versions As JArray = Json("versions") If Versions.Count < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Json.ToString & ")") @@ -235,7 +235,7 @@ ''' Public DlClientListBmclapiLoader As New LoaderTask(Of String, DlClientListResult)("DlClientList Bmclapi", AddressOf DlClientListBmclapiMain) Private Sub DlClientListBmclapiMain(Loader As LoaderTask(Of String, DlClientListResult)) - Dim Json As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json", IsJson:=True) + Dim Json As JObject = GetJson(NetRequestByClientRetry("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json")) Try Dim Versions As JArray = Json("versions") If Versions.Count < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Json.ToString & ")") @@ -377,7 +377,7 @@ ''' Public DlOptiFineListOfficialLoader As New LoaderTask(Of Integer, DlOptiFineListResult)("DlOptiFineList Official", AddressOf DlOptiFineListOfficialMain) Private Sub DlOptiFineListOfficialMain(Loader As LoaderTask(Of Integer, DlOptiFineListResult)) - Dim Result As String = NetGetCodeByClient("https://optifine.net/downloads", Encoding.Default) + Dim Result As String = NetRequestByClientRetry("https://optifine.net/downloads", Encoding:=Encoding.Default) If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")") Try '获取所有版本信息 @@ -413,7 +413,7 @@ ''' Public DlOptiFineListBmclapiLoader As New LoaderTask(Of Integer, DlOptiFineListResult)("DlOptiFineList Bmclapi", AddressOf DlOptiFineListBmclapiMain) Private Sub DlOptiFineListBmclapiMain(Loader As LoaderTask(Of Integer, DlOptiFineListResult)) - Dim Json As JArray = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/optifine/versionList", IsJson:=True) + Dim Json As JArray = GetJson(NetRequestByClientRetry("https://bmclapi2.bangbang93.com/optifine/versionList")) Try Dim Versions As New List(Of DlOptiFineListEntry) For Each Token As JObject In Json @@ -483,7 +483,7 @@ ''' Public DlForgeListOfficialLoader As New LoaderTask(Of Integer, DlForgeListResult)("DlForgeList Official", AddressOf DlForgeListOfficialMain) Private Sub DlForgeListOfficialMain(Loader As LoaderTask(Of Integer, DlForgeListResult)) - Dim Result As String = NetGetCodeByRequestRetry("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.2.4.html", Encoding.Default, "text/html", UseBrowserUserAgent:=True) + Dim Result As String = NetRequestByClientRetry("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.2.4.html", Encoding:=Encoding.Default, Accept:="text/html", UseBrowserUserAgent:=True) If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")") '获取所有版本信息 Dim Names As List(Of String) = RegexSearch(Result, "(?<=a href=""index_)[0-9.]+(_pre[0-9]?)?(?=.html)") @@ -497,7 +497,7 @@ ''' Public DlForgeListBmclapiLoader As New LoaderTask(Of Integer, DlForgeListResult)("DlForgeList Bmclapi", AddressOf DlForgeListBmclapiMain) Private Sub DlForgeListBmclapiMain(Loader As LoaderTask(Of Integer, DlForgeListResult)) - Dim Result As String = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/forge/minecraft", Encoding.Default) + Dim Result As String = NetRequestByClientRetry("https://bmclapi2.bangbang93.com/forge/minecraft", Encoding:=Encoding.Default) If Result.Length < 200 Then Throw New Exception("获取到的版本列表长度不足(" & Result & ")") '获取所有版本信息 Dim Names As List(Of String) = RegexSearch(Result, "[0-9.]+(_pre[0-9]?)?") @@ -629,7 +629,7 @@ Public Sub DlForgeVersionOfficialMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry))) Dim Result As String Try - Result = NetGetCodeByLoader("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" & + Result = NetRequestByLoader("https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" & Loader.Input.Replace("-", "_") & '兼容 Forge 1.7.10-pre4,#4057 ".html", UseBrowserUserAgent:=True) Catch ex As Exception @@ -700,9 +700,8 @@ ''' Forge 版本列表,BMCLAPI。 ''' Public Sub DlForgeVersionBmclapiMain(Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry))) - Dim Json As JArray = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/forge/minecraft/" & - Loader.Input.Replace("-", "_"), '兼容 Forge 1.7.10-pre4,#4057 - IsJson:=True) + Dim Json As JArray = GetJson(NetRequestByClientRetry("https://bmclapi2.bangbang93.com/forge/minecraft/" & + Loader.Input.Replace("-", "_"))) '兼容 Forge 1.7.10-pre4,#4057 Dim Versions As New List(Of DlForgeVersionEntry) Try Dim Recommended As String = McDownloadForgeRecommendedGet(Loader.Input) @@ -836,8 +835,8 @@ Public DlNeoForgeListOfficialLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Official", AddressOf DlNeoForgeListOfficialMain) Private Sub DlNeoForgeListOfficialMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult)) '获取版本列表 JSON - Dim ResultLatest As String = NetGetCodeByLoader("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True) - Dim ResultLegacy As String = NetGetCodeByLoader("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True) + Dim ResultLatest As String = NetRequestByClientRetry("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge") + Dim ResultLegacy As String = NetRequestByClientRetry("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge") If ResultLatest.Length < 100 OrElse ResultLegacy.Length < 100 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")") '解析 Try @@ -854,8 +853,8 @@ Public DlNeoForgeListBmclapiLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Bmclapi", AddressOf DlNeoForgeListBmclapiMain) Public Sub DlNeoForgeListBmclapiMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult)) '获取版本列表 JSON - Dim ResultLatest As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True) - Dim ResultLegacy As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True) + Dim ResultLatest As String = NetRequestByClientRetry("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge") + Dim ResultLegacy As String = NetRequestByClientRetry("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge") If ResultLatest.Length < 100 OrElse ResultLegacy.Length < 100 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")") '解析 Try @@ -960,7 +959,7 @@ ''' Public DlLiteLoaderListOfficialLoader As New LoaderTask(Of Integer, DlLiteLoaderListResult)("DlLiteLoaderList Official", AddressOf DlLiteLoaderListOfficialMain) Private Sub DlLiteLoaderListOfficialMain(Loader As LoaderTask(Of Integer, DlLiteLoaderListResult)) - Dim Result As JObject = NetGetCodeByRequestRetry("https://dl.liteloader.com/versions/versions.json", IsJson:=True) + Dim Result As JObject = GetJson(NetRequestByClientRetry("https://dl.liteloader.com/versions/versions.json")) Try Dim Json As JObject = Result("versions") Dim Versions As New List(Of DlLiteLoaderListEntry) @@ -988,7 +987,7 @@ ''' Public DlLiteLoaderListBmclapiLoader As New LoaderTask(Of Integer, DlLiteLoaderListResult)("DlLiteLoaderList Bmclapi", AddressOf DlLiteLoaderListBmclapiMain) Private Sub DlLiteLoaderListBmclapiMain(Loader As LoaderTask(Of Integer, DlLiteLoaderListResult)) - Dim Result As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json", IsJson:=True) + Dim Result As JObject = GetJson(NetRequestByClientRetry("https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")) Try Dim Json As JObject = Result("versions") Dim Versions As New List(Of DlLiteLoaderListEntry) @@ -1059,7 +1058,7 @@ ''' Public DlFabricListOfficialLoader As New LoaderTask(Of Integer, DlFabricListResult)("DlFabricList Official", AddressOf DlFabricListOfficialMain) Private Sub DlFabricListOfficialMain(Loader As LoaderTask(Of Integer, DlFabricListResult)) - Dim Result As JObject = NetGetCodeByRequestRetry("https://meta.fabricmc.net/v2/versions", IsJson:=True) + Dim Result As JObject = GetJson(NetRequestByClientRetry("https://meta.fabricmc.net/v2/versions")) Try Dim Output = New DlFabricListResult With {.IsOfficial = True, .SourceName = "Fabric 官方源", .Value = Result} If Output.Value("game") Is Nothing OrElse Output.Value("loader") Is Nothing OrElse Output.Value("installer") Is Nothing Then Throw New Exception("获取到的列表缺乏必要项") @@ -1074,7 +1073,7 @@ ''' Public DlFabricListBmclapiLoader As New LoaderTask(Of Integer, DlFabricListResult)("DlFabricList Bmclapi", AddressOf DlFabricListBmclapiMain) Private Sub DlFabricListBmclapiMain(Loader As LoaderTask(Of Integer, DlFabricListResult)) - Dim Result As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/fabric-meta/v2/versions", IsJson:=True) + Dim Result As JObject = GetJson(NetRequestByClientRetry("https://bmclapi2.bangbang93.com/fabric-meta/v2/versions")) Try Dim Output = New DlFabricListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Result} If Output.Value("game") Is Nothing OrElse Output.Value("loader") Is Nothing OrElse Output.Value("installer") Is Nothing Then Throw New Exception("获取到的列表缺乏必要项") @@ -1101,50 +1100,14 @@ #Region "DlMod | Mod 镜像源请求" ''' - ''' 对可能涉及 Mod 镜像源的请求进行处理,返回字符串或 JObject。 - ''' 调用 NetGetCodeByRequest,会进行重试。 - ''' - Public Function DlModRequest(Url As String, Optional IsJson As Boolean = False) As Object - Dim Urls As New List(Of KeyValuePair(Of String, Integer)) - Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5)) - Urls.Add(New KeyValuePair(Of String, Integer)(Url, 20)) - 'Dim McimUrl As String = DlSourceModGet(Url) - 'If McimUrl <> Url Then - ' Select Case Setup.Get("ToolDownloadMod") - ' Case 0 - ' Urls.Add(New KeyValuePair(Of String, Integer)(McimUrl, 5)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(McimUrl, 10)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(Url, 15)) - ' Case 1 - ' Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(McimUrl, 5)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(Url, 15)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(McimUrl, 10)) - ' Case Else - ' Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(Url, 15)) - ' Urls.Add(New KeyValuePair(Of String, Integer)(McimUrl, 10)) - ' End Select - 'End If - Dim Exs As String = "" - For Each Source In Urls - Try - Return NetGetCodeByRequestOnce(Source.Key, Encode:=Encoding.UTF8, Timeout:=Source.Value * 1000, IsJson:=IsJson, UseBrowserUserAgent:=True) - Catch ex As Exception - Exs += ex.Message + vbCrLf - End Try - Next - Throw New Exception(Exs) - End Function - - ''' - ''' 对可能涉及 Mod 镜像源的请求进行处理。 + ''' 对可能涉及 Mod 镜像源的请求进行处理,返回 JToken。 ''' 调用 NetRequest,会进行重试。 ''' - Public Function DlModRequest(Url As String, Method As String, Data As String, ContentType As String) As String + Public Function DlModRequest(Url As String, Optional Method As HttpMethod = Nothing, + Optional Content As String = Nothing, Optional ContentType As String = Nothing) As JToken Dim Urls As New List(Of KeyValuePair(Of String, Integer)) Urls.Add(New KeyValuePair(Of String, Integer)(Url, 5)) - Urls.Add(New KeyValuePair(Of String, Integer)(Url, 20)) + Urls.Add(New KeyValuePair(Of String, Integer)(Url, 30)) 'Dim McimUrl As String = DlSourceModGet(Url) 'If McimUrl <> Url Then ' Select Case Setup.Get("ToolDownloadMod") @@ -1166,7 +1129,7 @@ Dim Exs As String = "" For Each Source In Urls Try - Return NetRequestOnce(Source.Key, Method, Data, ContentType, Timeout:=Source.Value * 1000) + Return GetJson(NetRequestByClient(Source.Key, Method, Content, ContentType, Timeout:=Source.Value * 1000, Encoding:=Encoding.UTF8)) Catch ex As Exception Exs += ex.Message + vbCrLf End Try diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb index bf45fec..5a55344 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb @@ -117,7 +117,7 @@ If Output = "" Then Throw New ApplicationException("尝试运行该 Java 失败") If ModeDebug Then Log("[Java] Java 检查输出:" & PathFolder & "java.exe" & vbCrLf & Output) If Output.Contains("/lib/ext exists") Then Throw New ApplicationException("无法运行该 Java,请在删除 Java 文件夹中的 /lib/ext 文件夹后再试") - If Output.Contains("a fatal error") Then Throw New ApplicationException("由于操作系统或 JVM 存在问题,无法运行该 Java。") + If Output.Contains("a fatal error") Then Throw New ApplicationException("无法运行该 Java,该 Java 或系统存在问题") '获取详细信息 Dim VersionString = If(RegexSeek(Output, "(?<=version "")[^""]+"), If(RegexSeek(Output, "(?<=openjdk )[0-9]+"), "")).Replace("_", ".").Split("-").First If VersionString.Split(".").Count > 4 Then VersionString = VersionString.Replace(".0.", ".") '#3493,VersionString = "21.0.2.0.2" @@ -460,10 +460,12 @@ NoUserJava: ''' 将 Java 按照适用性排序。 ''' Public Function JavaSorter(Left As JavaEntry, Right As JavaEntry) As Boolean + Dim PathInfo As New DirectoryInfo(ShortenPath(Path)) + Dim PathMcInfo As New DirectoryInfo(ShortenPath(PathMcFolder)) '1. 尽量在当前文件夹或当前 Minecraft 文件夹 Dim ProgramPathParent As String, MinecraftPathParent As String = "" - ProgramPathParent = If(New DirectoryInfo(Path).Parent, New DirectoryInfo(Path)).FullName - If PathMcFolder <> "" Then MinecraftPathParent = If(New DirectoryInfo(PathMcFolder).Parent, New DirectoryInfo(PathMcFolder)).FullName + ProgramPathParent = If(PathInfo.Parent, PathInfo).FullName + If PathMcFolder <> "" Then MinecraftPathParent = If(PathMcInfo.Parent, PathMcInfo).FullName If Left.PathFolder.StartsWithF(ProgramPathParent) AndAlso Not Right.PathFolder.StartsWithF(ProgramPathParent) Then Return True If Not Left.PathFolder.StartsWithF(ProgramPathParent) AndAlso Right.PathFolder.StartsWithF(ProgramPathParent) Then Return False If PathMcFolder <> "" Then @@ -530,7 +532,7 @@ NoUserJava: If Disk.DriveType = DriveType.Network Then Continue For '跳过网络驱动器(#3705) JavaSearchFolder(Disk.Name, JavaPreList, False) Next - '查找 ~/.jdks 文件夹中的 Java + '查找 .jdks 文件夹中的 Java JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.jdks\", JavaPreList, False) '查找 APPDATA 文件夹中的 Java JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\", JavaPreList, False) @@ -680,8 +682,8 @@ Wait: '若该目录有 Java,则加入结果 If File.Exists(Path & "javaw.exe") Then Results(Path) = Source '查找其下的所有文件夹 - '不应使用网易的 Java:https://github.com/HMeloong-Git/PCL/issues/1279#issuecomment-2761489121 - Dim Keywords = {"java", "jdk", "env", "环境", "run", "软件", "jre", "mc", "dragon", + '不应使用网易的 Java:https://github.com/Meloong-Git/PCL/issues/1279#issuecomment-2761489121 + Dim Keywords = {"java", "jdk", "env", "环境", "run", "软件", "jre", "mc", "dragon", "bin", "soft", "cache", "temp", "corretto", "roaming", "users", "craft", "program", "世界", "net", "游戏", "oracle", "game", "file", "data", "jvm", "服务", "server", "客户", "client", "整合", "应用", "运行", "前置", "mojang", "官启", "新建文件夹", "eclipse", "microsoft", "hotspot", @@ -748,7 +750,7 @@ Wait: Private LastJavaBaseDir As String = Nothing '用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java Private Sub JavaFileList(Loader As LoaderTask(Of Integer, List(Of NetFile))) Log("[Java] 开始获取 Java 下载信息") - Dim IndexFileStr As String = NetGetCodeByLoader(DlVersionListOrder( + Dim IndexFileStr As String = NetRequestByLoader(DlVersionListOrder( {"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"}, {"https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"} ), IsJson:=True) @@ -760,7 +762,7 @@ Wait: Dim Address As String = TargetEntry.Value("manifest")("url") Log($"[Java] 准备下载 Java {TargetEntry.Value("version")("name")}({TargetEntry.Key}):{Address}") '获取文件列表 - Dim ListFileStr As String = NetGetCodeByLoader(DlSourceOrder({Address}, {Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com")}), IsJson:=True) + Dim ListFileStr As String = NetRequestByLoader(DlSourceOrder({Address}, {Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com")}), IsJson:=True) LastJavaBaseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\runtime\" & TargetEntry.Key & "\" Dim Results As New List(Of NetFile) For Each File As JProperty In CType(GetJson(ListFileStr), JObject)("files") diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb index 00469b8..2e1f9fd 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb @@ -684,11 +684,10 @@ LoginFinish: '发送登录请求 Dim RequestData As New JObject( New JProperty("accessToken", AccessToken), New JProperty("clientToken", ClientToken), New JProperty("requestUser", True)) - NetRequestRetry( - Url:=Data.Input.BaseUrl & "/validate", - Method:="POST", - Data:=RequestData.ToString(0), - Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh-CN"}}, + NetRequestByClientRetry( + Data.Input.BaseUrl & "/validate", HttpMethod.Post, + Content:=RequestData.ToString(Newtonsoft.Json.Formatting.None), + Headers:={{"Accept-Language", "zh-CN"}}, ContentType:="application/json; charset=utf-8") '没有返回值的 '将登录结果输出 Data.Output.AccessToken = AccessToken @@ -701,10 +700,9 @@ LoginFinish: End Sub Private Sub McLoginRequestRefresh(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult), RequestUser As Boolean) McLaunchLog("刷新登录开始(Refresh, " & Data.Input.Token & ")") - Dim LoginJson As JObject = GetJson(NetRequestRetry( - Url:=Data.Input.BaseUrl & "/refresh", - Method:="POST", - Data:=New JObject( + Dim LoginJson As JObject = GetJson(NetRequestByClientRetry( + Data.Input.BaseUrl & "/refresh", HttpMethod.Post, + Content:=New JObject( New JProperty("selectedProfile", New JObject( New JProperty("name", Setup.Get($"Cache{Data.Input.Token}Name")), New JProperty("id", Setup.Get($"Cache{Data.Input.Token}Uuid")) @@ -712,7 +710,7 @@ LoginFinish: New JProperty("accessToken", Setup.Get($"Cache{Data.Input.Token}Access")), New JProperty("requestUser", True) ).ToString(Newtonsoft.Json.Formatting.None), - Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh-CN"}}, + Headers:={{"Accept-Language", "zh-CN"}}, ContentType:="application/json; charset=utf-8")) '将登录结果输出 If LoginJson("selectedProfile") Is Nothing Then Throw New Exception("选择的角色 " & Setup.Get("Cache" & Data.Input.Token & "Name") & " 无效!") @@ -739,11 +737,10 @@ LoginFinish: New JProperty("username", Data.Input.UserName), New JProperty("password", Data.Input.Password), New JProperty("requestUser", True)) - Dim LoginJson As JObject = GetJson(NetRequestRetry( - Url:=Data.Input.BaseUrl & "/authenticate", - Method:="POST", - Data:=RequestData.ToString(0), - Headers:=New Dictionary(Of String, String) From {{"Accept-Language", "zh-CN"}}, + Dim LoginJson As JObject = GetJson(NetRequestByClientRetry( + Data.Input.BaseUrl & "/authenticate", HttpMethod.Post, + Content:=RequestData.ToString(Newtonsoft.Json.Formatting.None), + Headers:={{"Accept-Language", "zh-CN"}}, ContentType:="application/json; charset=utf-8")) '检查登录结果 If LoginJson("availableProfiles").Count = 0 Then @@ -858,8 +855,9 @@ LoginFinish: '初始请求 Retry: McLaunchLog("开始微软登录步骤 1/6(原始登录)") - Dim PrepareJson As JObject = GetJson(NetRequestRetry("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", "POST", - $"client_id={OAuthClientId}&tenant=/consumers&scope=XboxLive.signin%20offline_access", "application/x-www-form-urlencoded")) + Dim PrepareJson As JObject = GetJson(NetRequestByClientRetry("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", HttpMethod.Post, + Content:=$"client_id={OAuthClientId}&tenant=/consumers&scope=XboxLive.signin%20offline_access", + Accept:="application/x-www-form-urlencoded")) McLaunchLog("网页登录地址:" & PrepareJson("verification_uri").ToString) '弹窗 @@ -888,9 +886,11 @@ Retry: Dim Result As String Try - Result = NetRequestMultiple("https://login.live.com/oauth20_token.srf", "POST", - $"client_id={OAuthClientId}&refresh_token={Uri.EscapeDataString(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access", - "application/x-www-form-urlencoded", 2) + Result = NetRequestByClientMultiple("https://login.live.com/oauth20_token.srf", HttpMethod.Post, + Content:=$"client_id={OAuthClientId}&refresh_token={Uri.EscapeDataString(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access", + ContentType:="application/x-www-form-urlencoded", + Headers:={{"Accept-Language", "en-US,en;q=0.5"}, {"X-Requested-With", "XMLHttpRequest"}}, + ThreadCount:=2) Catch ex As Exception If ex.Message.ContainsF("must sign in again", True) OrElse ex.Message.ContainsF("password expired", True) OrElse (ex.Message.Contains("refresh_token") AndAlso ex.Message.Contains("is not valid")) Then '#269 @@ -918,8 +918,8 @@ Retry: ""RelyingParty"": ""http://auth.xboxlive.com"", ""TokenType"": ""JWT"" }" - Dim Result As String = NetRequestMultiple("https://user.auth.xboxlive.com/user/authenticate", "POST", Request, "application/json", 3) - + Dim Result As String = NetRequestByClientMultiple("https://user.auth.xboxlive.com/user/authenticate", HttpMethod.Post, + Content:=Request, ContentType:="application/json") Dim ResultJson As JObject = GetJson(Result) Dim XBLToken As String = ResultJson("Token").ToString Return XBLToken @@ -940,7 +940,8 @@ Retry: }" Dim Result As String Try - Result = NetRequestMultiple("https://xsts.auth.xboxlive.com/xsts/authorize", "POST", Request, "application/json", 3) + Result = NetRequestByClientMultiple("https://xsts.auth.xboxlive.com/xsts/authorize", HttpMethod.Post, + Content:=Request, ContentType:="application/json") Catch ex As Net.WebException '参考 https://github.com/PrismarineJS/prismarine-auth/blob/master/src/common/Constants.js If ex.Message.Contains("2148916227") Then @@ -971,10 +972,15 @@ Retry: End If End Try - Dim ResultJson As JObject = GetJson(Result) - Dim XSTSToken As String = ResultJson("Token").ToString - Dim UHS As String = ResultJson("DisplayClaims")("xui")(0)("uhs").ToString - Return {XSTSToken, UHS} + Try '这个 Try 块仅为了追踪 #6683 而添加 + Dim ResultJson As JObject = GetJson(Result) + Dim XSTSToken As String = ResultJson("Token").ToString + Dim UHS As String = ResultJson("DisplayClaims")("xui")(0)("uhs").ToString + Return {XSTSToken, UHS} + Catch + Log("[Launch] 返回内容:" & vbCrLf & Result) + Throw + End Try End Function '微软登录步骤 4:从 {XSTSToken, UHS} 获取 Minecraft AccessToken Private Function MsLoginStep4(Tokens As String()) As String @@ -983,7 +989,8 @@ Retry: Dim Request As String = New JObject(New JProperty("identityToken", $"XBL3.0 x={Tokens(1)};{Tokens(0)}")).ToString(0) Dim Result As String Try - Result = NetRequestRetry("https://api.minecraftservices.com/authentication/login_with_xbox", "POST", Request, "application/json") + Result = NetRequestByClientRetry("https://api.minecraftservices.com/authentication/login_with_xbox", HttpMethod.Post, + Content:=Request, ContentType:="application/json") Catch ex As Net.WebException Dim Message As String = GetExceptionSummary(ex) If Message.Contains("(429)") Then @@ -1005,7 +1012,8 @@ Retry: Private Sub MsLoginStep5(AccessToken As String) McLaunchLog("开始微软登录步骤 5/6") - Dim Result As String = NetRequestMultiple("https://api.minecraftservices.com/entitlements/mcstore", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}}) + Dim Result As String = NetRequestByClientMultiple("https://api.minecraftservices.com/entitlements/mcstore", + ContentType:="application/json", ThreadCount:=2, Headers:={{"Authorization", "Bearer " & AccessToken}}) Try Dim ResultJson As JObject = GetJson(Result) If Not (ResultJson.ContainsKey("items") AndAlso ResultJson("items").Any) Then @@ -1026,7 +1034,8 @@ Retry: Dim Result As String Try - Result = NetRequestMultiple("https://api.minecraftservices.com/minecraft/profile", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}}) + Result = NetRequestByClientMultiple("https://api.minecraftservices.com/minecraft/profile", + ContentType:="application/json", ThreadCount:=2, Headers:={{"Authorization", "Bearer " & AccessToken}}) Catch ex As Net.WebException Dim Message As String = GetExceptionSummary(ex) If Message.Contains("(429)") Then @@ -1099,7 +1108,7 @@ Retry: If Len(Uuid) = 32 Then Return Uuid '从官网获取 Try - Dim GotJson As JObject = NetGetCodeByRequestRetry("https://api.mojang.com/users/profiles/minecraft/" & Name, IsJson:=True) + Dim GotJson As JObject = GetJson(NetRequestByClientRetry("https://api.mojang.com/users/profiles/minecraft/" & Name)) If GotJson Is Nothing Then Throw New FileNotFoundException("正版玩家档案不存在(" & Name & ")") Uuid = If(GotJson("id"), "") Catch ex As Exception @@ -1205,6 +1214,12 @@ Retry: End If End If + 'LiteLoader 检测 + If McVersionCurrent.Version.HasLiteLoader AndAlso McVersionCurrent.Version.IsStandardVersion Then '不管非标准版本 + '最高 Java 8 + MaxVer = If(New Version(1, 8, 999, 999) < MaxVer, New Version(1, 8, 999, 999), MaxVer) + End If + '统一通行证检测 If Setup.Get("LoginType") = McLoginType.Nide Then '至少 Java 8u101 @@ -1356,16 +1371,6 @@ Retry: If McLaunchJavaSelected.VersionCode >= 18 Then If Not Arguments.Contains("-Dfile.encoding=") Then Arguments = "-Dfile.encoding=COMPAT " & Arguments End If - '替换参数 - Dim ReplaceArguments = McLaunchArgumentsReplace(McVersionCurrent, Loader) - If String.IsNullOrWhiteSpace(ReplaceArguments("${version_type}")) Then - '若自定义信息为空,则去掉该部分 - Arguments = Arguments.Replace(" --versionType ${version_type}", "") - ReplaceArguments("${version_type}") = """""" - End If - For Each entry As KeyValuePair(Of String, String) In ReplaceArguments - Arguments = Arguments.Replace(entry.Key, If(entry.Value.Contains(" ") OrElse entry.Value.Contains(":\"), """" & entry.Value & """", entry.Value)) - Next 'MJSB Arguments = Arguments.Replace(" -Dos.name=Windows 10", " -Dos.name=""Windows 10""") '全屏 @@ -1374,31 +1379,47 @@ Retry: For Each Arg In CurrentLaunchOptions.ExtraArgs Arguments += " " & Arg.Trim Next + '自定义 + Dim ArgumentGame As String = Setup.Get("VersionAdvanceGame", Version:=McVersionCurrent) + Arguments += " " & If(ArgumentGame = "", Setup.Get("LaunchAdvanceGame"), ArgumentGame) + '进行参数替换 + Dim ReplaceArguments = McLaunchArgumentsReplace(McVersionCurrent, Loader) + If String.IsNullOrWhiteSpace(ReplaceArguments("${version_type}")) Then + '若自定义信息为空,则去掉该部分 + Arguments = Arguments.Replace(" --versionType ${version_type}", "") + ReplaceArguments("${version_type}") = """""" + End If + Dim FinalArguments As String = "" + For Each Argument In Arguments.Split(" ") + For Each Entry As KeyValuePair(Of String, String) In ReplaceArguments + Argument = Argument.Replace(Entry.Key, Entry.Value) + Next + If (Argument.Contains(" ") OrElse Argument.Contains(":\")) AndAlso Not Argument.EndsWithF("""") Then Argument = $"""{Argument}""" + FinalArguments += Argument & " " + Next + FinalArguments = FinalArguments.TrimEnd '进服 Dim Server As String = If(String.IsNullOrEmpty(CurrentLaunchOptions.ServerIp), Setup.Get("VersionServerEnter", McVersionCurrent), CurrentLaunchOptions.ServerIp) If Server.Length > 0 Then If McVersionCurrent.ReleaseTime > New Date(2023, 4, 4) Then 'QuickPlay - Arguments += $" --quickPlayMultiplayer ""{Server}""" + FinalArguments += $" --quickPlayMultiplayer ""{Server}""" Else '老版本 If Server.Contains(":") Then '包含端口号 - Arguments += " --server " & Server.Split(":")(0) & " --port " & Server.Split(":")(1) + FinalArguments += $" --server {Server.Split(":")(0)} --port {Server.Split(":")(1)}" Else '不包含端口号 - Arguments += " --server " & Server & " --port 25565" + FinalArguments += $" --server {Server} --port 25565" End If If McVersionCurrent.Version.HasOptiFine Then Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Critical) End If End If - '自定义 - Dim ArgumentGame As String = Setup.Get("VersionAdvanceGame", Version:=McVersionCurrent) - Arguments += " " & If(ArgumentGame = "", Setup.Get("LaunchAdvanceGame"), ArgumentGame) '输出 McLaunchLog("Minecraft 启动参数:") - McLaunchLog(Arguments) - McLaunchArgument = Arguments + McLaunchLog(FinalArguments) + McLaunchArgument = FinalArguments End Sub 'Jvm 部分(第一段) @@ -1424,12 +1445,12 @@ Retry: End If 'Authlib-Injector If McLoginLoader.Output.Type = "Auth" Then - If (McLaunchJavaSelected.VersionCode >= 6) Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") + If McLaunchJavaSelected.VersionCode >= 6 Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") '信任系统根证书(5252#) Dim Server As String = If(McLoginLoader.Input.Type = McLoginType.Legacy, "http://hiperauth.tech/api/yggdrasil-hiper/", 'HiPer 登录 Setup.Get("VersionServerAuthServer", McVersionCurrent)) Try - Dim Response As String = NetGetCodeByRequestRetry(Server, Encoding.UTF8) + Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8) DataList.Insert(0, "-javaagent:""" & PathPure & "authlib-injector.jar""=" & Server & " -Dauthlibinjector.side=client" & " -Dauthlibinjector.yggdrasil.prefetched=" & Convert.ToBase64String(Encoding.UTF8.GetBytes(Response))) @@ -1494,12 +1515,12 @@ NextVersion: End If 'Authlib-Injector If McLoginLoader.Output.Type = "Auth" Then - If (McLaunchJavaSelected.VersionCode >= 6) Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") + If McLaunchJavaSelected.VersionCode >= 6 Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") '信任系统根证书(5252#) Dim Server As String = If(McLoginLoader.Input.Type = McLoginType.Legacy, "http://hiperauth.tech/api/yggdrasil-hiper/", 'HiPer 登录 Setup.Get("VersionServerAuthServer", Version:=McVersionCurrent)) Try - Dim Response As String = NetGetCodeByRequestRetry(Server, Encoding.UTF8) + Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8) DataList.Insert(0, "-javaagent:""" & PathPure & "authlib-injector.jar""=" & Server & " -Dauthlibinjector.side=client" & " -Dauthlibinjector.yggdrasil.prefetched=" & Convert.ToBase64String(Encoding.UTF8.GetBytes(Response))) @@ -1533,10 +1554,8 @@ NextVersion: DeDuplicateDataList.Add(CurrentEntry.Trim.Replace("McEmu= ", "McEmu=")) Next - '#3511 的清理 - DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M") - '去重 + DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M") '#3511 的清理 Dim Result As String = Join(DeDuplicateDataList.Distinct.ToList, " ") '添加 MainClass diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb index 5ef12ee..422cf67 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb @@ -169,8 +169,8 @@ Public Module ModMinecraft _McVersionCurrent = value '由于有可能是 Nothing,导致无法初始化,才得这样弄一圈 _McVersionLast = value If value Is Nothing Then Return - '重置缓存的 Mod 文件夹 - PageDownloadCompDetail.CachedFolder = Nothing + '重置缓存的下载文件夹 + PageDownloadCompDetail.CachedFolder.Clear() '统一通行证重判 If AniControlEnabled = 0 AndAlso Setup.Get("VersionServerNide", Version:=value) <> Setup.Get("CacheNideServer") AndAlso @@ -440,6 +440,33 @@ VersionSearchFinish: ''' Public ReleaseTime As New Date(1970, 1, 1, 15, 0, 0) + ''' + ''' 获取该版本的 JSON 路径。 + ''' + Public Function GetJsonPath() As String + Dim JsonPath As String = Path & Name & ".json" + If Not File.Exists(JsonPath) Then + '尝试寻找 JSON 文件 + JsonPath = Nothing + For Each JsonCandidatePath In Directory.GetFiles(Path, "*.json") + Try + Dim JsonCandidate As JObject = GetJson(ReadFile(JsonCandidatePath)) + If Not JsonCandidate.ContainsKey("mainClass") Then Continue For + If Not JsonCandidate.ContainsKey("type") Then Continue For + If Not JsonCandidate.ContainsKey("id") Then Continue For + Catch + End Try + JsonPath = JsonCandidatePath + Exit For + Next + If JsonPath Is Nothing Then + Throw New Exception($"未找到版本 JSON 文件:{Path}{Name}.json") + Else + Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, LogLevel.Debug) + End If + End If + Return JsonPath + End Function ''' ''' 该版本的 Json 文本。 ''' @@ -452,17 +479,7 @@ VersionSearchFinish: Return TrimedJson.StartsWithF("{") AndAlso TrimedJson.EndsWithF("}") End Function If _JsonText Is Nothing Then - Dim JsonPath As String = Path & Name & ".json" - If Not File.Exists(JsonPath) Then - '如果文件夹下只有一个 JSON 文件,则将其作为版本 JSON - Dim JsonFiles As String() = Directory.GetFiles(Path, "*.json") - If JsonFiles.Count = 1 Then - JsonPath = JsonFiles(0) - Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, LogLevel.Debug) - Else - Throw New Exception($"未找到版本 JSON 文件:{Path}{Name}.json") - End If - End If + Dim JsonPath As String = GetJsonPath() _JsonText = ReadFile(JsonPath) '如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试 If Not FastJsonCheck(_JsonText) Then @@ -540,9 +557,15 @@ Recheck: '继续循环 If Inherit.InheritVersion = InheritVersion Then Throw New Exception("版本依赖项出现嵌套:" & InheritVersion) InheritVersion = Inherit.InheritVersion - '合并 + '合并 Libraries 项:子版本放在前面,父版本放在后面(5978#:如果多个 jar 包中含有相同的类,Java 8 和之前的版本按照 -cp 指定的顺序选择第一个) + Dim CurrentLib As JArray = _JsonObject("libraries").DeepClone() + For Each LibToken In Inherit.JsonObject("libraries") + CurrentLib.Add(LibToken) + Next + '合并其他项:子版本优先于父版本 Inherit.JsonObject.Merge(_JsonObject) _JsonObject = Inherit.JsonObject + _JsonObject("libraries") = CurrentLib GoTo Recheck End If Catch ex As Exception @@ -580,20 +603,18 @@ Recheck: Get If Not JsonVersionInited Then JsonVersionInited = True - If File.Exists(Path & Name & ".jar") Then - Try - Using JarArchive As New ZipArchive(New FileStream(Path & Name & ".jar", FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - Dim VersionJson As ZipArchiveEntry = JarArchive.GetEntry("version.json") - If VersionJson IsNot Nothing Then - Using VersionJsonStream As New StreamReader(VersionJson.Open) - _JsonVersion = GetJson(VersionJsonStream.ReadToEnd) - End Using - End If + Try + If Not File.Exists($"{Path}{Name}.jar") Then Exit Try + Using JarArchive As New ZipArchive(New FileStream(Path & Name & ".jar", FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + Dim VersionJson As ZipArchiveEntry = JarArchive.GetEntry("version.json") + If VersionJson Is Nothing Then Exit Try + Using VersionJsonStream As New StreamReader(VersionJson.Open) + _JsonVersion = GetJson(VersionJsonStream.ReadToEnd) End Using - Catch ex As Exception - Log(ex, "从版本 jar 中读取 version.json 失败") - End Try - End If + End Using + Catch ex As Exception + Log(ex, $"从版本 jar 中读取 version.json 失败({Path}{Name}.jar)") + End Try End If Return _JsonVersion End Get @@ -1538,7 +1559,7 @@ OnLoaded: ''' 要求玩家选择一个皮肤文件,并进行相关校验。 ''' Public Function McSkinSelect() As McSkinInfo - Dim FileName As String = SelectFile("皮肤文件(*.png;*.jpg;*.webp)|*.png;*.jpg;*.webp", "选择皮肤文件") + Dim FileName As String = SelectFile("皮肤文件(*.png;*.jpg;*.jpeg;*.webp)|*.png;*.jpg;*.jpeg;*.webp", "选择皮肤文件") '验证有效性 If FileName = "" Then Return New McSkinInfo With {.IsVaild = False} @@ -1589,7 +1610,7 @@ OnLoaded: Case Else Throw New ArgumentException("皮肤地址种类无效:" & If(Type, "null")) End Select - Dim SkinString = NetGetCodeByRequestRetry(Url & Uuid) + Dim SkinString = NetRequestByClientRetry(Url & Uuid) If SkinString = "" Then Throw New Exception("皮肤返回值为空,可能是未设置自定义皮肤的用户") '处理皮肤地址 Dim SkinValue As String @@ -1611,7 +1632,7 @@ OnLoaded: Throw New Exception("用户未设置自定义皮肤") Else Dim SkinUrl As String = SkinJson("textures")("skin")("url").ToString - SkinValue = If(SkinUrl.Contains("minecraft.net"),SkinUrl.Replace("http:", "https:"),SkinUrl) + SkinValue = If(SkinUrl.Contains("minecraft.net/"), SkinUrl.Replace("http://", "https://"), SkinUrl) End If '保存缓存 WriteIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid, SkinValue) @@ -1628,9 +1649,9 @@ OnLoaded: Dim FileAddress As String = PathTemp & "Cache\Skin\" & GetHash(Address) & ".png" SyncLock McSkinDownloadLock If Not File.Exists(FileAddress) Then - NetDownloadByClient(Address, FileAddress & NetDownloadEnd) + NetDownloadByClient(Address, FileAddress & ".PCLDownloading") File.Delete(FileAddress) - FileSystem.Rename(FileAddress & NetDownloadEnd, FileAddress) + FileSystem.Rename(FileAddress & ".PCLDownloading", FileAddress) Log("[Minecraft] 皮肤下载成功:" & FileAddress) End If Return FileAddress @@ -1773,61 +1794,60 @@ OnLoaded: '获取当前支持库列表 Log("[Minecraft] 获取支持库列表:" & Version.Name) - McLibListGet = McLibListGetWithJson(Version.JsonObject) - If Not IncludeVersionJar Then Return McLibListGet + Dim Result = McLibListGetWithJson(Version.JsonObject) '需要添加原版 Jar - Dim RealVersion As McVersion - Dim RequiredJar As String = Version.JsonObject("jar")?.ToString - If Version.IsHmclFormatJson OrElse RequiredJar Is Nothing Then - 'HMCL 项直接使用自身的 Jar - '根据 Inherit 获取最深层版本 - Dim OriginalVersion As McVersion = Version - '1.17+ 的 Forge 不寻找 Inherit - If Not ((Version.Version.HasForge OrElse Version.Version.HasNeoForge) AndAlso Version.Version.McCodeMain >= 17) Then - Do Until OriginalVersion.InheritVersion = "" - If OriginalVersion.InheritVersion = OriginalVersion.Name Then Exit Do - OriginalVersion = New McVersion(PathMcFolder & "versions\" & OriginalVersion.InheritVersion & "\") - Loop + If IncludeVersionJar Then + Dim RealVersion As McVersion + Dim RequiredJar As String = Version.JsonObject("jar")?.ToString + If Version.IsHmclFormatJson OrElse RequiredJar Is Nothing Then + 'HMCL 项直接使用自身的 Jar + '根据 Inherit 获取最深层版本 + Dim OriginalVersion As McVersion = Version + '1.17+ 的 Forge 不寻找 Inherit + If Not ((Version.Version.HasForge OrElse Version.Version.HasNeoForge) AndAlso Version.Version.McCodeMain >= 17) Then + Do Until OriginalVersion.InheritVersion = "" + If OriginalVersion.InheritVersion = OriginalVersion.Name Then Exit Do + OriginalVersion = New McVersion(PathMcFolder & "versions\" & OriginalVersion.InheritVersion & "\") + Loop + End If + '需要新建对象,否则后面的 Check 会导致 McVersionCurrent 的 State 变回 Original + '复现:启动一个 Snapshot 版本 + RealVersion = New McVersion(OriginalVersion.Path) + Else + 'Json 已提供 Jar 字段,使用该字段的信息 + RealVersion = New McVersion(RequiredJar) End If - '需要新建对象,否则后面的 Check 会导致 McVersionCurrent 的 State 变回 Original - '复现:启动一个 Snapshot 版本 - RealVersion = New McVersion(OriginalVersion.Path) - Else - 'Json 已提供 Jar 字段,使用该字段的信息 - RealVersion = New McVersion(RequiredJar) + Dim ClientUrl As String, ClientSHA1 As String + '判断需求的版本是否存在 + '不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在 + If Not File.Exists(RealVersion.Path & RealVersion.Name & ".json") Then + RealVersion = Version + Log("[Minecraft] 可能缺少前置版本 " & RealVersion.Name & ",找不到对应的 json 文件", LogLevel.Debug) + End If + '获取详细下载信息 + If RealVersion.JsonObject("downloads") IsNot Nothing AndAlso RealVersion.JsonObject("downloads")("client") IsNot Nothing Then + ClientUrl = RealVersion.JsonObject("downloads")("client")("url") + ClientSHA1 = RealVersion.JsonObject("downloads")("client")("sha1") + Else + ClientUrl = Nothing + ClientSHA1 = Nothing + End If + '把所需的原版 Jar 添加进去 + Result.Add(New McLibToken With {.LocalPath = RealVersion.Path & RealVersion.Name & ".jar", .Size = 0, .IsNatives = False, .Url = ClientUrl, .SHA1 = ClientSHA1}) End If - Dim ClientUrl As String, ClientSHA1 As String - '判断需求的版本是否存在 - '不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在 - If Not File.Exists(RealVersion.Path & RealVersion.Name & ".json") Then - RealVersion = Version - Log("[Minecraft] 可能缺少前置版本 " & RealVersion.Name & ",找不到对应的 json 文件", LogLevel.Debug) - End If - '获取详细下载信息 - If RealVersion.JsonObject("downloads") IsNot Nothing AndAlso RealVersion.JsonObject("downloads")("client") IsNot Nothing Then - ClientUrl = RealVersion.JsonObject("downloads")("client")("url") - ClientSHA1 = RealVersion.JsonObject("downloads")("client")("sha1") - Else - ClientUrl = Nothing - ClientSHA1 = Nothing - End If - '把所需的原版 Jar 添加进去 - McLibListGet.Add(New McLibToken With {.LocalPath = RealVersion.Path & RealVersion.Name & ".jar", .Size = 0, .IsNatives = False, .Url = ClientUrl, .SHA1 = ClientSHA1}) + Return Result End Function ''' ''' 获取 Minecraft 某一版本忽视继承的支持库列表,即结果中没有继承项。 ''' Public Function McLibListGetWithJson(JsonObject As JObject, Optional KeepSameNameDifferentVersionResult As Boolean = False, Optional CustomMcFolder As String = Nothing) As List(Of McLibToken) CustomMcFolder = If(CustomMcFolder, PathMcFolder) - Dim BasicArray As New List(Of McLibToken) - - '添加基础 Json 项 - Dim AllLibs As JArray = JsonObject("libraries") '转换为 LibToken - For Each Library As JObject In AllLibs.Children + Dim BasicArray As New List(Of McLibToken) + For Each Library As JObject In CType(JsonObject("libraries"), JArray).Children '清理 null 项(BakaXL 会把没有的项序列化为 null,但会被 Newtonsoft 转换为 JValue,导致 Is Nothing = false;这导致了 #409) For i = Library.Properties.Count - 1 To 0 Step -1 @@ -1946,8 +1966,7 @@ OnLoaded: Try Log("[Minecraft] 开始获取统一通行证下载信息") '测试链接:https://auth.mc-user.com:233/00000000000000000000000000000000/ - DownloadInfo = GetJson(NetGetCodeByLoader({ - "https://auth.mc-user.com:233/" & Setup.Get("VersionServerNide", Version:=Version)}, IsJson:=True)) + DownloadInfo = GetJson(NetRequestByClientRetry("https://auth.mc-user.com:233/" & Setup.Get("VersionServerNide", Version:=Version))) Catch ex As Exception Log(ex, "获取统一通行证下载信息失败") End Try @@ -1969,10 +1988,9 @@ OnLoaded: '获取下载信息 Try Log("[Minecraft] 开始获取 Authlib-Injector 下载信息") - DownloadInfo = GetJson(NetGetCodeByLoader({ + DownloadInfo = GetJson(NetRequestByClientRetry( "https://authlib-injector.yushi.moe/artifact/latest.json", - "https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json" - }, IsJson:=True)) + BackupUrl:="https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json")) Catch ex As Exception Log(ex, "获取 Authlib-Injector 下载信息失败") End Try @@ -2017,12 +2035,16 @@ OnLoaded: '获取 For Each Token As McLibToken In Libs '检查文件 - Dim Checker As New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1) + Dim Checker As FileChecker + If Token.Name.ContainsF("labymod") Then + Checker = New FileChecker '不检查 LabyMod 的文件,它们提供的文件校验信息是错的(#3225) + Else + Checker = New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1) + End If If Checker.Check(Token.LocalPath) Is Nothing Then Continue For '文件不符合,添加下载 Dim Urls As New List(Of String) - If Token.Url Is Nothing AndAlso Token.Name = "net.minecraftforge:forge:universal" Then - '特判修复 Forge 部分 universal 文件缺失 URL(#5455) + If Token.Url Is Nothing AndAlso Token.Name = "net.minecraftforge:forge:universal" Then '特判修复 Forge 部分 universal 文件缺失 URL(#5455) Token.Url = "https://maven.minecraftforge.net" & Token.LocalPath.Replace(CustomMcFolder & "libraries", "").Replace("\", "/") End If If Token.Url IsNot Nothing Then @@ -2194,16 +2216,16 @@ OnLoaded: Dim LocalPath As String If Json("map_to_resources") IsNot Nothing AndAlso Json("map_to_resources").ToObject(Of Boolean) Then 'Remap - LocalPath = Version.PathIndie & "resources\" & File.Name.Replace("/", "\") + LocalPath = $"{Version.PathIndie}resources\{File.Name}" ElseIf Json("virtual") IsNot Nothing AndAlso Json("virtual").ToObject(Of Boolean) Then 'Virtual - LocalPath = PathMcFolder & "assets\virtual\legacy\" & File.Name.Replace("/", "\") + LocalPath = $"{PathMcFolder}assets\virtual\legacy\{File.Name}" Else '正常 - LocalPath = PathMcFolder & "assets\objects\" & Left(File.Value("hash").ToString, 2) & "\" & File.Value("hash").ToString + LocalPath = $"{PathMcFolder}assets\objects\{Left(File.Value("hash").ToString, 2)}\{File.Value("hash")}" End If Result.Add(New McAssetsToken With { - .LocalPath = LocalPath, + .LocalPath = LocalPath.Replace("/", "\"), .SourcePath = File.Name, .Hash = File.Value("hash").ToString, .Size = File.Value("size").ToString diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb index bb9b3d5..e6225e4 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb @@ -1,7 +1,7 @@ Imports System.IO.Compression Public Module ModMod - Private Const LocalModCacheVersion As Integer = 7 + Private Const LocalModCacheVersion As Integer = 8 Public Class McMod @@ -946,8 +946,8 @@ Finished: Try '步骤 1:获取 Hash 与对应的工程 ID Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList() - Dim ModrinthVersion = CType(GetJson(DlModRequest("https://api.modrinth.com/v2/version_files", "POST", - $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject) + Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post, + $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json") Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息") '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 If ModrinthVersion.Count = 0 Then Return @@ -967,9 +967,8 @@ Finished: Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息") '步骤 3:获取工程信息 If Not ModrinthMapping.Any() Then Return - Dim ModrinthProject = CType(GetJson(DlModRequest( - $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]", - "GET", "", "application/json")), JArray) + Dim ModrinthProject As JArray = DlModRequest( + $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]") For Each ProjectJson In ModrinthProject Dim Project As New CompProject(ProjectJson) For Each Entry In ModrinthMapping(Project.Id) @@ -978,9 +977,9 @@ Finished: Next Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息") '步骤 4:获取更新信息 - Dim ModrinthUpdate = CType(GetJson(DlModRequest("https://api.modrinth.com/v2/version_files/update", "POST", + Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post, $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"", - ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersion}""]}}", "application/json")), JObject) + ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersion}""]}}", "application/json") For Each Entry In Mods If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod) @@ -1015,8 +1014,8 @@ Finished: CurseForgeHashes.Add(Entry.CurseForgeHash) If Loader.IsAbortedWithThread(CurrentTaskThread) Then Return Next - Dim CurseForgeRaw = CType(CType(GetJson(DlModRequest("https://api.curseforge.com/v1/fingerprints/432", "POST", - $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer) + Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post, + $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches") Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息") '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 If Not CurseForgeRaw.Any() Then Return @@ -1038,8 +1037,8 @@ Finished: Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息") '步骤 3:获取工程信息 If Not CurseForgeMapping.Any() Then Return - Dim CurseForgeProject = CType(GetJson(DlModRequest("https://api.curseforge.com/v1/mods", "POST", - $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")), JObject)("data") + Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post, + $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data") Dim UpdateFileIds As New Dictionary(Of Integer, List(Of McMod)) 'FileId -> 本地 Mod 文件列表 Dim FileIdToProjectSlug As New Dictionary(Of Integer, String) For Each ProjectJson In CurseForgeProject @@ -1079,8 +1078,8 @@ Finished: Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息") '步骤 4:获取更新文件信息 If Not UpdateFileIds.Any() Then Return - Dim CurseForgeFiles = CType(GetJson(DlModRequest("https://api.curseforge.com/v1/mods/files", "POST", - $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")), JObject)("data") + Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, + $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data") Dim UpdateFiles As New Dictionary(Of McMod, CompFile) For Each FileJson In CurseForgeFiles Dim File As New CompFile(FileJson, CompType.Mod) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb index ce0c443..d1de713 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb @@ -244,7 +244,7 @@ Retry: '获取 Mod 下载信息 ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息", Sub(Task As LoaderTask(Of Integer, JArray)) - Task.Output = GetJson(DlModRequest("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(ModList, ",") & "]}", "application/json"))("data") + Task.Output = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(ModList, ",") & "]}", "application/json")("data") '如果文件已被删除,则 API 会跳过那一项 If ModList.Count > Task.Output.Count Then Throw New Exception("整合包中的部分 Mod 版本已被 Mod 作者删除,所以没法继续安装了,请向整合包作者反馈该问题") End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s diff --git a/Plain Craft Launcher 2/Modules/ModSecret.vb b/Plain Craft Launcher 2/Modules/ModSecret.vb index 3c74d22..c690527 100644 --- a/Plain Craft Launcher 2/Modules/ModSecret.vb +++ b/Plain Craft Launcher 2/Modules/ModSecret.vb @@ -1,6 +1,5 @@ '由于包含加解密等安全信息,本文件中的部分代码已被删除 -Imports System.Net.Http Imports System.Security.Cryptography Friend Module ModSecret @@ -14,7 +13,6 @@ Friend Module ModSecret 'CurseForge API Key Public CurseForgeAPIKey As String = If(Environment.GetEnvironmentVariable("PCL_CURSEFORGE_API_KEY"), "") - Friend Sub SecretOnApplicationStart() '提升 UI 线程优先级 Thread.CurrentThread.Priority = ThreadPriority.Highest @@ -73,7 +71,7 @@ Friend Module ModSecret #Region "网络鉴权" - Friend Function SecretCdnSign(UrlWithMark As String) + Friend Function SecretCdnSign(UrlWithMark As String) As String If Not UrlWithMark.EndsWithF("{CDN}") Then Return UrlWithMark Return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20") End Function @@ -92,7 +90,6 @@ Friend Module ModSecret If Url.Contains("api.curseforge.com") Then Req.Headers.Add("x-api-key", CurseForgeAPIKey) End Sub - #End Region #Region "字符串加解密" diff --git a/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb b/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb deleted file mode 100644 index 1f5ea45..0000000 --- a/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb +++ /dev/null @@ -1,59 +0,0 @@ -Imports System.Net - -Public Class CookieWebClient - Inherits WebClient - - Public Sub New(container As CookieContainer, Headers As Dictionary(Of String, String)) - Me.New(container) - For Each keyVal In Headers - Me.Headers(keyVal.Key) = keyVal.Value - Next - End Sub - - Public Sub New() - Me.New(New CookieContainer()) - End Sub - - Public Sub New(container As CookieContainer) - Me.container = container - End Sub - - Private Shadows ReadOnly container As New CookieContainer() - - ''' - ''' 以毫秒为单位的超时。 - ''' - Public Timeout As Integer = 600000 - - Protected Overrides Function GetWebRequest(address As Uri) As WebRequest - Dim r As WebRequest = MyBase.GetWebRequest(address) - Dim request = TryCast(r, HttpWebRequest) - - If request IsNot Nothing Then - request.CookieContainer = container - request.Timeout = Timeout - End If - - Return r - End Function - - Protected Overrides Function GetWebResponse(request As WebRequest, result As IAsyncResult) As WebResponse - Dim response As WebResponse = MyBase.GetWebResponse(request, result) - ReadCookies(response) - Return response - End Function - - Protected Overrides Function GetWebResponse(request As WebRequest) As WebResponse - Dim response As WebResponse = MyBase.GetWebResponse(request) - ReadCookies(response) - Return response - End Function - - Private Sub ReadCookies(r As WebResponse) - Dim response = TryCast(r, HttpWebResponse) - If response IsNot Nothing Then - Dim cookies As CookieCollection = response.Cookies - container.Add(cookies) - End If - End Sub -End Class diff --git a/Plain Craft Launcher 2/Modules/Base/ModDragHelper.vb b/Plain Craft Launcher 2/Modules/ThirdParty/DragHelper.vb similarity index 68% rename from Plain Craft Launcher 2/Modules/Base/ModDragHelper.vb rename to Plain Craft Launcher 2/Modules/ThirdParty/DragHelper.vb index 8db1bf5..ff950c4 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModDragHelper.vb +++ b/Plain Craft Launcher 2/Modules/ThirdParty/DragHelper.vb @@ -1,11 +1,10 @@ -Imports System +'来源:https://blog.csdn.net/simpleman2000/article/details/140952294 + Imports System.ComponentModel Imports System.Runtime.InteropServices -Imports System.Text Imports System.Windows.Interop - -Public Class DragFileHelper +Public Class DragHelper Public Event DragDrop As EventHandler @@ -32,22 +31,22 @@ Public Class DragFileHelper Public Property HwndIntPtrSource As HwndSource Public Sub AddHook() - Me.RemoveDragHook() - Me.HwndIntPtrSource.AddHook(AddressOf WndProc) - Dim handle As IntPtr = Me.HwndIntPtrSource.Handle + RemoveDragHook() + HwndIntPtrSource.AddHook(AddressOf WndProc) + Dim handle As IntPtr = HwndIntPtrSource.Handle If IsUserAnAdmin() Then RevokeDragDrop(handle) DragAcceptFiles(handle, True) ChangeMessageFilter(handle) End Sub Public Sub RemoveDragHook() - Me.HwndIntPtrSource.RemoveHook(AddressOf WndProc) - DragAcceptFiles(Me.HwndIntPtrSource.Handle, False) + HwndIntPtrSource.RemoveHook(AddressOf WndProc) + DragAcceptFiles(HwndIntPtrSource.Handle, False) End Sub - Private Function WndProc(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr, ByRef handled As Boolean) As IntPtr + Private Function WndProc(hwnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr, ByRef handled As Boolean) As IntPtr Dim filePaths As String() = Nothing - Dim point As POINT = New POINT() + Dim point As New POINT() If TryGetDropInfo(msg, wParam, filePaths, point) Then DropPoint = point @@ -59,31 +58,31 @@ Public Class DragFileHelper End Function - Private Shared Function ChangeWindowMessageFilterEx(ByVal hWnd As IntPtr, ByVal msg As UInteger, ByVal action As UInteger, ByRef pChangeFilterStruct As CHANGEFILTERSTRUCT) As Boolean + Private Shared Function ChangeWindowMessageFilterEx(hWnd As IntPtr, msg As UInteger, action As UInteger, ByRef pChangeFilterStruct As CHANGEFILTERSTRUCT) As Boolean End Function - Private Shared Function ChangeWindowMessageFilter(ByVal msg As UInteger, ByVal flags As UInteger) As Boolean + Private Shared Function ChangeWindowMessageFilter(msg As UInteger, flags As UInteger) As Boolean End Function - Private Shared Sub DragAcceptFiles(ByVal hWnd As IntPtr, ByVal fAccept As Boolean) + Private Shared Sub DragAcceptFiles(hWnd As IntPtr, fAccept As Boolean) End Sub - Private Shared Function DragQueryFile(ByVal hWnd As IntPtr, ByVal iFile As UInteger, ByVal lpszFile As StringBuilder, ByVal cch As Integer) As UInteger + Private Shared Function DragQueryFile(hWnd As IntPtr, iFile As UInteger, lpszFile As StringBuilder, cch As Integer) As UInteger End Function - Private Shared Function DragQueryPoint(ByVal hDrop As IntPtr, ByRef lppt As POINT) As Boolean + Private Shared Function DragQueryPoint(hDrop As IntPtr, ByRef lppt As POINT) As Boolean End Function - Private Shared Sub DragFinish(ByVal hDrop As IntPtr) + Private Shared Sub DragFinish(hDrop As IntPtr) End Sub - Private Shared Function RevokeDragDrop(ByVal hWnd As IntPtr) As Integer + Private Shared Function RevokeDragDrop(hWnd As IntPtr) As Integer End Function @@ -109,13 +108,13 @@ Public Class DragFileHelper Private Const MSGFLT_ADD As UInteger = 1 Private Const MAX_PATH As Integer = 260 - Private Shared Sub ChangeMessageFilter(ByVal handle As IntPtr) + Private Shared Sub ChangeMessageFilter(handle As IntPtr) Dim ver As Version = Environment.OSVersion.Version Dim isVistaOrHigher As Boolean = ver >= New Version(6, 0) Dim isNt61OrHiger As Boolean = ver >= New Version(6, 1) If isVistaOrHigher Then - Dim status As CHANGEFILTERSTRUCT = New CHANGEFILTERSTRUCT With {.cbSize = 8} + Dim status As New CHANGEFILTERSTRUCT With {.cbSize = 8} For Each msg As UInteger In New UInteger() {WM_DROPFILES, WM_COPYGLOBALDATA, WM_COPYDATA} Dim [error] As Boolean = False If isNt61OrHiger Then @@ -129,7 +128,7 @@ Public Class DragFileHelper End If End Sub - Private Shared Function TryGetDropInfo(ByVal msg As Integer, ByVal wParam As IntPtr, ByRef dropFilePaths As String(), ByRef dropPoint As POINT) As Boolean + Private Shared Function TryGetDropInfo(msg As Integer, wParam As IntPtr, ByRef dropFilePaths As String(), ByRef dropPoint As POINT) As Boolean dropFilePaths = Nothing dropPoint = New POINT() @@ -141,7 +140,7 @@ Public Class DragFileHelper For i As UInteger = 0 To fileCount - 1 Dim sb As New StringBuilder(MAX_PATH) Dim result As UInteger = DragQueryFile(wParam, i, sb, sb.Capacity) - If result > 0 Then dropFilePaths(CInt(i)) = sb.ToString() + If result > 0 Then dropFilePaths(i) = sb.ToString() Next DragQueryPoint(wParam, dropPoint) @@ -149,4 +148,4 @@ Public Class DragFileHelper Return True End Function -End Class \ No newline at end of file +End Class diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index 8d8a259..9de41ac 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/My Project/Resources.Designer.vb b/Plain Craft Launcher 2/My Project/Resources.Designer.vb index 81e998d..dfc568a 100644 --- a/Plain Craft Launcher 2/My Project/Resources.Designer.vb +++ b/Plain Craft Launcher 2/My Project/Resources.Designer.vb @@ -63,9 +63,9 @@ Namespace My.Resources ''' ''' 查找 System.Byte[] 类型的本地化资源。 ''' - Friend ReadOnly Property Custom() As Byte() + Friend ReadOnly Property CacheCow_Client() As Byte() Get - Dim obj As Object = ResourceManager.GetObject("Custom", resourceCulture) + Dim obj As Object = ResourceManager.GetObject("CacheCow.Client", resourceCulture) Return CType(obj,Byte()) End Get End Property @@ -73,9 +73,29 @@ Namespace My.Resources ''' ''' 查找 System.Byte[] 类型的本地化资源。 ''' - Friend ReadOnly Property Dialogs() As Byte() + Friend ReadOnly Property CacheCow_Client_FileStore() As Byte() Get - Dim obj As Object = ResourceManager.GetObject("Dialogs", resourceCulture) + Dim obj As Object = ResourceManager.GetObject("CacheCow.Client.FileStore", resourceCulture) + Return CType(obj,Byte()) + End Get + End Property + + ''' + ''' 查找 System.Byte[] 类型的本地化资源。 + ''' + Friend ReadOnly Property CacheCow_Common() As Byte() + Get + Dim obj As Object = ResourceManager.GetObject("CacheCow.Common", resourceCulture) + Return CType(obj,Byte()) + End Get + End Property + + ''' + ''' 查找 System.Byte[] 类型的本地化资源。 + ''' + Friend ReadOnly Property Custom() As Byte() + Get + Dim obj As Object = ResourceManager.GetObject("Custom", resourceCulture) Return CType(obj,Byte()) End Get End Property @@ -105,7 +125,7 @@ Namespace My.Resources ''' Friend ReadOnly Property Imazen_WebP() As Byte() Get - Dim obj As Object = ResourceManager.GetObject("Imazen_WebP", resourceCulture) + Dim obj As Object = ResourceManager.GetObject("Imazen.WebP", resourceCulture) Return CType(obj,Byte()) End Get End Property @@ -120,16 +140,6 @@ Namespace My.Resources End Get End Property - ''' - ''' 查找 System.Byte[] 类型的本地化资源。 - ''' - Friend ReadOnly Property Json() As Byte() - Get - Dim obj As Object = ResourceManager.GetObject("Json", resourceCulture) - Return CType(obj,Byte()) - End Get - End Property - ''' ''' 查找 System.Byte[] 类型的本地化资源。 ''' @@ -160,6 +170,36 @@ Namespace My.Resources End Get End Property + ''' + ''' 查找 System.Byte[] 类型的本地化资源。 + ''' + Friend ReadOnly Property Newtonsoft_Json() As Byte() + Get + Dim obj As Object = ResourceManager.GetObject("Newtonsoft.Json", resourceCulture) + Return CType(obj,Byte()) + End Get + End Property + + ''' + ''' 查找 System.Byte[] 类型的本地化资源。 + ''' + Friend ReadOnly Property Ookii_Dialogs_Wpf() As Byte() + Get + Dim obj As Object = ResourceManager.GetObject("Ookii.Dialogs.Wpf", resourceCulture) + Return CType(obj,Byte()) + End Get + End Property + + ''' + ''' 查找 System.Byte[] 类型的本地化资源。 + ''' + Friend ReadOnly Property System_Net_Http_Formatting() As Byte() + Get + Dim obj As Object = ResourceManager.GetObject("System.Net.Http.Formatting", resourceCulture) + Return CType(obj,Byte()) + End Get + End Property + ''' ''' 查找 System.Byte[] 类型的本地化资源。 ''' diff --git a/Plain Craft Launcher 2/My Project/Resources.resx b/Plain Craft Launcher 2/My Project/Resources.resx index 8f1da65..8db2019 100644 --- a/Plain Craft Launcher 2/My Project/Resources.resx +++ b/Plain Craft Launcher 2/My Project/Resources.resx @@ -118,27 +118,30 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\CacheCow.Client.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\CacheCow.Client.FileStore.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\CacheCow.Common.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\resources\custom.xaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - ..\resources\ookii.dialogs.wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ..\Resources\forge-installer.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ..\Resources\Help.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + ..\Resources\Imazen.WebP.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ..\Resources\java-wrapper.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - ..\Resources\Newtonsoft.Json.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ..\Resources\libwebp64.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -148,6 +151,15 @@ ..\Resources\NAudio.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Newtonsoft.Json.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\ookii.dialogs.wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\System.Net.Http.Formatting.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\transformer.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml index 2e3c8c5..944bca6 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml @@ -92,4 +92,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb index 18be6d0..f3d278c 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb @@ -99,7 +99,6 @@ Public Class PageComp End If TargetVersion = Nothing If TargetName IsNot Nothing Then - '设置搜索框内容 TextSearchName.Text = TargetName TargetName = Nothing End If diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb index 3f1b0c9..32a0c61 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb @@ -73,13 +73,14 @@ '筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表) Private Function GetResults() As List(Of CompFile) Dim Results As List(Of CompFile) = CompFileLoader.Output - If PageType = CompType.Any Then - Results = Results.Where(Function(r) r.Type <> CompType.Plugin).ToList - ElseIf PageType = CompType.Shader OrElse PageType = CompType.ResourcePack Then - Results = Results.ToList - Else - Results = Results.Where(Function(r) r.Type = PageType).ToList - End If + Select Case PageType + Case CompType.Any + Results = Results.Where(Function(r) r.Type <> CompType.Plugin).ToList + Case CompType.Shader, CompType.ResourcePack + '不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(#6473) + Case Else + Results = Results.Where(Function(r) r.Type = PageType).ToList + End Select Return Results End Function Private Sub Load_OnFinish() @@ -358,7 +359,7 @@ Return False End Function '获取常规资源默认下载位置 - If CachedFolder.ContainsKey(Project.Type) AndAlso Not String.IsNullOrEmpty(CachedFolder(Project.Type)) Then + If CachedFolder.ContainsKey(Project.Type) Then DefaultFolder = CachedFolder(Project.Type) Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}") ElseIf McVersionCurrent IsNot Nothing AndAlso IsVersionSuitable(McVersionCurrent) Then @@ -424,13 +425,7 @@ If Not Target.Contains("\") Then Return '构造步骤加载器 Dim LoaderName As String = Desc & "下载:" & GetFileNameWithoutExtentionFromPath(Target) & " " - If Target <> DefaultFolder Then - If CachedFolder.ContainsKey(Project.Type) Then - CachedFolder(Project.Type) = GetPathFromFullPath(Target) - Else - CachedFolder.Add(Project.Type, GetPathFromFullPath(Target)) - End If - End If + If Target <> DefaultFolder Then CachedFolder(Project.Type) = GetPathFromFullPath(Target) Dim Loaders As New List(Of LoaderBase) Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True}) '启动 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb index 5829841..02c92e2 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb @@ -570,7 +570,8 @@ pause" '官方源 Dim PageData As String Try - PageData = NetGetCodeByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, New UTF8Encoding(False), 15000, "text/html", True) + PageData = NetRequestByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, + Encoding:=New UTF8Encoding(False), Timeout:=15000, Accept:="text/html", UseBrowserUserAgent:=True) Task.Progress = 0.8 Sources.Add("https://optifine.net/" & RegexSearch(PageData, "downloadx\?f=[^""']+")(0)) Log("[Download] OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址:" & Sources.Last) @@ -733,7 +734,8 @@ Retry: '官方源 Dim PageData As String Try - PageData = NetGetCodeByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, New UTF8Encoding(False), 15000, "text/html", True) + PageData = NetRequestByClient("https://optifine.net/adloadx?f=" & DownloadInfo.NameFile, + Encoding:=New UTF8Encoding(False), Timeout:=15000, Accept:="text/html", UseBrowserUserAgent:=True) Task.Progress = 0.8 Sources.Add("https://optifine.net/" & RegexSearch(PageData, "downloadx\?f=[^""']+")(0)) Log("[Download] OptiFine " & DownloadInfo.NameDisplay & " 官方下载地址:" & Sources.Last) @@ -1340,7 +1342,7 @@ Retry: If Json("data") IsNot Nothing AndAlso Json("data")("MOJMAPS") IsNot Nothing Then '下载原版 Json 文件 Task.Progress = 0.4 - Dim RawJson As JObject = GetJson(NetGetCodeByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True)) + Dim RawJson As JObject = GetJson(NetRequestByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True)) '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg] Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@") Dim Address = McLibGet(OriginalName).Replace(".jar", "-mappings." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1)) @@ -1641,30 +1643,31 @@ Retry: Public Sub McDownloadForgeRecommendedRefresh() If IsForgeRecommendedRefreshed Then Return IsForgeRecommendedRefreshed = True - RunInNewThread(Sub() - Try - Log("[Download] 刷新 Forge 推荐版本缓存开始") - Dim Result As String = NetGetCodeByLoader("https://bmclapi2.bangbang93.com/forge/promos") - If Result.Length < 1000 Then Throw New Exception("获取的结果过短(" & Result & ")") - Dim ResultJson As JContainer = GetJson(Result) - '获取所有推荐版本列表 - Dim RecommendedList As New List(Of String) - For Each Version As JObject In ResultJson - If Version("name") Is Nothing OrElse Version("build") Is Nothing Then Continue For - Dim Name As String = Version("name") - If Not Name.EndsWithF("-recommended") Then Continue For - '内容为:"1.15.2":"31.2.0" - RecommendedList.Add("""" & Name.Replace("-recommended", """:""" & Version("build")("version").ToString & """")) - Next - If RecommendedList.Count < 5 Then Throw New Exception("获取的推荐版本数过少(" & Result & ")") - '保存 - Dim CacheJson As String = "{" & Join(RecommendedList, ",") & "}" - WriteFile(PathTemp & "Cache\ForgeRecommendedList.json", CacheJson) - Log("[Download] 刷新 Forge 推荐版本缓存成功") - Catch ex As Exception - Log(ex, "刷新 Forge 推荐版本缓存失败") - End Try - End Sub, "ForgeRecommendedRefresh") + RunInNewThread( + Sub() + Try + Log("[Download] 刷新 Forge 推荐版本缓存开始") + Dim Result As String = NetRequestByClientRetry("https://bmclapi2.bangbang93.com/forge/promos") + If Result.Length < 1000 Then Throw New Exception("获取的结果过短(" & Result & ")") + Dim ResultJson As JContainer = GetJson(Result) + '获取所有推荐版本列表 + Dim RecommendedList As New List(Of String) + For Each Version As JObject In ResultJson + If Version("name") Is Nothing OrElse Version("build") Is Nothing Then Continue For + Dim Name As String = Version("name") + If Not Name.EndsWithF("-recommended") Then Continue For + '内容为:"1.15.2":"31.2.0" + RecommendedList.Add("""" & Name.Replace("-recommended", """:""" & Version("build")("version").ToString & """")) + Next + If RecommendedList.Count < 5 Then Throw New Exception("获取的推荐版本数过少(" & Result & ")") + '保存 + Dim CacheJson As String = "{" & Join(RecommendedList, ",") & "}" + WriteFile(PathTemp & "Cache\ForgeRecommendedList.json", CacheJson) + Log("[Download] 刷新 Forge 推荐版本缓存成功") + Catch ex As Exception + Log(ex, "刷新 Forge 推荐版本缓存失败") + End Try + End Sub, "ForgeRecommendedRefresh") End Sub Private IsForgeRecommendedRefreshed As Boolean = False diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml index 5578905..35c1a4e 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml @@ -27,8 +27,7 @@ - - + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb index 0e347f7..61eedf1 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb @@ -476,11 +476,6 @@ Else HintModOptiFine.Visibility = Visibility.Collapsed End If - If SelectedFabric IsNot Nothing AndAlso VersionSortInteger(SelectedMinecraftId, "1.21.4") > 0 Then - HintModOptiFineHigh.Visibility = Visibility.Visible - Else - HintModOptiFineHigh.Visibility = Visibility.Collapsed - End If '结束 IsReloading = False End Sub @@ -676,13 +671,16 @@ If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容" If LoadOptiFine Is Nothing OrElse LoadOptiFine.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "加载中……" If LoadOptiFine.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadOptiFine.State, Object).Error.Message - '检查 Fabric 1.20.5+:没有 OptiFabric 故全部不兼容 - If SelectedFabric IsNot Nothing AndAlso VersionSortInteger(SelectedMinecraftId, "1.20.4") > 0 Then Return "与 Fabric 不兼容" '检查 Forge 1.13 - 1.14.3:全部不兼容 If SelectedForge IsNot Nothing AndAlso VersionSortInteger(SelectedMinecraftId, "1.13") >= 0 AndAlso VersionSortInteger("1.14.3", SelectedMinecraftId) >= 0 Then Return "与 Forge 不兼容" End If + '检查 Fabric 1.20.5+:全部不兼容 + If SelectedFabric IsNot Nothing AndAlso + VersionSortInteger(SelectedMinecraftId, "1.20.4") > 0 Then + Return "与 Fabric 不兼容" + End If '检查 Forge 版本 Dim HasAny As Boolean = False Dim HasRequiredVersion As Boolean = False @@ -988,8 +986,11 @@ Private Function LoadFabricGetError() As String If LoadFabric Is Nothing OrElse LoadFabric.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "加载中……" If LoadFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadFabric.State, Object).Error.Message - '检查 Fabric 1.20.5+:没有 OptiFabric 故全部不兼容 - If SelectedOptiFine IsNot Nothing AndAlso VersionSortInteger(SelectedMinecraftId, "1.20.4") > 0 Then Return "与 OptiFine 不兼容" + '检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容 + If SelectedOptiFine IsNot Nothing AndAlso + VersionSortInteger(SelectedMinecraftId, "1.20.4") > 0 Then + Return "与 OptiFine 不兼容" + End If For Each Version As JObject In DlFabricListLoader.Output.Value("game") If Version("version").ToString = SelectedMinecraftId.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") Then If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容" diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb index 533ce72..7181978 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb @@ -105,13 +105,15 @@ Dim UnknownFailureCount As Integer = 0 Do While Not MyConverter.IsExited Try - Dim Result = NetRequestOnce( - "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", "POST", - "grant_type=urn:ietf:params:oauth:grant-type:device_code" & "&" & - "client_id=" & OAuthClientId & "&" & - "device_code=" & DeviceCode & "&" & - "scope=XboxLive.signin%20offline_access", - "application/x-www-form-urlencoded", 5000 + UnknownFailureCount * 5000, MakeLog:=False) + Dim Result = NetRequestByClient( + "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", HttpMethod.Post, + Content:= + "grant_type=urn:ietf:params:oauth:grant-type:device_code" & "&" & + "client_id=" & OAuthClientId & "&" & + "device_code=" & DeviceCode & "&" & + "scope=XboxLive.signin%20offline_access", + ContentType:="application/x-www-form-urlencoded", + Timeout:=5000 + UnknownFailureCount * 5000) '获取结果 Dim ResultJson As JObject = GetJson(Result) McLaunchLog($"令牌过期时间:{ResultJson("expires_in")} 秒") @@ -125,8 +127,8 @@ ElseIf ex.Message.Contains("expired_token") Then Finished(New Exception("$登录用时太长啦,重新试试吧!")) Return - Else If ex.Message.Contains("Account security interrupt") Then - Finished(New Exception("$非常抱歉,该账号由于安全问题无法登陆,请前往 Microsoft 账户页获取更多信息。")) + ElseIf ex.Message.Contains("Account security interrupt") Then + Finished(New Exception("$该账号由于安全问题无法登陆,请前往微软账户页获取更多信息。")) Return ElseIf ex.Message.Contains("service abuse") Then Finished(New Exception("$非常抱歉,该账号已被微软封禁,无法登录。")) diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb index 190326d..352c69b 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb @@ -259,12 +259,13 @@ Retry: End Sub) If SelId Is Nothing Then Return '发送请求 - Dim Result As String = NetRequestRetry("https://api.minecraftservices.com/minecraft/profile/capes/active", - If(SelId = 0, "DELETE", "PUT"), - If(SelId = 0, "", New JObject(New JProperty("capeId", SkinData("capes")(SelId - 1)("id"))).ToString(0)), - "application/json", Headers:=New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}}) + Dim Result As String = NetRequestByClientRetry("https://api.minecraftservices.com/minecraft/profile/capes/active", + If(SelId = 0, HttpMethod.Delete, HttpMethod.Put), + Content:=If(SelId = 0, "", New JObject(New JProperty("capeId", SkinData("capes")(SelId - 1)("id"))).ToString(0)), + ContentType:="application/json", + Headers:={{"Authorization", "Bearer " & AccessToken}}) If Result.Contains("""errorMessage""") Then - Hint("更改披风失败:" & GetJson(Result)("errorMessage"), HintType.Critical) + Hint("更改披风失败:" & GetJson(Result)("errorMessage").ToString, HintType.Critical) Return Else Hint("更改披风成功!", HintType.Finish) diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb index f9a2be3..2fafa6f 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb @@ -156,7 +156,7 @@ Download: Dim Version As String = "" Dim NeedDownload As Boolean = True Try - Version = NetGetCodeByRequestOnce(VersionAddress, Timeout:=10000) + Version = NetRequestByClientRetry(VersionAddress) If Version.Length > 1000 Then Throw New Exception($"获取的主页版本过长({Version.Length} 字符)") Dim CurrentVersion As String = Setup.Get("CacheSavedPageVersion") If Version <> "" AndAlso CurrentVersion <> "" AndAlso Version = CurrentVersion Then @@ -171,7 +171,7 @@ Download: End Try '实际下载 If NeedDownload Then - Dim FileContent As String = NetGetCodeByRequestRetry(Address) + Dim FileContent As String = NetRequestByClientRetry(Address) Log($"[Page] 已联网下载主页,内容长度:{FileContent.Length},来源:{Address}") Setup.Set("CacheSavedPageUrl", Address) Setup.Set("CacheSavedPageVersion", Version) @@ -231,7 +231,9 @@ Download: Try '修改时应同时修改 PageOtherHelpDetail.Init Content = HelpArgumentReplace(Content) - If Content.Contains("xmlns") Then Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间 + Do While Content.Contains("xmlns") + Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间 + Loop Content = "" & Content & "" Log($"[Page] 实例化:加载主页 UI 开始,最终内容长度:{Content.Count}") PanCustom.Children.Add(GetObjectFromXML(Content)) diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml index ff528d2..2ca71d0 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:PCL" mc:Ignorable="d" d:DesignWidth="297.6"> - + diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb index f3879f4..33cafbb 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb @@ -64,6 +64,7 @@ Public Class PageLoginLegacy Private Sub ComboLegacy_TextChanged(sender As Object, e As TextChangedEventArgs) Handles ComboName.TextChanged If Setup.Get("LaunchSkinType") = 0 Then PageLaunchLeft.SkinLegacy.Start(IsForceRestart:=True) HintChinese.Visibility = If(RegexCheck(ComboName.Text, "^[0-9A-Za-z_]*$"), Visibility.Collapsed, Visibility.Visible) + RunInUi(Sub() PanMain.InvalidateMeasure(), True) '由于 WPF 的 Bug,它不一定会自动更新 HintChinese 的大小(#6627) End Sub Private Sub Skin_Click() Handles Skin.Click If (Setup.Get("UiHiddenPageSetup") OrElse Setup.Get("UiHiddenSetupLaunch")) AndAlso Not PageSetupUI.HiddenForceShow Then diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml index 170f534..f6c72da 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml @@ -23,9 +23,11 @@ - - + + + + diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb index dd33df6..325a077 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb @@ -107,7 +107,7 @@ Retry: McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True) GoTo Retry ElseIf Result.Contains("""error""") Then - Hint("更改皮肤失败:" & GetJson(Result)("error"), HintType.Critical) + Hint("更改皮肤失败:" & GetJson(Result)("error").ToString, HintType.Critical) Return End If '获取新皮肤地址 diff --git a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml index eb864b7..cff69c5 100644 --- a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml +++ b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml @@ -1,468 +1,491 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb index b553cc7..19f89cf 100644 --- a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb @@ -227,7 +227,7 @@ DeleteDirectory(Version.Path) Hint("版本 " & Version.Name & " 已永久删除!", HintType.Finish) Else - FileIO.FileSystem.DeleteDirectory(Version.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + FileIO.FileSystem.DeleteDirectory(Version.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin) Hint("版本 " & Version.Name & " 已删除到回收站!", HintType.Finish) End If Case 2 diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml index 1416b83..a04f7f7 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml @@ -205,7 +205,7 @@ - + @@ -240,10 +240,10 @@ ToolTip="还原" ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1" Logo="M530 0c287 0 521 229 521 511s-233 511-521 511c-233 0-436-151-500-368a63 63 0 0 1 44-79 65 65 0 0 1 80 43c48 162 200 276 375 276 215 0 390-171 390-383s-174-383-390-383c-103 0-199 39-270 106l21-5a63 63 0 0 1 33 123l-157 42a65 65 0 0 1-90-42l-49-183a65 65 0 1 1 126-33l6 26A524 524 0 0 1 530 0z" LogoScale="0.9" /> - - + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml index 4142ee9..9fd7422 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml @@ -198,7 +198,7 @@ - + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb index a4cb54d..e39438e 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb @@ -1,6 +1,4 @@ -Imports NAudio.Wave - -Public Class PageSetupUI +Public Class PageSetupUI Public Shadows IsLoaded As Boolean = False @@ -276,7 +274,7 @@ Public Class PageSetupUI '顶部栏 Private Sub BtnLogoChange_Click(sender As Object, e As EventArgs) Handles BtnLogoChange.Click - Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp;*.jpeg)|*.png;*.jpg;*.gif;*.webp;*.jpeg", "选择图片") + Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.jpeg;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.gif;*.webp", "选择图片") If FileName = "" Then Return Try '拷贝文件 @@ -321,7 +319,7 @@ Refresh: Return End If '没有图片则要求选择 - Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp)|*.png;*.jpg;*.gif;*.webp", "选择图片") + Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp", "选择图片") If FileName = "" Then FrmMain.ImageTitleLogo.Source = Nothing e.Handled = True @@ -359,15 +357,9 @@ Refresh: PanMusicVolume.Visibility = Visibility.Visible PanMusicDetail.Visibility = Visibility.Visible BtnMusicClear.Visibility = Visibility.Visible - Dim validMusicCount As Integer = 0 - For Each File In EnumerateFiles(Path & "PCL\Musics\") - Try - If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(File.Extension.ToLower) Then Continue For - validMusicCount += 1 - Catch ex As Exception - End Try - Next - CardMusic.Title = "背景音乐(" & validMusicCount & " 首)" + CardMusic.Title = "背景音乐(" & + EnumerateFiles(Path & "PCL\Musics\").Count(Function(f) {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(f.Extension.ToLower)) & + " 首)" Else PanMusicVolume.Visibility = Visibility.Collapsed PanMusicDetail.Visibility = Visibility.Collapsed diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb index 6a5e6b0..9f03eed 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb @@ -564,8 +564,8 @@ Public Class PageVersionExport Sub() Try Dim ModrinthHashes = Loader.Input.Select(Function(m) m.ModrinthHash) - Dim ModrinthRaw = CType(GetJson(DlModRequest("https://api.modrinth.com/v2/version_files", "POST", - $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject) + Dim ModrinthRaw As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post, + $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json") For Each ModFile In Loader.Input '查找对应的文件 If Not ModrinthRaw.ContainsKey(ModFile.ModrinthHash) Then Continue For @@ -589,8 +589,8 @@ Public Class PageVersionExport Try If ModrinthUploadMode Then Return 'Modrinth 上传模式下,不能从 CurseForge 获取信息 Dim CurseForgeHashes = Loader.Input.Select(Function(m) m.CurseForgeHash) - Dim CurseForgeRaw = CType(CType(GetJson(DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", "POST", - $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer) + Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", HttpMethod.Post, + $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches") For Each ResultJson As JObject In CurseForgeRaw If Not ResultJson.ContainsKey("file") Then Continue For Dim File As JObject = ResultJson("file") diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb index 3fe78d0..ed319a7 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb @@ -381,8 +381,9 @@ Install: ''' 下载 Mod。 ''' Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click - PageComp.TargetVersion = PageVersionLeft.Version '将当前版本设置为筛选器 - PageComp.TargetName = SearchBox.Text '将当前搜索框内容设置为筛选器 + '预设置当前版本和搜索框内容 + PageComp.TargetVersion = PageVersionLeft.Version + PageComp.TargetName = SearchBox.Text FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod) End Sub @@ -429,6 +430,11 @@ Install: If FrmMain.PageRight IsNot Me Then Return If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then ChangeAllSelected(True) End Sub + Private Sub SearchBox_PreviewKeyDown(sender As Object, e As KeyEventArgs) Handles SearchBox.PreviewKeyDown + 'Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获 + If SearchBox.Text.Any Then Return + If My.Computer.Keyboard.CtrlKeyDown AndAlso e.Key = Key.A Then PanBack.Focus() + End Sub #End Region @@ -637,14 +643,14 @@ Install: Try For Each Entry As McMod In ModList If File.Exists(Entry.Path) Then - My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin) Else Log($"[Mod] 未找到更新前的 Mod 文件,跳过对它的删除:{Entry.Path}", LogLevel.Debug) End If Next For Each Entry As KeyValuePair(Of String, String) In FileCopyList If File.Exists(Entry.Value) Then - My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin) Log($"[Mod] 更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}", LogLevel.Debug) End If If Directory.Exists(GetPathFromFullPath(Entry.Value)) Then @@ -733,7 +739,7 @@ Install: If IsShiftPressed Then File.Delete(ModEntity.Path) Else - My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin) End If Catch ex As OperationCanceledException Log(ex, "删除 Mod 被主动取消") diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb index 67afc38..075e7eb 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb @@ -116,12 +116,14 @@ Dim TempName As String = NewName & "_temp" Dim TempPath As String = PathMcFolder & "versions\" & TempName & "\" Dim IsCaseChangedOnly As Boolean = NewName.ToLower = OldName.ToLower - '重新加载版本 Json 信息,避免 HMCL 项被合并 + '重新读取版本 JSON 信息,避免 JsonObject 中已被合并的项被重新存储 Dim JsonObject As JObject Try - JsonObject = GetJson(ReadFile(PageVersionLeft.Version.Path & PageVersionLeft.Version.Name & ".json")) + Dim JsonPath As String = PageVersionLeft.Version.GetJsonPath() + JsonObject = GetJson(ReadFile(JsonPath)) + File.Delete(JsonPath) Catch ex As Exception - Log(ex, "重命名读取 json 时失败") + Log(ex, "在重命名读取 json 时失败") JsonObject = PageVersionLeft.Version.JsonObject End Try '重命名主文件夹 @@ -130,32 +132,24 @@ '清理 ini 缓存 IniClearCache(PageVersionLeft.Version.PathIndie & "options.txt") IniClearCache(PageVersionLeft.Version.Path & "PCL\Setup.ini") - '重命名 JAR 文件、JSON 文件与 natives 文件夹 - If Directory.Exists(NewPath & OldName & "-natives") Then + '重命名 jar 文件与 natives 文件夹 + '不能进行遍历重命名,否则在版本名很短的时候容易误伤其他文件(#6443) + If Directory.Exists($"{NewPath}{OldName}-natives") Then If IsCaseChangedOnly Then - My.Computer.FileSystem.RenameDirectory(NewPath & OldName & "-natives", OldName & "natives" & "_temp") - My.Computer.FileSystem.RenameDirectory(NewPath & OldName & "-natives" & "_temp", NewName & "-natives") + My.Computer.FileSystem.RenameDirectory($"{NewPath}{OldName}-natives", $"{OldName}natives_temp") + My.Computer.FileSystem.RenameDirectory($"{NewPath}{OldName}-natives_temp", $"{NewName}-natives") Else - DeleteDirectory(NewPath & NewName & "-natives") - My.Computer.FileSystem.RenameDirectory(NewPath & OldName & "-natives", NewName & "-natives") + DeleteDirectory($"{NewPath}{NewName}-natives") + My.Computer.FileSystem.RenameDirectory($"{NewPath}{OldName}-natives", $"{NewName}-natives") End If End If - If File.Exists(NewPath & OldName & ".jar") Then + If File.Exists($"{NewPath}{OldName}.jar") Then If IsCaseChangedOnly Then - My.Computer.FileSystem.RenameFile(NewPath & OldName & ".jar", OldName & "_temp" & ".jar") - My.Computer.FileSystem.RenameFile(NewPath & OldName & "_temp" & ".jar", NewName & ".jar") + My.Computer.FileSystem.RenameFile($"{NewPath}{OldName}.jar", $"{OldName}_temp.jar") + My.Computer.FileSystem.RenameFile($"{NewPath}{OldName}_temp.jar", $"{NewName}.jar") Else - File.Delete(NewPath & NewName & ".jar") - My.Computer.FileSystem.RenameFile(NewPath & OldName & ".jar", NewName & ".jar") - End If - End If - If File.Exists(NewPath & OldName & ".json") Then - If IsCaseChangedOnly Then - My.Computer.FileSystem.RenameFile(NewPath & OldName & ".json", OldName & "_temp" & ".json") - My.Computer.FileSystem.RenameFile(NewPath & OldName & "_temp" & ".json", NewName & ".json") - Else - File.Delete(NewPath & NewName & ".json") - My.Computer.FileSystem.RenameFile(NewPath & OldName & ".json", NewName & ".json") + File.Delete($"{NewPath}{NewName}.jar") + My.Computer.FileSystem.RenameFile($"{NewPath}{OldName}.jar", $"{NewName}.jar") End If End If '替换版本设置文件中的路径 @@ -166,15 +160,13 @@ If ReadIni(PathMcFolder & "PCL.ini", "Version") = OldName Then WriteIni(PathMcFolder & "PCL.ini", "Version", NewName) End If - '更改版本 Json - If File.Exists(NewPath & NewName & ".json") Then - Try - JsonObject("id") = NewName - WriteFile(NewPath & NewName & ".json", JsonObject.ToString) - Catch ex As Exception - Log(ex, "重命名版本 json 失败") - End Try - End If + '写入版本 Json + Try + JsonObject("id") = NewName + WriteFile(NewPath & NewName & ".json", JsonObject.ToString) + Catch ex As Exception + Log(ex, "重命名版本 json 失败") + End Try '刷新与提示 Hint("重命名成功!", HintType.Finish) PageVersionLeft.Version = New McVersion(NewName).Load() @@ -192,7 +184,7 @@ '选择 自定义 时修改图片 Try If ComboDisplayLogo.SelectedItem Is ItemDisplayLogoCustom Then - Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.jpeg)|*.png;*.jpg;*.gif;*.jpeg", "选择图片") + Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp", "选择图片") If FileName = "" Then Reload() '还原选项 Return diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml index dc55cae..a933ed1 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml @@ -209,10 +209,10 @@ - - + diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj index aa02536..999063b 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj @@ -140,6 +140,21 @@ false + + False + Resources\CacheCow.Client.dll + False + + + False + Resources\CacheCow.Client.FileStore.dll + False + + + False + Resources\CacheCow.Common.dll + False + False Resources\Imazen.WebP.dll @@ -149,8 +164,9 @@ False False - - ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + False + Resources\Newtonsoft.Json.dll False @@ -158,13 +174,20 @@ Resources\Ookii.Dialogs.Wpf.dll False + + + False + Resources\System.Net.Http.Formatting.dll + False + + @@ -195,11 +218,14 @@ MyIconTextButton.xaml - + + + + MyLocalModItem.xaml @@ -233,9 +259,6 @@ - - Component - PageComp.xaml @@ -499,6 +522,7 @@ FormMain.xaml Code + MSBuild:Compile Designer @@ -721,6 +745,7 @@ + @@ -946,6 +971,7 @@ + @@ -957,5 +983,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Resources/CacheCow.Client.FileStore.dll b/Plain Craft Launcher 2/Resources/CacheCow.Client.FileStore.dll new file mode 100644 index 0000000..e0457c9 Binary files /dev/null and b/Plain Craft Launcher 2/Resources/CacheCow.Client.FileStore.dll differ diff --git a/Plain Craft Launcher 2/Resources/CacheCow.Client.dll b/Plain Craft Launcher 2/Resources/CacheCow.Client.dll new file mode 100644 index 0000000..33c79be Binary files /dev/null and b/Plain Craft Launcher 2/Resources/CacheCow.Client.dll differ diff --git a/Plain Craft Launcher 2/Resources/CacheCow.Common.dll b/Plain Craft Launcher 2/Resources/CacheCow.Common.dll new file mode 100644 index 0000000..a47fb9e Binary files /dev/null and b/Plain Craft Launcher 2/Resources/CacheCow.Common.dll differ diff --git a/Plain Craft Launcher 2/Resources/Custom.xaml b/Plain Craft Launcher 2/Resources/Custom.xaml index 28d3524..ae282bc 100644 --- a/Plain Craft Launcher 2/Resources/Custom.xaml +++ b/Plain Craft Launcher 2/Resources/Custom.xaml @@ -134,6 +134,10 @@ Text="切换到下载页面" EventType="切换页面" EventData="1|1"/> + + diff --git a/Plain Craft Launcher 2/Resources/Help.zip b/Plain Craft Launcher 2/Resources/Help.zip index 54d1dd0..8936ebe 100644 Binary files a/Plain Craft Launcher 2/Resources/Help.zip and b/Plain Craft Launcher 2/Resources/Help.zip differ diff --git a/Plain Craft Launcher 2/Resources/ModData.txt b/Plain Craft Launcher 2/Resources/ModData.txt index d5d9eac..88ddec9 100644 --- a/Plain Craft Launcher 2/Resources/ModData.txt +++ b/Plain Craft Launcher 2/Resources/ModData.txt @@ -19,7 +19,7 @@ modular-forcefield-system|模块化力场系统* minechem-recompile|化学 (MineChem) - 非官方重编译版¨73373|化学 (MineChem) - 已失效 iron-chests@|更多箱子*¨iron-chests-for-fabric@cyberanner-ironchest|更多箱子 (Iron Chests)¨iron-chests-fabric@|更多箱子 (Iron Chests) balkons-weaponmod|更多武器 (Balkon's WeaponMod) -advanced-machines|高级机械 (AdvancedMachines)¨@advanced-machines-uuum|高级机械 (AdvancedMachines) - Immibis版 +advanced-machines|高级机械 (AdvancedMachines) - Chocohead版¨@advanced-machines-uuum|高级机械 (AdvancedMachines) - Immibis版 advanced-solar-panels|高级太阳能* nuclear-control-2|核电控制2* @@ -121,7 +121,7 @@ blockphysics_by_id_miner|方块物理学 (BlockPhysics) remain-in-motion|运动红石机构 (Remain in Motion) -modular-powersuits|模块化动力套装* +modular-powersuits@modularpowersuits|模块化动力套装* @@ -189,7 +189,7 @@ mekanism@|通用机械* magic-crusade-rpg-mod|魔法远征:魔法十字军 (Magic Crusade) invasions|入侵 (Invasion) -mb-battlegear-2|挖矿与砍杀:双持与战备2 (Mine & Blade: Battlegear 2) +mb-battlegear-2|挖矿与砍杀:双持与战备 2 (Mine & Blade: Battlegear 2) galacticraft-legacy@|星系 (GalactiCraft) @@ -490,7 +490,7 @@ sim-u-kraft-old|模拟城市 (Sim-U-Kraft) superheroes-unlimited|超级英雄无限* oceancraft-mod|海洋世界 (Oceancraft) - +more-enchantments-legacy|更多附魔 (More Enchantments) witching-gadgets|巫师宝具* buildcraft-compat@|建筑兼容* @@ -1006,7 +1006,7 @@ roost|鸡窝* croparia@|矿石作物/魔种之咏* simple-corn|简单玉米* thaumic-jei@ -tinkers-steelworks|匠魂炼钢厂 (Tinkers' Steelworks) +tinkers-steelworks|匠魂炼钢厂 (Tinkers' Steelworks)¨tinkers-steelworks-fork|匠魂炼钢厂 (Tinkers' Steelworks) unicode-font-extension|Unicode 字体扩展* placeable-items|可放置物品* compendium|工匠防御 (Tinkers' Compendium (Tinkers' Defense)) @@ -1187,7 +1187,7 @@ minecraft-transport-simulator@immersive-vehicles|沉浸车辆 (Immersive Vehicle chest-transporter|搬箱器* i18nupdatemod@|自动汉化更新 (I18nUpdateMod) realistic-item-drops|真实物品掉落* -pixelmon@|像素精灵宝可梦 重铸* +pixelmon@|像素宝可梦 重铸 (Pixelmon Reforged) controlling@|键位冲突显示*¨controlling-for-fabric|键位冲突显示 (Controlling) @@ -1250,7 +1250,7 @@ career-bees|职业蜜蜂* level-up|升级!重置 (Level Up!Reloaded) level-up-legacy|升级!前版 (Level Up!Legacy) moar-boats|模块化船* -numina|模块化动力装甲前置* +numina@|模块化动力装甲前置* tinkers-complement@|匠魂补充 (Tinkers' Complement) everlastingabilities@everlasting-abilities|永恒能力 (Everlasting Abilities) abyssalcraft-heads|深渊国度头颅 (AbyssalCraft Heads) @@ -1274,7 +1274,7 @@ just-enough-pattern-banners primitive-mobs|远古生物 / 原始生物* realistic-cobwebs@ simpleharvest|简单收获 (Simple Harvest) -mrcrayfishs-device-mod|MrCrayfish的设备 (MrCrayfish's Device Mod) +mrcrayfishs-device-mod|MrCrayfish 的设备 (MrCrayfish's Device Mod) block-armor|方块盔甲* stockpile|储物桶* loot-slash-conquer @@ -1378,7 +1378,7 @@ arcade-mod|街机* asmodeuscore@ building-gadgets|建筑小帮手* dooglamoo-worlds|Dooglamoo的世界* -scp-lockdown|SCP基金会:封锁 (SCP: Lockdown) +scp-lockdown@|SCP基金会:封锁 (SCP: Lockdown) middle-earth-thaumaturgy|中土神秘学 (Middle-Earth Thaumaturgy) structpro-mod-fast-schematic-spawning-system|自然神殿 (Structpro) betterfps|更好的FPS (BetterFPS) @@ -1525,7 +1525,7 @@ better-questing-standard-expansion|更好的任务-基础扩展 (Better Questing exchangers|方块交换器* flexible-tools|灵活的工具* dimensional-control|维度控制* -valkyrien-warfare@valkyrien-skies|瓦尔基里天空/瓦尔基里战争 (Valkyrien Skies/Valkyrien Warfare) +valkyrien-skies@|瓦尔基里天空* item-scroller@|物品滚轮* better-advancements@|更好的进度* lucraft-core @@ -2165,7 +2165,7 @@ betterachievements|更好的成就 (Better Achievements) extra-bows|更多的弓* forgotten-relics|失落遗物学* ore-core@|矿石核心* -ordinary-coins|普通硬币 (Ordinary Coin) +ordinary-coins|普通硬币* peanut-mod|花生! (Peanuts!) in-game-wiki-mod @@ -2233,7 +2233,7 @@ citadel@ screenshot-browser|内置截图管理器* -curio-of-undying@charm-of-undying|不死图腾槽位 (Curio of Undying / Trinket of Undying / Charm of Undying)¨curio-of-undying-fabric|不死图腾槽位 (Curio of Undying / Trinket of Undying / Charm of Undying) +curio-of-undying@charm-of-undying|不死图腾插槽/不死图腾槽位 (Curio of Undying / Trinket of Undying / Charm of Undying)¨curio-of-undying-fabric|不死图腾插槽/不死图腾槽位 (Curio of Undying / Trinket of Undying / Charm of Undying) explorercraft-worldexpansion|探险者 (Explorercraft) enigmatic-legacy@|神秘遗物* @@ -2246,7 +2246,7 @@ the-rc-mod rubybyimxiaoanag|红宝石 (Ruby) jojo-hamon-era@|JoJo-波纹时代 (JoJo-Hamon Era) -OpenSecurity|开放式防卫* +OpenSecurity|开放式安全* the-legend-era|传说纪元* level-up-hp-forge|生命提升 (Level Up HP)¨level-up-hp|生命提升 (Level Up HP) more-planet-extras @@ -2442,7 +2442,7 @@ not-enough-crashes@notenoughcrashes|崩溃优化*¨not-enough-crashes-forge|崩 crash-to-main-menu|崩溃返回主菜单* tinkers-jei -better-questing-quest-book|更好的任务-任务书 (Better Questing - Quest Book) +better-questing-quest-book@|更好的任务-任务书 (Better Questing - Quest Book) engineers-doors|工程师的门 (Engineer's Doors) tps-generator|TPS发电机 (TPS Generator) better-drowning@|更好的溺水* @@ -2491,7 +2491,7 @@ beesourceful middle-earth-industry|中洲工业 (Middle-Earth Industry) thut-wearables after-the-drizzle|细雨田园 (After the Drizzle) -cmdcam|CMD 指令摄影机 (CMDCam) +cmdcam@|CMD 指令摄影机 (CMDCam) climate-control-geographicraft@geographicraft|气候控制 / 地理世界 (Climate Control / Geographicraft) cosmetic-armor-reworked|时装盔甲重置版* @@ -2743,7 +2743,7 @@ vials|瓶瓶罐罐* advanced-lightsabers|高级光剑* fall_attack|下落击杀 (fall_attack) swimsuit|泳装 (swimsuit) -more-overlays-updated +more-overlays-updated@ dothack-hack-weapons immersive-energy|沉浸能源* crimson-moon|绯红之月* @@ -3262,7 +3262,7 @@ devtech|GTCEu-CrT开发套件* -jurassic-world-reborn-mod|侏罗纪世界重生* +jurassic-world-reborn-mod|侏罗纪重生 (Jurassic Reborn) cardboard@ enchanting-with-thaumcraft|神秘附魔学* @@ -3920,7 +3920,7 @@ the-elemelon-mod|元素西瓜* dcsp|钻石压缩 (Diamonds Compressor SP) corpse-complex@ modularvoicechat|模块化语音聊天 (ModularVoiceChat) -elytra-slot@|鞘翅槽位 (Curious Elytra / Elytra Trinket / Elytra Slot)¨elytra-slot-fabric|鞘翅槽位 (Curious Elytra / Elytra Trinket / Elytra Slot) +elytra-slot@|鞘翅插槽/鞘翅槽位 (Curious Elytra / Elytra Trinket / Elytra Slot)¨elytra-slot-fabric|鞘翅插槽/鞘翅槽位 (Curious Elytra / Elytra Trinket / Elytra Slot) tconstructcompatible|匠魂兼容 (TConstructCompatible) @@ -3955,7 +3955,7 @@ satako-library¨satako-fabric unique-enchantments-utils|独特的附魔:效用扩展 (Unique Enchantments - Utils) forge-ae2-additional-opportunity@ae2ao¨ae2ao¨legacy-ae2-additional-opportunity iconexporter@icon-exporter -curious-shulker-boxes@shulker-box-slot|潜影盒槽位 (Shulker Box Slot)¨curious-shulker-boxes-fabric|潜影盒槽位 (Shulker Box Slot) +curious-shulker-boxes@shulker-box-slot|潜影盒插槽/潜影盒槽位 (Shulker Box Slot)¨curious-shulker-boxes-fabric|潜影盒插槽/潜影盒槽位 (Shulker Box Slot) xenoclus-2 darker-depths|深岩之下* terracraft-journey @@ -4136,7 +4136,7 @@ falling-leaves-forge@fallingleavesforge|落叶 (Falling Leaves (NeoForge/Forge)) custom-entity-models-cem@cem|自定义实体模型 (Custom Entity Models) -tweakmyclient +tweakmyclient@ lovely-snails@lovely_snails|可爱蜗牛* clipboard|备忘录* @@ -4288,7 +4288,7 @@ reroll|刷新附魔* tax-free-levels@|免税附魔* portable-dimensions|便携式传送门* stone-age-by-yanny -terra-world-generator|Terra - 已经失效¨@terra +terra-world-generator|Terra - 已失效¨@terra omnis|灾厄村民拓展* omnis-backpacks @@ -4472,7 +4472,7 @@ travelers-titles@|旅人标题 (Traveler's Titles)¨travelers-titles-fabric|旅 expand-chinese-sword|中式剑拓展 (Arsenal Integration) littleframes@littlepictureframes|小画 (LittlePictureFrames) -lavalogged-blocks|岩浆充填 (Lavalogged blocks) +lavalogged-blocks@|岩浆充填 (Lavalogged blocks) awoken-world gt4-reimagined|格雷科技4:重构想 (GregTech 4 Reimagined) camouflaged-creepers @@ -4925,7 +4925,7 @@ illagers@illagers+|更多的灾厄村民 (Illagers+) villagers-follow-emeralds-fabric@|村民跟随绿宝石 (Villagers follow Emeralds) -diabloloot|暗黑破坏神战利品 (DiabloLoot) +diabloloot|暗黑破坏神战利品 (DiabloLoot - Bosses Update) yggdrasil|世界树* ftb-dripper-forge forge-snad|子沙1.14+ (Snad)¨fabric-snad|子沙1.14+ (Snad) @@ -4967,7 +4967,7 @@ happiness-is-a-warm-gun@ kvc-blocks|蛋挞君的原版补全方块篇 (KVCBlocks) enigtech2-util -environmental-creepers|环保型苦力怕* +environmental-creepers@|环保型苦力怕* look-at-that advanced-peripherals@advancedperipherals|高级外设* @@ -4986,9 +4986,9 @@ expandability squirrels|松鼠* ftb-power-pots the-graveyard-forge@|墓园 (The Graveyard)¨the-graveyard-fabric@|墓园 (The Graveyard) -nether-agriculture|下界农业* +nether-agriculture@netheragriculture|下界农业* conjuring@ -awesome-dungeon-forge@¨awesome-dungeon-fabric@ +awesome-dungeon-forge@awesome-dungeon¨awesome-dungeon-fabric¨awesome-dungeon-neoforge out-of-sight@ decorative-blocks-modded-compat|装饰方块模组兼容 (Decorative Blocks modded compat) camp-chair @@ -5283,7 +5283,7 @@ ore-prospecting-stick-forge|探矿之戒 (Rings Of Prospecting)¨ore-prospecting worldedit-express-forge¨worldedit-express-fabric awesome-dungeon-edition-ocean-forge@awesome-dungeon-edition-ocean¨awesome-dungeon-edition-ocean-fabric pistorder -escape-rope-pokemon-item-forge¨awesome-dungeon-neoforge +escape-rope-pokemon-item-forge awesome-flooring-forge@¨awesome-flooring-fabric@ medievial-entity-forge@medieval-entity-forge|中世纪生物 (Medieval Entity)¨medievial-entity-fabric@medieval-entity-fabric|中世纪生物 (Medieval Entity) upgraded-shulkers|更多潜影盒* @@ -5292,7 +5292,7 @@ bobby@|服务器区块缓存* boosted-brightness@|亮度增强* effective@¨effective-forge -voidtotem|虚空图腾 (Void Totem)¨voidtotem-fabric|虚空图腾 (Void Totem) +voidtotem@|虚空图腾 (Void Totem)¨voidtotem-fabric|虚空图腾 (Void Totem) tool-leveling-plus@tool-leveling|工具升级 (Tool Leveling+)¨tool-leveling-plus-fabric|工具升级 (Tool Leveling+) mythicmetals@|神话金属 (Mythic Metals) creeper-firework@|烟花苦力怕* @@ -5635,7 +5635,7 @@ just-enough-piglin-bartering|JEI猪灵以物易物* @aftersans53228-fabric-road|暗访谈的Fabric道路 (aftersans53228's fabric roads) primal-magick@|原初魔法* -ore-creeper|矿石苦力怕* +ore-creeper@|矿石苦力怕* panthalassa|泛古洋* stardew-armory|星露谷武器*¨stardew-armory-fabric|星露谷武器 (Stardew Armory) no-more-nether-desert-edition @@ -5693,13 +5693,13 @@ storage-drawers-crafting-recipe enhanced-attack-indicator@|增强攻击提示器* parasites-addon-overlast|寄生兽:末世扩展 (OverLast) cancel-block-update@|取消方块更新* -franciscofunari|中世纪工艺 (Medieval Craft) +franciscofunari@medieval-craft-(structures)|中世纪工艺 (Medieval Craft) d3core green-thumb-v1-2-03 greenthumb tfc-adventure|群峦冒险 (TFC+ Adventure) just-zoom-forge@just-zoom -@holograms|全息图 (Holograms) +server-holograms@holograms|全息图 (Holograms) eidolons-hexblades|幻梦咒剑 (Eidolon's Hexblades) monke-madness @@ -5770,7 +5770,7 @@ zettai-psi psicaster tweakermore@ legacy-display@ -the-backrooms|后室* +the-backrooms creatures-of-the-snow@creatures-from-the-snow-fabric|雪地里的生物! (Creatures From The Snow!)¨creatures-from-the-snow-forge|雪地里的生物! (Creatures From The Snow!) collisions-lib@ wooled-boots@|羊毛靴子* @@ -5827,10 +5827,10 @@ realistic-block-physics@ domestication-innovation@|驯养革新* snuffles@|魄罗* flan-forge@flan¨flan -awesome-dungeon-the-end-forge@¨awesome-dungeon-the-end-fabric@ +awesome-dungeon-the-end-forge@awesome-dungeon-the-end¨awesome-dungeon-the-end-fabric tehnuts-torcherino library-ferret-forge@library-ferret¨library-ferret-fabric¨library-ferret-neoforge -awesome-dungeon-nether-forge@¨awesome-dungeon-nether-fabric@ +awesome-dungeon-nether-forge@awesome-dungeon-nether¨awesome-dungeon-nether-fabric ae2-extended-life|应用能源非官方延续版 (AE2 Unofficial Extended Life) refined-cooking@|精致厨房* @@ -5996,7 +5996,7 @@ oedldoedl-resources oedldoedl-gear disguiseheads@ simple-tcon -enchanted-tooltips +enchanted-tooltips@ stackablepotions-forge¨stackablepotions mobifier¨mobifier-fabric art-of-forging-a-tetra-addon|锻造之艺 (Art of Forging) @@ -6084,7 +6084,7 @@ onastick-forge@onastick¨onastick tradernpcs more-pads|更多Pad* texture-dump -spartan-weaponry-ice-and-fire|斯巴达的武器:冰火传说 (Spartan Weaponry: Ice and Fire) +spartan-weaponry-ice-and-fire@|斯巴达的武器:冰火传说 (Spartan Weaponry: Ice and Fire) hefty-crops@ @strangefuelsmod|奇奇怪怪的燃料 (Strange Fuels Mod) custom-player-models@|自定义玩家模型 (Customizable Player Models)¨custom-player-models-fabric|自定义玩家模型 (Customizable Player Models) @@ -6482,7 +6482,7 @@ ninjaxxs-needs pixelmon-gui-blocks more-mekanism-processing@ -item-banning +item-banning@ probejs@ impaled@|更多三叉戟 (Impaled🔱) laserio @@ -6507,7 +6507,7 @@ advancements-enlarger better-muffling|更好的消音器* lightest-lamps@¨lightest-lamps-fabric bigbuckets|大铁桶 (Big Buckets)¨bigbuckets-fabric|大铁桶 (Big Buckets) -dimstorage +dimstorage@ spawner-imbuer@ unnamed-delight|无名乐事* @@ -6858,7 +6858,7 @@ the-nexus-deep-dark-dimension jepp|JEI绘画预览 (Just Enough Painting Previews) create-curios -ctm-refabricated +ctm-refabricated¨@ctm-refabricated placeable-pumpkin-pie|可放置南瓜派* birch-forest-upgrade|白桦林更新* ok-zoomer-forge @@ -7011,7 +7011,7 @@ withcoffee|咖啡相伴/天堂之路 (WithCoffee) emeraldstuff grass-slabs-carpets-stairs not-mutually-exclusive|不冲突*¨@not-mutually-exclusive|不冲突 (Not Mutually Exclusive) - 已失效 -builders-delight-forge +builders-delight-forge@builders-delight miners-minerals@ days-to-mine@7-days-to-mine|七日杀 (7 Days to Mine) pieces-of-ore @@ -7082,7 +7082,7 @@ auto-workstations-fabric@auto-workstations|自动化工作站 (Auto Workstations l2-backpack@l2backpack|莱特兰-背包* @mbp -capybara-fabric¨unnamed-animal-mod-capybara-only +capybara-uam¨unnamed-animal-mod-capybara-only just-enough-petroleum diamethysts@diamethysts! twigs@ @@ -7713,7 +7713,7 @@ odysseymod libipn@ hulsealib blues-dynamic-lights -gtc-expansion +gtc-expansion@ nether-depths-upgrade@|炽海生机* cloud-factory disc-holders-dj @@ -7901,7 +7901,7 @@ your-items-are-safe@¨your-items-are-safe-fabric techemistry the-missing-villages continents -yungs-better-ocean-monuments@|YUNG的海底神殿优化 (YUNG's Better Ocean Monuments)¨yungs-better-ocean-monuments-fabric|YUNG的海底神殿优化 (YUNG's Better Ocean Monuments)¨yungs-better-ocean-monuments-neoforge|YUNG的海底神殿优化 (YUNG's Better Ocean Monuments) +yungs-better-ocean-monuments@|YUNG 的海底神殿优化 (YUNG's Better Ocean Monuments)¨yungs-better-ocean-monuments-fabric|YUNG 的海底神殿优化 (YUNG's Better Ocean Monuments)¨yungs-better-ocean-monuments-neoforge|YUNG 的海底神殿优化 (YUNG's Better Ocean Monuments) echoing-depths advanced-finders @@ -7968,13 +7968,13 @@ status-effect-timer@statuseffecttimer logical-zoom@ the-space-age-mod|太空时代* -touhou-origins@|起源:东方project附加包* +touhou-origins@|起源:东方 Project 附加包* earth-mobs@earthmobs|地球生物* simpleores-tin just-enough-keys¨just-enough-keys-fabric why-am-i-on-fire@ -bouncing-balls-api@ +bouncing-balls-api@|弹跳球 API (Bouncing Balls API) bouncing-balls@|弹跳球* teletubbies-mod|天线宝宝* almost-unified@ @@ -7985,15 +7985,15 @@ time-arrow catch-me-if-you-can-cmiyc mc-modsis-one-way-glass|Alli的单向玻璃 (Alli's One Way Glass) sanguis -ender-trigon +ender-trigon@ litlaunch puzzle@ - +why-am-i-on-fire-refoxed @tdfp|暮色乐事:冰雪豪饮 (Twilight Delight: Frozen Pride) ftb-pack-companion teams-stages -kiwi-boi +kiwi-boi@ rubidium-toolkit@|铷 · 工具箱* advanced-combat|高级战斗装备* advanced-combat-revitalized@|高级战斗装备重置 (Advanced Combat (Revitalized)) @@ -8033,7 +8033,7 @@ better-stats@|更好的统计信息界面 (Better Statistics Screen) abyssalblocks|深渊国度幸运方块 (AbyssalBlocks) -archon +archon@ stackie @cullclouds @superfancyclouds @@ -8050,14 +8050,14 @@ berdinskiybears-armor-hud tiered-tooltips rgb-blocks-forge|调色方块 (RGB Blocks) -quicksand-fabric|流沙 (Quicksand) -better-durability|更好的耐久度* +quicksand-fabric@quicksand|流沙 (Quicksand) +better-durability@|更好的耐久度* passthrough-signs excorrelation@ packet-size-doubler @packet-size-double-forge arachnophobia-mode-for-minecraft@arachnophobia-mode -villagerconfig@|村民配置 (VillagerConfig) +villagerconfig@|村民配置 (Villager Config) jasm@speedometer paginated-advancements@paginatedadvancements|分页进度 (Paginated Advancements & Custom Frames) @backport-gamerules @@ -8070,7 +8070,7 @@ ingame-info-xml-addon-serene-seasons staff-of-building|建筑之杖 (Staff of Building) -dark-mode-everywhere|夜间模式无处不在* +dark-mode-everywhere@|夜间模式无处不在* custom-shop|自定义商店 (CustomShop) another-liquid-milk@ dark-mode-everywhere-fabric@|夜间模式无处不在Fabric版 (Dark Mode Everywhere - Fabric) @@ -8820,7 +8820,7 @@ glacier-ice gtnhmixins@ dawn-to-dusk open-tower-defence@|开放式塔防* -mtr-station-decoration@station-decoration|MTR车站装饰 (MTR Station Decoration) +mtr-station-decoration@station-decoration|MTR车站装饰 (Station Decoration) perviaminvenire silverbirch@silver-birch @@ -8841,7 +8841,7 @@ extended-slabs-plus brewin-and-chewin-fabric@|饮酒作乐 Fabric 版 (Brewin' And Chewin' [Fabric]) particular longer-login-times -broglis-plants +broglis-plants@ rlmixins@ simplehats@simple-hats|简单帽子 (Simple Hats) timeoutout-fabric@timeoutout @@ -8855,7 +8855,7 @@ create-industrial-chemistry@|机械动力:工业化学 (Create: Industrial Che cruelars-triforcemod @morespawneggs|更多刷怪蛋 (MoreSpawnEggs) -beautify-refabricated@|美化!Fabric版 (Beautify: Refabricated) +beautify-refabricated@|美化!重织 (Beautify: Refabricated) manatweaks chicken-nugget @@ -8908,7 +8908,7 @@ clear-skies-forge-port spell-power@ morecrashinfo|更多崩溃报告信息 (MoreCrashInfo) -epicfight-saopack@epicacg|史诗战斗:ACG附属包 (EpicFight ACG AddonPack) +epicfight-saopack|史诗战斗:ACG附属包 (EpicFight ACG AddonPack) screenshot-viewer@|截图查看器* gml@ @futurepack-additions|飞向未来附加 (Futurepack Additions) @@ -9981,13 +9981,13 @@ tskimi-seirans-fruit-wine@tskimiseirans-fruit-wine|月见清兰的果酒 (Tskimi graduated-cylinders@|刻度量筒* fps-monitor -stitch@ +athena@athena-mod calm-mornings@ fog-looks-good-now@ guarding@ cats-expanded@ modular-bosses -experience-obelisk@ +experience-obelisk@cognition here-be-no-dragons gnumus-settlement fading-night-vision @@ -10003,7 +10003,7 @@ mannequins simple-mango@ betterdrawhoveringtext|更好的工具信息显示 (betterDrawHoveringText) -badges-by-harmex +badges-by-harmex@cobblebadges2 terrarian-arsenal zombie-veterinarian|僵尸科兽医* just-enough-sculk-sensor @@ -10453,7 +10453,7 @@ easydropper|轻松投掷 (Easy Dropper) varied-adventure kubejs-delight block-physics-overhaul -searchlight-forge|探照灯 (Searchlight(& Wall Lights))¨searchlight|探照灯 (Searchlight(& Wall Lights)) +searchlight-forge|探照灯 (Searchlight (& Wall Lights))¨searchlight|探照灯 (Searchlight (& Wall Lights)) ancient-manuscripts@ gazebos@ scoreboard-get-time|记分板获取时间 (scoreboard get time) @@ -10705,7 +10705,7 @@ enhancedsnowman new-world-height-and-depth @inertiaanticheat -scroll-of-harvests +scroll-of-harvests@ @scripts-chunk-loaders clean-tooltips@ @@ -10893,7 +10893,7 @@ do-api@ grabby-mobs@ pandas-falling-trees@ iron-ender-chests@ -delightful-creators-fabric@|动力乐事 (Delightful Creators) +delightful-creators-forge@delightful-creators|动力乐事 (Delightful Creators)¨delightful-creators-fabric|动力乐事 (Delightful Creators) deathbutthree@death-but-three|逝不过三 (Death But Three) comparties|组队兼容* @@ -10906,7 +10906,7 @@ light-the-night tweakerge@ terrafirmaships@ better-ocelots -actually-useful-smithing-table +actually-useful-smithing-table@ denyblocks sculk-horn@-sculk-horn ydms-red-panda@ @@ -11052,7 +11052,7 @@ toomuchrain@ thirst-was-remade|口渴-重制*¨@thirst-was-remade|口渴-重制 (Thirst Was Remade) - 已失效 ies-tweaks-fabric@ brown-mooshrooms -@whetstones +whetstones@ @better-log4j-config @harvest wooden-hoppers @@ -11168,7 +11168,7 @@ shulker-meat accurate-block-placement-reborn@ favorite-items -@smart-villagers-follow-emeralds|见钱眼开 (Smart Villagers Follow Emeralds) +smart-villagers-follow-emeralds@|见钱眼开* @villager-pickup step-it-up@stepitup @@ -11307,7 +11307,7 @@ jagms-kiwis@ improved-trident@ mystic-bows forgiving-world -@skillcloaks +skill-cloaks@skillcloaks swordinthestone@ lazy-ae2-patch|懒人AE2补丁 (Lazy AE2 Patch) @@ -11322,7 +11322,7 @@ rei-custom-command|REI自定义命令 (REI Custom Command) copperative@|铜心协力* growable-storage-cells growable-cells-2 -cinchcraft +cinchcraft@ day-counter@ag-day-counter makecold|冷境 (MakeCold) @@ -11362,9 +11362,9 @@ cute-kiwi-birds-new-animal moremobheads@ ouchies@ @traditional-asphalt -@thaumic_boots +thaumic-boots@thaumic_boots carpeted-stairs@ -just-enough-archaeology +just-enough-archaeology@ bedrock_world world-preview@|世界预览* @@ -11426,7 +11426,7 @@ gigeresque lookaround projectileimmunityfix @betterzoom -@player-visibility +player-visibility@ bag-of-holding-forge@bag-of-holding @@ -11645,7 +11645,7 @@ simply-skills@ cctv-craft -redeco@ +redeco@|重铸家具 (Re:Deco) pocket-storage animated-player-mod|动画化玩家* its-thirst@ @@ -12254,7 +12254,7 @@ more-crafting-tables-mct@more-crafting-tables-lieonlion @cobblemon-quests akkamaddis-classical-alchemy|Classical Alchemy - 新 @big-pony|彩虹大马 (Big Pony) - +qraftyfied @theft akkamaddis-golden-glitter@ @obsidianbucket @@ -12331,7 +12331,7 @@ advancement-based-spawning@ heroic-armory-legacy yungs-menu-tweaks@|YUNG的菜单调整 (YUNG's Menu Tweaks)¨yungs-menu-tweaks-fabric|YUNG的菜单调整 (YUNG's Menu Tweaks)¨yungs-menu-tweaks-neoforge|YUNG的菜单调整 (YUNG's Menu Tweaks) -entity-distance-1-12-2|实体距离 (Entity Distance) +entity-distance-1-12-2@entity-distance-1.12.2|实体距离 (Entity Distance) coolesthardcore@coolest-hardcore taxcastlepillager pesky-pirates@ @@ -12412,7 +12412,7 @@ halloween-luckyblocks spooky-halloween isle-of-berk-variant-loader@ spooky-arms - +qraftys-end-villages bed-is-not-too-far-away@ @enderman-tweaks @@ -12542,7 +12542,7 @@ the-monkey-bomb@ @cobblemon-hunt ancient-animals beavery@ - +qraftys-halloween-villages vemerioraptor technique-to-kill-thrasher|毙鲨技 (technique to kill thrasher) the-open-sauce-toast-killer@ @@ -12552,7 +12552,7 @@ better-wake-up-position@|更好的起床位置 (Better Wake-up Position) railroad-blocks-forge@railroad-blocks¨railroad-blocks fossilmon@ -opack2reload|资源包惰性重载/抑验怠载 (0Pack2Reload) +opack2reload|资源包惰性重载/抑验怠载 (0Pack2Reload/OPack2Reload) wits@ @motleys-music-discs @@ -12678,7 +12678,7 @@ cheat-command pumpkinpal johnnys-biological-notes@johnnys-biological-notes-1.0-beta|Johnny的生物笔记 (Johnny's biological notes) -@autodrop +autodrop@ mutation-rerun @compactspawners @@ -12936,7 +12936,7 @@ combatify@ go-fish-updated|一起钓鱼吧!(更新版) (Go Fish (updated)) what-are-they-up-to@ -@fumo +fumofumo|Fumo - 仅 16.0.0+¨@fumo @elytraplus mcaic@ @zombification @@ -12994,7 +12994,7 @@ default-world-generator-ssp homyatos-explosives infchest@|无限箱子 (Inf Chest) @squashed -unremovableeffects +unremovableeffects@unremovable-effects traders-in-disguise-the-illager-assassin-project@ cherished-items verdantvibes@|绿意盎然 (VerdantVibes) @@ -13077,7 +13077,7 @@ rpg-parties@ astikor-carts-tfc@tfc-astikor-carts nanexs-mobs no-feather-trample@ -embers-extended-life|余烬非官方延续版 (Embers Unofficial Extended Life) +embers-extended-life@embers-unofficial-extended-life|余烬非官方延续版 (Embers Unofficial Extended Life) terrafirmajousting@ jujutsu-animation @@ -13199,12 +13199,12 @@ pescatore@ apocalypse-now@ cenozoic-nightmare-the-horror-of-prehistory -quickbackupmulti@|多槽位备份 (QuickBackupMulti-Fabric) +quickbackupmulti@|多槽位备份 (QuickBackupMulti) slashblade-japanese-addon-pack-reforked|拔刀剑日系附属重置版 (SlashBlade Japanese Addon Pack Reforked) @create-fabric-fixes crate-delight-forge@crate-delight|装箱乐事 (Crate Delight)¨crate-delight-fabric|装箱乐事 (Crate Delight) -cultural-creators-fabric-create-and-cultural +cultural-creators-forge-create-and-cultural@cultural-crators¨cultural-creators-fabric-create-and-cultural shuffle@ probejs-legacy create-destroy@destroy|坍毁化合 (Destroy) @@ -13256,7 +13256,7 @@ bagus-cosmetic@ floatingitems ars-ocultas@ tetracelium@ -fugue +fugue@ create-food@ @@ -13367,7 +13367,7 @@ magical-jewelry epicsoulsbosses epic-puss-in-boots soul-like-npc -epic-empires@ +epic-empires¨@epic-empires|Epic Empires - 已失效 @@ -13530,7 +13530,7 @@ timeless-heroes@superhero sky-origins@sky glitchcore@ light-level-overlay-reloaded -eirin-yagokoros-pharmacy@eirin-yagokoro-pharmacy|八意永琳的药房 (Eirin Yagokoro's Pharmacy) +eirin-yagokoros-pharmacy@eirin-yagokoro-pharmacy|蓬莱药局 (Eirin Yagokoro's Pharmacy) tfc-ore-excavation tac-interact-key|TAC交互优化 (TAC Interact Key) @@ -13687,7 +13687,7 @@ missions@ votemission@ immersive-vehicles-automobilwerke-schwikau-veb cheesemaking -cardiac +cardiac@ materialtweaker@ whirlwind@ warden-tools@ @@ -13888,7 +13888,7 @@ ad-astra-extra-additions@ infinite-fluid-bucket@|无限流体桶* @voxy -vmod|VMod - 已失效¨@vmod +vmod@ minestuck star-wars-planets-ad-astra@star-wars-planets @@ -13900,7 +13900,7 @@ cat_jam@ remastered-structure spacore fancyvideo-api -goatman-jar-v2-dweller +goatman-jar-v2-dweller@the-goatman-dwller mineral-overhaul-shellstone-tuff clear-gpu-cache@|清除GPU缓存* @@ -13958,7 +13958,7 @@ ash-of-sin-custom-entity-item|罪业余烬:自定义实体装备 (Ash Of Sin: ash-of-sin-eternal-entity|罪业余烬:永恒实体 (Ash Of Sin: Eternal Entity) fundamental@ -wendigo +wendigo-jar-dweller the-super-god-road@|超神之路 (TheSuperGodRoad) @backrooms @@ -14042,7 +14042,7 @@ melee-ex-machina-meexma@melee-ex-machina arc@ cores@ -@citymod|我的城市 (City Mod) +@citymod|我的城市:交通与生活 (CityMod:Traffic and life) too-hard-to-dig@|隐岩砥凿 (Too Hard to Dig) simplecobblestonegenerator@simple-cobble-gen natural-baby-animals @@ -14138,7 +14138,7 @@ mace-backport @steve-eat-stew imaginary kuvalich|赤毒玄骸 (KuvaLich) -ae2-import-export-card@ +ae2-import-export-card@|AE2输入输出卡 (AE2 Import Export Card) @magick-experience @repairable-disc-11 @@ -14255,7 +14255,7 @@ simple-dimensions|简单维度* portals-gui@|传送门GUI* interworlds-splashscreens@|进入维度加载界面* @creative-menu-tweaks -vampires-delight@ +vampires-delight@|血族乐事 (Vampire's Delight) thats-my-bed@ caligo@ @sharpenedswords @@ -14264,7 +14264,7 @@ donut-and-mochi ducky@ducky! lost-idols simple-trashcan@ -darwin-water-ambience +darwin-water-ambience@ decorative-wooden-lattices apotheotic-additions lithosphere @@ -14687,8 +14687,8 @@ bananarangs@ remorphed@ particle-core@ fermiumbooter@ -ytech -@bbrb +ytech@ +better-biome-reblend@bbrb mcterra@ siren-head-the-arrival classic-musical-discs@ @@ -14740,7 +14740,7 @@ seed-delight@|种子乐事* transmog@|幻化* nuclear-guitars cubic-racers - +lotr-ucp|魔戒非官方兼容性补丁 (LOTR UCP) delightful-sandwiches@ shader-fixer@|光影修复 (Shader fixer) resiever@ @@ -14842,7 +14842,7 @@ dwarven-realm|矮人诸国* renewed-eras-of-arda-addon|阿尔达纪元:复兴 (Eras of Arda: Renewed Addon (First and Second Age)) legendary-item@|魔戒传说物品* lotr-middleearth-tweaks|中洲微调 (MiddleEarth Tweaks) -thaumcraft-integration +thaumcraft-integration@ reach-display@ gravitation-suite-patcher anti-rat-dungeon @@ -14916,7 +14916,7 @@ frostbytes-improved-inventory@ mekanism-extras@mekanism_extra|通用机械:扩展* leaf-beds@ unidentified-enchantments@unidentified_enchantments -protection-pixel@|机素防护* +protection-pixel|机素防护*¨@protection-pixel|机素防护 (Protection Pixel) - 已失效 createdpcompat@ @@ -15313,7 +15313,7 @@ more-affordable-than-ever-never-too-expensive@more-affordable-than-ever what-am-i @camoverlay -autoplant +autoplant|自动种植 (AutoPlant) @skip-backup-screen @polysprint @crashpatch @@ -15458,7 +15458,7 @@ lush-cave-trader@ oceans-enhancements@ xp-jellies aromatic -onekeyminer-nf@onekeyminer_nf|一键连锁(Neo)Forge (OneKeyMiner(Forge/NeoForge)) +onekeyminer-nf@onekeyminer_nf|一键连锁(Neo)Forge (OneKeyMiner(Neo)Forge) retargetato@|约谊敌众 (ReTargetato) iceandfire-ce@|冰火传说社区版 (IceAndFire Community Edition) @@ -15485,7 +15485,7 @@ hungry-zombies @gravity-gourds usa-delight|USA Delight - 已失效¨@usa-delight experience-farm -ad-extendra-dandi2k8-version@ad_extendra-continuation +ad-extendra-dandi2k8-version|Dandi2k8's Ad Extendra - 已失效¨@ad_extendra-continuation nebulus-juice-trader@ cthulhu-fishing@ large-super-potato|大型超级马铃薯* @@ -15851,7 +15851,7 @@ nbt-tool -dglab-x-minecraft@ +dglab-x-minecraft¨@dglab-x-minecraft|Minecraft x DgLab - 已失效 twintails|双马尾 (TwinTails)¨twin-tails|双马尾 (TwinTails) gt-wireless|格雷无线科技 (GT Wireless) forcejava21@|强制 Java21 (ForceJava21) @@ -15946,7 +15946,7 @@ to-tweaks-irons-spells@ horde-moon@ laser-mod-2 -mekanism-lasers +mekanism-lasers@ wizards-reborn@ tinkervillager|工匠村民 (Tinker Villager) customnpc-contentback@cnpc-items @@ -15992,7 +15992,7 @@ refreshdisplaylists vulkanmod-android-libs @black-bar-concealer building-shift@ -mob-ai-tweaks +mob-ai-tweaks@ the-masquerade-illager-boss@the-masquerade-(illager-boss) better-armory-x sweetys-horse-armor-tweaks@ @@ -16163,7 +16163,7 @@ ynet-an-xnet-fork rosatech nutrition-gtceu@ nutrition-unofficial-extended-life@|营养学非官方延续版* -@manametalmod-tweaks|魔金微调 (ManaMetalMod Tweaks) +manametalmod-tweaks-m3-t@manametalmod-tweaks|魔金微调 (ManaMetalMod Tweaks) ae2-crafting-tree|合成树 (AE2: Crafting Tree) client-automatic-doors@client_automatic_doors|客户端自动门* @@ -16207,7 +16207,7 @@ fibers-guns-pack-for-tacz beams@ discarnate@ @fishmc -mob-armor-trims@ +mob-armor-trims bubbles-a-baubles-fork arst-motion-blur@ @mpalladium|钯 (Palladium) @@ -16469,7 +16469,7 @@ water-balloon@ cataclysm-spellbooks@ call-of-drowner@|溺亡者之嚎 (Call of Drowner) - +endofday|终焉之日 (Endofdays) fastfood-delight-unofficial|重启速食/快餐乐事非官方版 (FastFood Delight Unofficial) faster-fly-block-breaking|飞行速速破* @@ -16830,7 +16830,7 @@ languagereloadunofficial@ mekanism-covers@ enchanted-wilderness-elegant-countryside@enchanted-wilderness-elegant-countryside.|雅致乡下 (Elegant Countryside) better-durability-reforged|更好的耐久度:重铸* -sos|我来助你! (SOS) +fast-tpa|我来助你! (Fast TPA) cherry-on-1-12-2@cherry_on_1.12.2|1.12.2的樱花 (Cherry_on_1.12.2) scape-and-spartan-parasites|寄体斯巴达 (Scape and Spartan: Parasites) @ripples-of-the-past-extra-stands-addon @@ -16850,7 +16850,7 @@ rple@ reign-of-nether-rts-in-minecraft@reign-of-nether-rts|下界之治 (Reign of Nether RTS) the-secret-doors - +immersive-vehicles-iv-mts-veb-iav-improved-2000|“改进-2000” tame-tools@|驯兽工具* @bia vounierns-turrets @@ -16937,7 +16937,7 @@ better-brightness-slider-respawn@ animatica-foxified@ mcchatgpt@ @aimobs -ambiances@ambiance|氛围! (Ambiance!) +ambiances@aurae|氛围! (Aurae!) screw-your-items@ mace-port bloodlines @@ -16994,7 +16994,7 @@ rank-system|评分系统* @creative-one-punch-forge bundle-inventory|Bundle Inventory - 问题反馈¨@bundle-inventory scroll-tweaks@scrolltweaks|鼠标滚轮调整* -neko-server@ +st@server-tools|服务器工具 (Server Tools) ride-every-thing@|骑乘一切* cit-resewn-neopatcher@ legendary-creatures|传说生物* @@ -17019,7 +17019,7 @@ passive-shield@ trap-expansion roost-2-flying-higher @amber -@liteminer +liteminer@ @lvlz-better-nether @lvlz-better-end cc-shops@ @@ -17081,7 +17081,7 @@ iron-nugget-recycling re-exposer@|Exposer 重置版 (Re-Exposer) gtmoreoreprocessing@ computercraftedu -bibliocraft-legacy +bibliocraft-legacy@ simple-clouds@ @@ -17107,7 +17107,7 @@ creeping-woods dragon-mounts-2-expanded@dragon-mounts-2|龙骑士2:延续 (DragonMounts2: Expanded) life-fruits@|生命果* ati-structures -fantasy-armor@fantasy_armor +fantasy-armor@fantasy_armor|幻想铠甲* beautiful-enchanted-books@beautiful-enchanted-books-mod-edition @@ -17125,7 +17125,7 @@ paladins-oath roundabout-the-jojo-mod@roundabout_the_jojo_mod -enviromine-continuation@ +enviromine-continuation@|更多生存要素:延续 (EnviroMine continuation) @toffys-hooks mutants-buff @@ -17152,7 +17152,7 @@ mutils@ better-campfires@|更好的营火* @mtr-renfe-addon gt-community-additions@ -more-create-burners|机械动力:燃烧室附加* +more-create-burners|机械动力:更多燃烧室* create-logistics@ create-pneumatic-equipment@ collisiondamage|碰撞伤害 (CollisionDamage) @@ -17293,7 +17293,7 @@ storage-drawers-unofficial@|储物抽屉非官方版* centrifuge-tiers-reproduced@ dense-mekanism@ simple-missles@ -galosphere-spellbooks +galosphere-spellbooks@ discerning-the-eldritch @hexcassettes wardens-plus @@ -17302,7 +17302,7 @@ ender-wyrmlings new-classic-battle-towers chitin-equipment@|甲壳装备* -mowzies-cataclysm|Mowzie的灾变 (Mowzie's Cataclysm) +mowzies-cataclysm@|Mowzie的灾变 (Mowzie's Cataclysm) stardew-fishing-fabric@ piglin-slayer@ cataclysm-tools@ @@ -17460,7 +17460,7 @@ book-scroll@ -@qca +qualia-carpet-addition@qca mouse-tweaks-unofficial|鼠标手势非官方版* baubles-lts elysium-api@ @@ -17470,7 +17470,7 @@ antibat@ illage-and-spell-age origincore|起源核心* - +pet-connect|收容宠物* heaven-destiny-moment|天命时刻* industrial-foregoing-extra-upgrades|工业先锋:更多升级 (Industrial Foregoing: Extra Upgrades) growableores-extension@|甘蔗转换机 (GrowableOres Extension) @@ -17618,7 +17618,7 @@ nanny@ resource-library@ resource-config-api@ @threatengl -hybridfix|混合修复 (HybridFix) +hybridfix@|混合修复 (HybridFix) @disable-portal-checks eat-eat-eat|吃吃吃* @recipe-stages-js|配方阶段 (Recipe Stages JS) @@ -17678,7 +17678,7 @@ cute-companions-ducks loot-n-explore@ epic-knights-ice-and-fire medieval-origins-revival@ -construction-sticks@ +construction-sticks@|建筑棒* sophisticated-storage-in-motion@|精妙物流 (Sophisticated Storage in Motion) imgur-display@ extra-sponges@extrasponges|更多海绵* @@ -17939,10 +17939,10 @@ mythic-charms@|神话护符* minecraft-add@|原版补充* stacked-blocks@|捆扎方块* stacked-blocks-farmers-delight@|捆扎方块:农夫乐事 (Stacked Blocks: Farmer's Delight) -@fowlplay +fowlplay@ ender-totem -@faewufs-diversity +diversity@faewufs-diversity tonsofenchants @@ -17951,7 +17951,7 @@ cave-root whos-there-ghost-players@ the-cuckoo-clock final-samurai -classic-battle-towers|经典战斗高塔* +classic-battle-towers@|经典战斗高塔* sihywtcamd-extensions@difficulty-extensions @heart-crystals @@ -17995,7 +17995,7 @@ lost-cities-survive-the-dead-edition power-of-the-void|虚空之力 (Power of the Void) aerlune-rpg -hitboxbackport +hitboxbackport@ fsmm@ create-pantographs-and-wires@ footwork-api@footwork @@ -18018,7 +18018,7 @@ sky-breaker-blue-skies-limit-remover-1-18-2|天穹突破_1.18.2 (Sky Breaker_1.1 xiyus-lucky-block|饩雨的幸运方块 (Xiyu's Lucky Block) @epicfight_oldsolar|史诗战斗:旧版solar (EpicFight_oldsolar) reskillable-reimagined -renaissance-core +renaissance-core@ @startup-chime|启动音效 (Startup Chime) carpet-takeneko-addition@tnca|竹猫的地毯扩展* chain-mod @@ -18072,7 +18072,7 @@ irons-spell-book-mob-attribute-addon epic-fightexsilium-gladius|EpicFight—放逐之刃 (Epic Fight — Exsilium Gladius) @kamenriderweaponcraft|假面骑士武器大全集 (KamenRiderWeaponCraft) -advancearmy@|先遣部队 (AdvanceArmy) +advancearmy@|先遣部队 (Advance Army) @blood-blade @mineimpact|方神 (MineImpact) jadens-nether-expansion-delight@ @@ -18258,7 +18258,7 @@ create-picky-waterwheels@create-picky-wheels fancy-lamps resizing-potion miku -advanced-smelter|荒古炼金炉 (Advanced Alchemical Furnace) +advanced-smelter@|荒古炼金炉 (Advanced Alchemical Furnace) @@ -18361,8 +18361,8 @@ scopic memory-leak-fix-unofficial-remastered @distance-traveled-calculation -@control-craft -reforbidden-magic|禁忌魔法:重生 (ReForbidden Magic) +@control-craft|控制学 (Control Craft) +reforbidden-magic@|禁忌魔法:重生 (ReForbidden Magic) moses-mod craftable-animals|可合成动物* avaritia-1-1x-kedition @@ -18386,10 +18386,10 @@ rhyme-pvz@|PVZ:润 (Rhyme PVZ) ichorium-kit reforgotten-relics|失落遗物学:重生 (ReForgotten Relics) in-your-world@ -warpapocalypse +warpapocalypse@warp-apocalypse arcane-arteries-kedition|奥法献祭学非官方版* -thaumic-tinkerer-unofficial|神秘工匠非官方版* +thaumic-tinkerer-unofficial@|神秘工匠非官方版* asyncparticles@|异步粒子 (AsyncParticles) @dragonfly-model-library @@ -18457,11 +18457,11 @@ gud_coolers suppergerrie2s-drone-mod data-essence@ -nautec@ +nautec@|海蓝科技 (NauTec) thorny-bush-protection@ gudlib -reflex-antilag +reflex-antilag@ tfc-nei nolan @@ -18480,7 +18480,7 @@ apotheosis-totem tbone - +fastfurnace-decreased|熔炉性能优化-- (FastFurnace--) @cdnperspective3d @volume-fix @@ -18519,13 +18519,13 @@ tinkers-advanced@|前沿匠艺 (Tinkers' Advanced) more-structure-processors@ quick-stack-to-nearby-chests@quickstack|快速堆叠 (QuickStack / Quick Stack To Nearby Chests) - +@kallfix yggdrasilproxy@yggdrasil-proxy thaumic-meme|神秘吐槽* starlightxae|星光xAE2修复 (StarlightxAE) - +@levelclear @drops-into-shulker ftb-team-dimensions epicfight-resurrection|史诗战斗-重生 (EpicFight : Resurrection) @@ -18627,7 +18627,7 @@ loquat-forge@loquat¨loquat @customhealthhud @fix-horizontal-camera-lag create-cyber-goggles@|机械动力:赛博护目镜 (Create: Cyber Goggles) -mekanism-science +mekanism-elements bots-lib@ botaniaoverpowered@ @@ -18839,12 +18839,12 @@ health-bar see-the-rarities-str|查看稀有度 (See The Rarities) craftpilot@ @game-of-reborn|复活赛 (Game of Reborn) -legendary-slash|传说之刃* +rough-blade|无锋道* @dota.rg @appledog|苹狗 (Appledog) superb-warfare@|卓越前线* -aetherworks-extended-life +aetherworks-extended-life@aetherworks-unofficial-extended-life pomkots-mechs-extension-pack@ @monkey-utils some-more-blocks@ @@ -18896,7 +18896,7 @@ create-deep-dark@ create-ranged@ create-enlightend@ create-winery@ -create-filters-anywhere +create-filters-anywhere@ create-cheese@ create-copper-zinc@ create-ultimate-factory@ @@ -18930,7 +18930,7 @@ apothic-science-moa-stats ye-gamol-chattels-reborn@ -@my-more-enchantments|我的更多附魔 (My More Enchantments) +mme@my-more-enchantments|我的更多附魔 (My More Enchantments) @some-little-enchant|一些小附魔 (Some Little Enchant) minepulse@|一脉相挖 (Mine Pulse) @@ -18966,12 +18966,12 @@ cable-facades@ easy-storage@ezstorage|简易存储* @tmwin|问君何所需 (TellMeWhatINeed) elrols-gui-elevator -@minecraft-found-footage +backrooms-found-footage@minecraft-found-footage exposure-polaroid@ fallingalchemy|坠落炼金 (FallingAlchemy) -fluxloading +fluxloading@ inventory-swapping seamless-trading@ sophisticated-auto-walk @@ -19009,7 +19009,7 @@ gluttony-oversaturation-overeat overeat oversaturation -etheria +etheria@ legends-of-slugterra@ gussdx-decoration @@ -19156,7 +19156,7 @@ summer-cottage@ marius0x-furniture youssefs-currency@youssefs_currency bibliobiomes-legacy@ -sniffers-delight +sniffers-delight@sniffers_delight easters-delight@ @@ -19179,7 +19179,7 @@ disturb-not-my-dream@|勿扰吾梦 / 起床气* lootbagsrenewal@lootbags|战利品袋重制版 (LootBagsRenewal) - +autoswaplowdurabilityelytra|自动鞘翅替换 (AutoSwapLowDurabilityElytra) no-elytra-equip@no-elytra-mod no-elytra title-changer@ @@ -19249,7 +19249,7 @@ epic-fight-mesh-model|史诗战斗网格模型模组* monster-of-the-night-skies@ spooky-doors@ @mclientapi -@addonapi +moffs-addonapi-dynload@addonapi @shiv @tab-list dokimobs@|萌化生物 (DokiMobs) @@ -19286,7 +19286,7 @@ blades-derby@|骐骥刀剑风云* create-simple-generator@|机械动力:简单发电机 (Create: Simple Generator) sophisticated-backpacks-create-integration@ -@ticex +ticex-tinkers-construct-ex@ticex eternal-starlight-vanilla-ores@eternal-starlight-vo supercritical supercritical-gtqt @@ -19317,7 +19317,7 @@ expanded-ae modular-machinery-reborn-ars-nouveau@ bonsai-crops@|盆栽作物* playing-cards-chips|扑克牌与筹码 (Playing Cards & Chips) -enderdrives +enderdrives@ lucky77 i-dont-see-you@ @@ -19343,7 +19343,7 @@ whimcraft@|奇思妙想 (WhimCraft) recipeconflict-fixer|合成冲突消除:修复版 (RecipeConflict Fixer) dooglamoo-painter-mod|Dooglamoo 的绘画* notafreaks-plushies@ -enchanting-vanilla +enchanting-vanilla@|原韵新生* quest-items-rpg@quest-items @@ -19397,7 +19397,7 @@ apotheosis-x-irons-spellbooks-compat customtab@ nightmares|噩梦* graveyards -sky-arena +sky-arena@demis-sky-arena progressive-mechanics-library@ @shy @@ -19452,7 +19452,7 @@ slightly-newer-art-of-forging custom-treasure-maps@ evolved-mekanism|通用机械:进化* - +brain-delight|脑子乐事* villager-library|村民图书馆* trade-enchant-overhaul more-additional-paintings@ @@ -19513,7 +19513,7 @@ applied-web-terminal@|AE网络终端* itemzoommore @noitemtooltips shuttfup|Stfu - 已停更¨@shuttfup -baublesreforked|饰品重铸 (BaublesReforked) +baublesreforked@|饰品重铸 (BaublesReforked) @wills-lively-villages tmrv@ @@ -19521,7 +19521,7 @@ twilight-treehouses@ improved-rails@|更好的铁轨(可转弯の动力铁轨!) (Improved Rail (TURNABLE POWERED RAILS)) redstone-signal-bind-wand@|非Create版无线红石* - +@ingametranslator|游戏内聊天翻译 (In game Translator) carnivorous-plants-addition mekanism-curios tetra-compat @@ -19563,7 +19563,7 @@ scaffolding-backported hold-my-items-reforged@ distraction-free-recipes@ -seeking-immortals-nightmare@|求仙者:噩梦之章 (Seeking Immortals : Nightmare) +seeking-immortals-nightmare@|月之石:噩梦之章/求仙者:噩梦之章 (Moonstone :Nightmare/Seeking Immortals : Nightmare) enchanted-crystal-arrow|魔法水晶箭 (Enchanted Crystal Arrows) touhou-danmaku-kagura-core|东方弹幕神乐核心 (Touhou Danmaku Kagura core) hah-ueuh@hah!-ueuh @@ -19583,7 +19583,7 @@ dontstarve4@|不要饿死4 (Don't Starve 4) spark-core@|星火核心* nether-core-useful-nether-rock|下界核心 (Nether Core) slashblade-js|拔刀剑 JS (SlashBlade JS) -@caerula-arbor|方块与海嗣与深蓝之树 (Caerula Arbor) +caerula-arbor@|方块与深蓝之树* alices-doll|元歌 (Alice's Doll) bedrock-redux@|基岩再现* item-entity-control|掉落物清理* @@ -19600,12 +19600,12 @@ pokecube-alternative create-propulsion@|机械动力:推进工程 (Create: Propulsion) -event-wrapper|事件包装器*¨@event-wrapper|事件包装器 (Event Wrapper) - 审核中 +event-wrapper@|事件包装器* @modwhitelist|模组白名单 (ModWhiteList) beacon-conduit-tweaks|信标与潮涌核心调整 (Beacon & Conduit Tweaks) -@tfc-tools-trim|群峦传说:工具纹饰 (TFC Tools Trim) +tfc-tools-trim@|群峦传说:工具纹饰 (TFC Tools Trim) @kill-youkai-with-knives mo-bees@|更多蜜蜂 (Mo' Bees) @@ -19618,7 +19618,7 @@ create-unlimited@ skeleton-ai-fix@ climate-rivers@ dungeonsdelight@dungeons_delight|地牢乐事 (Dungeons Delight) -cleanroom-relauncher-unofficial|Cleanroom 模组式启动器非官方版* +cleanroom-relauncher-unofficial@|Cleanroom 模组式启动器非官方版* silents-power-scale@ trident-return-fabric resource-world@|资源世界* @@ -19702,7 +19702,7 @@ alienevo banit|干掉它! (BanIt) alexs-mobs-naturalist-compat@ - +tinkers-ingenuity|匠心之作 (Tinkers' Ingenuity) curiosities@|趣味物品* @cs2_c4|Qis的C4 (C4) @@ -19790,7 +19790,7 @@ verdant-wonders@ underlay@ lazy-item-generator|咸鱼物品生成器* -crab-street-light|螃蟹的路灯* +crab-street-light@|螃蟹的路灯* undertale-delight@|决心乐事* @@ -19828,7 +19828,7 @@ projecteteams@ battle-frenzy|越战越勇* @plane-advancements cosmopolitan@|四海一家* - +redtassel|红缨 (redtassel) sridr|拔刀剑锻造增伤改革 (SlashBlade Refine Increased Damage Reform) mianbaos-modernwarfare|面包军火库 (Mianbao's ModernWarfare) @@ -19838,7 +19838,7 @@ create-no-touching create-aether-industry|机械动力:天境工业* create-entitycontroller@create-entity-control|机械动力:实体控制 (Create: Entity Control) better-advancements-blessed-edtion|更好的进度 - 祝福版 (Better Advancements - Blessed Edtion) -@gloom-raider +@gloom-raiders herobrine-prime legend-of-the-dwerden@ dnt-stronghold-overhaul-lite-edition @@ -19859,7 +19859,7 @@ fluxed-core@ backpack-plus|背倍加* @hydration-mod -defiled-lands-reborn|污秽之地重置版* +defiled-lands-reborn@|污秽之地重置版* @copper-bulb-early|铜灯补充 (Copper Bulb Early) future-fireproof@|未来防火* @low-durability-switcher @@ -19888,11 +19888,11 @@ aesthetic-shelving arboreal-nature@ warborn-resurgence@warbornresurgence +@ceres - -super-creeper|智能爆破专家* +super-creeper|智能爆破专家 (Creeper Super) expanded-experience -reworked-phantoms +reworked-phantoms@ ex-nihilo-fabricio@ hybrid-delights@ create-clothes@|机械动力:服装 (Create: Clothes) @@ -19947,4 +19947,924 @@ tinkers-jewelry-ex|工匠珠宝拓展 (Tinkers' Jewelry EX) lovely-sparkle-pieces|璀璨饰品* yuns-weather@ -qliphoth-awakening@ \ No newline at end of file +qliphoth-awakening@ + + +universal-mod-localizer@universal_mod_localizer|通用模组汉化器* +smart-creeper@|智慧苦力怕* +peaceful-depletion@peaceful_depletion|和平消耗* +@everything-is-tnt +structure-pool-api@ +create-bluemap@ +tragicmc-2-continuation|悲惨世界2:延续 (TragicMC 2 Continuation) +nebula-parasites@ +bonsai-trees-unofficial-extended-life|盆栽非官方延续版* +@its-as-shrimple-as-that +worldedit-items@ +slashblade-skin@|拔刀皮肤 (SlashBlade Skin) +iron-door-key@ +just-enough-fishing-jef@just-enough-fishing-(jef) +containersearch@container-search +mousepets@ +backported-minecarts@ +simple-incubator@ +amethyst-stairs-slabs-and-walls +@vessels + +legendary-weapons-byititor +slender-reborn +craft-vania-lite +pollution-of-create +create-chocolate-fountain@|机械动力:巧克力喷泉 (Create: Chocolate Fountain) +create-currency-shops@|机械动力:货币商店 (Create: Currency Shops) +create-nether-industry@|机械动力:下界工业 (Create: Nether Industry) +backported-diamond-ore-gen +tfthreadsafetyaddon +serverautosaver +infinistack@|无限堆叠 (InfiniStack) +powerful-dummy|强力假人* +beyond-bosses@beyondbosses +mini-utilities-y +concoction@concoction! +zombietactics2@ +radiation-zone-reborn|辐射荒原:再行 (Radiation Zone: Reborn) + +@crumbling-hearts + +dye-the-world@ +modernmarkings@ +hololive-product@ +fetzis-displays@ +shadered@ +sweety-garden@ + +@twilight-forest-aurora-palace-regeared + + +medieval-buildings-nether-edition +@ribbits-croaks +echoes-of-time@ +horror-alexbrine +zzz-craft|绝区零·星见雅 (ZZZ Miyabi) + +cozy-cabins-and-cottages +servereye +deepseek@deepseek-neoforge +ai-command|AI指令生成 (AI Command Generator) + +flighthud-do-a-barrel-roll-fix + +@fast-container-placement|快速容器放置 (Fast Container Placement) +@teleport-mod +@what-meme +crackers-wither-storm-mod-v-sides-soundtrack@ + +roberts-game-tweaks|Robert的游戏调整 (Robert's Game Tweaks) + +@datapack-load-error-fix|数据包错误修复:重生 (Datapack Load Error Fix Reborn) +@whats-this +@additional-jewelryz +tacz-tes-compat@ + + +directionhud +raw-netherite-reforged +blast-travel-reborn@blasttravelreborn|旅行火炮·重生* +shanxi-skeleton@shanxiskeleton|山西骷髅* +@gunpowder-overhaul|火药改革 (Gunpowder Overhaul) +smoking-stuff-a-wizards-hobby +@alloy-smelter +@autocrafter-early +time-in-a-bottle-charger@ +just-a-sculk-vial@ +lockylocks@ +server-i18n-api¨@server-i18n-api|Server I18n API - 审核中 +fragmentum-forge@fragmentum¨fragmentum-neoforge¨fragmentum-fabric +@monument-data-fix|Monument 数据修复 (Monument Data Fix) +@better_tp +waystone-currencys@|消币则至 / 传送石碑:货币 (Waystone: Currencys) +@armor-stand|盔甲架 (ArmorStand) +threadtweak-reforged|ThreadTweak (Neo)Forge 移植版 (ThreadTweak Reforged) +@environmental-creepers-backport +com-bat +touhou-little-maid-rpg-class-task + +heat-and-climate-patch|热量与气候修复* + +@clientupdater + +modern-lucky-block@ +foxrecipemaker +@global-mob-health-multiplier|全局生物血量倍率 (Global Mob Health Multiplier) +chatbox-for-mc@chatbox|对话框 (ChatBox) +perfectly-balanced-bows +@playerex-alcohol|PlayerEx 酒精版 (PlayerEx: Alcohol) +hak-minepiece +grease@ +cleaver-compedium +phayriosis-parasite-infection +srp-calamity-catastrophe-cataclysm@|逃寄:无妄之灾 (SRP: Calamity Catastrophe Cataclysm) +moss-overgrowth +create-pillager-arise +create-qol@ +create-remote-terminal@|机械动力:远程终端 (Create: Remote Terminal) + + +songs-of-war@|战争之歌 (Songs of War) +cataclysm-summons@ +cutepuppymod@ +giant-swamp-tree|沼泽巨树* +big-oak-tree@|大橡树* +giant-tree-stump|大树桩* + +polars-mad-tweaks@ +cobblemon-ranked@|方块宝可梦-排位赛* +eiramoticons +horse-breeding-fix@ +tablistextension@|TAB 列表扩展 (TabListExtension) +rally-of-the-guard-guardvillagers +bncci +@recipe-maker +portaltransform@|传送门嬗变 (PortalTransform) +@a555sd|更多的画 (More Paintings) +unusual-furniture@ +xiaoanmooncake@|小安的月饼狂热 (Xiaoan's Mooncake Fever) +foragers-insight@ +palestinian-delight@ +mekanism-more-machine@mekanismmoremachine|通用机械:更多机器 (Mekanism: More Machine) +custom-battleroyale@|自定义大逃杀 (Custom BattleRoyale) +improved-pillager-outpost +rebalanced-enchanting@ +dark-doppelganger@ +enders-spellsandstuff +hazen-n-stuff +spongeforge@ + +@don%22t-die|补药趋势啊! (Don't Die!) +sdm-market@ +@omnilook + +survivalist-essentials@ +villager-trading-plus@trading+ +mending-fix|经验修补修复* + +colorwheel@ +tlib@ +smoothscreen@ + +@fuzz +advanced-loot-info@ +resourcepackcached@ +aotake-sweep|竹叶清* + +effectual@ +cold-light@|冷晖之枪* + +huanles-weapons@|Huanle的武器 (Huanle's weapons) +@clay-soldiers-remake +threateningly-mobs@ +basic-spinosaurus +european-wildlife +marvelous-menagerie@ +roman-weaponary +compat-structure@ +flower-bed@ +archery-expansion-squared +create-more-linked-remote@ + + + +gambagoons-better-emeralds +ingameime-vintage@|游戏内输入法-1.12.2 (IngameIME-Vintage) +extreme-sound-muffler-legacy@ +amazing-trophies + + +global-server-configs@global-server-config|全局服务器配置* +expandedbonemeal +@aireplyassistant|AI回复助手 (AIReplyAssistant) +utiljs +moon-info-updated@ +@server-market|服务器市场 (Server Market) +nbt-tooltip@nbttooltip +delicate-dyes@ +@modernmixins +sdm-economy +sdm-ui-lib@ +mcllm¨@mcllm|MCLLM - 审核中 +@inventory-pause-yipeekiyaay +chestsearch|容器检索 (ChestSearch) +clicktor|Clicktor连点器 (Clicktor - Auto Clicker) +extended-bone-meal@¨extended-bone-meal-fabric +show-me-the-enchantment|怎魔,你不附器* +better-sign-edit@ +disc-recipes@|唱片合成 (Disc Recipe) + + +scoutreforked|侦察兵 重构 (Scout Reforked)¨@scoutreforked|侦察兵 重构 (Scout Reforked) - 审核中 +immersion-in-traffic-context|沉浸交通道路 (Immersion in Traffic Context)¨@immersion-in-traffic-context|沉浸交通道路 (Immersion in Traffic Context) - 审核中 +arknightsfurniture@|明日方舟家具 (ArknightsFurniture) +gunsmith-lib + +experiencelib@ +mystical-mobs + +progressive-difficulty-chaos-integration@ +ottapomos-scps +scp-foundation-requisites@ +thermal-construct@ +war-wither|战争凋灵* +status-effect-bars-reforged +dark-tower-maze + +the-occult +wolftailui@ +better-client@better-client-enc|更好的客户端* +clientharvest@ +auto-afk@ +particle-effects-reforged +hold-my-items-refoxed +dont-claim-our-structures +cosy-critters-creepy-crawlies@cosy-critters-creepy-crawlies-neo +thin-air-ad-astra-compat +yyzsbackpack@yyzs-backpack|yyz的背包 (Yyz's Backpack) + +vital-delight + +@mining-in-place +pathfinder-navigation|寻路者导航* +adorable-eggs@ +food-addition@ +everything-totem|万物皆可图腾* +thaumic-renaissance@thaumcraft-renaissance +@executive +@make-i18n-great-again +steve-goes-fishing@ +voltaic-api@voltaic +boat-delete-begone +zs-packet +raise-them-hotbars +quick-leafdecay +footprint-particle-returns +inventory-crafter-returns +colorwheel-patcher@ + + + + +difficulty-lock@¨difficulty-lock-fabric +ryymr@ +@lazybeefix + +lazy-ae2-tweaks +coordinates-hud@coordinateshud +just-enough-botania +fumo-fumo +touhoupixelcanteen-a-farmers-delight-plugin@touhoupixelcanteen|东方像素食堂 (TouhouPixelCanteen) +perfect-parity-the-garden-awakens-edition@ +perfect-parity-spring-to-life-reforged +hypertubes@hypertube|机械动力:超级管道 (Create: Hypertubes) +perfect-parity-spring-to-life-edition@perfect-parity-1-21-5 +create-geography|机械动力:地理学 (Create: Geography) +create-apothic-enchanting@ +demis-enigmatic-dice@ + +@mpem|月夜性能优化 (Moonlit Performance Enhancement Module) +@eventbenchmark|事件性能测试 (EventBenchmark) +no-more-garbage-in-cell +pick-up-filter-puf@pick-up-filter + +identity-fix-morph@identity-fix + +stick-it@ +aimod +srp-reset-mod|逃逸寄生体:重置 +legendary-relics|传说遗物* + +true-power@|真正的力量 (True POWER) +ender-villages|末地村庄* +twilight-forest-villages@|暮色森林村庄* +poland-nature +amethyst-extra +villagers-windmill +extended-wrenches@ +why-stack-only-64|为什么才64个呢? (Why Stack Only 64?) +@digcooldown|挖掘冷却控制 (DigCoolDown) + + +potentials@ +deltaboxlib@ +gobbobs-animated-blocks|Gobbob 的动画方块 (GobBob's Animated Blocks) +glowingfootsteps|发光足迹粒子 (GlowingFootsteps) +simply-swords-overhaul@ +farms-and-foods@farms-foods +@achievement-fix|成就修复 (Achievement Fix) + +lang-magic-tweaker +spawn-dimension-choice|诞生维度自选* +reforge-stone@|重铸石* +numericalbotany +cameraoverhaul-vintage@ + + +advanced-hoppers + +ars-meteorites +the-hard-herobrine-mod + +tektopia-addons +jason-voorhees +biome-improvement-mindthemoods-pile-of-rocks +mochila@|简彩跃光* +soulytra +@advanced-tfc-blueprint|高级群峦蓝图 (Advanced TFC Blueprint) +@accesswidener-loader +improperui-forged@ + +enchantment-to-attack-faster|并非环取/攻击速度附魔非官方版 (Attack Speed Enchantment Unofficial) +@stopgrowth|停止生长 (StopGrowth) +squid-no-glitch +@permanent-night-vision|无痕夜视 (Permanent Night Vision) +@rems-carpet-addition + +colorful-hex@ + +bbl-no-rain-fog + +@be-quiet-negotiator +link15s-no-i-frames +jei-integration-reborn@|JEI扩展:重生 (JEI Integration: Reborn) +appliedsync +jei-uu-assembler@ +ominous-overlays +create-nuclear-shell@ +world-of-wonders +horrible-meteor-shower|陨石雨* +desertification@ +cinchs-abandoned-houses@ +curse-breaker@ +hundred-years-warfare@|百年战争* +biologica@ +oritech-things +potato-vehicle-industries +warfare-wings@ +avaritias-delight@|无尽乐事 (Avaritia's Delight) +mamas-herbs-and-harvest@ +vs-orbit@|瓦尔基里:轨道学 (VS Orbit) +touhou-little-maid-spell@ +despawn-notifier|余影待终/掉落物消失提醒* +customnpcs-unofficial-from-betazavr|自定义 NPC BetaZavr 非官方版 (CustomNPCs Unofficial from BetaZavr) + +efficient-core-automatic-mining|Efficient Core自动挖掘器* +@bento_box|便当盒 (Bento Box) +custom-100-day-boss@|自定义100天Boss (Custom 100-Day BOSS) + +cinchs-extra-torches@ +corpse-x-cosmetic-armor-reworked-compat +modular-angel-ring +magic-feather-replumed@magicfeather +essentialcraft-magic-reflux +ad-astra-star +neomopeds +create-more-parallel-pipes@ +create-garnished-reworked@ +create-shimmer|机械动力:微光 (Create: Shimmer) +blue-archive-school-banners +cialloblade +windows-keyboard-fixes-returns +aquatic-torches-returns +cratetech-crate-storage +bigger-and-better@ +blabber@ +reducedores@ +durabilityrarity@ +ironfist-lite +shear-chicken@shear-chickens +no-moon-jar@no_moon.jar +@deep_space +blue-grove +quarkponders@ +sussy-sniffers +firefly-bush-backport@ +agritech-trees-automated-trees@agritech-trees +agritech-evolved@ +wealizh|WLH工业故事* +@hydrometallurgy|湿法冶金学 (Hydrometallurgy) +mekanism-ecoenergistics-unofficial + +woodlandfoxverses@|林间狐咏 (WoodlandFoxVerses) + + +swaying-garden|摇曳花园* +time-feeds-villager@|逝者如饲:村民 (Time Feeds: Villager) +ftb-backups-3|FTB 备份 3 (FTB Backups 3) +kurolib@ +trimmed@ +free-camera-api-tripod@|自由相机API-三脚架 (Free Camera API Tripod) +abelia¨@abelia|Abelia - 审核中 + +simple-weather +simple-copy@ +natures-aura-render-fix@ +creakingchopperenchantment@ +daily-rewards@ +initialdimensionreborn + + + + +the-plugin-mod@ +cave-vision +@el-addon-retrieve-to-slot +aether-gloves-for-all +reducedloot +@irisfixes|Iris 修复 (IrisFixes) +musicjs + +suspendtheserver|自动挂起服务器 (Suspend The Server) +screenshot-layers +anti-spider-climbing +rechiseled-chipped@ + +weatherdisplay@ +radium-fabric|Radium - 已失效¨@radium-mod +cinnabar + +pquality@ + +mte-patches +happy-ghast-breeding +happy-ghast-flight +the-angry-ghast@ +@morfonica-in-minecraft +for-vanilla-flowers +@skillpractise|技能练习 (SkillPractise) + + + +bedrock-hotbar-offset¨@bedrock-hotbar|Bedrock Hotbar Offset - 审核中 +mss-more-slot-swap¨more-slot-swap +show-all-advancements|显示所有进度*¨@show-all-advancements|显示所有进度 (Show All Advancements) - 审核中 + +mana-display@ + +curse-forge|诅咒锻造* + + +disconnect-packet-fix@ +happyghast-on-1-12-2@happyghast_on_1.12.2|1.12.2的快乐恶魂 (HappyGhast_on_1.12.2) +automationera@|自动化时代 (Automation Era) +@tears-backport +lava-chicken-disc +lava-dennis +album-go-wall +forgotten-java +johanness-renaissance-rebirth@johanness-renaissancerebirth|约翰内斯的文艺复兴:火器重生 (Johannes's Renaissance:Musket Rebirth) +swarm-infection@ +necro-rooted +@mycelial-crisis|菌丝危机:崛起 (Mycelial Crisis: Mushroom Rising) + +lethalsteelproject|致命拔刀计划 (Lethal Steel Project) +lovecraft-girlfriends-3@ +@faithful-mace +bard-craft + +items-timer +neo-language-reload@ +@oxidizium +ixeris@ +substrate-sodium-addon@substrate +hide-item-frame@|隐藏物品展示框* +@smoothmaps +neoessentials@ +structure-layout-optimizer@ +mutant-creatures-legacy +dynamites-overhaul@ +dynamite-addon¨dynamite-addon-fabric +maid-mana-source|女仆魔源* +maidens-merry-making@mamas-merrymaking +bbl-invisible-lights@ +superior-trees@ +underground-rivers@ +improved-village-placement@ +@photosynthesis +@mineno +slashbladetetra + +irons-spells-n-spellbooks-dynamic-skill-trees@ +spell-elemental@ +ward-ring-extension-for-irons-spells-n-spellbooks +dungeons-and-combat-x-irons-spells +smallcolonies +farmers-cutting-regions-unexplored +serene-landscapes@ +blood-arsenal-reawakened@ + + +spell-colonies +nature-reimagined +ominous-bottle-add +only-hammers@ +auto-resource|自动资源* +iu-additions +rubiks-cube|魔方* +clockworklib + +fabridge + +true-fullbright +quickslots +hide-hud-in-third-person-view@hide-hud-third-person +automlg@auto_mlg +elytraautopilot@elytra_autopilot +simple-conveyor-belts@ +smidgeon-o-bliss-remastered +xl-food-reborn|超多食物:重生 (XL Food Mod Reborn) +create-spin-spawner +vs-rotanator +create-refilling-boxes +cbc-chemical-warfare|机械动力火炮:化学与生物战争 (CBC: Chemical Warfare) +cbc-warium-projectiles|机械动力火炮:Warium炮弹 (CBC: Warium Projectiles) +create-colored +create-law-and-order|机械动力:法律与秩序 (Create: Law and Order) +blockexporter +fix-attack-lag@|修复攻击卡顿* +@clienthings +@misinput +lavablocker@ +gildedarmors@gilded-armor|鎏金盔甲 (Gilded Armor) +inventory-management-deluxe +runningmapmusic|跑图音乐 (RunningMapMusic) +lava-chicken-music-disc +porting-dead-libs +panoramica¨panoramica-fabric +create-mt-integration|资源统合-机械动力配方附加* +@valkyrien-warpeace|瓦尔基里:战争与和平 (Valkyrien War and Peace) +sillys-auto-totem +repairables-anvil@|可修复的铁砧* +@throwable-bricks +@now-time|准点报时 (Now Time) +fluidjs¨@fluidjs|FluidJS - 审核中 +hybridcompat|混合兼容 (HybridCompat) +@recipecooldown +counter-mod +lotr-renewed-class-mod|魔戒种族 (LOTR Races) + +craftable-chain-armors +item-cooling-reduction@|物品冷却缩减* +yosbs + + +@no-bugs +dark-window-bar@ +sodium-core-shader-support@ +@anvilcraft-headtap|铁砧工艺:敲你脑袋 (AnvilCraft: HeadTap) +hearts-in-a-bottle@ +copper-tools-and-shears@ +windchime-unofficial-continued@ + +potionsplus +tinkers-wood@ +illuminated@ +galosphere-argent-edge +brutality-a-terramity-addon +free-epic-games@|FE 游戏框架 (FreeEpicGames) + +fabric-database-h2@|H2数据库* +tokien-theme-bq|更好的任务-托尔金主题 (Tolkien Theme [BQ]) +tpa-utilities +more-diamond-spawn|更多钻石生成* +shutup-opengl +maid-command|女仆指令* +speedy-hopper-vanilla +suggestions-fix-fabric +no-ixapi@no.ixmodapi + +epicfight-zylob|史诗战斗:刀光修改 (EpicFight Sword Light Modifications) +perfect-parity-pale-garden-awakens-neo +expressive-advanced +philia-amulet|友爱护符 (PhiLia Amulet) +stronger-fire-boss|原焰帝臻* +myec@|更多渴望:附魔与制造 (More of Yearn: Enchants & Crafts) +abandoned-urban|废弃城市 (Abandoned urban) +fungal-hazard|蕈骸迷途 +more-useful-copper-2-0@more-useful-copper|更有用的铜 (More Useful Copper) +boom-bombs@ +polars-exploration-additions@ + +ozymandias-sundries +tensura-enigmatic + +create-collection-bundles@ +create-enhanced-mastery +create-industrial-tools +create-smart-bounds +create-fantasizing-again@|机械动力:又在幻想了 (Create: Fantasizing Again) +crafting-station-jei-edition-updated +uncraft-everything@ +@videoplayer +@net-music-play-list|网络音乐机:播放列表 (NetMusicList) + +gregs-modern-construct + +smart-key-prompts@|灵活按键提示 (SmartKeyPrompts) +@zero-whitelist|Zero白名单 (Zero Whitelist) + +@pearlchunkloading|珍珠加载区块移植 (Pearl Chunk Loading) +clean-drops +auto-replace +@footprint + +simple-villager-follow@ +tinkers-experience +hedgehog-companion +i-want-to-spin@|我要开转 (I want to spin) +@kiss-fabric + + + +nuggets-repair-items@|矿粒修复物品* + + +terra-atmos|群峦环境音效* +tlm-true-power@true-power-of-maid|车万女仆:真正的力量 (TLM: True POWER) + + + +@splash-window + +@showcase +enhancedtooltips@ +locate-fixer +journeymap-fixes +ancient-structure@ancient-structures + +wizard-samurai-irons-spells-addon¨@wizard-samurai-irons-spells-addon|Wizard Samurai - Iron's Spells Addon - 更新较慢 +survival-instinct +enderzoo-patched +illager-keep +codex-of-champions +ars-controle@ +medieval-craft-historical-firearms +too-many-withers +reliquified-l-ender-s-cataclysm@ + +@log-creator-finder-findshit|找史大王 / 日志溯源 (findshit) +enchantment-lookup@|附魔查询* + + + + +@mods-checker +no-enchanting-xp-requirment +client-dynamic-light|客户端动态光源* + +multiyggdrasil@ +thaumcraft-4-patched +patchful +gtnh-customizer@ +view-sounds@ +ae-inventory-profiles +tooltips-reforged@|提示框:重铸* +@wyr +soaring-phantoms@ +tick128¨@tick128|Tick128 - 审核中 +ritchies-projectile-library@rpl +modopedia@ +@translate-allinone|一体化翻译 (Translate All in One) +button-fix@ +@birthday-fabric|生日模组 (Birthday Fabric) +bucketstacking +creeper-no-break-blocks@ +create-cog-hud@|机械动力:齿轮 HUD (Create: Cog HUD) + +create-pipe-networks + +multicolored-redstone-lamps +reliquified-twilight-forest@ +witch-tower +the-nightmare-000 +meowmere +@fallenflowers-server-economy|Fallenflower的服务器经济 (Fallenflower's Server Economy) +deadout-medical-supplies|死亡突围医疗用品 (Deadout Medical) +@thermal-shock +better-questing-npc-integration|更好的任务-NPC扩展 (Better Questing - NPC Integration) + +eu2emc|EU转EMC能量转换器 (EU2EMC) + +simplesorter@|简单整理 (SimpleSorter) +spawncursion|生成式入侵 (SpawnCursion) +grass-be-gone|不要草* +tacz-unidict|TACZ:铳械协议 (TACZ: Unidict) +gem-lichens|宝石地衣* +lethality-a-terramity-addon@ +lootr-fixes +no-name-provideds-potion-fruit@potion-fruit +fungus-stew +auto-sapling|自动树苗种植* +alans-bees +snack-sticks@ +fish-fix +stuttering-screen-lag-protection +tilefinder@ +immersive-furniture@|沉浸式家具* +modernnetworking +like1-8 +spoduos-future-mineration +enderio-continuous +enchantment-broker@ + +new-dawn +enhanced-glass +hospitals +comfy-cozy +epicnpc|史诗战斗NPC (epicnpc) +epic-fight-script-extension|史诗战斗:脚本扩展版* +@undead-transformation + +skullskinless|头颅无皮肤/易颜得帧 (SkullSkinless) +staynight|永夜定驻 (Stay Night) +blockblocker|方块放置阻止/抑用挡置 (BlockBlocker) +shoving-shovels@ +electric-coating|不止电池 / 电力镀层 (NotOnlyBattery / Electric Coating) +araxers-bestiary +better-questing-immersive-engineering-multiblock +bloodyquests +croaker@ +crawl-on-demand@ +gaze-a-malum-addon +osce|矿石菌种社区版 (OreSpawn Community Edition) +explosive-arrow +more-relics@ +no-sun@no-sun-mod|暗无天日* +opposing-force@ + +@pingfix +@volumescroll +talk-balloons@ +health-indicator-txf@ + +snowy-weather + + +fluidium|锍* + +peritia + + +corpse-kibou-no-hana|希望の花 (Corpse: Kibou no Hana) +redyeable +@natural-motion-blur +@modpack-checker +convenient-achievements +vintage-vibes@ + +@smelter-the-hedgehog +@hopper-the-hedgehog +custom-trades-mod|自定义村民交易* +tameable-zombie-horse-and-skeleton-horse|可驯服的僵尸马和骷髅马* +mi-tweaks +qa-villager-tweaks|Qa村调整* +multiblocked-extended-compatibility +enum-extender-js +@btc + +no-pigs +vanimals@ +wind-charged-weaponry@ +untagged-mobs +enchanted-potato +cit-for-nbt + +bonemealtweaker +hellstone-golem|狱岩魔像* +netherstar-crafting|可合成的下界之星* +tutes-cn-sweetinks-items|塔兹与甜墨的物品 (Tutes-cn & Sweetink's Items) +i-am-steve@i..am-steve +backported-stufff + +tumbleweeds¨@tumbleweeds|Tumbleweeds - 审核中 +mending-mirror|破镜重圆* + +natures-life +yggdrasils + + +@deepseeks-enchant|DeepSeek的附魔 (DeepSeek's Enchant) + + + +isitwaxed +simple-hp-bar +login-hp-fix +shape-shifter-curse|幻形者诅咒* + + +tinkers-planner-reborn|匠魂蓝图:重燃 (Tinker's Planner Reborn)¨@tinkers-planner-reborn|匠魂蓝图:重燃 (Tinker's Planner Reborn) - 审核中 +hunger-overflow +nocreeper +mo-creatures-extended +skippy-pearls +spelunkery-plus +forge-better-redstone +jeb-just-enough-book@justenoughbook +pal@ +data-anchor@ +elytra-hopping +oh-more-wandering-trades|更多游商交易*¨@oh-more-wandering-trades|更多游商交易 (Oh More Wandering Trades) - 审核中 +beacon-anti-phantom@|不时轻轻肘击幻翼的邻座信标同学 (Beacon Anti-Phantom) + + +cooler-animations + +hardcore-architect + +carpet-neoforged@neoforge-carpet +no-campfire-smoke +tiny-tasks +fancy-outline +echo-compass-hud@ +decorative-stands +confluence-otherworld-gun-fix@ + +sbm-cardboardboxes + + +bedrock-age|基岩时代* + + +@koniavacraft|娜拉工艺 (Koniavacraft) +emojicraft|Emoji工艺* +kylin-arm|麒麟臂* +server-hats@ +forgotten-ruins + +creeperhost-presents-steves-carts@ +perspatium@per-spatium +face-stealer +offlawn-legacy +metalborn +@wizards-mod +mo-creatures-nostalgia-edition@ +mocreatures-awakened +sterling +cuisine-tfc|料理群峦 (Cuisine TFC) +@natural-temperature +silent-gear-metalworks +treetap@ +better-cave-update +@affix +starry-end +dynamic-trees-for-cobblemon +immersive-portal-gun-for-forge@ +adorable-hamster-pets@ +infinite-lava@|无限熔岩* +ethereal-shots|虚无射击* + +epic-fight-super-golem|超级铁傀儡 (Epic Fight - Super Golem) +fallen-log-hideout +pastel@pastel-mod|彩绘世界* +mob-catcher-unofficial +retro-pet-items +confluence-dimension-patch|汇流来世维度补丁* +convenient-storage|便捷存储* +legendreliclib|传世遗产库 (LegendRelicLib) +jauml +nemexlib +cryonicconfig@ +@knight-lib + + +@okom + +craft-slabs-back-into-blocks@ +highland-statue +restore-ore@|复原矿石* +factory-blocks@|工厂方块* + +rotten-planks-and-logs + +create-unify-updated-to-create-6 +create-arm-made-cuisine|机械动力:臂力臂力炒菜 (Create: Arm-made Cuisine) +create-cultivation-craft@|机械动力:栽培工艺 +kaleidoscope-cookery@|森罗物语:烹饪 (kaleidoscope cookery) +retroemi-forge@ +@pearlecho +@mcdaycounter +minecolonies-war-n-taxes +falling-down@ +more-red-x-cc-tweaked-compat + + + +wither-reincarnated + +toros-auto-attack +creeperfire|自定义烧货 (CreeperFire) +fastspawner|刷怪笼渲染优化/产器速算 (Fast Spawner) +sophisticated-storage-plus|存取思深/精致存储扩容* +whaleborne + +newspaper-big-book-bibliocraft-legacy-backport +bic-clipboard +templates-2@ \ No newline at end of file diff --git a/Plain Craft Launcher 2/Resources/Newtonsoft.Json.dll b/Plain Craft Launcher 2/Resources/Newtonsoft.Json.dll index 8069902..341d08f 100644 Binary files a/Plain Craft Launcher 2/Resources/Newtonsoft.Json.dll and b/Plain Craft Launcher 2/Resources/Newtonsoft.Json.dll differ diff --git a/Plain Craft Launcher 2/Resources/System.Net.Http.Formatting.dll b/Plain Craft Launcher 2/Resources/System.Net.Http.Formatting.dll new file mode 100644 index 0000000..e9e80be Binary files /dev/null and b/Plain Craft Launcher 2/Resources/System.Net.Http.Formatting.dll differ diff --git a/Plain Craft Launcher 2/Resources/java-wrapper.jar b/Plain Craft Launcher 2/Resources/java-wrapper.jar index bd14bd6..9ab92b2 100644 Binary files a/Plain Craft Launcher 2/Resources/java-wrapper.jar and b/Plain Craft Launcher 2/Resources/java-wrapper.jar differ