using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using PCL.Core.App; using PCL.Core.App.Configuration; using PCL.Core.App.Configuration.Storage; using PCL.Core.App.Localization; using PCL.Core.IO.Net.Http; using PCL.Core.Minecraft; using PCL.Core.UI; using PCL.Core.Utils; using PCL.Network; using PCL.Network.Loaders; namespace PCL; public static class ModDownloadLib { /// /// 如果 OptiFine 与 Forge 同时开始安装,就会导致 Forge 安装失败。 /// private static readonly object installSyncLock = new(); /// /// 如果 OptiFine 与 Forge 同时复制原版 Jar,就会导致复制文件时冲突。 /// private static readonly object vanillaSyncLock = new(); /// /// 将远程元数据提供的名称作为单个目录名拼接到缓存目录下,并阻止路径穿越。 /// private static string CombineCacheSubfolder(string parentFolder, string childFolderName) { if (string.IsNullOrWhiteSpace(childFolderName) || childFolderName is "." or ".." || childFolderName.IndexOfAny(new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0' }) >= 0 || Path.IsPathRooted(childFolderName)) CancelUnsafeCacheSubfolder(childFolderName, "包含非法路径字符"); var parentFullPath = Path.GetFullPath(parentFolder) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var combinedFullPath = Path.GetFullPath(Path.Combine(parentFullPath, childFolderName)); if (!combinedFullPath.StartsWith(parentFullPath, StringComparison.OrdinalIgnoreCase)) CancelUnsafeCacheSubfolder(childFolderName, "导致缓存路径越界"); return combinedFullPath; } private static void CancelUnsafeCacheSubfolder(string childFolderName, string reason) { var message = "远程版本名" + reason + ":" + childFolderName; ModBase.Log("[Download] " + message); HintService.Hint(message, HintType.Error); throw new ModBase.CancelledException(); } #region Minecraft 下载 /// /// 下载某个 Minecraft 实例,这会创造一个单独的下载任务,失败会跳过执行并要求反馈。 /// 返回正在下载的任务,若跳过或失败,则返回 Nothing。 /// /// 所下载的 Minecraft 的版本名。 /// Json 文件的 Mojang 官方地址。 public static ModLoader.LoaderCombo McDownloadClient(NetPreDownloadBehaviour behaviour, string id, string jsonUrl = null) { try { var versionFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", id); // 重复任务检查 foreach (var ongoingLoader in ModLoader.loaderTaskbar.ToList()) { if (ongoingLoader.name != Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id)) continue; if (behaviour == NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) return (ModLoader.LoaderCombo)ongoingLoader; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return (ModLoader.LoaderCombo)ongoingLoader; } // 已有实例检查 if (behaviour != NetPreDownloadBehaviour.IgnoreCheck && File.Exists(Path.Combine(versionFolder, id + ".json")) && File.Exists(Path.Combine(versionFolder, id + ".jar"))) { if (behaviour == NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) return null; if (ModMain.MyMsgBox( Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists", id, "\r\n"), Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel") ) == 1) { File.Delete(Path.Combine(versionFolder, id + ".jar")); File.Delete(Path.Combine(versionFolder, id + ".json")); } else { return null; } } // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id), McDownloadClientLoader(id, jsonUrl)) { OnStateChanged = McInstallState }; loader.Start(versionFolder); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); return loader; } catch (Exception ex) { ModBase.Log( ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); return null; } } /// /// 保存某个 Minecraft 实例的核心文件(仅 Json 与核心 Jar)。 /// /// 所下载的 Minecraft 的版本名。 /// Json 文件的 Mojang 官方地址。 public static void McDownloadClientCore(string id, string jsonUrl, NetPreDownloadBehaviour behaviour) { try { var versionFolder = SystemDialogs.SelectFolder(); if (!versionFolder.Contains(@"\")) return; versionFolder = Path.Combine(versionFolder, id); // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id) ?? "")) continue; if (behaviour == NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) return; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } var loaders = new List(); // 下载实例 Json 文件 loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadInstanceJson"), new List { new(ModDownload.DlSourceLauncherOrMetaGet(jsonUrl), Path.Combine(versionFolder, id + ".json"), new ModBase.FileChecker(canUseExistsFile: false, isJson: true)) }) { ProgressWeight = 2d }); // 获取支持库文件地址 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeCoreJarUrl"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 0.5d, show = false }); // 下载支持库文件 loaders.Add( new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadCoreJar"), new List()) { ProgressWeight = 5d }); // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(id); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 获取下载某个 Minecraft 版本的加载器列表。 /// 它必须安装到 McFolderSelected,但是可以自定义版本名(不过自定义的实例名不会修改 Json 中的 id 项)。 /// private static List McDownloadClientLoader(string id, string jsonUrl = null, string instanceName = null) { instanceName = instanceName ?? id; var instanceFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName); var loaders = new List(); // 下载实例 Json 文件 if (jsonUrl is null) loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainVanillaJsonUrl"), task => { var jsonAddress = ModDownload.DlClientListGet(id)?.ToString(); task.output = new List { new(ModDownload.DlSourceLauncherOrMetaGet(jsonAddress), Path.Combine(instanceFolder, instanceName + ".json")) }; }) { ProgressWeight = 2d, show = false }); loaders.Add(new LoaderDownload(mcDownloadClientJsonName, new List { new(ModDownload.DlSourceLauncherOrMetaGet(jsonUrl ?? ""), Path.Combine(instanceFolder, instanceName + ".json"), new ModBase.FileChecker(canUseExistsFile: false, isJson: true)) }) { ProgressWeight = 3d }); // 下载支持库文件 var loadersLib = new List(); loadersLib.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeVanillaLibraries.Side"), task => { var jsonPath = Path.Combine(instanceFolder, instanceName + ".json"); ModBase.WaitForFileReady(jsonPath); ModBase.Log("[Download] 开始分析原版支持库文件:" + instanceFolder); if (id == "1.16.5" && Config.Download.FixAuthLib) // 1.16.5 Authlib 修复 try { var json = ModBase.ReadFile(jsonPath); json = json.Replace("2.1.28/authlib-2.1.28.jar", "2.3.31/authlib-2.3.31.jar") .Replace("com.mojang:authlib:2.1.28", "com.mojang:authlib:2.3.31") .Replace("ad54da276bf59983d02d5ed16fc14541354c71fd", "bbd00ca33b052f73a6312254780fc580d2da3535") .Replace("76328", "87662"); ModBase.WriteFile(jsonPath, json); } catch (Exception ex) { ModBase.Log("[Download] 替换 Authlib 版本失败: " + ex.Message); } task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(instanceFolder)); }) { ProgressWeight = 1d, show = false }); loadersLib.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadVanillaLibraries.Side"), new List()) { ProgressWeight = 13d, show = false }); loaders.Add(new ModLoader.LoaderCombo(mcDownloadClientLibName, loadersLib) { block = false, ProgressWeight = 14d }); // 下载资源文件 var loadersAssets = new List(); loadersAssets.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeAssetsIndex.Side"), task => { ModBase.WaitForFileReady(Path.Combine(instanceFolder, instanceName + ".json")); try { var assetIndex = new McInstance(instanceFolder); task.output = new List { ModDownload.DlClientAssetIndexGet(assetIndex) }; } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.AssetIndexAnalysisFailed"), ex); } // 顺手添加 Json 项目 try { var versionJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(Path.Combine(instanceFolder, instanceName + ".json"))); versionJson.Add("clientVersion", id); ModBase.WriteFile(Path.Combine(instanceFolder, instanceName + ".json"), versionJson.ToString()); } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.AddClientVersionFailed"), ex); } }) { ProgressWeight = 1d, show = false }); loadersAssets.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssetsIndex.Side"), new List()) { ProgressWeight = 3d, show = false }); loadersAssets.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeRequiredAssets.Side"), task => { ModLoader.LoaderBase argprogressFeed = task; task.output = ModAssets.McAssetsFixList(new McInstance(instanceFolder), true, ref argprogressFeed); task = (ModLoader.LoaderTask>)argprogressFeed; }) { ProgressWeight = 0.01d, show = false }); loadersAssets.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssets.Side"), new List()) { ProgressWeight = 14d, show = false }); loaders.Add( new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.DownloadVanillaAssets"), loadersAssets) { block = false, ProgressWeight = 18d }); return loaders; } private static readonly string mcDownloadClientLibName = Lang.Text("Minecraft.Download.Stage.VanillaLibrariesDownload"); private static readonly string mcDownloadClientJsonName = Lang.Text("Minecraft.Download.Stage.VanillaJsonDownload"); #endregion #region Minecraft 下载菜单 public static MyListItem McDownloadListItem(JsonObject entry, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 确定图标 string logo = entry["type"].ToString() switch { "release" => ModBase.pathImage + "Blocks/Grass.png", "snapshot" => ModBase.pathImage + "Blocks/CommandBlock.png", "pending" => ModBase.pathImage + "Blocks/CommandBlock.png", "special" => ModBase.pathImage + "Blocks/GoldBlock.png", _ => ModBase.pathImage + "Blocks/CobbleStone.png" }; // 建立控件 var formattedVersion = McFormatter.FormatVersion(entry["id"].ToString()).Replace("_", " "); var newItem = new MyListItem { Logo = logo, SnapsToDevicePixels = true, Title = formattedVersion, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry }; if (entry["lore"] is null) { if (formattedVersion != (string)entry["id"]) newItem.Info = Lang.Date(entry["releaseTime"].ToObject(), "g") + " | " + entry["id"]; else newItem.Info = Lang.Date(entry["releaseTime"].ToObject(), "g"); } else if (formattedVersion != (string)entry["id"]) { newItem.Info = entry["lore"] + " | " + entry["id"]; } else { newItem.Info = entry["lore"].ToString(); } if (entry["url"].ToString().Contains("unlisted-versions-of-minecraft")) newItem.Tags = Lang.Text("Download.Source.Tag.Uvmc"); newItem.Click += onClick; // 建立菜单 if (isSaveOnly) newItem.ContentHandler = McDownloadSaveMenuBuild; else newItem.ContentHandler = McDownloadMenuBuild; // 结束 return newItem; } private static void McDownloadSaveMenuBuild(object sender, EventArgs _) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (ss, ee) => McDownloadMenuLog(ss, (dynamic)ee); var btnServer = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/server", ToolTip = Lang.Text("Download.Version.DownloadServer") }; ToolTipService.SetPlacement(btnServer, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnServer, 30d); ToolTipService.SetHorizontalOffset(btnServer, 2d); btnServer.Click += (ss, ee) => McDownloadMenuSaveServer(ss, (dynamic)ee); ((dynamic)sender).Buttons = new[] { btnServer, btnInfo }; } private static void McDownloadMenuBuild(object sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (a, b) => McDownloadMenuSave(a, (dynamic)b); // dynamic! var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (a, b) => McDownloadMenuLog(a, (dynamic)b); // dynamic! var btnServer = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/server", ToolTip = Lang.Text("Download.Version.DownloadServer") }; ToolTipService.SetPlacement(btnServer, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnServer, 30d); ToolTipService.SetHorizontalOffset(btnServer, 2d); btnServer.Click += (a, b) => McDownloadMenuSaveServer(a, (dynamic)b); // dynamic! ((dynamic)sender).Buttons = new[] { btnSave, btnInfo, btnServer }; } private static void McDownloadMenuLog(object sender, RoutedEventArgs e) { JsonNode version; if (((dynamic)sender).Tag is not null) version = (JsonNode)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (JsonNode)((dynamic)sender).Parent.Tag; else version = (JsonNode)((dynamic)sender).Parent.Parent.Tag; McUpdateLogShow(version); } private static void McDownloadMenuSaveServer(object sender, RoutedEventArgs e) { MyListItem version; if (sender is MyListItem) version = (MyListItem)sender; else if (((dynamic)sender).Parent is MyListItem) version = (MyListItem)((dynamic)sender).Parent; else version = (MyListItem)((dynamic)sender).Parent.Parent; try { var id = version.Title; string jsonUrl = ((dynamic)version.Tag)["url"].ToString(); var versionFolder = SystemDialogs.SelectFolder(); if (!versionFolder.Contains(@"\")) return; versionFolder = Path.Combine(versionFolder, id); // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.MinecraftServerDownload", id) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.ServerDownloading"), HintType.Error); return; } var loaders = new List(); // 下载实例 JSON 文件 loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadInstanceJson"), new List { new(ModDownload.DlSourceLauncherOrMetaGet(jsonUrl), Path.Combine(versionFolder, id + ".json"), new ModBase.FileChecker(canUseExistsFile: false, isJson: true)) }) { ProgressWeight = 2d }); // 构建服务端 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.BuildServer"), task => { // 分析服务端 JAR 文件下载地址 var mcInstance = new McInstance(versionFolder); if (mcInstance.JsonObject["downloads"] is null || mcInstance.JsonObject["downloads"]["server"] is null || mcInstance.JsonObject["downloads"]["server"]["url"] is null) { File.Delete(Path.Combine(versionFolder, id + ".json")); if (!new DirectoryInfo(versionFolder).GetFileSystemInfos().Any()) Directory.Delete(versionFolder); task.output = new List(); HintService.Hint(Lang.Text("Minecraft.Download.Error.NoOfficialServerDownload", id), HintType.Error); Thread.Sleep(2000); // 等玩家把上一个提示看完 task.Abort(); return; } var jarUrl = (string)mcInstance.JsonObject["downloads"]["server"]["url"]; var checker = new ModBase.FileChecker(1024L, (long)(mcInstance.JsonObject["downloads"]["server"]["size"] ?? -1), (string)mcInstance.JsonObject["downloads"]["server"]["sha1"]); task.output = new List { new(ModDownload.DlSourceLauncherOrMetaGet(jarUrl), Path.Combine(versionFolder, id + "-server.jar"), checker) }; // 添加启动脚本 var bat = $""" @echo off title {Lang.Text("Minecraft.Download.ServerBatch.Title", id)} echo {Lang.Text("Minecraft.Download.ServerBatch.InstructionJavaPath")} echo {Lang.Text("Minecraft.Download.ServerBatch.InstructionPclSettings")} echo ------------------------------ echo {Lang.Text("Minecraft.Download.ServerBatch.InstructionEula")} echo ------------------------------ "java" -server -XX:+UseG1GC -Xmx4096M -Xms1024M -XX:+UseCompressedOops -jar {id}-server.jar nogui echo ---------------------- echo {Lang.Text("Minecraft.Download.ServerBatch.ServerStopped")} pause """; ModBase.WriteFile(Path.Combine(versionFolder, "Launch Server.bat"), bat.Replace("\n", "\r\n"), encoding: Encoding.Default.Equals(Encoding.UTF8) ? Encoding.UTF8 : Encoding.GetEncoding("GB18030")); // 删除实例 JSON File.Delete(Path.Combine(versionFolder, id + ".json")); }) { ProgressWeight = 0.5d, show = false }); // 下载服务端文件 loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadServerFile"), []) { ProgressWeight = 5d }); // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.MinecraftServerDownload", id), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(id); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 Minecraft 服务端下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } public static void McDownloadMenuSave(object sender, RoutedEventArgs e) { var element = (FrameworkElement)sender; MyListItem version; if (element is MyListItem s1) version = s1; else if (element.Parent is MyListItem s2) version = s2; else version = (MyListItem)((FrameworkElement)element.Parent).Parent; try { var id = version.Title; var jsonUrl = ((JsonObject)version.Tag)["url"]!.ToString(); var versionFolder = SystemDialogs.SelectFolder(); if (!versionFolder.Contains(@"\")) return; versionFolder = Path.Combine(versionFolder, id); // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } var loaders = new List(); // 下载实例 JSON 文件 loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadInstanceJson"), new List { new(ModDownload.DlSourceLauncherOrMetaGet(jsonUrl), Path.Combine(versionFolder, id + ".json"), new ModBase.FileChecker(canUseExistsFile: false, isJson: true)) }) { ProgressWeight = 2d }); // 获取支持库文件地址 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeCoreJarUrl"), task => task.output = new List { ModDownload.DlClientJarGet(new McInstance(versionFolder), false) }) { ProgressWeight = 0.5d, show = false }); // 下载支持库文件 loaders.Add( new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadCoreJar"), new List()) { ProgressWeight = 5d }); // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.MinecraftDownload", id), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(id); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 显示某 Minecraft 版本的更新日志。 /// /// 在 version_manifest.json 中的对应项。 public static void McUpdateLogShow(JsonNode versionJson) { var wikiName = McFormatter.GetWikiUrlSuffix(versionJson["id"].ToString()); var wikiUrl = $"{McFormatter.GetWikiBaseUrl()}/{wikiName.TrimStart('/')}"; ModBase.OpenWebsite(wikiUrl); } #endregion #region OptiFine 下载 public static void McDownloadOptiFine(ModDownload.DlOptiFineListEntry downloadInfo) { try { var id = downloadInfo.NameVersion; var versionFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", id); var isNewVersion = McVersionComparer.CompareVersionGe(downloadInfo.Inherit, "1.14"); var target = isNewVersion ? Path.Combine(ModBase.pathTemp, "Cache", "Code", downloadInfo.NameVersion + "_" + ModBase.GetUuid()) : Path.Combine(ModFolder.mcFolderSelected, "libraries", "optifine", "OptiFine", downloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", ""), downloadInfo.NameFile.Replace("OptiFine_", "OptiFine-").Replace("preview_", "")); // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.OptiFineDownload", downloadInfo.DisplayName) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 已有实例检查 if (File.Exists(Path.Combine(versionFolder, id + ".json"))) { if (ModMain.MyMsgBox( Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists", id, "\r\n"), Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel") ) == 1) { File.Delete(Path.Combine(versionFolder, id + ".jar")); File.Delete(Path.Combine(versionFolder, id + ".json")); } else { return; } } // 启动 var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.OptiFineDownload", downloadInfo.DisplayName), McDownloadOptiFineLoader(downloadInfo)) { OnStateChanged = McInstallState }; loader.Start(versionFolder); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 OptiFine 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } private static void McDownloadOptiFineSave(ModDownload.DlOptiFineListEntry downloadInfo) { try { var id = downloadInfo.NameVersion; var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), downloadInfo.NameFile, "OptiFine Jar (*.jar)|*.jar"); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.OptiFineDownload", downloadInfo.DisplayName) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.OptiFineDownload", downloadInfo.DisplayName), McDownloadOptiFineSaveLoader(downloadInfo, target)) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(downloadInfo); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 OptiFine 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } private static void McDownloadOptiFineInstall(string baseMcFolderHome, string target, ModLoader.LoaderTask, bool> task, bool useJavaWrapper, Version javaVersion) { // 选择 Java JavaEntry java; lock (ModJava.javaLock) { java = ModJava.JavaSelect(Lang.Text("Minecraft.Download.Error.InstallationCanceled"), javaVersion, enforceVersionRange: true); if (java is null) { if (!ModJava.JavaDownloadConfirm(Lang.Text("Minecraft.Download.Error.JavaVersionRequired"))) throw new Exception(Lang.Text("Minecraft.Download.Error.JavaNotFoundInstallCanceled")); // 开始自动下载 var downloadJavaMajor = javaVersion.Major >= 9 ? javaVersion.Major : 8; var javaLoader = ModJava.GetJavaDownloadLoader(); try { javaLoader.Start(downloadJavaMajor, true); while (javaLoader.State == ModBase.LoadState.Loading && !task.IsAborted) Thread.Sleep(10); } finally { javaLoader.Abort(); // 确保取消时中止 Java 下载 } // 检查下载结果 java = ModJava.JavaSelect(Lang.Text("Minecraft.Download.Error.InstallationCanceled"), javaVersion, enforceVersionRange: true); if (task.IsAborted) return; if (java is null) throw new Exception(Lang.Text("Minecraft.Download.Error.JavaNotFoundInstallCanceled")); } } // 添加 Java Wrapper 作为主 Jar string arguments; if (useJavaWrapper && !(dynamic)Config.Launch.DisableJlw) // dynamic! arguments = $"-Doolloo.jlw.tmpdir=\"{ModBase.pathPure.TrimEnd('\\')}\" -Duser.home=\"{baseMcFolderHome.TrimEnd('\\')}\" -cp \"{target}\" -jar \"{ModLaunch.ExtractJavaWrapper()}\" optifine.Installer"; else arguments = $"-Duser.home=\"{baseMcFolderHome.TrimEnd('\\')}\" -cp \"{target}\" optifine.Installer"; if (java.Installation.MajorVersion >= 9) arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " + arguments; // 开始启动 lock (installSyncLock) { var info = new ProcessStartInfo { FileName = java.Installation.JavaExePath, Arguments = arguments, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, WorkingDirectory = ModBase.ShortenPath(baseMcFolderHome) }; if (info.EnvironmentVariables.ContainsKey("appdata")) info.EnvironmentVariables["appdata"] = baseMcFolderHome; else info.EnvironmentVariables.Add("appdata", baseMcFolderHome); ModBase.Log("[Download] 开始安装 OptiFine:" + target); var totalLength = 0; var process = new Process { StartInfo = info }; var lastResult = ""; using (var outputWaitHandle = new AutoResetEvent(false)) { using (var errorWaitHandle = new AutoResetEvent(false)) { process.OutputDataReceived += (_, e) => { try { if (e.Data is null) { outputWaitHandle.Set(); } else { lastResult = e.Data; if (ModBase.modeDebug) ModBase.Log("[Installer] " + lastResult); totalLength += 1; task.Progress += 0.9d / 7000d; } } catch (ObjectDisposedException ex) { } catch (Exception ex) { ModBase.Log(ex, "读取 OptiFine 安装器信息失败"); } try { if (task.State == ModBase.LoadState.Aborted && !process.HasExited) { ModBase.Log("[Installer] 由于任务取消,已中止 OptiFine 安装"); process.Kill(); } } catch { } }; process.ErrorDataReceived += (sender, e) => { try { if (e.Data is null) { errorWaitHandle.Set(); } else { lastResult = e.Data; if (ModBase.modeDebug) ModBase.Log("[Installer] " + lastResult); totalLength += 1; task.Progress += 0.9d / 7000d; } } catch (ObjectDisposedException ex) { } catch (Exception ex) { ModBase.Log(ex, "读取 OptiFine 安装器错误信息失败"); } try { if (task.State == ModBase.LoadState.Aborted && !process.HasExited) { ModBase.Log("[Installer] 由于任务取消,已中止 OptiFine 安装"); process.Kill(); } } catch { } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); // 等待 while (!process.HasExited) Thread.Sleep(10); // 输出 outputWaitHandle.WaitOne(10000); errorWaitHandle.WaitOne(10000); process.Dispose(); if (totalLength < 1000 || lastResult.Contains("at ")) throw new Exception(Lang.Text("Minecraft.Download.Error.InstallerFailedLastLine", lastResult)); } } } } /// /// 获取下载某个 OptiFine 实例的加载器列表。 /// private static List McDownloadOptiFineLoader(ModDownload.DlOptiFineListEntry downloadInfo, string mcFolder = null, ModLoader.LoaderCombo clientDownloadLoader = null, string clientFolder = null, bool fixLibrary = true) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var id = downloadInfo.NameVersion; var versionFolder = Path.Combine(mcFolder, "versions", id); var isNewVersion = downloadInfo.Inherit.Contains("w") || McVersionComparer.CompareVersionGe(downloadInfo.Inherit, "1.14"); var target = isNewVersion ? $"{ModMain.RequestTaskTempFolder()}OptiFine.jar" : $@"{mcFolder}libraries\optifine\OptiFine\{downloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", "")}\{downloadInfo.NameFile.Replace("OptiFine_", "OptiFine-").Replace("preview_", "")}"; var loaders = new List(); var javaVersion = McVersionComparer.CompareVersionGe(downloadInfo.Inherit, "26.1.2") ? new Version(21, 0, 0, 0) : new Version(1, 8, 0, 0); // 获取下载地址 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainOptiFineUrl"), task => { // 启动依赖实例的下载 if (clientDownloadLoader is null) { if (isCustomFolder) throw new Exception(Lang.Text("Minecraft.Download.Error.NoVanillaLoaderSpecified")); clientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, downloadInfo.Inherit); } task.Progress = 0.1d; var sources = new List(); // BMCLAPI 源 var bmclapiInherit = downloadInfo.Inherit; if (bmclapiInherit == "1.8" || bmclapiInherit == "1.9") bmclapiInherit += ".0"; // #4281 if (downloadInfo.IsPreview) sources.Add("https://bmclapi2.bangbang93.com/optifine/" + bmclapiInherit + "/HD_U_" + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "").Replace(" ", "/")); else sources.Add("https://bmclapi2.bangbang93.com/optifine/" + bmclapiInherit + "/HD_U/" + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "")); // 官方源 string pageData; try { using (var resp = HttpRequest .Create("https://optifine.net/adloadx?f=" + downloadInfo.NameFile) .WithHeader("Accept", "text/html") .WithHeader("Accept-Language", "en-US,en;q=0.5") .WithHeader("X-Requested-With", "XMLHttpRequest") .SendAsync() .GetAwaiter() .GetResult()) { resp.EnsureSuccessStatusCode(); pageData = resp.AsString(); } task.Progress = 0.8d; sources.Add("https://optifine.net/" + pageData.RegexSearch(@"downloadx\?f=[^""']+")[0]); ModBase.Log("[Download] OptiFine " + downloadInfo.DisplayName + " 官方下载地址:" + sources.Last()); } catch (Exception ex) { ModBase.Log(ex, "获取 OptiFine " + downloadInfo.DisplayName + " 官方下载地址失败"); } // 构造文件请求 task.output = new List { new(sources.ToArray(), target, new ModBase.FileChecker(300 * 1024)) }; }) { ProgressWeight = 8d }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadOptiFineMainFile"), []) { ProgressWeight = 8d }); loaders.Add(new ModLoader.LoaderTask, bool>( Lang.Text("Minecraft.Download.Stage.WaitVanillaDownload"), task => { // 等待原版文件下载完成 if (clientDownloadLoader is null) return; var targetLoaders = clientDownloadLoader.GetLoaderList() .Where(l => (l.name ?? "") == mcDownloadClientLibName || (l.name ?? "") == mcDownloadClientJsonName) .Where(l => l.State != ModBase.LoadState.Finished).ToList(); if (targetLoaders.Any()) ModBase.Log("[Download] OptiFine 安装正在等待原版文件下载完成"); while (targetLoaders.Any() && !task.IsAborted) { targetLoaders = targetLoaders.Where(l => l.State != ModBase.LoadState.Finished).ToList(); Thread.Sleep(50); } if (task.IsAborted) return; // 拷贝原版文件 if (!isCustomFolder) return; lock (vanillaSyncLock) { var clientName = ModBase.GetFolderNameFromPath(clientFolder); Directory.CreateDirectory(Path.Combine(mcFolder, "versions", downloadInfo.Inherit)); if (!File.Exists(Path.Combine(mcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".json"))) ModBase.CopyFile($"{clientFolder}{clientName}.json", $@"{mcFolder}versions\{downloadInfo.Inherit}\{downloadInfo.Inherit}.json"); if (!File.Exists(Path.Combine(mcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".jar"))) ModBase.CopyFile($"{clientFolder}{clientName}.jar", $@"{mcFolder}versions\{downloadInfo.Inherit}\{downloadInfo.Inherit}.jar"); } }) { ProgressWeight = 0.1d, show = false }); // 安装(新旧方式均需要原版 Jar 和 Json) if (isNewVersion) { ModBase.Log("[Download] 检测为新版 OptiFine:" + downloadInfo.Inherit); loaders.Add(new ModLoader.LoaderTask, bool>( Lang.Text("Minecraft.Download.Stage.InstallOptiFine.MethodA"), task => { var baseMcFolderHome = ModMain.RequestTaskTempFolder(); var baseMcFolder = Path.Combine(baseMcFolderHome, ".minecraft"); try { // 准备安装环境 if (Directory.Exists(Path.Combine(baseMcFolder, "versions", downloadInfo.Inherit))) ModBase.DeleteDirectory(Path.Combine(baseMcFolder, "versions", downloadInfo.Inherit)); Directory.CreateDirectory(Path.Combine(baseMcFolder, "versions", downloadInfo.Inherit)); ModFolder.McFolderLauncherProfilesJsonCreate(baseMcFolder); ModBase.CopyFile( Path.Combine(mcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".json"), Path.Combine(baseMcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".json")); ModBase.CopyFile( Path.Combine(mcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".jar"), Path.Combine(baseMcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".jar")); task.Progress = 0.06d; // 进行安装 var useJavaWrapper = ModBase.IsUtf8CodePage(); Retry: ; try { McDownloadOptiFineInstall(baseMcFolderHome, target, task, useJavaWrapper, javaVersion); } catch (Exception ex) { if (!useJavaWrapper) { ModBase.Log(ex, "不使用 JavaWrapper 安装 OptiFine 失败,将使用 JavaWrapper 并重试"); useJavaWrapper = true; goto Retry; } throw new Exception(Lang.Text("Minecraft.Download.Error.OptiFineInstallerRunFailed"), ex); } task.Progress = 0.96d; // 复制文件 File.Delete(Path.Combine(baseMcFolder, "launcher_profiles.json")); ModBase.CopyDirectory(baseMcFolder, mcFolder); task.Progress = 0.98d; // 清理文件 File.Delete(target); ModBase.DeleteDirectory(baseMcFolderHome); } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.OptiFineInstallFailed.MethodA"), ex); } }) { ProgressWeight = 8d }); } else { ModBase.Log("[Download] 检测为旧版 OptiFine:" + downloadInfo.Inherit); // 新建实例文件夹 // 复制 Jar 文件 // 建立 Json 文件 loaders.Add(new ModLoader.LoaderTask, bool>( Lang.Text("Minecraft.Download.Stage.InstallOptiFine.MethodB"), task => { try { Directory.CreateDirectory(versionFolder); task.Progress = 0.1d; if (File.Exists(Path.Combine(versionFolder, id + ".jar"))) File.Delete(Path.Combine(versionFolder, id + ".jar")); ModBase.CopyFile( Path.Combine(mcFolder, "versions", downloadInfo.Inherit, downloadInfo.Inherit + ".jar"), Path.Combine(versionFolder, id + ".jar")); task.Progress = 0.7d; var inheritInstance = new McInstance(Path.Combine(mcFolder, "versions", downloadInfo.Inherit)); var json = @"{ ""id"": """ + id + @""", ""inheritsFrom"": """ + downloadInfo.Inherit + @""", ""time"": """ + (string.IsNullOrEmpty(downloadInfo.ReleaseTime) ? inheritInstance.releaseTime.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture) : downloadInfo.ReleaseTime.Replace("/", "-")) + @"T23:33:33+08:00"", ""releaseTime"": """ + (string.IsNullOrEmpty(downloadInfo.ReleaseTime) ? inheritInstance.releaseTime.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture) : downloadInfo.ReleaseTime.Replace("/", "-")) + @"T23:33:33+08:00"", ""type"": ""release"", ""libraries"": [ {""name"": ""optifine:OptiFine:" + downloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "") .Replace("preview_", "") + // 输出旧版 Json 格式 @"""}, {""name"": ""net.minecraft:launchwrapper:1.12""} ], ""mainClass"": ""net.minecraft.launchwrapper.Launch"","; task.Progress = 0.8d; if (inheritInstance.IsOldJson) json += @" ""minimumLauncherVersion"": 18, ""minecraftArguments"": """ + inheritInstance.JsonObject["minecraftArguments"] + // 输出新版 Json 格式 @" --tweakClass optifine.OptiFineTweaker"" }"; else json += @" ""minimumLauncherVersion"": ""21"", ""arguments"": { ""game"": [ ""--tweakClass"", ""optifine.OptiFineTweaker"" ] } }"; ModBase.WriteFile(Path.Combine(versionFolder, id + ".json"), json); } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.OptiFineInstallFailed.MethodB"), ex); } }) { ProgressWeight = 1d }); } // 下载支持库 if (fixLibrary) { loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeOptiFineLibraries"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 1d, show = false }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadOptiFineLibraries"), new List()) { ProgressWeight = 4d }); } return loaders; } /// /// 获取保存某个 OptiFine 版本的加载器列表。 /// private static List McDownloadOptiFineSaveLoader(ModDownload.DlOptiFineListEntry downloadInfo, string targetFolder) { var loaders = new List(); // 获取下载地址 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainOptiFineDownloadUrl"), task => { var sources = new List(); // BMCLAPI 源 var bmclapiInherit = downloadInfo.Inherit; if (bmclapiInherit == "1.8" || bmclapiInherit == "1.9") bmclapiInherit += ".0"; // #4281 if (downloadInfo.IsPreview) sources.Add("https://bmclapi2.bangbang93.com/optifine/" + bmclapiInherit + "/HD_U_" + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "").Replace(" ", "/")); else sources.Add("https://bmclapi2.bangbang93.com/optifine/" + bmclapiInherit + "/HD_U/" + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "")); // 官方源 string pageData; try { using (var resp = HttpRequest .Create("https://optifine.net/adloadx?f=" + downloadInfo.NameFile) .WithHeader("Accept", "text/html") .WithHeader("Accept-Language", "en-US,en;q=0.5") .WithHeader("X-Requested-With", "XMLHttpRequest") .SendAsync().GetAwaiter().GetResult()) { resp.EnsureSuccessStatusCode(); pageData = resp.AsString(); } task.Progress = 0.8d; sources.Add("https://optifine.net/" + pageData.RegexSearch(@"downloadx\?f=[^""']+")[0]); ModBase.Log("[Download] OptiFine " + downloadInfo.DisplayName + " 官方下载地址:" + sources.Last()); } catch (Exception ex) { ModBase.Log(ex, "获取 OptiFine " + downloadInfo.DisplayName + " 官方下载地址失败"); } task.Progress = 0.9d; // 构造文件请求 task.output = new List { new(sources.ToArray(), targetFolder, new ModBase.FileChecker(64 * 1024)) }; }) { ProgressWeight = 6d }); // 下载 loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadOptiFineMainFile"), new List()) { ProgressWeight = 10d, block = true }); return loaders; } #endregion #region OptiFine 下载菜单 public static MyListItem OptiFineDownloadListItem(ModDownload.DlOptiFineListEntry entry, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { var infoParts = new List { entry.IsPreview ? Lang.Text("Download.Version.Type.Preview") : Lang.Text("Download.Version.Type.Release") }; if (!string.IsNullOrEmpty(entry.ReleaseTime)) infoParts.Add(Lang.Text("Download.Version.ReleaseDate", entry.ReleaseTime)); if (entry.RequiredForgeVersion is null) infoParts.Add(Lang.Text("Download.Version.Optifine.IncompatibleForge")); else if (!string.IsNullOrEmpty(entry.RequiredForgeVersion)) infoParts.Add(Lang.Text("Download.Version.Optifine.CompatibleForge", entry.RequiredForgeVersion)); var newItem = new MyListItem { Title = entry.DisplayName, SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = string.Join(" | ", infoParts), Logo = ModBase.pathImage + "Blocks/GrassPath.png" }; newItem.Click += onClick; // 建立菜单 newItem.ContentHandler = isSaveOnly ? OptiFineSaveContMenuBuild : OptiFineContMenuBuild; // 结束 return newItem; } private static void OptiFineSaveContMenuBuild(object sender, EventArgs e) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (sender, e) => OptiFineLog_Click(sender, (RoutedEventArgs)e); ((dynamic)sender).Buttons = new[] { btnInfo }; } private static void OptiFineContMenuBuild(object sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); //btnSave.Click += () ModDownloadLib.OptiFineSave_Click; var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (sender, e) => OptiFineLog_Click(sender, (RoutedEventArgs)e); ((dynamic)sender).Buttons = new[] { btnSave, btnInfo }; } private static void OptiFineLog_Click(object sender, RoutedEventArgs e) { ModDownload.DlOptiFineListEntry version; if (((dynamic)sender).Tag is not null) version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Tag; else version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Parent.Tag; ModBase.OpenWebsite("https://optifine.net/changelog?f=" + version.NameFile); } public static void OptiFineSave_Click(object sender, RoutedEventArgs e) { ModDownload.DlOptiFineListEntry version; if (((dynamic)sender).Tag is not null) version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Tag; else version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Parent.Tag; McDownloadOptiFineSave(version); } #endregion #region LiteLoader 下载 public static void McDownloadLiteLoader(ModDownload.DlLiteLoaderListEntry downloadInfo) { try { var id = downloadInfo.Inherit; var target = Path.Combine(ModBase.pathTemp, "Download", id + "-Liteloader.jar"); var versionName = downloadInfo.Inherit + "-LiteLoader"; var versionFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", versionName); // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.LiteLoaderDownload", id) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 已有实例检查 if (File.Exists(Path.Combine(versionFolder, versionName + ".json"))) { if (ModMain.MyMsgBox( Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists", versionName, "\r\n"), Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel") ) == 1) { File.Delete(Path.Combine(versionFolder, versionName + ".jar")); File.Delete(Path.Combine(versionFolder, versionName + ".json")); } else { return; } } // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.LiteLoaderDownload", id), McDownloadLiteLoaderLoader(downloadInfo)) { OnStateChanged = McInstallState }; loader.Start(versionFolder); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 LiteLoader 下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } private static void McDownloadLiteLoaderSave(ModDownload.DlLiteLoaderListEntry downloadInfo) { try { var id = downloadInfo.Inherit; var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), downloadInfo.FileName.Replace("-SNAPSHOT", ""), Lang.Text("Minecraft.Download.Stage.ForgelikeInstallerFilter", "LiteLoader", "jar")); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.LiteLoaderDownload", id) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 构造步骤加载器 var loaders = new List(); // 下载 var address = new List(); if (downloadInfo.IsLegacy) // 老版本 switch (downloadInfo.Inherit ?? "") { case "1.7.10": { address.Add("https://dl.liteloader.com/redist/1.7.10/liteloader-installer-1.7.10-04.jar"); break; } case "1.7.2": { address.Add("https://dl.liteloader.com/redist/1.7.2/liteloader-installer-1.7.2-04.jar"); break; } case "1.6.4": { address.Add("https://dl.liteloader.com/redist/1.6.4/liteloader-installer-1.6.4-01.jar"); break; } case "1.6.2": { address.Add("https://dl.liteloader.com/redist/1.6.2/liteloader-installer-1.6.2-04.jar"); break; } case "1.5.2": { address.Add("https://dl.liteloader.com/redist/1.5.2/liteloader-installer-1.5.2-01.jar"); break; } default: { throw new NotSupportedException(Lang.Text("Minecraft.Download.Error.UnknownMinecraftVersion", downloadInfo.Inherit)); } } else // 官方源 address.Add("http://jenkins.liteloader.com/job/LiteLoaderInstaller%20" + downloadInfo.Inherit + "/lastSuccessfulBuild/artifact/" + (downloadInfo.Inherit == "1.8" ? "ant/dist/" : "build/libs/") + downloadInfo.FileName); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), new List { new(address.ToArray(), target, new ModBase.FileChecker(1024 * 1024)) }) { ProgressWeight = 15d }); // 启动 var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LiteLoaderInstallerDownload", id), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(downloadInfo); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 LiteLoader 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 获取下载某个 LiteLoader 实例的加载器列表。 /// private static List McDownloadLiteLoaderLoader(ModDownload.DlLiteLoaderListEntry downloadInfo, string mcFolder = null, ModLoader.LoaderCombo clientDownloadLoader = null, bool fixLibrary = true) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var id = downloadInfo.Inherit; var target = Path.Combine(ModBase.pathTemp, "Download", id + "-Liteloader.jar"); var versionName = downloadInfo.Inherit + "-LiteLoader"; var versionFolder = Path.Combine(mcFolder, "versions", versionName); var loaders = new List(); // 启动依赖实例的下载 if (clientDownloadLoader is null) loaders.Add(new ModLoader.LoaderTask( Lang.Text("Minecraft.Download.Stage.StartLiteLoaderDependencyDownload"), _ => { if (isCustomFolder) throw new Exception(Lang.Text("Minecraft.Download.Error.NoVanillaLoaderSpecified")); clientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, downloadInfo.Inherit); }) { ProgressWeight = 0.2d, show = false, block = false }); // 安装 // 新建实例文件夹 // 构造实例 Json // 输出 Json 文件 loaders.Add(new ModLoader.LoaderTask(Lang.Text("Minecraft.Download.Stage.InstallLiteLoader"), _ => { try { Directory.CreateDirectory(versionFolder); var versionJson = new JsonObject(); versionJson.Add("id", versionName); var releaseDate = DateTime.Parse(downloadInfo.ReleaseTime, Lang.Culture); versionJson.Add("time", releaseDate); versionJson.Add("releaseTime", releaseDate); versionJson.Add("type", "release"); versionJson.Add("arguments", (JsonNode)ModBase.GetJson("{\"game\":[\"--tweakClass\",\"" + downloadInfo.jsonToken["tweakClass"] + "\"]}")); versionJson.Add("libraries", downloadInfo.jsonToken["libraries"]?.DeepClone()); versionJson["libraries"].AsArray().Add(ModBase.GetJson("{\"name\": \"com.mumfrey:liteloader:" + downloadInfo.jsonToken["version"] + "\",\"url\": \"https://dl.liteloader.com/versions/\"}")); versionJson.Add("mainClass", "net.minecraft.launchwrapper.Launch"); versionJson.Add("minimumLauncherVersion", 18); versionJson.Add("inheritsFrom", downloadInfo.Inherit); versionJson.Add("jar", downloadInfo.Inherit); ModBase.WriteFile(Path.Combine(versionFolder, versionName + ".json"), versionJson.ToString()); } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.LiteLoaderInstallFailed"), ex); } }) { ProgressWeight = 1d }); // 下载支持库 if (fixLibrary) { loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeLiteLoaderLibraries"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 1d, show = false }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLiteLoaderLibraries"), new List()) { ProgressWeight = 6d }); } return loaders; } #endregion #region LiteLoader 下载菜单 public static MyListItem LiteLoaderDownloadListItem(ModDownload.DlLiteLoaderListEntry entry, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { var infoParts = new List { entry.IsPreview ? Lang.Text("Download.Version.Type.Preview") : Lang.Text("Download.Version.Type.Stable") }; if (!string.IsNullOrEmpty(entry.ReleaseTime)) infoParts.Add(Lang.Text("Download.Version.ReleaseDate", entry.ReleaseTime)); // 建立控件 var newItem = new MyListItem { Title = entry.Inherit, SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = string.Join(" | ", infoParts), Logo = ModBase.pathImage + "Blocks/Egg.png" }; newItem.Click += onClick; // 建立菜单 newItem.ContentHandler = isSaveOnly ? LiteLoaderSaveContMenuBuild : LiteLoaderContMenuBuild; // 结束 return newItem; } private static void LiteLoaderSaveContMenuBuild(MyListItem sender, EventArgs e) { if ((bool)((dynamic)sender.Tag).IsLegacy) { sender.Buttons = Array.Empty(); } else { var btnList = new MyIconButton { SvgIcon = "lucide/list", ToolTip = Lang.Text("Download.Version.ViewAllVersions"), Tag = sender }; ToolTipService.SetPlacement(btnList, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnList, 30d); ToolTipService.SetHorizontalOffset(btnList, 2d); btnList.Click += (sender, e) => LiteLoaderAll_Click(sender, (RoutedEventArgs)e); sender.Buttons = new[] { btnList }; } } private static void LiteLoaderContMenuBuild(MyListItem sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveInstaller"), Tag = sender }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (sender, e) => LiteLoaderSave_Click(sender, (RoutedEventArgs)e); if ((bool)((dynamic)sender.Tag).IsLegacy) { sender.Buttons = [btnSave]; } else { var btnList = new MyIconButton { SvgIcon = "lucide/list", ToolTip = Lang.Text("Download.Version.ViewAllVersions"), Tag = sender }; ToolTipService.SetPlacement(btnList, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnList, 30d); ToolTipService.SetHorizontalOffset(btnList, 2d); btnList.Click += (sender, e) => LiteLoaderAll_Click(sender, (RoutedEventArgs)e); sender.Buttons = [btnSave, btnList]; } } private static void LiteLoaderAll_Click(object sender, RoutedEventArgs e) { ModDownload.DlLiteLoaderListEntry version; if (((dynamic)sender).Tag is ModDownload.DlLiteLoaderListEntry) version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag; else version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag.Tag; ModBase.OpenWebsite("https://jenkins.liteloader.com/view/" + version.Inherit); } public static void LiteLoaderSave_Click(object sender, RoutedEventArgs e) { // ListItem 与小按钮都会调用这个方法 ModDownload.DlLiteLoaderListEntry version; if (((dynamic)sender).Tag is ModDownload.DlLiteLoaderListEntry) version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag; else version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag.Tag; McDownloadLiteLoaderSave(version); } #endregion #region Forgelike 下载 public static void McDownloadForgelikeSave(ModDownload.DlForgelikeEntry info) { try { var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), $"{info.LoaderName}-{info.Inherit}-{info.VersionName}.{info.FileExtension}", Lang.Text("Minecraft.Download.Stage.ForgelikeInstallerFilter", info.LoaderName, info.FileExtension)); var displayName = $"{info.LoaderName} {info.Inherit} - {info.VersionName}"; if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.ForgelikeDownload", displayName) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 获取下载地址 var files = new List(); if (info.forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge) { // NeoForge var neo = (ModDownload.DlNeoForgeListEntry)info; var url = neo.UrlBase + "-installer.jar"; files.Add(new DownloadFile( new[] { url.Replace("maven.neoforged.net/releases", "bmclapi2.bangbang93.com/maven"), url }, target, new ModBase.FileChecker(64 * 1024))); } else if (info.forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Cleanroom) { // Cleanroom var clr = (ModDownload.DlCleanroomListEntry)info; var url = clr.UrlBase + "-installer.jar"; files.Add(new DownloadFile(new[] { url }, target, new ModBase.FileChecker(64 * 1024))); } else { // Forge var forge = (ModDownload.DlForgeVersionEntry)info; files.Add(new DownloadFile( new[] { $"https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{forge.Inherit}-{forge.FileVersion}/forge-{forge.Inherit}-{forge.FileVersion}-{forge.Category}.{forge.FileExtension}", $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{forge.Inherit}-{forge.FileVersion}/forge-{forge.Inherit}-{forge.FileVersion}-{forge.Category}.{forge.FileExtension}" }, target, new ModBase.FileChecker(64 * 1024, hash: forge.Hash))); } // 构造加载器 var loaders = new List(); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), files) { ProgressWeight = 6d }); // 启动 var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.ForgelikeDownload", displayName), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(info); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, $"开始 {info.LoaderName} 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } private static void ForgelikeInjector(string target, ModLoader.LoaderTask task, string mcFolder, bool useJavaWrapper, ModDownload.DlForgelikeEntry.ForgelikeType forgeType) { // 选择 Java JavaEntry java; lock (ModJava.javaLock) { java = ModJava.JavaSelect(Lang.Text("Minecraft.Download.Error.InstallationCanceled"), new Version(1, 8, 0, 60)); if (java is null) { if (!ModJava.JavaDownloadConfirm(Lang.Text("Minecraft.Download.Error.JavaVersionRequired"))) throw new Exception(Lang.Text("Minecraft.Download.Error.JavaNotFoundInstallCanceled")); // 开始自动下载 var javaLoader = ModJava.GetJavaDownloadLoader(); try { javaLoader.Start(17, true); while (javaLoader.State == ModBase.LoadState.Loading && !task.IsAborted) Thread.Sleep(10); } finally { javaLoader.Abort(); // 确保取消时中止 Java 下载 } // 检查下载结果 java = ModJava.JavaSelect(Lang.Text("Minecraft.Download.Error.InstallationCanceled"), new Version(1, 8, 0, 60)); if (task.IsAborted) return; if (java is null) throw new Exception(Lang.Text("Minecraft.Download.Error.JavaNotFoundInstallCanceled")); } } // 添加 Java Wrapper 作为主 Jar string arguments; if (useJavaWrapper && !Config.Launch.DisableJlw) arguments = $@"-Doolloo.jlw.tmpdir=""{ModBase.pathPure.TrimEnd('\\')}"" -cp ""{ModBase.pathTemp}Cache\forge_installer.jar;{target}"" -jar ""{ModLaunch.ExtractJavaWrapper()}"" com.bangbang93.ForgeInstaller ""{mcFolder}"; else arguments = $@"-cp ""{ModBase.pathTemp}Cache\forge_installer.jar;{target}"" com.bangbang93.ForgeInstaller ""{mcFolder}"; if (java.Installation.MajorVersion >= 9) arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " + arguments; // 开始启动 lock (installSyncLock) { var info = new ProcessStartInfo { FileName = java.Installation.JavaExePath, Arguments = arguments, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true }; string loaderName = ModBase.GetStringFromEnum(forgeType); ModBase.Log($"[Download] 开始安装 {loaderName}:" + arguments); var process = new Process { StartInfo = info }; var lastResults = new Queue(); using (var outputWaitHandle = new AutoResetEvent(false)) { using (var errorWaitHandle = new AutoResetEvent(false)) { process.OutputDataReceived += (sender, e) => { try { if (e.Data is null) { outputWaitHandle.Set(); } else { lastResults.Enqueue(e.Data); if (lastResults.Count > 100) lastResults.Dequeue(); ForgelikeInjectorLine(e.Data, task); } } catch (ObjectDisposedException ex) { } catch (Exception ex) { ModBase.Log(ex, $"读取 {loaderName} 安装器信息失败"); } try { if (task.State == ModBase.LoadState.Aborted && !process.HasExited) { ModBase.Log($"[Installer] 由于任务取消,已中止 {loaderName} 安装"); process.Kill(); } } catch { } }; process.ErrorDataReceived += (sender, e) => { try { if (e.Data is null) { errorWaitHandle.Set(); } else { lastResults.Enqueue(e.Data); if (lastResults.Count > 100) lastResults.Dequeue(); ForgelikeInjectorLine(e.Data, task); } } catch (ObjectDisposedException ex) { } catch (Exception ex) { ModBase.Log(ex, $"读取 {loaderName} 安装器错误信息失败"); } try { if (task.State == ModBase.LoadState.Aborted && !process.HasExited) { ModBase.Log($"[Installer] 由于任务取消,已中止 {loaderName} 安装"); process.Kill(); } } catch { } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); // 等待 while (!process.HasExited) Thread.Sleep(10); // 输出 outputWaitHandle.WaitOne(10000); errorWaitHandle.WaitOne(10000); process.Dispose(); // 检查是否安装成功:最后 5 行中是否有 true(true 可能在倒数数行,见 #832) if (lastResults.Reverse().Take(5).Any(l => l == "true")) return; ModBase.Log(lastResults.Join("\r\n")); var lastLines = ""; for (int i = Math.Max(0, lastResults.Count - 5), loopTo = lastResults.Count - 1; i <= loopTo; i++) // 最后 5 行 lastLines += "\r\n" + lastResults.ElementAtOrDefault(i); throw new Exception(Lang.Text("Minecraft.Download.Error.LoaderInstallerFailedLastLine", loaderName, lastLines)); } } } } private static void ForgelikeInjectorLine(string content, ModLoader.LoaderTask task) { switch (content ?? "") { case "Extracting json": { ModBase.Log("[Installer] " + content); task.Progress = 0.07d; break; } case "Downloading libraries": { ModBase.Log("[Installer] " + content); task.Progress = 0.08d; break; } case " File exists: Checksum validated.": { if (ModBase.modeDebug) ModBase.Log("[Installer] " + content); task.Progress += 0.003d; break; } case "Building Processors": { task.Progress = 0.18d; break; } case "Task: DOWNLOAD_MOJMAPS": // B { task.Progress = 0.2d; break; } case "Task: MERGE_MAPPING": // B { task.Progress = 0.3d; break; } case "Splitting: ": { task.Progress = 0.35d; break; } case "Parameter Annotations": // B { task.Progress = 0.4d; break; } case "Processing Complete": // B { task.Progress = 0.5d; break; } case "log: null": // new { task.Progress = 0.5d; break; } case "Sorting": // new { task.Progress = 0.65d; break; } case "Remapping final jar": // A { task.Progress = 0.72d; break; } case "Remapping jar... 50%": // A { task.Progress = 0.76d; break; } case "Remapping jar... 100%": // A { task.Progress = 0.81d; break; } case "Injecting profile": { task.Progress = 0.91d; break; } default: { if (ModBase.modeDebug) ModBase.Log("[Installer] " + content); return; } } ModBase.Log("[Installer] " + content); } /// /// 获取下载某个 Forgelike 实例的加载器列表。 /// private static List McDownloadForgelikeLoader(ModDownload.DlForgelikeEntry.ForgelikeType forgeType, string loaderVersion, string targetVersion, string inherit, ModDownload.DlForgelikeEntry info = null, string mcFolder = null, ModLoader.LoaderCombo clientDownloadLoader = null, string clientFolder = null) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; if (forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge && info is null) { // 需要传入 API Name,但整合包版本可能不以 1.20.1- 开头,所以需要进行特别处理 if (inherit == "1.20.1" && !loaderVersion.StartsWithF("1.20.1-")) info = new ModDownload.DlNeoForgeListEntry("1.20.1-" + loaderVersion); else info = new ModDownload.DlNeoForgeListEntry(loaderVersion); } if (forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Cleanroom && info is null) info = new ModDownload.DlCleanroomListEntry(loaderVersion); if (forgeType != ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge && loaderVersion.StartsWithF("1.") && loaderVersion.Contains("-")) { // 类似 1.19.3-41.2.8 格式,优先使用 Version 中要求的版本而非 Inherit(例如 1.19.3 却使用了 1.19 的 Forge) inherit = loaderVersion.BeforeFirst("-"); loaderVersion = loaderVersion.AfterLast("-"); } string loaderName = ModBase.GetStringFromEnum(forgeType); var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var installerAddress = ModMain.RequestTaskTempFolder() + "forge_installer.jar"; var versionFolder = $@"{mcFolder}versions\{targetVersion}\"; var displayName = $"{loaderName} {inherit} - {loaderVersion}"; var loaders = new List(); var libVersionFolder = $@"{ModFolder.mcFolderSelected}versions\{targetVersion}\"; // 作为 Lib 文件目标的实例文件夹 // 获取 Forge 下载信息 if (info is null) loaders.Add(new ModLoader.LoaderTask( Lang.Text("Minecraft.Download.Stage.ObtainLoaderDetails", loaderName), task => { // 获取 Forge 对应 MC 版本列表 var forgeLoader = new ModLoader.LoaderTask>( "McDownloadForgeLoader " + inherit, ModDownload.DlForgeVersionMain); forgeLoader.WaitForExit(inherit); task.Progress = 0.8d; // 查找对应版本 foreach (var ForgeVersion in forgeLoader.output) if (McVersionComparer.CompareVersion(ForgeVersion.version.ToString(), loaderVersion) == 0) { info = ForgeVersion; return; } throw new Exception(Lang.Text("Minecraft.Download.Error.LoaderDetailsNotFound", loaderName, inherit, loaderVersion)); }) { ProgressWeight = 3d }); // 下载 Forgelike 主文件 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.PrepareLoaderDownload", loaderName), task => { // 启动依赖实例的下载 if (clientDownloadLoader is null) { if (isCustomFolder) throw new Exception(Lang.Text("Minecraft.Download.Error.NoVanillaLoaderSpecified")); clientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, inherit); } // 添加主文件下载 var files = new List(); if (info.forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge) { // NeoForge var neo = (ModDownload.DlNeoForgeListEntry)info; var url = neo.UrlBase + "-installer.jar"; files.Add(new DownloadFile( new[] { url.Replace("maven.neoforged.net/releases", "bmclapi2.bangbang93.com/maven"), url }, installerAddress, new ModBase.FileChecker(64 * 1024))); } else if (info.forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Cleanroom) { // Cleanroom var clr = (ModDownload.DlCleanroomListEntry)info; var url = clr.UrlBase + "-installer.jar"; files.Add(new DownloadFile(new[] { url }, installerAddress, new ModBase.FileChecker(64 * 1024))); } else { // Forge var forge = (ModDownload.DlForgeVersionEntry)info; var fileName = $"{forge.Inherit.Replace("-", "_")}-{forge.FileVersion}/forge-{forge.Inherit.Replace("-", "_")}-{forge.FileVersion}-{forge.Category}.{forge.FileExtension}"; files.Add(new DownloadFile( new[] { $"https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{fileName}", $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{fileName}" }, installerAddress, new ModBase.FileChecker(64 * 1024, hash: forge.Hash))); } task.output = files; }) { ProgressWeight = 0.5d, show = false }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderMainFile", loaderName), new List()) { ProgressWeight = 9d }); // 安装(仅在新版安装时需要原版 Jar) if (forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge || Convert.ToDouble(loaderVersion.BeforeFirst(".")) >= 20d) { ModBase.Log($"[Download] 检测为{(forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? "新版 Forge" : " " + forgeType)}:" + loaderVersion); List libs = null; loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeLoaderLibraries", loaderName), task => { task.output = new List(); ZipArchive installer = null; try { // 解压并获取、合并两个 Json 的信息 ModBase.WaitForFileReady(installerAddress); installer = new ZipArchive(new FileStream(installerAddress, FileMode.Open)); task.Progress = 0.2d; var json = (JsonObject)ModBase.GetJson( ModBase.ReadFile(installer.GetEntry("install_profile.json").Open())); var json2 = (JsonObject)ModBase.GetJson(ModBase.ReadFile(installer.GetEntry("version.json").Open())); json.Merge(json2); // 如果是 1.16.5 就升级一下 Authlib if (inherit == "1.16.5" && (bool)Config.Download.FixAuthLib) json = (JsonObject)ModBase.GetJson(json.ToString() .Replace("2.1.28/authlib-2.1.28.jar", "2.3.31/authlib-2.3.31.jar") .Replace("com.mojang:authlib:2.1.28", "com.mojang:authlib:2.3.31") .Replace("ad54da276bf59983d02d5ed16fc14541354c71fd", "bbd00ca33b052f73a6312254780fc580d2da3535").Replace("76328", "87662")); // 获取 Lib 下载信息 libs = ModLibrary.McLibListGetWithJson(json, true); // 添加 Mappings 下载信息 if (json["data"] is not null && json["data"]["MOJMAPS"] is not null) { // 下载原版 Json 文件 task.Progress = 0.4d; var rawJson = (JsonObject)ModBase.GetJson(ModNet.NetGetCodeByLoader( ModDownload.DlSourceLauncherOrMetaGet( ModDownload.DlClientListGet(inherit)?.ToString()), isJson: true)); // [net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg] var originalName = json["data"]["MOJMAPS"]["client"].ToString().Trim("[]".ToCharArray()) .BeforeFirst("@"); var address = ModLibrary.McLibGet(originalName).Replace(".jar", "-mappings." + json["data"]["MOJMAPS"]["client"].ToString().Trim("[]".ToCharArray()) .Split("@")[1]); var clientMappings = rawJson["downloads"]["client_mappings"]; libs.Add(new ModLibrary.McLibToken { IsNatives = false, LocalPath = address, OriginalName = originalName, Url = (string)clientMappings["url"], size = (long)clientMappings["size"], Sha1 = (string)clientMappings["sha1"] }); ModBase.Log( $"[Download] 需要下载 Mappings:{clientMappings["url"]} (SHA1: {clientMappings["sha1"]})"); } task.Progress = 0.8d; // 去除其中的原始 Forgelike 项 for (int i = 0, loopTo = libs.Count - 1; i <= loopTo; i++) if (libs[i].LocalPath.EndsWithF($"{loaderName.ToLower()}-{inherit}-{loaderVersion}.jar") || libs[i].LocalPath.EndsWithF($"{loaderName.ToLower()}-{inherit}-{loaderVersion}-client.jar")) { ModBase.Log($"[Download] 已从待下载 {loaderName} 支持库中移除:" + libs[i].LocalPath, ModBase.LogLevel.Debug); libs.RemoveAt(i); break; } task.output = ModLibrary.McLibNetFilesFromTokens(libs); } catch (Exception ex) { throw new Exception( Lang.Text("Minecraft.Download.Error.LoaderLibraryListFailed", forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? Lang.Text("Minecraft.Download.Loader.NewForge") : " " + forgeType), ex); } finally { // 释放文件 if (installer is not null) installer.Dispose(); } }) { ProgressWeight = 2d }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderLibraries", loaderName), new List()) { ProgressWeight = 12d }); loaders.Add(new ModLoader.LoaderTask, bool>( Lang.Text("Minecraft.Download.Stage.GetLoaderLibraries", loaderName), task => { #region Forgelike 文件 if (isCustomFolder) foreach (var LibFile in libs) { var realPath = LibFile.LocalPath.Replace(ModFolder.mcFolderSelected, mcFolder); if (!File.Exists(realPath)) { Directory.CreateDirectory(Path.GetDirectoryName(realPath)); ModBase.CopyFile(LibFile.LocalPath, realPath); } if (ModBase.modeDebug) ModBase.Log($"[Download] 复制的 {loaderName} 支持库文件:" + LibFile.LocalPath); } #endregion #region 原版文件 // 等待原版文件下载完成 if (clientDownloadLoader is null) return; var targetLoaders = clientDownloadLoader.GetLoaderList() .Where(l => (l.name ?? "") == mcDownloadClientLibName || (l.name ?? "") == mcDownloadClientJsonName) .Where(l => l.State != ModBase.LoadState.Finished).ToList(); if (targetLoaders.Any()) ModBase.Log($"[Download] {loaderName} 安装正在等待原版文件下载完成"); while (targetLoaders.Any() && !task.IsAborted) { targetLoaders = targetLoaders.Where(l => l.State != ModBase.LoadState.Finished).ToList(); Thread.Sleep(50); } if (task.IsAborted) return; // 拷贝原版文件 if (!isCustomFolder) return; lock (vanillaSyncLock) { var clientName = ModBase.GetFolderNameFromPath(clientFolder); Directory.CreateDirectory(Path.Combine(mcFolder, "versions", inherit)); if (!File.Exists(Path.Combine(mcFolder, "versions", inherit, inherit + ".json"))) ModBase.CopyFile(Path.Combine(clientFolder, clientName + ".json"), Path.Combine(mcFolder, "versions", inherit, inherit + ".json")); if (!File.Exists(Path.Combine(mcFolder, "versions", inherit, inherit + ".jar"))) ModBase.CopyFile(Path.Combine(clientFolder, clientName + ".jar"), Path.Combine(mcFolder, "versions", inherit, inherit + ".jar")); } #endregion }) { ProgressWeight = 0.1d, show = false }); loaders.Add(new ModLoader.LoaderTask( forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? Lang.Text("Minecraft.Download.Stage.InstallForge.MethodA") : Lang.Text("Minecraft.Download.Stage.InstallForgeType", forgeType), task => { ModBase.WaitForFileReady(installerAddress); var installer = new ZipArchive(new FileStream(installerAddress, FileMode.Open)); try { // 记录当前文件夹列表(在新建目标文件夹之前) ModBase.Log("[Download] 开始进行 Forgelike 安装:" + installerAddress); // 解压并获取信息 var oldList = new DirectoryInfo(mcFolder + "versions/") .EnumerateDirectories().Select(i => i.FullName).ToList(); // 新建目标实例文件夹 var json = ModBase.GetJson(ModBase.ReadFile(installer.GetEntry("install_profile.json").Open())); Directory.CreateDirectory(versionFolder); task.Progress = 0.04d; // 释放 launcher_installer.json ModFolder.McFolderLauncherProfilesJsonCreate(mcFolder); task.Progress = 0.05d; // 运行 Forge 安装器 var useJavaWrapper = ModBase.IsUtf8CodePage(); Retry: try { // 释放 Forge 注入器 ModBase.WriteFile(Path.Combine(ModBase.pathTemp, "Cache", "forge_installer.jar"), ModBase.GetResourceStream("Resources/forge-installer.jar")); task.Progress = 0.06d; // 运行注入器 ForgelikeInjector(installerAddress, task, mcFolder, useJavaWrapper, forgeType); task.Progress = 0.97d; } catch (Exception ex) { if (!useJavaWrapper) { ModBase.Log(ex, $"不使用 JavaWrapper 安装 {loaderName} 失败,将使用 JavaWrapper 并重试"); useJavaWrapper = true; goto Retry; } throw new Exception( Lang.Text("Minecraft.Download.Error.LoaderInstallerRunFailed", loaderName), ex); // 拷贝新增的实例 Json } var deltaList = new DirectoryInfo(mcFolder + "versions/").EnumerateDirectories() .SkipWhile(i => oldList.Contains(i.FullName)).ToList(); if (deltaList.Count > 1) // 它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151) // 也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空 deltaList = deltaList .Where(l => l.Name.ContainsF("forge", true) && l.EnumerateFiles().Any()) .ToList(); // 如果没有新增文件夹,那么预测的文件夹名就是正确的 // 如果只新增 1 个文件夹,那么拷贝 Json 文件 if (deltaList.Count == 1) { var jsonFile = deltaList[0].EnumerateFiles().First(); ModBase.WriteFile(Path.Combine(versionFolder, targetVersion + ".json"), ModBase.ReadFile(jsonFile.FullName)); ModBase.Log( $"[Download] 已拷贝新增的实例 Json 文件:{jsonFile.FullName} -> {versionFolder}{targetVersion}.json"); } else if (deltaList.Count > 1) { // 新增了多个文件夹 //Enumerable.Select((IEnumerable)DeltaList, d => d.Name).Join(";") ModBase.Log( $"[Download] 有多个疑似的新增实例,无法确定:{string.Join(";", deltaList.Select(d => d.Name))}"); } else { // 没有新增文件夹 ModBase.Log("[Download] 未找到新增的实例文件夹"); } } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.LoaderInstallFailed", loaderName), ex); } finally { // 清理文件 try { if (installer is not null) installer.Dispose(); if (File.Exists(installerAddress)) File.Delete(installerAddress); } catch (Exception ex) { ModBase.Log(ex, $"安装 {loaderName} 清理文件时出错"); } } }) { ProgressWeight = 10d }); } else { ModBase.Log("[Download] 检测为非新版 Forge:" + loaderVersion); loaders.Add(new ModLoader.LoaderTask, bool>( $"{(forgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? Lang.Text("Minecraft.Download.Stage.InstallForge.MethodB") : Lang.Text("Minecraft.Download.Stage.InstallForgeType", forgeType))}", task => { ZipArchive installer = null; try { // 解压并获取信息 ModBase.WaitForFileReady(installerAddress); installer = new ZipArchive(new FileStream(installerAddress, FileMode.Open)); task.Progress = 0.2d; var json = (JsonObject)ModBase.GetJson( ModBase.ReadFile(installer.GetEntry("install_profile.json").Open())); task.Progress = 0.4d; // 新建实例文件夹 Directory.CreateDirectory(versionFolder); task.Progress = 0.5d; if (json["install"] is null) { // 中版:Legacy 方式 1 ModBase.Log("[Download] 开始进行 Forge 安装,Legacy 方式 1:" + installerAddress); // 建立 Json 文件 var jsonVersion = (JsonObject)ModBase.GetJson( ModBase.ReadFile(installer.GetEntry(json["json"].ToString().TrimStart('/')).Open())); jsonVersion["id"] = targetVersion; ModBase.WriteFile(Path.Combine(versionFolder, targetVersion + ".json"), jsonVersion.ToString()); task.Progress = 0.6d; // 解压支持库文件 installer.Dispose(); var unrarDir = Path.Combine(Path.GetDirectoryName(installerAddress), "_unrar"); ModBase.ExtractFile(installerAddress, unrarDir); ModBase.CopyDirectory(Path.Combine(unrarDir, "maven"), Path.Combine(mcFolder, "libraries")); ModBase.DeleteDirectory(unrarDir); } else { // 旧版:Legacy 方式 2 ModBase.Log("[Download] 开始进行 Forge 安装,Legacy 方式 2:" + installerAddress); // 解压 Jar 文件 var jarAddress = ModLibrary.McLibGet((string)json["install"]["path"], customMcFolder: mcFolder); if (File.Exists(jarAddress)) File.Delete(jarAddress); ModBase.WriteFile(jarAddress, installer.GetEntry((string)json["install"]["filePath"]).Open()); task.Progress = 0.9d; // 建立 Json 文件 json["versionInfo"]["id"] = targetVersion; if (json["versionInfo"]["inheritsFrom"] is null) ((JsonObject)json["versionInfo"]).Add("inheritsFrom", inherit); ModBase.WriteFile(Path.Combine(versionFolder, targetVersion + ".json"), json["versionInfo"].ToString()); } } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.ForgeOldInstallFailed"), ex); } finally { try { // 清理文件 if (installer is not null) installer.Dispose(); if (File.Exists(installerAddress)) File.Delete(installerAddress); var unrarDir = Path.Combine(Path.GetDirectoryName(installerAddress), "_unrar"); if (Directory.Exists(unrarDir)) ModBase.DeleteDirectory(unrarDir); } catch (Exception ex) { ModBase.Log(ex, "非新版方式安装 Forge 清理文件时出错"); } } }) { ProgressWeight = 1d }); } return loaders; } #endregion #region Forge 下载菜单 public static void ForgeDownloadListItemPreload(StackPanel stack, List entries, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 如果只有一个版本,则不特别列出 if (entries.Count == 1) return; // 获取推荐版本与最新版本 ModDownload.DlForgeVersionEntry freshVersion = null; if (entries.Any()) freshVersion = entries[0]; else ModBase.Log("[System] 未找到可用的 Forge 版本", ModBase.LogLevel.Debug); ModDownload.DlForgeVersionEntry recommendedVersion = null; foreach (var Entry in entries) if (Entry.IsRecommended) recommendedVersion = Entry; // 若推荐版本与最新版本为同一版本,则仅显示推荐版本 if (freshVersion is not null && ReferenceEquals(freshVersion, recommendedVersion)) freshVersion = null; // 显示各个版本 if (recommendedVersion is not null) { var recommended = ForgeDownloadListItem(recommendedVersion, onClick, isSaveOnly); recommended.Info = Lang.Text("Download.Version.Type.Recommended") + (string.IsNullOrEmpty(recommended.Info) ? "" : " | " + recommended.Info); stack.Children.Add(recommended); } if (freshVersion is not null) { var fresh = ForgeDownloadListItem(freshVersion, onClick, isSaveOnly); fresh.Info = Lang.Text("Download.Version.Latest.Title") + (string.IsNullOrEmpty(fresh.Info) ? "" : " | " + fresh.Info); stack.Children.Add(fresh); } // 添加间隔 stack.Children.Add(new TextBlock { Text = Lang.Text("Download.Version.AllVersions", entries.Count), HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(6d, 13d, 0d, 4d) }); } public static MyListItem ForgeDownloadListItem(ModDownload.DlForgeVersionEntry entry, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { var infoParts = new List(); if (!string.IsNullOrEmpty(entry.ReleaseTime)) infoParts.Add(Lang.Text("Download.Version.ReleaseDate", entry.ReleaseTime)); if (ModBase.modeDebug) infoParts.Add(Lang.Text("Download.Version.Forge.Type", entry.Category)); // 建立控件 var newItem = new MyListItem { Title = entry.VersionName, SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = string.Join(" | ", infoParts), Logo = ModBase.pathImage + "Blocks/Anvil.png" }; newItem.Click += onClick; // 建立菜单 if (isSaveOnly) newItem.ContentHandler = ForgeSaveContMenuBuild; else newItem.ContentHandler = ForgeContMenuBuild; // 结束 return newItem; } private static void ForgeContMenuBuild(MyListItem sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (ss, ee) => ForgeSave_Click(ss, (dynamic)ee); var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (ss, ee) => ForgeLog_Click(ss, (dynamic)ee); sender.Buttons = new[] { btnSave, btnInfo }; } private static void ForgeSaveContMenuBuild(MyListItem sender, EventArgs e) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (ss, ee) => ForgeLog_Click(ss, (dynamic)e); sender.Buttons = new[] { btnInfo }; } private static void ForgeLog_Click(object sender, RoutedEventArgs e) { ModDownload.DlForgeVersionEntry version; if (((dynamic)sender).Tag is not null) version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Tag; else version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Parent.Tag; ModBase.OpenWebsite( $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{version.Inherit}-{version.VersionName}/forge-{version.Inherit}-{version.VersionName}-changelog.txt"); } public static void ForgeSave_Click(object sender, RoutedEventArgs e) { ModDownload.DlForgeVersionEntry version; if (((dynamic)sender).Tag is not null) version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Tag; else version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Parent.Tag; McDownloadForgelikeSave(version); } #endregion #region Forge 推荐版本获取 /// /// 尝试刷新 Forge 推荐版本缓存。 /// public static void McDownloadForgeRecommendedRefresh() { if (isForgeRecommendedRefreshed) return; isForgeRecommendedRefreshed = true; // 获取所有推荐版本列表 // 内容为:"1.15.2":"31.2.0" // 保存 ModBase.RunInNewThread(() => { try { ModBase.Log("[Download] 刷新 Forge 推荐版本缓存开始"); var result = ModNet.NetGetCodeByLoader("https://bmclapi2.bangbang93.com/forge/promos"); if (result.Length < 1000) throw new Exception(Lang.Text("Minecraft.Download.Error.ForgePromosResultTooShort", result)); var resultJson = (JsonNode)ModBase.GetJson(result); var recommendedList = new List(); foreach (JsonObject Version in resultJson.AsArray()) { if (Version["name"] is null || Version["build"] is null) continue; var name = (string)Version["name"]; if (!name.EndsWithF("-recommended")) continue; recommendedList.Add("\"" + name.Replace("-recommended", "\":\"" + Version["build"]["version"] + "\"")); } if (recommendedList.Count < 5) throw new Exception(Lang.Text("Minecraft.Download.Error.ForgeRecommendedTooFew", result)); var cacheJson = "{" + recommendedList.Join(",") + "}"; ModBase.WriteFile(Path.Combine(ModBase.pathTemp, "Cache", "ForgeRecommendedList.json"), cacheJson); ModBase.Log("[Download] 刷新 Forge 推荐版本缓存成功"); } catch (Exception ex) { ModBase.Log(ex, "刷新 Forge 推荐版本缓存失败"); } }, "ForgeRecommendedRefresh"); } private static bool isForgeRecommendedRefreshed; /// /// 尝试获取某个 MC 版本对应的 Forge 推荐版本。如果不可用会返回 Nothing。 /// public static string McDownloadForgeRecommendedGet(string mcInstance) { try { if (mcInstance is null) return null; var list = ModBase.ReadFile(Path.Combine(ModBase.pathTemp, "Cache", "ForgeRecommendedList.json")); if (list is null || string.IsNullOrEmpty(list)) { ModBase.Log("[Download] 没有 Forge 推荐版本缓存文件"); return null; } var json = (JsonObject)ModBase.GetJson(list); if (json is null || !(mcInstance ?? "null").Contains(".") || !json.ContainsKey(mcInstance)) return null; return (json[mcInstance] ?? "").ToString(); } catch (Exception ex) { ModBase.Log( ex, "获取 Forge 推荐版本失败(" + (mcInstance ?? "null") + ")", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); return null; } } #endregion #region NeoForge 下载菜单 public static void NeoForgeDownloadListItemPreload(StackPanel stack, List entries, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 如果只有一个版本,则不特别列出 if (entries.Count == 1) return; // 获取最新稳定版和测试版 ModDownload.DlNeoForgeListEntry freshStableVersion = null; ModDownload.DlNeoForgeListEntry freshBetaVersion = null; if (entries.Any()) foreach (var Entry in entries.ToList()) if (Entry.IsBeta) { if (freshBetaVersion is null) freshBetaVersion = Entry; } else { freshStableVersion = Entry; break; } else ModBase.Log("[System] 未找到可用的 NeoForge 版本", ModBase.LogLevel.Debug); // 显示各个版本 if (freshStableVersion is not null) { var fresh = NeoForgeDownloadListItem(freshStableVersion, onClick, isSaveOnly); fresh.Info = string.IsNullOrEmpty(fresh.Info) ? Lang.Text("Download.Version.Fresh.Stable") : Lang.Text("Download.Version.Fresh.Latest") + " | " + fresh.Info; stack.Children.Add(fresh); } if (freshBetaVersion is not null) { var fresh = NeoForgeDownloadListItem(freshBetaVersion, onClick, isSaveOnly); fresh.Info = string.IsNullOrEmpty(fresh.Info) ? Lang.Text("Download.Version.Fresh.Development") : Lang.Text("Download.Version.Fresh.Latest") + " | " + fresh.Info; stack.Children.Add(fresh); } // 添加间隔 stack.Children.Add(new TextBlock { Text = Lang.Text("Download.Version.AllVersions", entries.Count), HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(6d, 13d, 0d, 4d) }); } public static MyListItem NeoForgeDownloadListItem(ModDownload.DlNeoForgeListEntry info, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 建立控件 var newItem = new MyListItem { Title = info.VersionName, SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = info, Info = info.IsBeta ? Lang.Text("Download.Version.Type.Preview") : Lang.Text("Download.Version.Type.Stable"), Logo = ModBase.pathImage + "Blocks/NeoForge.png" }; newItem.Click += onClick; // 建立菜单 if (isSaveOnly) newItem.ContentHandler = NeoForgeSaveContMenuBuild; else newItem.ContentHandler = NeoForgeContMenuBuild; // 结束 return newItem; } private static void NeoForgeContMenuBuild(MyListItem sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (sender, e) => NeoForgeSave_Click(sender, (RoutedEventArgs)e); var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (sender, e) => NeoForgeLog_Click(sender, (RoutedEventArgs)e); sender.Buttons = new[] { btnSave, btnInfo }; } private static void NeoForgeSaveContMenuBuild(MyListItem sender, EventArgs e) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (sender, e) => NeoForgeLog_Click(sender, (RoutedEventArgs)e); sender.Buttons = new[] { btnInfo }; } private static void NeoForgeLog_Click(object sender, RoutedEventArgs e) { ModDownload.DlNeoForgeListEntry info; if (((dynamic)sender).Tag is not null) info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Tag; else info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Parent.Tag; ModBase.OpenWebsite(info.UrlBase + "-changelog.txt"); } public static void NeoForgeSave_Click(object sender, RoutedEventArgs e) { ModDownload.DlNeoForgeListEntry info; if (((dynamic)sender).Tag is not null) info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Tag; else info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Parent.Tag; McDownloadForgelikeSave(info); } #endregion #region Cleanroom 下载菜单 public static void CleanroomDownloadListItemPreload(StackPanel stack, List entries, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 获取最新稳定版和测试版 // Dim FreshStableVersion As DlCleanroomListEntry = Nothing ModDownload.DlCleanroomListEntry freshBetaVersion = null; if (entries.Any()) freshBetaVersion = entries[0]; else ModBase.Log("[System] 未找到可用的 Cleanroom 版本", ModBase.LogLevel.Debug); if (freshBetaVersion is not null) { var fresh = CleanroomDownloadListItem(freshBetaVersion, onClick, isSaveOnly); fresh.Info = string.IsNullOrEmpty(fresh.Info) ? Lang.Text("Download.Version.Fresh.Development") : Lang.Text("Download.Version.Fresh.Latest") + " | " + fresh.Info; stack.Children.Add(fresh); } // 添加间隔 stack.Children.Add(new TextBlock { Text = Lang.Text("Download.Version.AllVersions", entries.Count), HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(6d, 13d, 0d, 4d) }); } public static MyListItem CleanroomDownloadListItem(ModDownload.DlCleanroomListEntry info, MyListItem.ClickEventHandler onClick, bool isSaveOnly) { // 建立控件 var newItem = new MyListItem { Title = info.VersionName, SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = info, Info = info.IsBeta ? Lang.Text("Download.Version.Type.Preview") : Lang.Text("Download.Version.Type.Stable"), Logo = ModBase.pathImage + "Blocks/Cleanroom.png" }; newItem.Click += onClick; // 建立菜单 if (isSaveOnly) newItem.ContentHandler = CleanroomSaveContMenuBuild; else newItem.ContentHandler = CleanroomContMenuBuild; // 结束 return newItem; } private static void CleanroomContMenuBuild(MyListItem sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (sender, _e) => CleanroomSave_Click(sender, (RoutedEventArgs)e); var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (sender, e) => CleanroomLog_Click(sender, (RoutedEventArgs)e); sender.Buttons = new[] { btnSave, btnInfo }; } private static void CleanroomSaveContMenuBuild(MyListItem sender, EventArgs e) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (a, b) => CleanroomLog_Click(a, (dynamic)b); sender.Buttons = new[] { btnInfo }; } private static void CleanroomLog_Click(object sender, RoutedEventArgs e) { ModDownload.DlCleanroomListEntry info; if (((dynamic)sender).Tag is not null) info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Tag; else info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Parent.Tag; ModBase.OpenWebsite(info.UrlBase + "-changelog.txt"); } public static void CleanroomSave_Click(object sender, RoutedEventArgs e) { ModDownload.DlCleanroomListEntry info; if (((dynamic)sender).Tag is not null) info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Tag; else info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Parent.Tag; McDownloadForgelikeSave(info); } #endregion #region Fabric 下载 public static void McDownloadFabricLoaderSave(JsonObject downloadInfo) { try { var url = downloadInfo["url"].ToString(); var fileName = ModBase.GetFileNameFromPath(url); var version = ModBase.GetFileNameFromPath(downloadInfo["version"].ToString()); var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), fileName, Lang.Text("Download.Version.Installer.Fabric.Filter")); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.FabricInstallerDownload", version) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 构造步骤加载器 var loaders = new List(); // 下载 // BMCLAPI 不支持 Fabric Installer 下载 var address = new List(); address.Add(url); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), new List { new(address.ToArray(), target, new ModBase.FileChecker(1024 * 64)) }) { ProgressWeight = 15d }); // 启动 var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.FabricInstallerDownload", version), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(downloadInfo); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 Fabric 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 获取下载某个 Fabric 实例的加载器列表。 /// private static List McDownloadFabricLoader(string fabricVersion, string minecraftName, string mcFolder = null, bool fixLibrary = true) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var id = "fabric-loader-" + fabricVersion + "-" + minecraftName; var versionFolder = Path.Combine(mcFolder, "versions", id); var loaders = new List(); // 下载 Json minecraftName = minecraftName.Replace("∞", "infinite"); // 放在 ID 后面避免影响实例文件夹名称 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainFabricMainFileUrl"), task => { // 启动依赖实例的下载 if (fixLibrary) McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, minecraftName); task.Progress = 0.5d; var safeName = minecraftName.Replace("∞", "infinite"); var bmclapiUrl = $"https://bmclapi2.bangbang93.com/fabric-meta/v2/versions/loader/{safeName}/{fabricVersion}/profile/json"; var officialUrl = $"https://meta.fabricmc.net/v2/versions/loader/{safeName}/{fabricVersion}/profile/json"; string json = null; foreach (var url in new[] { bmclapiUrl, officialUrl }) { try { json = Requester.FetchString(url, new RequestParam { UseBrowserUserAgent = true, Timeout = 5000, Retries = 2 }); if (json is not null) break; } catch (Exception ex) { ModBase.Log(ex, $"[Download] 从 {url} 下载 Fabric meta 失败"); } } if (json is null) throw new Exception(Lang.Text("Minecraft.Download.Error.FabricMetaDownloadFailed", $"{bmclapiUrl} and {officialUrl}")); Directory.CreateDirectory(versionFolder); File.WriteAllText(Path.Combine(versionFolder, id + ".json"), json, Encoding.UTF8); task.output = new List(); }) { ProgressWeight = 0.5d }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderMainFile", "Fabric"), new List()) { ProgressWeight = 2.5d }); // 下载支持库 if (fixLibrary) { loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeFabricLibraries"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 1d, show = false }); loaders.Add( new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLabyModClientJson"), new List()) { ProgressWeight = 8d }); } return loaders; } #endregion #region LegacyFabric 下载 public static void McDownloadLegacyFabricLoaderSave(JsonObject downloadInfo) { try { var url = downloadInfo["url"].ToString(); var fileName = ModBase.GetFileNameFromPath(url); var version = ModBase.GetFileNameFromPath(downloadInfo["version"].ToString()); var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), fileName, Lang.Text("Download.Version.Installer.LegacyFabric.Filter")); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.LegacyFabricInstallerDownload", version) ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 构造步骤加载器 var loaders = new List(); // 下载 var address = new List(); address.Add(url); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), new List { new(address.ToArray(), target, new ModBase.FileChecker(1024 * 64)) }) { ProgressWeight = 15d }); // 启动 var loader = new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LegacyFabricInstallerDownload", version), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(downloadInfo); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 Legacy Fabric 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 获取下载某个 LegacyFabric 实例的加载器列表。 /// private static List McDownloadLegacyFabricLoader(string legacyFabricVersion, string minecraftName, string mcFolder = null, bool fixLibrary = true) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var id = "legacy-fabric-loader-" + legacyFabricVersion + "-" + minecraftName; var versionFolder = Path.Combine(mcFolder, "versions", id); var loaders = new List(); // 下载 Json loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainLegacyFabricMainFileUrl"), task => { // 启动依赖实例的下载 if (fixLibrary) McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, minecraftName); task.Progress = 0.5d; // 构造文件请求 task.output = new List { new( new[] { "https://meta.legacyfabric.net/v2/versions/loader/" + minecraftName + "/" + legacyFabricVersion + "/profile/json" }, Path.Combine(versionFolder, id + ".json"), new ModBase.FileChecker(isJson: true)) }; }) { ProgressWeight = 0.5d }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderMainFile", "Legacy Fabric"), new List()) { ProgressWeight = 2.5d }); // 下载支持库 if (fixLibrary) { loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeLegacyFabricLibraries"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 1d, show = false }); loaders.Add(new LoaderDownload( Lang.Text("Minecraft.Download.Stage.DownloadLoaderLibraries", "Legacy Fabric"), new List()) { ProgressWeight = 8d }); } return loaders; } #endregion #region Fabric 下载菜单 public static MyListItem FabricDownloadListItem(JsonObject entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry["version"].ToString().Replace("+build", ""), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry["stable"].ToObject() ? Lang.Text("Download.Version.Type.Stable") : Lang.Text("Download.Version.Type.Preview"), Logo = ModBase.pathImage + "Blocks/Fabric.png" }; newItem.Click += onClick; newItem.ContentHandler = FabricContMenuBuild; // 结束 return newItem; } private static void FabricContMenuBuild(object sender, EventArgs e) { var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (a, b) => FabricLog_Click(a, (dynamic)b); ((dynamic)sender).Buttons = new[] { btnInfo }; } private static void FabricLog_Click(object sender, RoutedEventArgs e) { ModBase.OpenWebsite("https://fabricmc.net/blog"); } public static MyListItem FabricApiDownloadListItem(ModComp.CompFile entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry.DisplayName.Split("]")[1].Replace("Fabric API ", "").Replace(" build ", ".").Trim(), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry.StatusDescription + Lang.Text("Download.Version.ReleaseDate", Lang.Date(entry.ReleaseDate, "g")), Logo = ModBase.pathImage + "Blocks/Fabric.png" }; newItem.Click += onClick; // 结束 return newItem; } public static MyListItem OptiFabricDownloadListItem(ModComp.CompFile entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry.DisplayName.ToLower().Replace("optifabric-", "").Replace(".jar", "").Trim().TrimStart('v'), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry.StatusDescription + Lang.Text("Download.Version.ReleaseDate", Lang.Date(entry.ReleaseDate, "g")), Logo = ModBase.pathImage + "Blocks/OptiFabric.png" }; newItem.Click += onClick; // 结束 return newItem; } #endregion #region LegacyFabric 下载菜单 public static MyListItem LegacyFabricDownloadListItem(JsonObject entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry["version"].ToString(), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry["stable"].ToObject() ? Lang.Text("Download.Version.Type.Stable") : Lang.Text("Download.Version.Type.Preview"), Logo = ModBase.pathImage + "Blocks/Fabric.png" }; newItem.Click += onClick; // 结束 return newItem; } public static MyListItem LegacyFabricApiDownloadListItem(ModComp.CompFile entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry.DisplayName.Replace("Legacy Fabric API ", ""), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry.StatusDescription + Lang.Text("Download.Version.ReleaseDate", Lang.Date(entry.ReleaseDate, "g")), Logo = ModBase.pathImage + "Blocks/Fabric.png" }; newItem.Click += onClick; // 结束 return newItem; } #endregion #region LabyMod 下载 public static void McDownloadLabyModProductionLoaderSave() { try { var url = "https://releases.labymod.net/api/v1/installer/production/java"; var fileName = "LabyMod4ProductionInstaller.jar"; var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), fileName, Lang.Text("Download.Version.Installer.LabyMod.Filter")); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.LabyModInstallerDownload") ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 构造步骤加载器 var loaders = new List(); // 下载 var address = new List(); address.Add(url); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), new List { new(address.ToArray(), target, new ModBase.FileChecker(1024 * 64)) }) { ProgressWeight = 15d }); // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.LabyModInstallerDownload"), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 LabyMod 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } public static void McDownloadLabyModSnapshotLoaderSave() { try { var url = "https://releases.labymod.net/api/v1/installer/snapshot/java"; var fileName = "LabyMod4SnapshotInstaller.jar"; var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Version.SelectSaveLocation"), fileName, Lang.Text("Download.Version.Installer.LabyMod.Filter")); if (!target.Contains(@"\")) return; // 重复任务检查 foreach (var OngoingLoader in ModLoader.loaderTaskbar.ToList()) { if ((OngoingLoader.name ?? "") != (Lang.Text("Minecraft.Download.Stage.LabyModInstallerDownload") ?? "")) continue; HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceDownloading"), HintType.Error); return; } // 构造步骤加载器 var loaders = new List(); // 下载 var address = new List(); address.Add(url); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadMainFile"), new List { new(address.ToArray(), target, new ModBase.FileChecker(1024 * 64)) }) { ProgressWeight = 15d }); // 启动 var loader = new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.LabyModInstallerDownload"), loaders) { OnStateChanged = LoaderStateChangedHintOnly }; loader.Start(); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); } catch (Exception ex) { ModBase.Log( ex, "开始 LabyMod 安装器下载失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); } } /// /// 获取下载某个 LabyMod 实例的加载器列表。 /// private static List McDownloadLabyModLoader(string labyModCommitRef, string labyModChannel, string minecraftName, string mcFolder = null, bool fixLibrary = true) { // 参数初始化 mcFolder = mcFolder ?? ModFolder.mcFolderSelected; var isCustomFolder = (mcFolder ?? "") != (ModFolder.mcFolderSelected ?? ""); var id = "labymod-" + labyModCommitRef + "-" + minecraftName; var versionFolder = Path.Combine(mcFolder, "versions", id); var loaders = new List(); // 下载 Json minecraftName = minecraftName.Replace("∞", "infinite"); // 放在 ID 后面避免影响实例文件夹名称 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.ObtainLabyModClientUrl"), task => { // 启动依赖实例的下载 if (fixLibrary) McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, minecraftName, $"https://releases.r2.labymod.net/api/v1/download/manifest/labymod4/{labyModChannel}/{minecraftName}/{labyModCommitRef}.json"); task.Progress = 0.5d; // 构造文件请求 task.output = new List { new( new[] { $"https://releases.r2.labymod.net/api/v1/download/manifest/labymod4/{labyModChannel}/{minecraftName}/{labyModCommitRef}.json" }, Path.Combine(versionFolder, id + ".json"), new ModBase.FileChecker(isJson: true)) }; task.Progress = 1d; }) { ProgressWeight = 2d }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderLibraries", "Fabric"), new List()) { ProgressWeight = 10d }); // 下载支持库 if (fixLibrary) { loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeLabyModLibraries"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder))) { ProgressWeight = 1d, show = false }); loaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLoaderLibraries", "LabyMod"), new List()) { ProgressWeight = 8d }); } return loaders; } /// /// 获取下载某个 Minecraft 实例的加载器列表。 /// 它必须安装到 PathMcFolder,但是可以自定义实例名(不过自定义的实例名不会修改 Json 中的 id 项)。 /// private static List McDownloadLabyModClientLoader(string id, string labyChannel, string labyCommitRef, string versionName = null) { versionName = versionName ?? id; var versionFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", versionName) + @"\"; var loaders = new List(); // 下载支持库文件 var loadersLib = new List(); loadersLib.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeVanillaAndLabyModLibrariesSide"), task => { ModBase.WaitForFileReady(Path.Combine(versionFolder, versionName + ".json")); ModBase.Log("[Download] 开始分析原版与 LabyMod 支持库文件:" + versionFolder); task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(versionFolder)); }) { ProgressWeight = 1d, show = false }); loadersLib.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadVanillaAndLabyModLibrariesSide"), new List()) { ProgressWeight = 13d, show = false }); loaders.Add(new ModLoader.LoaderCombo(mcDownloadClientLibName, loadersLib) { block = false, ProgressWeight = 14d }); // 下载资源文件 var loadersAssets = new List(); loadersAssets.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeAssetsIndex.Side"), task => { try { var version = new McInstance(versionFolder); task.output = new List { ModDownload.DlClientAssetIndexGet(version) }; } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.AssetIndexAnalysisFailed"), ex); } // 顺手添加 Json 项目 try { var versionJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(Path.Combine(versionFolder, versionName + ".json"))); versionJson.Add("clientVersion", id); ModBase.WriteFile(Path.Combine(versionFolder, versionName + ".json"), versionJson.ToString()); } catch (Exception ex) { throw new Exception(Lang.Text("Minecraft.Download.Error.AddClientVersionFailed"), ex); } }) { ProgressWeight = 1d, show = false }); loadersAssets.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssetsIndex.Side"), new List()) { ProgressWeight = 3d, show = false }); loadersAssets.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeRequiredAssets.Side"), task => { ModLoader.LoaderBase argprogressFeed = task; task.output = ModAssets.McAssetsFixList(new McInstance(versionFolder), true, ref argprogressFeed); task = (ModLoader.LoaderTask>)argprogressFeed; }) { ProgressWeight = 3d, show = false }); loadersAssets.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssets.Side"), new List()) { ProgressWeight = 14d, show = false }); loaders.Add( new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.DownloadVanillaAssets"), loadersAssets) { block = false, ProgressWeight = 21d }); return loaders; } #endregion #region LabyMod 下载菜单 public static MyListItem LabyModDownloadListItem(JsonObject entry, MyListItem.ClickEventHandler onClick) { // 建立控件 var newItem = new MyListItem { Title = entry["version"] + " " + (entry["channel"].ToString().Contains("snapshot") ? Lang.Text("Download.Version.Type.Snapshot") : Lang.Text("Download.Version.Type.Stable")), SnapsToDevicePixels = true, Height = 42d, Type = MyListItem.CheckType.Clickable, Tag = entry, Info = entry["channel"].ToString().Contains("snapshot") ? Lang.Text("Download.Version.Type.Snapshot") : Lang.Text("Download.Version.Type.Stable"), Logo = ModBase.pathImage + "Blocks/LabyMod.png" }; newItem.Click += onClick; newItem.ContentHandler = LabyModContMenuBuild; // 结束 return newItem; } private static void LabyModContMenuBuild(object sender, EventArgs e) { var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") }; ToolTipService.SetPlacement(btnSave, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnSave, 30d); ToolTipService.SetHorizontalOffset(btnSave, 2d); btnSave.Click += (a, b) => LabyModSave_Click(a, (dynamic)b); var btnInfo = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/info", ToolTip = Lang.Text("Download.Version.Changelog") }; ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); ToolTipService.SetVerticalOffset(btnInfo, 30d); ToolTipService.SetHorizontalOffset(btnInfo, 2d); btnInfo.Click += (a, b) => LabyModLog_Click(a, (dynamic)b); ((dynamic)sender).Buttons = new[] { btnSave, btnInfo }; } private static void LabyModLog_Click(object sender, RoutedEventArgs e) { ModBase.OpenWebsite("https://www.labymod.net/zh_Hans/download"); } private static void LabyModSave_Click(object sender, RoutedEventArgs e) { JsonObject version; if (((dynamic)sender).Tag is not null) version = (JsonObject)((dynamic)sender).Tag; else if (((dynamic)sender).Parent.Tag is not null) version = (JsonObject)((dynamic)sender).Parent.Tag; else version = (JsonObject)((dynamic)sender).Parent.Parent.Tag; if ((string)version["channel"] == "snapshot") McDownloadLabyModSnapshotLoaderSave(); else McDownloadLabyModProductionLoaderSave(); } #endregion #region 合并安装 /// /// 安装请求。 /// public class McInstallRequest { /// /// 欲下载的 Cleanroom。 /// public ModDownload.DlCleanroomListEntry cleanroomEntry = null; // 若要下载 Cleanroom,则需要在下面两项中完成至少一项 /// /// 欲下载的 Cleanroom 版本名。 /// public string cleanroomVersion; /// /// 欲下载的 Fabric API 信息。 /// public ModComp.CompFile fabricApi = null; /// /// 欲下载的 Fabric Loader 版本名。 /// public string fabricVersion = null; /// /// 欲下载的 Forge。 /// public ModDownload.DlForgeVersionEntry forgeEntry = null; // 若要下载 Forge,则需要在下面两项中完成至少一项 /// /// 欲下载的 Forge 版本名。接受例如 36.1.4 / 14.23.5.2859 / 1.19-41.1.0 的输入。 /// public string forgeVersion; /// /// 欲下载的 LabyMod 通道。 /// public string labyModChannel = null; /// /// 欲下载的 LabyMod 版本。 /// public string labyModCommitRef = null; /// /// 欲下载的 Legacy Fabric API 信息。 /// public ModComp.CompFile legacyFabricApi = null; /// /// 欲下载的 Legacy Fabric Loader 版本名。 /// public string legacyFabricVersion = null; /// /// 欲下载的 LiteLoader 详细信息。 /// public ModDownload.DlLiteLoaderListEntry liteLoaderEntry = null; /// /// 可选。欲下载的 Minecraft Json 地址。 /// public string minecraftJson = null; /// /// 必填。欲下载的 Minecraft 的版本名。 /// public string minecraftName = null; /// /// 若 MMC 整合包安装包含特殊参数,则填写此项。 /// public ModModpack.MMCPackInfo mmcPackInfo = null; /// /// 欲下载的 NeoForge。 /// public ModDownload.DlNeoForgeListEntry neoForgeEntry = null; // 若要下载 NeoForge,则需要在下面两项中完成至少一项 /// /// 欲下载的 NeoForge 版本名。 /// public string neoForgeVersion; /// /// 欲下载的 OptiFabric 信息。 /// public ModComp.CompFile optiFabric = null; /// /// 欲下载的 OptiFine 详细信息。 /// public ModDownload.DlOptiFineListEntry optiFineEntry; // 若要下载 OptiFine,则需要在下面两项中完成至少一项 /// /// 欲下载的 OptiFine 版本名。例如 HD_U_F6_pre1。 /// public string optiFineVersion; /// /// 必填。安装目标文件夹。 /// public string targetInstanceFolder; /// /// 必填。安装目标实例名称。 /// public string targetInstanceName; } /// /// 在加载器状态改变后显示一条提示。 /// 不会进行任何其他操作。 /// public static void LoaderStateChangedHintOnly(object loaderObj) { var loader = (ModLoader.LoaderBase)loaderObj; switch (loader.State) { case ModBase.LoadState.Finished: HintService.Hint($"{loader.name}{Lang.Text("Common.Status.Success")}", HintType.Success); break; case ModBase.LoadState.Failed: HintService.Hint($"{loader.name}{Lang.Text("Common.Status.Failure")}{loader.Error.Message}", HintType.Error); break; case ModBase.LoadState.Aborted: HintService.Hint($"{loader.name}{Lang.Text("Common.Status.Cancelled")}"); break; } } /// /// 安装加载器状态改变后进行提示和重载文件夹列表的方法。 /// public static void McInstallState(object loaderObj) { var loader = (ModLoader.LoaderBase)loaderObj; var combo = (ModLoader.LoaderCombo)loader; switch (loader.State) { case ModBase.LoadState.Finished: { if (Config.Download.AutoSelectInstance) { var versionName = loader.name; ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "Version", versionName.Remove(versionName.Length - 3, 3)); } ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "InstanceCache", ""); // 清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存) ModBase.DeleteDirectory($"{combo.input}PCLInstallBackups\\"); HintService.Hint($"{loader.name}{Lang.Text("Common.Status.Success")}", HintType.Success); break; } case ModBase.LoadState.Failed: { HintService.Hint( $"{loader.name}{Lang.Text("Common.Status.Failure")}{loader.Error.Message}", HintType.Error); break; } case ModBase.LoadState.Aborted: { HintService.Hint($"{loader.name}{Lang.Text("Common.Status.Cancelled")}"); break; } case ModBase.LoadState.Loading: { return; // 不重新加载实例列表 } } if (loader.State != ModBase.LoadState.Finished && Directory.Exists( $"{combo.input}PCLInstallBackups\\")) // 实例修改失败回滚 { ModBase.CopyDirectory( $"{combo.input}PCLInstallBackups\\", (string)combo.input); File.Delete($"{combo.input}.pclignore"); ModBase.DeleteDirectory( $"{combo.input}PCLInstallBackups\\"); } else { McInstallFailedClearFolder(loader); } ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected, ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); } public static void McInstallFailedClearFolder(object loader) { try { Thread.Sleep(1000); // 防止存在尚未完全释放的文件,导致清理失败(例如整合包安装) if (((ModLoader.LoaderBase)loader).State == ModBase.LoadState.Failed || ((ModLoader.LoaderBase)loader).State == ModBase.LoadState.Aborted) { // 删除实例文件夹 ModBase.Log($"[Download] 由于下载失败或取消,清理实例文件夹:{((ModLoader.LoaderCombo)loader).input}", ModBase.LogLevel.Developer); var instancePath = (string)((ModLoader.LoaderCombo)loader).input; ((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache(instancePath); ModBase.DeleteDirectory(instancePath); } } catch (Exception ex) { ModBase.Log(ex, "下载失败或取消后清理实例文件夹失败"); } } private const string mcInstallDefaultType = "安装"; /// /// 进行合并安装。返回是否已经开始安装(例如如果没有安装 Java 则会进行提示并返回 False) /// public static bool McInstall(McInstallRequest request, string type = mcInstallDefaultType) { try { var subLoaders = McInstallLoader(request, ignoreDump: type != mcInstallDefaultType); if (subLoaders is null) return false; var loader = new ModLoader.LoaderCombo(request.targetInstanceName + " " + type, subLoaders) { OnStateChanged = McInstallState }; // 启动 loader.Start(request.targetInstanceFolder); ModLoader.LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); return true; } catch (ModBase.CancelledException ex) { return false; } catch (Exception ex) { ModBase.Log( ex, "开始合并安装失败", ModBase.LogLevel.Feedback, userSummary: Lang.Text("Minecraft.Download.Error.OperationFailed")); try { if (Directory.Exists(request.targetInstanceFolder)) { var files = Directory.GetFiles(request.targetInstanceFolder); var dirs = Directory.GetDirectories(request.targetInstanceFolder); if (files.Length <= 1 && dirs.Length == 0) { ((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)) .InvalidateCache(request.targetInstanceFolder); ModBase.DeleteDirectory(request.targetInstanceFolder); } } } catch (Exception innerEx) { ModBase.Log(innerEx, "清理未完成的实例文件夹失败"); } return false; } } /// /// 获取合并安装加载器列表,并进行前期的缓存清理与 Java 检查工作。 /// /// public static List McInstallLoader(McInstallRequest request, bool dontFixLibraries = false, bool ignoreDump = false) { // 获取缓存目录(安装 Mod 加载器的文件夹不能包含空格) var tempMcFolder = ModMain.RequestTaskTempFolder(request.optiFineEntry is not null || request.forgeEntry is not null || request.neoForgeEntry is not null); // 获取参数 var instanceFolder = Path.Combine(ModFolder.mcFolderSelected, "versions", request.targetInstanceName); if (Directory.Exists(tempMcFolder)) ModBase.DeleteDirectory(tempMcFolder); string optiFineFolder = null; if (request.optiFineVersion is not null) { if (request.optiFineVersion.Contains("_HD_U_")) request.optiFineVersion = "HD_U_" + request.optiFineVersion.AfterLast("_HD_U_"); // #735 request.optiFineEntry = new ModDownload.DlOptiFineListEntry { DisplayName = request.minecraftName + " " + request.optiFineVersion.Replace("HD_U_", "") .Replace("_", "").Replace("pre", " pre"), Inherit = request.minecraftName, IsPreview = request.optiFineVersion.ContainsF("pre", true), NameVersion = request.minecraftName + "-OptiFine_" + request.optiFineVersion, NameFile = (request.optiFineVersion.ContainsF("pre", true) ? "preview_" : "") + "OptiFine_" + request.minecraftName + "_" + request.optiFineVersion + ".jar" }; } if (request.optiFineEntry is not null) optiFineFolder = Path.Combine(tempMcFolder, "versions", request.optiFineEntry.NameVersion); string forgeFolder = null; if (request.forgeEntry is not null) request.forgeVersion = request.forgeVersion ?? request.forgeEntry.VersionName; if (request.forgeVersion is not null) forgeFolder = Path.Combine(tempMcFolder, "versions", "forge-" + request.forgeVersion); string neoForgeFolder = null; if (request.neoForgeEntry is not null) request.neoForgeVersion = request.neoForgeVersion ?? request.neoForgeEntry.VersionName; if (request.neoForgeVersion is not null) neoForgeFolder = Path.Combine(tempMcFolder, "versions", "neoforge-" + request.neoForgeVersion); string cleanroomFolder = null; if (request.cleanroomEntry is not null) request.cleanroomVersion = request.cleanroomVersion ?? request.cleanroomEntry.VersionName; if (request.cleanroomVersion is not null) cleanroomFolder = Path.Combine(tempMcFolder, "versions", "cleanroom-" + request.cleanroomVersion); string fabricFolder = null; if (request.fabricVersion is not null) fabricFolder = Path.Combine(tempMcFolder, "versions", "fabric-loader-" + request.fabricVersion + "-" + request.minecraftName); string legacyFabricFolder = null; if (request.legacyFabricVersion is not null) legacyFabricFolder = Path.Combine(tempMcFolder, "versions", "legacy-fabric-loader-" + request.legacyFabricVersion + "-" + request.minecraftName); string labyModFolder = null; if (request.labyModCommitRef is not null) labyModFolder = Path.Combine(tempMcFolder, "versions", "labymod-" + request.labyModCommitRef + "-" + request.minecraftName); string liteLoaderFolder = null; if (request.liteLoaderEntry is not null) liteLoaderFolder = Path.Combine(tempMcFolder, "versions", request.minecraftName + "-LiteLoader"); // 判断 OptiFine 是否作为 Mod 进行下载 var modable = request.fabricVersion is not null || request.legacyFabricVersion is not null || request.forgeEntry is not null || request.neoForgeEntry is not null || request.liteLoaderEntry is not null; var modsTempFolder = Path.Combine(tempMcFolder, "mods") + @"\"; var optiFineAsMod = request.optiFineEntry is not null && modable; // 选择了 OptiFine 与任意 Mod 加载器 if (optiFineAsMod) { ModBase.Log("[Download] OptiFine 将作为 Mod 进行下载"); if (request.liteLoaderEntry is not null) optiFineFolder = CombineCacheSubfolder(modsTempFolder, request.minecraftName); else optiFineFolder = modsTempFolder; } // 记录日志 if (optiFineFolder is not null) ModBase.Log("[Download] OptiFine 缓存:" + optiFineFolder); if (forgeFolder is not null) ModBase.Log("[Download] Forge 缓存:" + forgeFolder); if (neoForgeFolder is not null) ModBase.Log("[Download] NeoForge 缓存:" + neoForgeFolder); if (cleanroomFolder is not null) ModBase.Log("[Download] Cleanroom 缓存:" + cleanroomFolder); if (fabricFolder is not null) ModBase.Log("[Download] Fabric 缓存:" + fabricFolder); if (legacyFabricFolder is not null) ModBase.Log("[Download] LegacyFabric 缓存:" + legacyFabricFolder); if (labyModFolder is not null) ModBase.Log("[Download] LabyMod 缓存:" + labyModFolder); if (liteLoaderFolder is not null) ModBase.Log("[Download] LiteLoader 缓存:" + liteLoaderFolder); ModBase.Log("[Download] 对应的原版版本:" + request.minecraftName); // 重复实例检查 if (File.Exists(Path.Combine(instanceFolder, request.targetInstanceName + ".json")) && !ignoreDump) { HintService.Hint(Lang.Text("Minecraft.Download.Error.InstanceAlreadyExists", request.targetInstanceName, ""), HintType.Error); throw new ModBase.CancelledException(); } var loaderList = new List(); // 添加忽略标识 loaderList.Add(new ModLoader.LoaderTask(Lang.Text("Minecraft.Download.Stage.AddIgnoreFlag"), _ => ModBase.WriteFile(Path.Combine(instanceFolder, ".pclignore"), "用于临时地在 PCL 的实例列表中屏蔽此实例。")) { show = false, block = false }); // Fabric API if (request.fabricApi is not null) loaderList.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadFabricApi"), new List { request.fabricApi.ToNetFile(modsTempFolder, ModComp.DownloadReason.Dependency, null, ModComp.CompLoaderType.Fabric) }) { ProgressWeight = 3d, block = false }); // LegacyFabric API if (request.legacyFabricApi is not null) loaderList.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLegacyFabricApi"), new List { request.legacyFabricApi.ToNetFile(modsTempFolder, ModComp.DownloadReason.Dependency, null, ModComp.CompLoaderType.Fabric) }) { ProgressWeight = 3d, block = false }); // OptiFabric if (request.optiFabric is not null) loaderList.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadOptiFabric"), new List { request.optiFabric.ToNetFile(modsTempFolder, ModComp.DownloadReason.Dependency, null, ModComp.CompLoaderType.Fabric) }) { ProgressWeight = 3d, block = false }); // LabyMod if (request.labyModCommitRef is not null) { loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "LabyMod", request.labyModCommitRef), McDownloadLabyModLoader(request.labyModCommitRef, request.labyModChannel, request.minecraftName, tempMcFolder, false)) { show = false, ProgressWeight = 10d, block = true }); goto LabyModSkip; } // 原版 var clientLoader = new ModLoader.LoaderCombo( Lang.Text( "Minecraft.Download.Stage.LoaderDownloadCombo", Lang.Text("Minecraft.Version.Vanilla"), request.minecraftName ), McDownloadClientLoader( request.minecraftName, request.minecraftJson, request.targetInstanceName ) ) { show = false, ProgressWeight = 39d, block = request.forgeVersion is null && request.neoForgeVersion is null && request.optiFineEntry is null && request.fabricVersion is null && request.liteLoaderEntry is null && request.cleanroomEntry is null && request.legacyFabricVersion is null }; loaderList.Add(clientLoader); // OptiFine if (request.optiFineEntry is not null) { if (optiFineAsMod) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "OptiFine", request.optiFineEntry.DisplayName), McDownloadOptiFineSaveLoader(request.optiFineEntry, Path.Combine(optiFineFolder, request.optiFineEntry.NameFile))) { show = false, ProgressWeight = 16d, block = request.forgeVersion is null && request.neoForgeVersion is null && request.fabricVersion is null && request.liteLoaderEntry is null }); else loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "OptiFine", request.optiFineEntry.DisplayName), McDownloadOptiFineLoader(request.optiFineEntry, tempMcFolder, clientLoader, request.targetInstanceFolder, false)) { show = false, ProgressWeight = 24d, block = request.forgeVersion is null && request.neoForgeVersion is null && request.fabricVersion is null && request.liteLoaderEntry is null }); } // Forge if (request.forgeVersion is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "Forge", request.forgeVersion), McDownloadForgelikeLoader(ModDownload.DlForgelikeEntry.ForgelikeType.Forge, request.forgeVersion, "forge-" + request.forgeVersion, request.minecraftName, request.forgeEntry, tempMcFolder, clientLoader, request.targetInstanceFolder)) { show = false, ProgressWeight = 25d, block = request.fabricVersion is null && request.liteLoaderEntry is null && request.neoForgeEntry is null }); // NeoForge if (request.neoForgeVersion is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "NeoForge", request.neoForgeVersion), McDownloadForgelikeLoader(ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge, request.neoForgeVersion, "neoforge-" + request.neoForgeVersion, request.minecraftName, request.neoForgeEntry, tempMcFolder, clientLoader, request.targetInstanceFolder)) { show = false, ProgressWeight = 25d, block = request.forgeEntry is null && request.fabricVersion is null && request.liteLoaderEntry is null }); // Cleanroom if (request.cleanroomVersion is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "Cleanroom", request.cleanroomVersion), McDownloadForgelikeLoader(ModDownload.DlForgelikeEntry.ForgelikeType.Cleanroom, request.cleanroomVersion, "cleanroom-" + request.cleanroomVersion, request.minecraftName, request.cleanroomEntry, tempMcFolder, clientLoader, request.targetInstanceFolder)) { show = false, ProgressWeight = 25d, block = request.forgeEntry is null && request.fabricVersion is null && request.liteLoaderEntry is null }); // LiteLoader if (request.liteLoaderEntry is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "LiteLoader", request.minecraftName), McDownloadLiteLoaderLoader(request.liteLoaderEntry, tempMcFolder, clientLoader, false)) { show = false, ProgressWeight = 1d, block = request.fabricVersion is null }); // Fabric if (request.fabricVersion is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "Fabric", request.fabricVersion), McDownloadFabricLoader(request.fabricVersion, request.minecraftName, tempMcFolder, false)) { show = false, ProgressWeight = 2d, block = true }); // LegacyFabric if (request.legacyFabricVersion is not null) loaderList.Add(new ModLoader.LoaderCombo( Lang.Text("Minecraft.Download.Stage.LoaderDownloadCombo", "Legacy Fabric", request.legacyFabricVersion), McDownloadLegacyFabricLoader(request.legacyFabricVersion, request.minecraftName, tempMcFolder, false)) { show = false, ProgressWeight = 2d, block = true }); LabyModSkip: ; // 合并安装 loaderList.Add(new ModLoader.LoaderTask(Lang.Text("Minecraft.Download.Stage.InstallGame"), task => { // 合并 JSON MergeJson(instanceFolder, instanceFolder, optiFineFolder, optiFineAsMod, forgeFolder, request.forgeVersion, neoForgeFolder, request.neoForgeVersion, cleanroomFolder, request.cleanroomVersion, fabricFolder, labyModFolder, request.labyModChannel, liteLoaderFolder, request.mmcPackInfo, legacyFabricFolder); task.Progress = 0.2d; // 迁移文件 if (Directory.Exists(Path.Combine(tempMcFolder, "libraries"))) ModBase.CopyDirectory(Path.Combine(tempMcFolder, "libraries"), Path.Combine(ModFolder.mcFolderSelected, "libraries")); task.Progress = 0.8d; // 创建 Mod 和资源包文件夹 var modsFolder = Path.Combine(new McInstance(instanceFolder).PathIndie, "mods"); // 版本隔离信息在此时被决定 if (Directory.Exists(modsTempFolder)) { ModBase.CopyDirectory(modsTempFolder, modsFolder); } else if (modable) { Directory.CreateDirectory(modsFolder); ModBase.Log("[Download] 自动创建 Mod 文件夹:" + modsFolder); } var resourcepacksFolder = Path.Combine(new McInstance(instanceFolder).PathIndie, "resourcepacks"); Directory.CreateDirectory(resourcepacksFolder); ModBase.Log("[Download] 自动创建资源包文件夹:" + resourcepacksFolder); }) { ProgressWeight = 2d, block = true }); // 补全文件 if (!dontFixLibraries && (request.optiFineEntry is not null || (request.forgeVersion is not null && Convert.ToDouble(request.forgeVersion.BeforeFirst(".")) >= 20d) || request.neoForgeVersion is not null || request.fabricVersion is not null || request.cleanroomVersion is not null || request.liteLoaderEntry is not null || request.labyModCommitRef is not null)) { var loadersLib = new List(); if (request.labyModCommitRef is not null) { var labyModClientLoader = new ModLoader.LoaderCombo( Lang.Text( "Minecraft.Download.Stage.LoaderDownloadCombo", Lang.Text("Minecraft.Version.Vanilla"), request.minecraftName ), McDownloadLabyModClientLoader( request.minecraftName, request.labyModChannel, request.labyModCommitRef, request.targetInstanceName ) ) { show = false, ProgressWeight = 39d, block = false }; loaderList.Add(labyModClientLoader); } else { loadersLib.Add(new ModLoader.LoaderTask>( Lang.Text("Minecraft.Download.Stage.AnalyzeGameLibrariesSide"), task => task.output = ModLibrary.McLibNetFilesFromInstance(new McInstance(instanceFolder))) { ProgressWeight = 1d, show = false }); loadersLib.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadGameLibrariesSide"), new List()) { ProgressWeight = 7d, show = false }); loaderList.Add( new ModLoader.LoaderCombo(Lang.Text("Minecraft.Download.Stage.DownloadGameLibraries"), loadersLib) { ProgressWeight = 8d }); } } // 删除忽略标识 loaderList.Add(new ModLoader.LoaderTask(Lang.Text("Minecraft.Download.Stage.DeleteIgnoreFlag"), _ => File.Delete(Path.Combine(instanceFolder, ".pclignore"))) { show = false }); // 总加载器 return loaderList; } /// /// 将多个实例 JSON 进行合并,如果目标已存在则直接覆盖。失败会抛出异常。 /// private static void MergeJson(string outputFolder, string minecraftFolder, string optiFineFolder = null, bool optiFineAsMod = false, string forgeFolder = null, string forgeVersion = null, string neoForgeFolder = null, string neoForgeVersion = null, string cleanroomFolder = null, string cleanroomVersion = null, string fabricFolder = null, string labyModFolder = null, string labyModChannel = null, string liteLoaderFolder = null, ModModpack.MMCPackInfo mMCPackInfo = null, string legacyFabricFolder = null) { ModBase.Log("[Download] 开始进行实例合并,输出:" + outputFolder + ",Minecraft:" + minecraftFolder + (optiFineFolder is not null ? ",OptiFine:" + optiFineFolder : "") + (forgeFolder is not null ? ",Forge:" + forgeFolder : "") + (neoForgeFolder is not null ? ",NeoForge:" + neoForgeFolder : "") + (cleanroomFolder is not null ? ",Cleanroom:" + cleanroomFolder : "") + (liteLoaderFolder is not null ? ",LiteLoader:" + liteLoaderFolder : "") + (fabricFolder is not null ? ",Fabric:" + fabricFolder : "") + (legacyFabricFolder is not null ? ",LegacyFabric:" + legacyFabricFolder : "") + (labyModFolder is not null ? ",LabyMod:" + labyModFolder : "")); Directory.CreateDirectory(outputFolder); var hasOptiFine = optiFineFolder is not null && !optiFineAsMod; var hasForge = forgeFolder is not null; var hasLegacyFabric = legacyFabricFolder is not null; var hasNeoForge = neoForgeFolder is not null; var hasCleanroom = cleanroomFolder is not null; var hasLiteLoader = liteLoaderFolder is not null; var hasFabric = fabricFolder is not null; var hasLabyMod = labyModFolder is not null; string outputName; string minecraftName; string optiFineName; string forgeName; string neoForgeName; string cleanroomName; string liteLoaderName; string fabricName; string legacyFabricName; string labyModName; string outputJsonPath; string minecraftJsonPath; string optiFineJsonPath = null; string forgeJsonPath = null; string neoForgeJsonPath = null; string cleanroomJsonPath = null; string liteLoaderJsonPath = null; string fabricJsonPath = null; string labyModJsonPath = null; string legacyFabricJsonPath = null; string outputJar; string minecraftJar; #region 初始化路径信息 if (!outputFolder.EndsWithF(@"\")) outputFolder += @"\"; outputName = ModBase.GetFolderNameFromPath(outputFolder); outputJsonPath = Path.Combine(outputFolder, outputName + ".json"); outputJar = Path.Combine(outputFolder, outputName + ".jar"); if (!minecraftFolder.EndsWithF(@"\")) minecraftFolder += @"\"; minecraftName = ModBase.GetFolderNameFromPath(minecraftFolder); minecraftJsonPath = Path.Combine(minecraftFolder, minecraftName + ".json"); minecraftJar = Path.Combine(minecraftFolder, minecraftName + ".jar"); if (hasOptiFine) { if (!optiFineFolder.EndsWithF(@"\")) optiFineFolder += @"\"; optiFineName = ModBase.GetFolderNameFromPath(optiFineFolder); optiFineJsonPath = Path.Combine(optiFineFolder, optiFineName + ".json"); } if (hasForge) { if (!forgeFolder.EndsWithF(@"\")) forgeFolder += @"\"; forgeName = ModBase.GetFolderNameFromPath(forgeFolder); forgeJsonPath = Path.Combine(forgeFolder, forgeName + ".json"); } if (hasNeoForge) { if (!neoForgeFolder.EndsWithF(@"\")) neoForgeFolder += @"\"; neoForgeName = ModBase.GetFolderNameFromPath(neoForgeFolder); neoForgeJsonPath = Path.Combine(neoForgeFolder, neoForgeName + ".json"); } if (hasCleanroom) { if (!cleanroomFolder.EndsWithF(@"\")) cleanroomFolder += @"\"; cleanroomName = ModBase.GetFolderNameFromPath(cleanroomFolder); cleanroomJsonPath = Path.Combine(cleanroomFolder, cleanroomName + ".json"); } if (hasLiteLoader) { if (!liteLoaderFolder.EndsWithF(@"\")) liteLoaderFolder += @"\"; liteLoaderName = ModBase.GetFolderNameFromPath(liteLoaderFolder); liteLoaderJsonPath = Path.Combine(liteLoaderFolder, liteLoaderName + ".json"); } if (hasFabric) { if (!fabricFolder.EndsWithF(@"\")) fabricFolder += @"\"; fabricName = ModBase.GetFolderNameFromPath(fabricFolder); fabricJsonPath = Path.Combine(fabricFolder, fabricName + ".json"); } if (hasLegacyFabric) { if (!legacyFabricFolder.EndsWithF(@"\")) legacyFabricFolder += @"\"; legacyFabricName = ModBase.GetFolderNameFromPath(legacyFabricFolder); legacyFabricJsonPath = Path.Combine(legacyFabricFolder, legacyFabricName + ".json"); } if (hasLabyMod) { if (!labyModFolder.EndsWithF(@"\")) labyModFolder += @"\"; labyModName = ModBase.GetFolderNameFromPath(labyModFolder); labyModJsonPath = Path.Combine(labyModFolder, labyModName + ".json"); } #endregion JsonObject outputJson; JsonObject minecraftJson = null; JsonObject optiFineJson = null; JsonObject forgeJson = null; JsonObject neoForgeJson = null; JsonObject legacyFabricJson = null; JsonObject cleanroomJson = null; JsonObject liteLoaderJson = null; JsonObject fabricJson = null; JsonObject labyModJson = null; #region 读取文件并检查文件是否合规 var minecraftJsonText = ModBase.ReadFile(minecraftJsonPath); if (!hasLabyMod) { if (!minecraftJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "Minecraft", minecraftJsonPath, minecraftJsonText.Substring(0, Math.Min(minecraftJsonText.Length, 1000)))); minecraftJson = (JsonObject)ModBase.GetJson(minecraftJsonText); } if (hasOptiFine) { var optiFineJsonText = ModBase.ReadFile(optiFineJsonPath); if (!optiFineJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "OptiFine", optiFineJsonPath, optiFineJsonText.Substring(0, Math.Min(optiFineJsonText.Length, 1000)))); optiFineJson = (JsonObject)ModBase.GetJson(optiFineJsonText); } if (hasForge) { var forgeJsonText = ModBase.ReadFile(forgeJsonPath); if (!forgeJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "Forge", forgeJsonPath, forgeJsonText.Substring(0, Math.Min(forgeJsonText.Length, 1000)))); forgeJson = (JsonObject)ModBase.GetJson(forgeJsonText); } if (hasNeoForge) { var neoForgeJsonText = ModBase.ReadFile(neoForgeJsonPath); if (!neoForgeJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "NeoForge", neoForgeJsonPath, neoForgeJsonText.Substring(0, Math.Min(neoForgeJsonText.Length, 1000)))); neoForgeJson = (JsonObject)ModBase.GetJson(neoForgeJsonText); } if (hasCleanroom) { var cleanroomJsonText = ModBase.ReadFile(cleanroomJsonPath); if (!cleanroomJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "Cleanroom", cleanroomJsonPath, cleanroomJsonText.Substring(0, Math.Min(cleanroomJsonText.Length, 1000)))); cleanroomJson = (JsonObject)ModBase.GetJson(cleanroomJsonText); } if (hasLiteLoader) { var liteLoaderJsonText = ModBase.ReadFile(liteLoaderJsonPath); if (!liteLoaderJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "LiteLoader", liteLoaderJsonPath, liteLoaderJsonText.Substring(0, Math.Min(liteLoaderJsonText.Length, 1000)))); liteLoaderJson = (JsonObject)ModBase.GetJson(liteLoaderJsonText); } if (hasFabric) { var fabricJsonText = ModBase.ReadFile(fabricJsonPath); if (!fabricJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "Fabric", fabricJsonPath, fabricJsonText.Substring(0, Math.Min(fabricJsonText.Length, 1000)))); fabricJson = (JsonObject)ModBase.GetJson(fabricJsonText); } if (hasLegacyFabric) { var legacyFabricJsonText = ModBase.ReadFile(legacyFabricJsonPath); if (!legacyFabricJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "Legacy Fabric", fabricJsonPath, legacyFabricJsonText.Substring(0, Math.Min(legacyFabricJsonText.Length, 1000)))); legacyFabricJson = (JsonObject)ModBase.GetJson(legacyFabricJsonText); } if (hasLabyMod) { var labyModJsonText = ModBase.ReadFile(labyModJsonPath); if (!labyModJsonText.StartsWithF("{")) throw new Exception(Lang.Text("Minecraft.Download.Error.JsonInvalid", "LabyMod", labyModJsonPath, labyModJsonText.Substring(0, Math.Min(labyModJsonText.Length, 1000)))); labyModJson = (JsonObject)ModBase.GetJson(labyModJsonText); } #endregion #region 处理 JSON 文件 // 获取 minecraftArguments var allArguments = (minecraftJson is not null ? (minecraftJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (labyModJson is not null ? (labyModJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (optiFineJson is not null ? (optiFineJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (forgeJson is not null ? (forgeJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (neoForgeJson is not null ? (neoForgeJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (liteLoaderJson is not null ? (liteLoaderJson["minecraftArguments"] ?? " ").ToString() : " ") + " " + (cleanroomJson is not null ? (cleanroomJson["minecraftArguments"] ?? " ").ToString() : " "); // 分割参数字符串 var rawArguments = allArguments.Split(" ").Where(l => !string.IsNullOrEmpty(l)).Select(l => l.Trim()).ToList(); var splitArguments = new List(); for (int i = 0, loopTo = rawArguments.Count - 1; i <= loopTo; i++) if (rawArguments[i].StartsWithF("-")) splitArguments.Add(rawArguments[i]); else if (splitArguments.Any() && splitArguments.Last().StartsWithF("-") && !splitArguments.Last().Contains(" ")) splitArguments[splitArguments.Count - 1] = splitArguments.Last() + " " + rawArguments[i]; else splitArguments.Add(rawArguments[i]); var realArguments = splitArguments.Distinct().ToList().Join(" "); // 合并 // 相关讨论见 #2801 if (mMCPackInfo is not null) { if (mMCPackInfo.isMinecraftOverrided) { ModBase.Log("[Download] 当前实例的 MC 核心已被修改,使用对应的 MMC 整合包参数"); outputJson = mMCPackInfo.overridedJson; } else { ModBase.Log("[Download] 存在无修改 MC 核心文件的 MMC 整合包信息,应用相关参数"); outputJson = minecraftJson; // 合并来自 MultiMC 的 JSON outputJson.Merge(mMCPackInfo.overridedJson); } } else { outputJson = minecraftJson; } if (hasOptiFine) { // 合并 OptiFine optiFineJson.Remove("releaseTime"); optiFineJson.Remove("time"); outputJson.Merge(optiFineJson); } if (hasForge) if (mMCPackInfo is null || !mMCPackInfo.isForgeOverrided) { // 合并 Forge forgeJson.Remove("releaseTime"); forgeJson.Remove("time"); outputJson.Merge(forgeJson); } if (hasNeoForge) if (mMCPackInfo is null || !mMCPackInfo.isNeoForgeOverrided) { // 合并 NeoForge neoForgeJson.Remove("releaseTime"); neoForgeJson.Remove("time"); outputJson.Merge(neoForgeJson); } if (hasCleanroom) if (mMCPackInfo is null || !mMCPackInfo.isCleanroomOverrided) { // 合并 Cleanroom cleanroomJson.Remove("releaseTime"); cleanroomJson.Remove("time"); outputJson.Merge(cleanroomJson); } if (hasLiteLoader) { // 合并 LiteLoader liteLoaderJson.Remove("releaseTime"); liteLoaderJson.Remove("time"); outputJson.Merge(liteLoaderJson); } if (hasFabric) if (mMCPackInfo is null || !mMCPackInfo.isFabricOverrided) { // 合并 Fabric fabricJson.Remove("releaseTime"); fabricJson.Remove("time"); outputJson.Merge(fabricJson); } if (hasLegacyFabric) if (mMCPackInfo is null || !mMCPackInfo.isFabricOverrided) { // 合并 Fabric legacyFabricJson.Remove("releaseTime"); legacyFabricJson.Remove("time"); outputJson.Merge(legacyFabricJson); } if (hasLabyMod) { // 合并 LabyMod labyModJson.Remove("releaseTime"); labyModJson.Remove("time"); if (outputJson is null) outputJson = new JsonObject(); outputJson.Merge(labyModJson); var labyModLib = (JsonObject)Requester.FetchJson( $"https://releases.r2.labymod.net/api/v1/libraries/{labyModChannel}.json", RequestParam.WithRetry); var labyModCore = (JsonObject)Requester.FetchJson( $"https://releases.r2.labymod.net/api/v1/manifest/{labyModChannel}/latest.json", RequestParam.WithRetry); var outputLibraries = new JsonArray(); var isolatedLibraries = new Dictionary(); var minecraftVersion = labyModJson["_minecraftVersion"]; foreach (var Library in labyModLib["isolated_libraries"].AsArray()) if (((JsonArray)Library["versions"]).Contains(minecraftVersion)) isolatedLibraries.Add(Library["name"].ToString(), true); foreach (var Library in labyModJson["libraries"].AsArray()) { var regexMatchResult = Library["name"].ToString().RegexSeek(RegexPatterns.CatchLwjglInLib); if (regexMatchResult is null || !isolatedLibraries.Contains(new KeyValuePair(regexMatchResult, true))) outputLibraries.Add(Library.DeepClone()); } foreach (var Library in labyModLib["libraries"].AsArray()) { var libraryUrl = Library?["url"]?.ToString() ?? ""; outputLibraries.Add(new JsonObject { ["name"] = Library?["name"]?.ToString(), ["downloads"] = new JsonObject { ["artifact"] = new JsonObject { ["path"] = libraryUrl.Substring(libraryUrl.LastIndexOfF("https://releases.r2.labymod.net/libraries/") + 42), ["sha1"] = Library?["sha1"]?.ToString(), ["size"] = Library?["size"]?.DeepClone(), ["url"] = libraryUrl } } }); } var labyModCommitReference = labyModCore["commitReference"]?.ToString() ?? ""; outputLibraries.Add(new JsonObject { ["name"] = "net.labymod:LabyMod:4", ["downloads"] = new JsonObject { ["artifact"] = new JsonObject { ["path"] = "net/labymod/LabyMod/4/LabyMod-4.jar", ["sha1"] = labyModCore["sha1"]?.ToString(), ["size"] = labyModCore["size"]?.DeepClone(), ["url"] = $"https://releases.r2.labymod.net/api/v1/download/labymod4/{labyModChannel}/{labyModCommitReference}.jar" } } }); outputJson["libraries"] = outputLibraries; outputJson.Add("labymod_data", new JsonObject { ["channelType"] = labyModChannel, ["commitReference"] = labyModCommitReference, ["version"] = labyModCore["labyModVersion"]?.ToString(), ["versionType"] = "release" }); } // 修改 if (realArguments is not null && !string.IsNullOrEmpty(realArguments.Replace(" ", ""))) outputJson["minecraftArguments"] = realArguments; if (mMCPackInfo is not null && mMCPackInfo.isMcArgsEdited) outputJson.Remove("minecraftArguments"); outputJson.Remove("_comment_"); outputJson.Remove("inheritsFrom"); outputJson.Remove("jar"); outputJson["id"] = outputName; #endregion #region 保存 ModBase.WriteFile(outputJsonPath, outputJson.ToString()); if ((minecraftJar ?? "") != (outputJar ?? "")) // 可能是同一个文件 { if (File.Exists(outputJar)) File.Delete(outputJar); ModBase.CopyFile(minecraftJar, outputJar); } ModBase.Log("[Download] 实例合并 " + outputName + " 完成"); #endregion } #endregion }