diff --git a/MeloongCore b/MeloongCore index 8bfb4f7..811438a 160000 --- a/MeloongCore +++ b/MeloongCore @@ -1 +1 @@ -Subproject commit 8bfb4f709f78a528a40e0bd48e6f77bef0e07e24 +Subproject commit 811438aa3c7c0437c7e60e85b516cc593dcf6cb1 diff --git a/PCLCS/Constants.cs b/PCLCS/Constants.cs index 4870d0c..2e1b7fd 100644 --- a/PCLCS/Constants.cs +++ b/PCLCS/Constants.cs @@ -68,7 +68,7 @@ public static class Versions { /// /// 版本管理中 Mod 信息缓存的版本号。 /// - public const int LocalModCacheVersion = 18; + public const int LocalModCacheVersion = 19; /// /// Minecraft 本地实例信息缓存的版本号。 /// diff --git a/PCLCS/Java.cs b/PCLCS/Java.cs index 4074c97..8c88fd3 100644 --- a/PCLCS/Java.cs +++ b/PCLCS/Java.cs @@ -204,7 +204,7 @@ public static class JavaUtils { static bool HasReparsePoint(FileSystemInfo info) { if (PathUtils.IsParentOf(Paths.AppData + @".minecraft\runtime\", info.FullName)) return false; // 不筛查 PCL 的下载路径(#8928) do { - if (info.Attributes.HasFlag(FileAttributes.ReparsePoint)) return true; + if (info.Attributes.HasFlagF(FileAttributes.ReparsePoint)) return true; info = info is FileInfo fileInfo ? fileInfo.Directory : ((DirectoryInfo) info).Parent; } while (info is not null); return false; diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 2ba60a7..1f350cc 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -11,11 +11,11 @@ Public Class FormMain Dim FeatureList As New List(Of KeyValuePair(Of Integer, String)) '统计更新日志条目 If BuildType = BuildTypes.Release Then - If LastVersion < 408 Then 'Release 2.13.1.0 + If LastVersion < 408 Then 'Release 2.13.1.1 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:版本设置中,添加打开截图文件夹的快捷方式")) FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:导出整合包时,可以勾选导出光影设置文件")) - FeatureCount += 21 - BugCount += 22 + FeatureCount += 23 + BugCount += 28 End If If LastVersion < 406 Then 'Release 2.13.0.1 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:重做 Java 管理与相关设置,允许调整 Java 优先级、指定 Java 版本范围等")) @@ -93,6 +93,11 @@ Public Class FormMain '3:BUG+ IMP* FEAT- '2:BUG* IMP- '1:BUG- + If LastVersion < 410 Then 'Snapshot 2.13.1.1 + If LastVersion = 409 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:更新 Mod 时可能更新成另一个 Mod 加载器的版本")) + FeatureCount += 2 + BugCount += 6 + End If If LastVersion < 409 Then 'Snapshot 2.13.1.0 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:版本设置中,添加打开截图文件夹的快捷方式")) FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:导出整合包时,可以勾选导出光影设置文件")) @@ -781,7 +786,7 @@ Public Class FormMain If PanMsg.Children.Any Then If e.Key = Key.Enter Then CType(PanMsg.Children(0), Object).Btn1_Click() - ElseIf e.Key = Key.Escape Then + ElseIf e.Key = Key.Escape OrElse (e.Key = Key.W AndAlso Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) 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 Msg.Btn3_Click() @@ -823,6 +828,11 @@ Public Class FormMain If TypeOf PageRight Is IRefreshable Then CType(PageRight, IRefreshable).Refresh() Return End If + 'Ctrl + w 关闭程序 + If e.Key = Key.W AndAlso Keyboard.Modifiers.HasFlag(ModifierKeys.Control) Then + EndProgram(True) + Return + End If '调用启动游戏 If e.Key = Key.Enter AndAlso PageCurrent = FormMain.PageType.Launch Then If IsAprilEnabled AndAlso Not IsAprilGiveup Then diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index b481080..197870c 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -6,12 +6,12 @@ Public Module ModBase #Region "声明" '下列版本信息由更新器自动修改 - Public Const VersionBaseName As String = "2.13.1.0" '显示用版本名 + Public Const VersionBaseName As String = "2.13.1.1" '显示用版本名 Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换 #If RELEASE Then Public Const VersionCode As Integer = 408 '正式版 #Else - Public Const VersionCode As Integer = 409 '快照版 + Public Const VersionCode As Integer = 410 '快照版 #End If '版本信息 diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index 357902b..34c994e 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -217,8 +217,7 @@ Retry: Optional Content As Object = Nothing, Optional Headers As Dictionary(Of String, String) = Nothing, Optional SimulateBrowserHeaders 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, HostIp As String = Nothing + Dim Request As HttpRequestMessage = Nothing, CancelToken As CancellationTokenSource = Nothing, Response As HttpResponseMessage = Nothing Try '构建 RequestClient Directory.CreateDirectory(PathTemp & "Cache\Http\") @@ -255,8 +254,6 @@ Retry: Retrier.Attempt(delay:=Function() TimeSpan.FromMilliseconds(200), isRetryAllowed:=Function(ex) TypeOf ex Is FileNotFoundException OrElse TypeOf ex Is IOException, action:= Sub(Attempt) CancelToken?.Dispose() : CancelToken = New CancellationTokenSource(Timeout) - HostIp = DNSLookup(Request, CancelToken.Token) - If HostIp IsNot Nothing Then IPReliability.GetOrAdd(HostIp, -0.01) '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率 Dim ClonedRequest As HttpRequestMessage = Request.Clone() 'HttpRequestMessage 只能发送一次,重试时需要克隆一个新的 Try Response = RequestClient.SendAsync(ClonedRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout) @@ -274,26 +271,22 @@ Retry: End Using End Using If Not Response.IsSuccessStatusCode Then - RecordIPReliability(HostIp, -0.7) Dim ResponseMessage = ResponseBytes.GetString(Encoding) Throw New HttpRequestCodeException( - $"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Method},{Url},{HostIp}" & + $"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Method},{Url}" & If(String.IsNullOrEmpty(ResponseMessage), "", vbCrLf & ResponseMessage), Response.StatusCode, ResponseMessage) End If - '输出 - RecordIPReliability(HostIp, 0.5) Return ResponseBytes Catch ex As HttpRequestException Throw Catch ex As TimeoutException - Throw New WebException($"连接服务器超时,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout) + Throw New WebException($"连接服务器超时,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url})", WebExceptionStatus.Timeout) Catch ex As Exception If TypeOf ex IsNot TaskCanceledException Then ex.ThrowIfCanceled() 'GetResultWithTimeout 会抛出 TaskCanceledException - RecordIPReliability(HostIp, -1) If ex.IsBadNetwork Then - Throw New WebException($"网络请求失败,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout) + Throw New WebException($"网络请求失败,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url})", WebExceptionStatus.Timeout) Else - Throw New Exception($"网络请求出现意外异常({Method}, {Url},{HostIp})", ex) + Throw New Exception($"网络请求出现意外异常({Method}, {Url})", ex) End If Finally Request?.Dispose() @@ -304,72 +297,6 @@ Retry: Private RequestClient As HttpClient = Nothing Private RequestClientLock As New Object - ''' - ''' 进行 DNS 解析。它仅在选择的 IP 与系统默认的不一致时才对 URL 中的 Host 进行替换。 - ''' 返回请求时应使用的 IP。若解析失败,则返回 Nothing。 - ''' - Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationToken) As String - Dim Host = Request.RequestUri.Host - '不对部分有严格 SNI 限制的域名进行 DNS 解析(#8295) - If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsIgnoreCase(h)) Then Return Nothing - '若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量 - Dim LastFailureTime As Date - If DNSFailureRecord.TryGetValue(Host, LastFailureTime) Then - If Date.Now - LastFailureTime < New TimeSpan(0, 1, 0) Then - Return Nothing - Else - DNSFailureRecord.Remove(Host) - End If - End If - '初步 DNS 解析 - Dim Candidates As IPAddress() - Try - Dim AsyncResult = Dns.BeginGetHostAddresses(Host, Nothing, Nothing) - If WaitHandle.WaitAny(New WaitHandle() {AsyncResult.AsyncWaitHandle, CancelToken.WaitHandle}) Then Throw New TimeoutException("DNS 解析超时") - Candidates = Dns.EndGetHostAddresses(AsyncResult).Distinct.ToArray - Catch ex As Exception - Logger.Warn(ex, $"DNS 解析失败({Host})") - DNSFailureRecord(Host) = Date.Now - Return Nothing - End Try - If Not Candidates.Any Then - Logger.Info($"DNS 解析无结果({Host})") - Return Nothing - End If - '若同时存在 IPv4 和 IPv6 地址,仅选择其中一类(因为 GFW 可能只屏蔽了 IPv4 或 IPv6) - Dim Reliabilities = Candidates.ToDictionary(Function(i) i, Function(i) IPReliability.GetOrDefault(i.ToString, 0)) - Dim IPv4Targets = Candidates.Where(Function(i) i.AddressFamily = AddressFamily.InterNetwork).ToArray - Dim IPv6Targets = Candidates.Where(Function(i) i.AddressFamily = AddressFamily.InterNetworkV6).ToArray - If IPv4Targets.Any AndAlso IPv6Targets.Any Then - Dim IPv4Reliability = IPv4Targets.Max(Function(i) Reliabilities(i)) - Dim IPv6Reliability = IPv6Targets.Max(Function(i) Reliabilities(i)) - If Host = "api.modrinth.com" Then IPv6Reliability -= 0.1 '让 Modrinth 优先使用 IPv4 地址(#6887) - Logger.Trace(Function() $"DNS IPv4/IPv6 选择({Host}),IPv4 {IPv4Reliability:0.000},IPv6 {IPv6Reliability:0.000}") - Candidates = If(IPv4Reliability >= IPv6Reliability, IPv4Targets, IPv6Targets) - End If - '选择可靠度最高的 IP - Dim Target As IPAddress = Candidates.MaxBy(Function(i) Reliabilities(i)) - Request.Headers.Host = Request.RequestUri.Authority - Request.RequestUri = (New UriBuilder(Request.RequestUri) With {.Host = If(Target.AddressFamily = AddressFamily.InterNetworkV6, $"[{Target}]", Target.ToString)}).Uri - Return Target.ToString - End Function - Private DNSFailureRecord As New ConcurrentDictionary(Of String, Date) - ''' - ''' 记录每个 IP 地址的请求可靠度。 - ''' 通常取值 -1 ~ +0.5,越高越好。未尝试过的 IP 应视为 0。 - ''' - Private IPReliability As New ConcurrentDictionary(Of String, Double) - ''' - ''' 根据请求结果,记录 IP 地址的可靠度。 - ''' - Private Sub RecordIPReliability(IP As String, Result As Double) - If IP Is Nothing Then Return - IPReliability.AddOrUpdate(IP, - Function() Result * 0.5, - Function(k, v) v * 0.5 + Result * 0.5 - ) - End Sub - #End Region #Region "多线程下载引擎" @@ -976,7 +903,7 @@ StartThread: Logger.Log($"{LocalName}:开始,起始点 {Th.DownloadStart},{Th.Source.Url}", If(Th.DownloadStart = 0, LogLevel.Info, LogLevel.Trace)) Dim ResultStream As Stream = Nothing, HttpRequest As HttpRequestMessage = Nothing, Response As HttpResponseMessage = Nothing, ResponseStream As Stream = Nothing, - CancelToken As CancellationTokenSource = Nothing, HostIp As String = Nothing + CancelToken As CancellationTokenSource = Nothing '部分下载源真的特别慢,并且只需要一个请求,例如 Ping 为 20s,如果增长太慢,就会造成类似 2.5s 5s 7.5s 10s 12.5s... 的极大延迟 '延迟过长会导致某些特别慢的链接迟迟不被掐死 Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 15000) * (1 + Th.Source.FailCount), 30000) @@ -987,7 +914,6 @@ StartThread: HttpRequest = New HttpRequestMessage(HttpMethod.Get, Th.Source.Url) SecretHeadersSign(Th.Source.Url, HttpRequest, SimulateBrowserHeaders) CancelToken = New CancellationTokenSource(Timeout) - HostIp = DNSLookup(HttpRequest, CancelToken.Token) 'DNS 预解析 If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New Headers.RangeHeaderValue(Th.DownloadStart, Nothing) Dim ContentLength As Long = 0 Response = ThreadClient.SendAsync(HttpRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout) @@ -1119,15 +1045,13 @@ SourceBreak: '本线程完成 Th.State = NetState.Finished Logger.Trace(Function() $"{LocalName}:完成,已下载 {Th.DownloadDone}({Th.DownloadStart}~{Th.DownloadEnd})") - RecordIPReliability(HostIp, 0.5) End If Catch ex As Exception If ex.IsBadNetwork OrElse TypeOf ex Is TaskCanceledException Then - Logger.Info(New TimeoutException($"{LocalName}:超时({Timeout}ms),{ex.Message},IP:{If(String.IsNullOrEmpty(HostIp), "自动", "")}")) + Logger.Info(New TimeoutException($"{LocalName}:超时({Timeout}ms),{ex.Message}")) Else - Logger.Info(ex, $"{LocalName}:出错,IP:{HostIp}") + Logger.Info(ex, $"{LocalName}:出错") End If - RecordIPReliability(HostIp, -0.7) SourceFail(Th, ex, False) Finally '释放资源 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb index d4ea6d0..f6ddaba 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb @@ -1379,11 +1379,16 @@ OnLoaded: If Instance.State <> McInstanceState.Error AndAlso ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionVanillaName", "Unknown") <> "Unknown" Then '读取版本信息 + Dim ReadCachedInfo = + Function(Key As String) As String + Dim Value = ReadIni(Instance.PathVersion & "PCL\Setup.ini", Key, Nothing) + Return If(Value = "", Nothing, Value) '对老版本 Setup.ini 的空值兼容 + End Function Dim VersionInfo As New McVersion With { - .Fabric = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionFabric", Nothing), - .Forge = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionForge", Nothing), - .NeoForge = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionNeoForge", Nothing), - .OptiFine = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionOptiFine", Nothing), + .Fabric = ReadCachedInfo("VersionFabric"), + .Forge = ReadCachedInfo("VersionForge"), + .NeoForge = ReadCachedInfo("VersionNeoForge"), + .OptiFine = ReadCachedInfo("VersionOptiFine"), .HasLiteLoader = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionLiteLoader", False), .VanillaName = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionVanillaName", "Unknown") } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb index 7d21482..5b86369 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb @@ -436,8 +436,8 @@ Public Module ModModpack Urls = ResourceVersion.ParseModrinthTrackArguments(Urls, ResourceVersion.DownloadReason.ModPack, TargetVersion.VanillaName, TargetVersion.ModLoaders).ToList 'Modrinth 来源追踪信息 Urls = Urls.Distinct.ToList() - Dim TargetPath As String = $"{McFolderSelected}versions\{InstanceName}\{File("path")}" - If Not Path.GetFullPath(TargetPath).StartsWithF(Path.GetFullPath($"{McFolderSelected}versions\{InstanceName}\")) Then + Dim TargetPath As String = Path.Combine(McFolderSelected, "versions", InstanceName, File("path").ToString().TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) + If Not PathUtils.IsParentOf($"{McFolderSelected}versions\{InstanceName}\", TargetPath) Then MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!{vbCrLf}目标:{Path.GetFullPath(TargetPath)}{vbCrLf}错误的文件:{TargetPath}", "文件路径校验失败", IsWarn:=True) Throw New OperationCanceledException End If diff --git a/Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb b/Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb index 5168084..4bed629 100644 --- a/Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb +++ b/Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb @@ -120,12 +120,12 @@ Public Module LocalResourceLoaders Next Logger.Info($"有 {Mods.Where(Function(m) m.Project Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Project IsNot Nothing).Count} 个 Mod 需要更新信息") If Not Mods.Any Then Return - '获取作为检查目标的加载器和版本 + '获取作为检查目标的版本 '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2 Dim TargetMcVersion As McVersion = Loader.Input.Target Dim VanillaVersion = TargetMcVersion.VanillaName '开始网络获取 - Logger.Info($"目标加载器:{ModLoaders},版本:{VanillaVersion}") + Logger.Info($"正在检查 Mod 信息的版本:{VanillaVersion},加载器 {ModLoaders}") Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False Dim CurrentTaskThread As Thread = Thread.CurrentThread '从 Modrinth 获取信息 @@ -140,6 +140,7 @@ Public Module LocalResourceLoaders '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 If ModrinthVersion.Count = 0 Then Return Dim ModrinthMapping As New Dictionary(Of String, List(Of LocalResourceFile)) + Dim ModrinthLoaders As New Dictionary(Of LocalResourceFile, List(Of String)) For Each Entry In Mods If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For @@ -149,6 +150,8 @@ Public Module LocalResourceLoaders ModrinthMapping(ProjectId).Add(Entry) '记录对应的 ProjectVersion Dim File = ResourceVersion.FromPlatformJson(ModrinthVersion(Entry.ModrinthHash), ResourceTypes.Mod) + ModrinthLoaders(Entry) = ModrinthVersion(Entry.ModrinthHash)("loaders"). + Select(Function(l) l.ToString.Lower).Distinct.OrderBy(Function(l) l).ToList If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File Else @@ -169,17 +172,20 @@ Public Module LocalResourceLoaders Next Logger.Info($"已从 Modrinth 获取本地 Mod 信息,继续获取更新信息") '步骤 4:获取更新信息 - If ModLoaders = ModLoaders.None Then - Logger.Warn("该 Minecraft 版本没有可用的 Mod 加载器,不获取 Mod 更新信息") - Return - End If - 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.Flags.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json") + Dim ModsToUpdate = ModrinthMapping.SelectMany(Function(l) l.Value).ToList + If Not ModsToUpdate.Any Then Return + Dim UpdateData As String = ModsToUpdate. + Select(Function(m) $"{{""hash"": ""{m.ModrinthHash}"", ""loaders"": [""{ModrinthLoaders(m).Join(""",""")}""], ""game_versions"": [""{VanillaVersion}""]}}"). + Join(",") + Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update_individual", HttpMethod.Post, + $"{{""algorithm"": ""sha1"", ""hashes"": [{UpdateData}]}}", "application/json") For Each Entry In Mods If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.ProjectVersion Is Nothing Then Continue For Dim UpdateFile = ResourceVersion.FromPlatformJson(ModrinthUpdate(Entry.ModrinthHash), ResourceTypes.Mod) If Not UpdateFile.DownloadAvailable Then Continue For + Dim UpdateLoaders = ModrinthUpdate(Entry.ModrinthHash)("loaders"). + Select(Function(l) l.ToString.Lower).Distinct.OrderBy(Function(l) l).ToList + If Not UpdateLoaders.SequenceEqual(ModrinthLoaders(Entry)) Then Continue For Logger.Trace(Function() $"本地文件 {Entry.ProjectVersion.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}") If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For '设置更新日志与更新文件 @@ -217,6 +223,7 @@ Public Module LocalResourceLoaders '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 Dim ProjectIdToLocalFiles As New Dictionary(Of Integer, List(Of LocalResourceFile)) + Dim CurseForgeLoaders As New Dictionary(Of LocalResourceFile, ModLoaders) For Each Project In CurseForgeRaw Dim ProjectId = Project("id").ToString Dim Hash As UInteger = Project("file")("fileFingerprint") @@ -226,6 +233,7 @@ Public Module LocalResourceLoaders ProjectIdToLocalFiles.AddIntoValueCollection(ProjectId, Entry) '记录对应的 ProjectVersion Dim File = ResourceVersion.FromPlatformJson(Project("file"), ResourceTypes.Mod) + CurseForgeLoaders(Entry) = File.ModLoaders If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File Next Next @@ -242,34 +250,34 @@ Public Module LocalResourceLoaders If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For '设置 Entry 中的工程信息 Dim Project As New ResourceProject(ProjectJson) - Dim TargetLoaders As ModLoaders = ModLoaders.None For Each LocalFile In ProjectIdToLocalFiles(Project.Id) '倒查防止 CurseForge 返回的内容有漏 If LocalFile.Project?.Platform = ResourcePlatforms.Modrinth Then LocalFile.Project = LocalFile.Project '再次触发修改事件 Else LocalFile.Project = Project - TargetLoaders = TargetLoaders Or LocalFile.Project.ModLoaders End If - Next - TargetLoaders = TargetLoaders And ModLoaders '与目标 MC 版本的加载器取交集 - '查找或许版本更新的文件列表 - If ModLoaders = ModLoaders.None Then Continue For '无法判断 MC 版本使用的加载器,跳过更新检查 - Dim LatestFiles = ProjectJson("latestFilesIndexes").Where( - Function(Entry) - 'Mod Loader 匹配 - If Entry("modLoader") Is Nothing Then Return False - Dim EntryLoader As ModLoaders = Resource.FromCurseForgeModLoaderType(Entry("modLoader").ToObject(Of Integer)) - If Not TargetLoaders.HasFlag(EntryLoader) Then Return False - 'MC 版本匹配 - Dim EntryVersion As String = Entry("gameVersion") - Return EntryVersion IsNot Nothing AndAlso EntryVersion = VanillaVersion - End Function). - MaxByAll(Function(Entry) Entry("gameVersion").ToString, New VersionComparer) '只保留最新的 MC 版本 - For Each Entry In LatestFiles - Dim FileId = Entry("fileId").ToObject(Of Integer) - If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of LocalResourceFile) - UpdateFileIds(FileId).AddRange(ProjectIdToLocalFiles(Project.Id)) - FileIdToProjectSlug(FileId) = Project.Slug + '查找可能有版本更新的文件列表 + Dim LatestFiles = ProjectJson("latestFilesIndexes").Where( + Function(Entry) + 'Mod Loader 匹配 + Dim EntryLoader As ModLoaders = If(Entry("modLoader") Is Nothing, ModLoaders.None, + Resource.FromCurseForgeModLoaderType(Entry("modLoader").ToObject(Of Integer))) + If CurseForgeLoaders(LocalFile) = ModLoaders.None Then + If EntryLoader <> ModLoaders.None Then Return False + ElseIf Not CurseForgeLoaders(LocalFile).HasFlagF(EntryLoader) Then + Return False + End If + 'MC 版本匹配 + Dim EntryVersion As String = Entry("gameVersion") + Return EntryVersion IsNot Nothing AndAlso EntryVersion = VanillaVersion + End Function). + MaxByAll(Function(Entry) Entry("gameVersion").ToString, New VersionComparer) '只保留最新的 MC 版本 + For Each Entry In LatestFiles + Dim FileId = Entry("fileId").ToObject(Of Integer) + If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of LocalResourceFile) + UpdateFileIds(FileId).Add(LocalFile) + FileIdToProjectSlug(FileId) = Project.Slug + Next Next Next Logger.Info($"已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息") @@ -283,6 +291,10 @@ Public Module LocalResourceLoaders Dim File = ResourceVersion.FromPlatformJson(FileJson, ResourceTypes.Mod) If Not File.DownloadAvailable Then Continue For For Each Entry As LocalResourceFile In UpdateFileIds(File.Id) + If File.ModLoaders <> CurseForgeLoaders(Entry) Then + Logger.Warn($"从 CurseForge 获取到的更新文件 {File.FileName} 加载器为 {File.ModLoaders},与本地文件 {Entry.File.Name} 的加载器 {CurseForgeLoaders(Entry)} 不一致,已忽略", LogBehavior.None) + Continue For + End If If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For UpdateFiles(Entry) = File Next diff --git a/Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb b/Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb index 516c26b..53e70d9 100644 --- a/Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb +++ b/Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb @@ -87,7 +87,7 @@ Public Class ResourceProject Get If Not LoadedDatabase Then LoadedDatabase = True - If Types.HasFlag(ResourceTypes.Mod) OrElse Types.HasFlag(ResourceTypes.DataPack) Then + If Types.HasFlagF(ResourceTypes.Mod) OrElse Types.HasFlagF(ResourceTypes.DataPack) Then _WikiEntry = WikiEntry.All.Value.FirstOrDefault(Function(c) c.Slugs.GetOrDefault(Platform) = Slug) End If End If @@ -415,7 +415,7 @@ Public Class ResourceProject If Not MissingProjectIds.Any() Then Return '联网获取剩余的工程对象 Dim Projects As JArray - If Platform.HasFlag(ResourcePlatforms.CurseForge) Then + If Platform.HasFlagF(ResourcePlatforms.CurseForge) Then Projects = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post, "{""modIds"": [" & MissingProjectIds.Join(","c) & "]}", "application/json")("data") Else diff --git a/Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb b/Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb index cf4a651..263be2f 100644 --- a/Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb +++ b/Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb @@ -21,8 +21,8 @@ Public Class ResourceSearcher ''' Public ReadOnly Property CanContinue As Boolean Get - If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0 - If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0 + If Tag.StartsWithF("/") OrElse Not Sources.HasFlagF(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0 + If Tag.EndsWithF("/") OrElse Not Sources.HasFlagF(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0 If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal End Get @@ -241,7 +241,7 @@ Public Class ResourceSearcher Next End Function 'CurseForge - If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) Then + If Request.Sources.HasFlagF(ResourcePlatforms.CurseForge) Then '数据库搜索 Static CurseForgeSearchEntries As List(Of SearchEntry(Of WikiEntry)) = GetSearchEntries(ResourcePlatforms.CurseForge).ToList() Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25) @@ -258,7 +258,7 @@ Public Class ResourceSearcher End If End If 'Modrinth - If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) Then + If Request.Sources.HasFlagF(ResourcePlatforms.Modrinth) Then '数据库搜索 Static ModrinthSearchEntries As List(Of SearchEntry(Of WikiEntry)) = GetSearchEntries(ResourcePlatforms.Modrinth).ToList() Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25) @@ -303,7 +303,7 @@ NextPage: Try 'CurseForge 搜索 - If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) AndAlso + If Request.Sources.HasFlagF(ResourcePlatforms.CurseForge) AndAlso Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足 (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续 WorkThreads.Add(RunInNewThread( @@ -317,7 +317,7 @@ NextPage: For Each JsonEntry As JObject In RequestResult("data") Dim Project As New ResourceProject(JsonEntry) If Request.Type = ResourceTypes.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包 - If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For '过滤分区不匹配的搜索结果(#8265) + If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlagF(Request.Type) Then Continue For '过滤分区不匹配的搜索结果(#8265) ProjectList.Add(Project) Next '更新结果 @@ -335,7 +335,7 @@ NextPage: End If 'Modrinth 搜索 - If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso + If Request.Sources.HasFlagF(ResourcePlatforms.Modrinth) AndAlso Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足 (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续 WorkThreads.Add(RunInNewThread( @@ -363,7 +363,7 @@ NextPage: End If 'Modrinth 直接获取工程 - If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso + If Request.Sources.HasFlagF(ResourcePlatforms.Modrinth) AndAlso Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足 ModrinthSlugs.Any Then '有直接获取的 Slug WorkThreads.Add(RunInNewThread( @@ -375,7 +375,7 @@ NextPage: For Each JsonEntry As JObject In DlModRequest(ModrinthUrl) Dim Project As New ResourceProject(JsonEntry) '应用筛选 - If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For + If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlagF(Request.Type) Then Continue For If Not String.IsNullOrEmpty(Request.Tag) AndAlso Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判 If Request.ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter AndAlso @@ -405,7 +405,7 @@ NextPage: '仅保留兼容 Forge 的 Mod,或老版本中没有标注任何 Mod Loader 的 Mod If IgnoreModLoaderFilter Then - RawResults.KeepIf(Function(p) p.ModLoaders = ModLoaders.None OrElse p.ModLoaders.HasFlag(ModLoaders.Forge)) + RawResults.KeepIf(Function(p) p.ModLoaders = ModLoaders.None OrElse p.ModLoaders.HasFlagF(ModLoaders.Forge)) End If '确保存在结果 diff --git a/Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb b/Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb index 79dc5d2..857c7e4 100644 --- a/Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb +++ b/Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb @@ -185,7 +185,7 @@ Public Class ResourceVersion For Each Loader As ModLoaders In EnumUtils.GetAllFlags(Of ModLoaders)() If RawLoaders.Contains(Loader.ToString.Lower) Then .ModLoaders = .ModLoaders Or Loader Next - If .ResourceType.HasFlag(ResourceTypes.Mod) OrElse .ResourceType.HasFlag(ResourceTypes.DataPack) Then + If .ResourceType.HasFlagF(ResourceTypes.Mod) OrElse .ResourceType.HasFlagF(ResourceTypes.DataPack) Then If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then .ResourceType = ResourceTypes.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric If RawLoaders.Contains("datapack") Then .ResourceType = ResourceTypes.DataPack If .ModLoaders.Flags().Any() Then .ResourceType = ResourceTypes.Mod diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index e39984b..c71c896 100644 --- a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb +++ b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb @@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 ' 方法是按如下所示使用“*” - - + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb index 38bd0d1..c1e0c69 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb @@ -11,6 +11,7 @@ Public Module ModDownloadLib Dim VersionFolder As String = McFolderSelected & "versions\" & InstanceName & "\" Dim Loaders As New List(Of LoaderBase) + Dim NewInstance As McInstance = Nothing '下载版本 Json 文件 If JsonUrl Is Nothing Then @@ -23,14 +24,19 @@ Public Module ModDownloadLib Loaders.Add(New LoaderDownload(McDownloadClientJsonName, New List(Of NetFile) From { New NetFile(DlSourceLauncherOrMetaGet(If(JsonUrl, "")), VersionFolder & InstanceName & ".json", New FileChecker With {.IsJson = True}) }) With {.ProgressWeight = 3}) + '读取 Json 文件并创建实例 + Loaders.Add(New LoaderTask(Of String, String)("读取 json 文件", + Sub(Task As LoaderTask(Of String, String)) + NewInstance = New McInstance(VersionFolder) + NewInstance.Load() + End Sub) With {.ProgressWeight = 0, .Show = False}) '下载支持库文件 Dim LoadersLib As New List(Of LoaderBase) LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析原版支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) - Thread.Sleep(50) '等待 JSON 文件实际写入硬盘(#3710) Logger.Info($"开始分析原版支持库文件:{VersionFolder}") - Task.Output = McLibNetFilesFromInstance(New McInstance(VersionFolder)) + Task.Output = McLibNetFilesFromInstance(NewInstance) End Sub) With {.ProgressWeight = 1, .Show = False}) LoadersLib.Add(New LoaderDownload("下载原版支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 13, .Show = False}) Loaders.Add(New LoaderCombo(Of String)(McDownloadClientLibName, LoadersLib) With {.Block = False, .ProgressWeight = 14}) @@ -40,7 +46,7 @@ Public Module ModDownloadLib LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析资源文件索引地址(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Try - Dim AssetIndex = DlClientAssetIndexGet(New McInstance(VersionFolder)) + Dim AssetIndex = DlClientAssetIndexGet(NewInstance) Task.Output = If(AssetIndex Is Nothing, New List(Of NetFile), New List(Of NetFile) From {AssetIndex}) Catch ex As Exception Throw New Exception("分析资源文件索引地址失败", ex) @@ -56,7 +62,7 @@ Public Module ModDownloadLib End Sub) With {.ProgressWeight = 1, .Show = False}) LoadersAssets.Add(New LoaderDownload("下载资源文件索引(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 3, .Show = False}) LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析所需资源文件(副加载器)", - Sub(Task) Task.Output = McAssetsFixList(New McInstance(VersionFolder), True, Task)) With {.ProgressWeight = 0.01, .Show = False}) + Sub(Task) Task.Output = McAssetsFixList(NewInstance, True, Task)) With {.ProgressWeight = 0.01, .Show = False}) LoadersAssets.Add(New LoaderDownload("下载资源文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 14, .Show = False}) Loaders.Add(New LoaderCombo(Of String)("下载原版资源文件", LoadersAssets) With {.Block = False, .ProgressWeight = 18}) diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb index 5edaa53..3c160cc 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb @@ -307,7 +307,7 @@ Public Class PageInstanceOverall Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown Dim IsHintIndie As Boolean = PageInstanceLeft.Instance.State <> McInstanceState.Error AndAlso PageInstanceLeft.Instance.PathIndie <> McFolderSelected Dim SavesFolder = DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie & "saves\") - Dim SaveEntries = If(SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String)) + Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String)) Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {PageInstanceLeft.Instance.Name} 吗?" & If(IsHintIndie, vbCrLf & "该版本对应的存档、资源包、Mod 等文件也将被一并删除!" & If(SaveEntries.Any, vbCrLf & vbCrLf & "这会删除以下存档:" & vbCrLf & "· " & SaveEntries.Join(vbCrLf & "· "), ""), ""), diff --git a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml index 59ed685..4621e50 100644 --- a/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml +++ b/Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml @@ -134,8 +134,10 @@ + + @@ -150,6 +152,7 @@ + @@ -158,16 +161,22 @@ + + + + + + @@ -213,7 +222,21 @@ + + + + + + + + + + + + + + @@ -224,21 +247,26 @@ + + + + + @@ -255,6 +283,7 @@ + @@ -273,12 +302,22 @@ + + + + + + + + + + @@ -292,12 +331,18 @@ + + + + + + @@ -332,11 +377,16 @@ + + + + + @@ -344,6 +394,7 @@ + @@ -360,6 +411,7 @@ + @@ -382,11 +434,13 @@ + + @@ -405,6 +459,7 @@ + @@ -439,13 +494,17 @@ + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb index 25393d3..63ae8c5 100644 --- a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb @@ -186,7 +186,7 @@ Public Class PageSelectRight Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown Dim IsHintIndie As Boolean = Instance.State <> McInstanceState.Error AndAlso Instance.PathIndie <> McFolderSelected Dim SavesFolder = DirectoryUtils.GetInfo(Instance.PathIndie & "saves\") - Dim SaveEntries = If(SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String)) + Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String)) Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {Instance.Name} 吗?" & If(IsHintIndie, vbCrLf & "该版本对应的存档、资源包、Mod 等文件也将被一并删除!", "") & If(SaveEntries.Any, vbCrLf & vbCrLf & "这会删除以下存档:" & vbCrLf & "· " & SaveEntries.Join(vbCrLf & "· "), ""),