using System.IO; using System.IO.Compression; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using DotNet.Globbing; using PCL.Core.App; using PCL.Core.UI; using PCL.Core.App.Localization; using PCL.Core.Utils; namespace PCL; public class ExportOption : DependencyObject { public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( nameof(Title), typeof(string), typeof(ExportOption) ); public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register( nameof(Description), typeof(string), typeof(ExportOption) ); public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } public string Rules { get; set; } /// /// 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。 /// 如果 ShowRules 也为空,则始终显示。 /// public string ShowRules { get; set; } public bool DefaultChecked { get; set; } public bool RequireModLoader { get; set; } public bool RequireOptiFine { get; set; } public bool RequireModLoaderOrOptiFine { get; set; } } public partial class PageInstanceExport : IRefreshable { private string currentVersion = ""; public PageInstanceExport() { InitializeComponent(); Loaded += (_, _) => PageInstanceExport_Loaded(); CardOptions.MouseLeftButtonDown += CardOptions_MouseLeftButtonDown; BtnAdvancedExport.Click += ExportConfig; BtnAdvancedImport.Click += ImportConfig; BtnExport.Click += StartExport; TextExportName.GotFocus += TextExportName_GotFocus; CheckAdvancedModrinth.Change += CheckAdvancedModrinth_Change; CheckAdvancedInclude.Change += CheckAdvancedInclude_Change; } void IRefreshable.Refresh() { RefreshAll(); } private void PageInstanceExport_Loaded() { ModAnimation.AniControlEnabled += 1; if ((currentVersion ?? "") != (PageInstanceLeft.McInstance.PathInstance ?? "")) RefreshAll(); // 切换到了另一个实例,重置页面 ModAnimation.AniControlEnabled -= 1; } public void RefreshAll() { ModBase.Log("[Export] 刷新导出页面"); HintOptiFine.Visibility = PageInstanceLeft.McInstance.Info.HasOptiFine ? Visibility.Visible : Visibility.Collapsed; currentVersion = PageInstanceLeft.McInstance.PathInstance; TextExportName.Text = ""; TextExportName.HintText = PageInstanceLeft.McInstance.Name; TextExportVersion.Text = ""; TextExportVersion.HintText = "1.0.0"; CheckAdvancedInclude.Checked = false; CheckAdvancedModrinth.Checked = false; GetExportOption(CheckOptionsBasic).Description = PageInstanceLeft.McInstance.GetDefaultDescription(); ResetConfigOverrides(); ReloadAllSubOptions(); RefreshAllOptionsUI(); PanBack.ScrollToHome(); } // 自动填写整合包名称 private void TextExportName_GotFocus(object sender, RoutedEventArgs routedEventArgs) { if (string.IsNullOrEmpty(TextExportName.Text)) { TextExportName.Text = TextExportName.HintText; TextExportName.SelectionStart = TextExportName.Text.Length; } } // 勾选 Modrinth 上传模式时,禁止打包 PCL private void CheckAdvancedModrinth_Change(object sender, bool user) { if (CheckAdvancedModrinth.Checked == true) CheckOptionsPcl.Checked = false; CheckOptionsPcl.IsEnabled = (bool)!CheckAdvancedModrinth.Checked; } // 勾选"其他文件夹"时,同步勾选/取消所有子选项 private void CheckOptionsOtherFolders_Change(object sender, bool user) { if (!user) return; foreach (var child in PanOptionsOtherFolders.Children) if (child is MyCheckBox childBox) childBox.Checked = CheckOptionsOtherFolders.Checked; } // 勾选打包资源文件时,禁止开启 Modrinth 上传模式 private void CheckAdvancedInclude_Change(object sender, bool user) { if (CheckAdvancedInclude.Checked == true) CheckAdvancedModrinth.Checked = false; CheckAdvancedModrinth.IsEnabled = (bool)!CheckAdvancedInclude.Checked; } #region 子选项 private readonly string[] subOptionBlackList = new[] { "Quark Programmer Art.zip", "+ EuphoriaPatches_" }; /// /// 动态生成子文件夹下的选项,例如资源包、存档等。 /// private void ReloadAllSubOptions() { ReloadSubOptions(PanOptionsResourcePacks, true, true, "resourcepacks", "texturepacks"); ReloadSubOptions(PanOptionsSaves, false, true, "saves"); ReloadSubOptions(PanOptionsShaderPacks, true, true, "shaderpacks"); ReloadOtherFolders(); } /// /// 扫描实例根目录下未被已有选项覆盖的文件夹,生成独立的复选框。 /// private void ReloadOtherFolders() { PanOptionsOtherFolders.Children.Clear(); var pathIndie = PageInstanceLeft.McInstance.PathIndie; var rootDir = new DirectoryInfo(pathIndie); if (!rootDir.Exists) { CheckOptionsOtherFolders.Visibility = Visibility.Collapsed; return; } var coveredFolders = new HashSet(StringComparer.OrdinalIgnoreCase) { // 模组 "mods", "coremods", "lib", // 整合包重要数据 "addons", "multiblocked", "modpack-update-checker", "global_packs", "global_resource_packs", "global_data_packs", "optional_data_packs", "maps", "mods-resourcepacks", "matmos", "resource_assorts", "patchouli_books", "datapacks", "openloader", "worldshape", "resources", "scripts", "structures", "fontfiles", "oresources", "packmenu", "craftpresence", "pointblanks", // 模组设置 "config", "defaultconfigs", "journeymap", "local", "essential", "gg.essential.mod", "CustomSkinLoader", // 地图 "xaero", "XaeroWaypoints", "XaeroWorldMap", // 资源包 "resourcepacks", "texturepacks", // 光影 "shaderpacks", // 截图 / 结构 / 录像 "screenshots", "schematics", "replay_recordings", "replay_videos", // 存档 / 设置文件夹 "saves", "configureddefaults", // 始终跳过(大量文件或无用缓存) "assets", "versions", "libraries", "structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache", // PCL 单独处理 "PCL", }; var coveredPrefixes = new[] { "kubejs", "template" }; var coveredSuffixes = new[] { "-natives" }; foreach (var subDir in rootDir.EnumerateDirectories()) { if (coveredFolders.Contains(subDir.Name)) continue; if (coveredPrefixes.Any(p => subDir.Name.StartsWithF(p))) continue; if (coveredSuffixes.Any(s => subDir.Name.EndsWithF(s, true))) continue; PanOptionsOtherFolders.Children.Add(new MyCheckBox { Tag = new ExportOption { Title = subDir.Name, DefaultChecked = false, Rules = ModBase.EscapeLikePattern($"{subDir.Name}/") } }); } CheckOptionsOtherFolders.Visibility = PanOptionsOtherFolders.Children.Count > 0 ? Visibility.Visible : Visibility.Collapsed; } private void ReloadSubOptions(StackPanel panel, bool acceptCompressedFile, bool acceptFolder, params string[] folders) { panel.Children.Clear(); foreach (var Folder in folders) { var targetFolder = new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie + Folder); if (!targetFolder.Exists) continue; // 查找文件夹下的对应项 if (acceptCompressedFile) foreach (var File in targetFolder.EnumerateFiles("*.zip").Concat(targetFolder.EnumerateFiles("*.rar"))) { if (subOptionBlackList.Any(b => File.Name.ContainsF(b))) continue; panel.Children.Add(new MyCheckBox { Tag = new ExportOption { Title = File.Name, DefaultChecked = true, Rules = ModBase.EscapeLikePattern($"{Folder}/{File.Name}") } }); if (Folder == "shaderpacks") // 处理光影包的配置文件 { var shaderConfig = new FileInfo(Path.Combine(File.Directory.FullName, $"{File.Name}.txt")); if (shaderConfig.Exists) panel.Children.Add(new MyCheckBox { Margin = new Thickness(30, 0, 0, 0), Tag = new ExportOption { Title = $"{shaderConfig.Name}", DefaultChecked = true, Description = Lang.Text("Instance.Export.Config.ShaderConfigSuffix"), Rules = ModBase.EscapeLikePattern($"{Folder}/{shaderConfig.Name}") } }); } } if (acceptFolder) foreach (var SubFolder in targetFolder.EnumerateDirectories().OrderByDescending(f => f.LastWriteTime)) { if (subOptionBlackList.Any(b => SubFolder.Name.ContainsF(b))) continue; if (!SubFolder.EnumerateFileSystemInfos().Any()) continue; var newCheckBox = new MyCheckBox { Tag = new ExportOption { Title = SubFolder.Name, DefaultChecked = true, Rules = ModBase.EscapeLikePattern($"{Folder}/{SubFolder.Name}/") } }; if (ReferenceEquals(panel, PanOptionsSaves)) GetExportOption(newCheckBox).Description = Lang.Date(SubFolder.LastWriteTime, "g"); panel.Children.Add(newCheckBox); if (Folder == "shaderpacks") // 处理文件夹形式光影包的配置文件 { var shaderConfig = new FileInfo(Path.Combine(targetFolder.FullName, $"{SubFolder.Name}.txt")); if (shaderConfig.Exists) panel.Children.Add(new MyCheckBox { Margin = new Thickness(30, 0, 0, 0), Tag = new ExportOption { Title = $"{shaderConfig.Name}", DefaultChecked = true, Description = Lang.Text("Instance.Export.Config.ShaderConfigSuffix"), Rules = ModBase.EscapeLikePattern($"{Folder}/{shaderConfig.Name}") } }); } } } } #endregion #region 选项 /// /// 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。 /// private void RefreshAllOptionsUI() { // 预先归纳所有至多二级的文件/文件夹 var allEntries = new List(); bool IsValidDirectory(DirectoryInfo folder) { try { return folder.Exists && folder.EnumerateFileSystemInfos() .Any(i => !subOptionBlackList.Any(b => i.Name.ContainsF(b))); } catch { return false; } } ; // 检查文件夹不为空 // 一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接) var pathInfo = new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie); allEntries.AddRange(pathInfo.EnumerateFiles().Select(f => f.Name)); foreach (var SubFolder in pathInfo.EnumerateDirectories().Where(IsValidDirectory)) { allEntries.Add($@"{SubFolder.Name}\"); allEntries.AddRange(SubFolder.EnumerateFiles().Select(f => $@"{SubFolder.Name}\{f.Name}")); allEntries.AddRange(SubFolder.EnumerateDirectories().Where(IsValidDirectory) .Select(d => $@"{SubFolder.Name}\{d.Name}\")); } ModBase.Log($"[Export] 共发现 {allEntries.Count} 个可行的二级文件/文件夹"); // 确认选项是否应该被显示 bool IsVisible(ExportOption targetOption) { // 检查需要 OptiFine 或 Mod 加载器 if (targetOption.RequireOptiFine && !PageInstanceLeft.McInstance.Info.HasOptiFine) return false; if (targetOption.RequireModLoader && !PageInstanceLeft.McInstance.Modable) return false; if (targetOption.RequireModLoaderOrOptiFine && !PageInstanceLeft.McInstance.Info.HasOptiFine && !PageInstanceLeft.McInstance.Modable) return false; // 粗略检查是否可能有符合规则的文件/文件夹 return StandardizeLines((targetOption.Rules ?? targetOption.ShowRules).Split('|'), true).Any(rule => { if (rule.StartsWithF("!")) return false; // 只看正向规则 // 检查前两级 try { if (allEntries.Any(entry => LikeString(entry, rule))) return true; } catch (Exception ex) { ModBase.Log( ex, $"错误的规则:{rule}", ModBase.LogLevel.Hint, userSummary: Lang.Text("Instance.Export.Error.OperationFailed")); return false; } // 粗略检查所有级 rule = rule.Trim("*?".ToCharArray()); if (rule.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries).Count() >= 3) { if (rule.EndsWithF(@"\")) return IsValidDirectory(new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie + rule)); // 文件夹有效 return File.Exists(PageInstanceLeft.McInstance.PathIndie + rule); // 文件有效 } return false; }); } ; // 逐个检查选项 foreach (var CheckBox in GetAllOptions(true)) { var targetOption = GetExportOption(CheckBox); // 名称与简介 CheckBox.Inlines.Clear(); CheckBox.Inlines.Add(new Run(targetOption.Title)); if (!string.IsNullOrEmpty(targetOption.Description)) CheckBox.Inlines.Add(new Run(" " + targetOption.Description) { Foreground = ThemeManager.colorGray5 }); // 可见性、默认勾选 if (string.IsNullOrEmpty(targetOption.Rules) && string.IsNullOrEmpty(targetOption.ShowRules)) { CheckBox.Visibility = Visibility.Visible; CheckBox.Checked = targetOption.DefaultChecked; } else { var pass = IsVisible(targetOption); CheckBox.Visibility = pass ? Visibility.Visible : Visibility.Collapsed; CheckBox.Checked = targetOption.DefaultChecked && pass; } } } /// /// 对文本行进行标准化处理,以便使用 Like 进行匹配。 /// private IEnumerable StandardizeLines(IEnumerable raw, bool addSuffixStarToFolderPath) { foreach (var IgnoreLineRaw in raw) { var ignoreLine = IgnoreLineRaw; ignoreLine = ignoreLine.Trim(); if (string.IsNullOrEmpty(ignoreLine) || ignoreLine.StartsWithF("#") || ignoreLine.StartsWithF("=")) continue; ignoreLine = ignoreLine.Replace("/", @"\"); yield return ignoreLine + (ignoreLine.EndsWithF(@"\") && addSuffixStarToFolderPath ? "**" : ""); } } /// /// 获取所有可作为选项的 CheckBox。 /// private IEnumerable GetAllOptions(bool includeHidden) { foreach (var Element in PanOptions.Children) { if (!includeHidden && ((UIElement)Element).Visibility != Visibility.Visible) continue; if (Element is MyCheckBox) yield return (MyCheckBox)Element; else if (Element is StackPanel) foreach (var SubElement in ((StackPanel)Element).Children) { if (!includeHidden && ((UIElement)SubElement).Visibility != Visibility.Visible) continue; if (SubElement is MyCheckBox) yield return (MyCheckBox)SubElement; } } } /// /// 获取该 CheckBox 对应的 ExportOption。 /// private ExportOption GetExportOption(MyCheckBox checkBox) { return (ExportOption)checkBox.Tag; } #endregion #region 配置文件 private const string sperator = "=============================================================="; // ================ 导出内容段 ================ /// /// 从配置文件中读取的规则。 /// 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。 /// private List RulesOverrides { get => field; set { field = value; if (value is null) { BtnOverrideCancel.Visibility = Visibility.Collapsed; PanOptions.Visibility = Visibility.Visible; CardOptions.Inlines.Clear(); CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionListTitle")) { FontWeight = FontWeights.Bold }); } else { BtnOverrideCancel.Visibility = Visibility.Visible; PanOptions.Visibility = Visibility.Collapsed; CardOptions.Inlines.Clear(); CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionListTitle") + ":    ") { FontWeight = FontWeights.Bold }); CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionList.FromConfig")) { FontWeight = FontWeights.Normal }); } } } /// /// 获取当前实际生效的所有规则。 /// private IEnumerable GetAllRules() { if (RulesOverrides is not null) { // 返回覆盖的列表 foreach (var Rule in RulesOverrides) yield return Rule; } else { // 从当前勾选的所有选项中获取所有规则行 yield return ""; yield return "# " + Lang.Text("Instance.Export.Config.Comment.ModifyRules"); yield return "# " + Lang.Text("Instance.Export.Config.Comment.ReverseMatch"); yield return ""; foreach (var CheckBox in GetAllOptions(false)) { if (CheckBox.Checked == false) continue; var targetOption = GetExportOption(CheckBox); if (targetOption.Rules is null) continue; yield return $"# {targetOption.Title}"; foreach (var Rule in targetOption.Rules.Split('|')) yield return Rule; yield return ""; } yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExcludedFiles"); yield return "!*.log"; yield return "!*.dat_old"; yield return "!*.BakaCoreInfo"; yield return "!hmclversion.cfg"; yield return "!log4j2.xml"; yield return ""; } } // ================ 追加内容段 ================ private List extraFiles; /// /// 获取当前实际生效的追加内容。 /// private IEnumerable GetExtraFileLines() { if (extraFiles is not null) { // 返回覆盖的列表 foreach (var File in extraFiles) yield return File; } else { // 从当前勾选的所有选项中获取所有规则行 yield return ""; yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExtraFiles"); yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExtraFiles2"); yield return ""; } } // ================ 重置 ================ /// /// 重置配置文件所带来的影响。 /// private void ResetConfigOverrides() { RulesOverrides = null; configPackPath = null; extraFiles = null; PanBack.ScrollToHome(); } private void CardOptions_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (RulesOverrides is null) return; ResetConfigOverrides(); } // ================ 保存 / 读取 ================ // 保存配置文件 private void ExportConfig(object sender, MouseButtonEventArgs e) { try { var configPath = SystemDialogs.SelectSaveFile(Lang.Text("Instance.Export.SelectFileLocation"), "export_config.txt", Lang.Text("Instance.Export.Config.FileFilter"), (string?)States.System.ExportConfigPath); if (string.IsNullOrEmpty(configPath)) return; States.System.ExportConfigPath = configPath; var configLines = new List(); // ini 段 configLines.Add("Name:" + TextExportName.Text); configLines.Add("Version:" + TextExportVersion.Text); configLines.Add(""); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.IncludeLauncher")); configLines.Add("IncludeLauncher:" + CheckOptionsPcl.Checked); configLines.Add(""); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.IncludeLauncherCustom")); configLines.Add("IncludeLauncherCustom:" + CheckOptionsPclCustom.Checked); configLines.Add(""); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles2")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles3")); configLines.Add("DontCheckHostedAssets:" + CheckAdvancedInclude.Checked); configLines.Add(""); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth2")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth3")); configLines.Add("ModrinthUploadMode:" + CheckAdvancedModrinth.Checked); configLines.Add(""); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath2")); configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath3")); configLines.Add("PackPath:" + (configPackPath ?? "")); configLines.Add(""); // 导出内容段 configLines.Add(sperator); configLines.AddRange(GetAllRules()); // 追加内容段 configLines.Add(sperator); configLines.AddRange(GetExtraFileLines()); // 结束 ModBase.WriteFile(configPath, configLines.Join("\r\n")); HintService.Hint(Lang.Text("Instance.Export.SaveSuccess", configPath), HintType.Success); ModBase.OpenExplorer(configPath); } catch (Exception ex) { ModBase.Log( ex, "保存配置失败", ModBase.LogLevel.Msgbox, userSummary: Lang.Text("Instance.Export.Error.OperationFailed")); } } #region 配置文件核心读取逻辑 /// /// 从指定路径读取配置文件(供按钮和拖放调用) /// /// 配置文件路径 private void ReadConfigFile(string configPath) { try { // 保存配置文件路径到缓存 States.System.ExportConfigPath = configPath; var fileContent = ModBase.ReadFile(configPath); var segments = fileContent.Split(sperator); if (segments.Length == 0) { HintService.Hint(Lang.Text("Instance.Export.Config.Invalid"), HintType.Error); return; } // === 解析INI段 === var ini = new Dictionary(); foreach (var LineRaw in segments[0].Split("\r\n".ToCharArray())) { var line = LineRaw; line = line.Trim(); if (string.IsNullOrEmpty(line) || line.StartsWithF("#") || line.StartsWithF("=")) continue; var index = line.IndexOfF(":"); if (index > 0) ini[line.Substring(0, index)] = line.Substring(index + 1); } // 赋值到界面控件 TextExportName.Text = ini.GetOrDefault("Name", ""); TextExportVersion.Text = ini.GetOrDefault("Version", ""); CheckOptionsPcl.Checked = Convert.ToBoolean(ini.GetOrDefault("IncludeLauncher", false.ToString())); CheckOptionsPclCustom.Checked = Convert.ToBoolean(ini.GetOrDefault("IncludeLauncherCustom", true.ToString())); CheckAdvancedModrinth.Checked = Convert.ToBoolean(ini.GetOrDefault("ModrinthUploadMode", false.ToString())); CheckAdvancedInclude.Checked = Convert.ToBoolean(ini.GetOrDefault("DontCheckHostedAssets", false.ToString())); configPackPath = ini.GetOrDefault("PackPath"); // === 解析导出内容段 === RulesOverrides = segments[1].Replace("\r", "\n") .Replace("\n" + "\n", "\n").Split("\n").ToList(); // === 解析追加内容段 === if (segments.Length > 2) extraFiles = segments[2].Replace("\r", "\n") .Replace("\n" + "\n", "\n").Split("\n").ToList(); else extraFiles = null; // 提示成功 HintService.Hint(Lang.Text("Instance.Export.ReadSuccess", configPath), HintType.Success); } catch (Exception ex) { ModBase.Log( ex, $"读取配置文件失败:{configPath}", ModBase.LogLevel.Msgbox, userSummary: Lang.Text("Instance.Export.Error.OperationFailed")); } } #endregion // 读取配置文件 private void ImportConfig(object sender, MouseButtonEventArgs e) { try { var configPath = SystemDialogs.SelectFile(Lang.Text("Instance.Export.Config.FileFilter"), Lang.Text("Instance.Export.SelectConfigFile"), (string?)States.System.ExportConfigPath); if (string.IsNullOrEmpty(configPath)) return; // 调用核心读取逻辑 ReadConfigFile(configPath); } catch (Exception ex) { ModBase.Log( ex, "选择配置文件失败", ModBase.LogLevel.Msgbox, userSummary: Lang.Text("Instance.Export.Error.OperationFailed")); } } #region 拖放事件处理 /// /// 文件拖入界面时触发:验证文件类型 /// private void PanAllBack_DragEnter(object sender, DragEventArgs e) { // 检查是否包含文件拖放数据 if (e.Data.GetDataPresent(DataFormats.FileDrop)) { // 获取拖入的文件路径数组 var files = (string[])e.Data.GetData(DataFormats.FileDrop); // 验证:仅允许单个.txt文件 if (files.Length == 1 && files[0].EndsWithF(".txt", true)) e.Effects = DragDropEffects.Copy; // 设置拖放效果为“复制” else e.Effects = DragDropEffects.None; // 不允许拖放 } else { e.Effects = DragDropEffects.None; } e.Handled = true; } /// /// 文件放下时触发:读取配置文件 /// private void PanAllBack_Drop(object sender, DragEventArgs e) { // 获取拖入的文件路径 if (e.Data.GetDataPresent(DataFormats.FileDrop)) { var files = (string[])e.Data.GetData(DataFormats.FileDrop); var configPath = files[0]; // 调用核心读取逻辑 ReadConfigFile(configPath); } e.Handled = true; } #endregion #endregion #region 导出 /// /// 配置文件中指定的导出位置。 /// private string configPackPath; /// /// 开始导出。 /// private void StartExport(object sender, MouseButtonEventArgs e) { var packName = string.IsNullOrEmpty(TextExportName.Text) ? TextExportName.HintText : TextExportName.Text; var packVersion = string.IsNullOrEmpty(TextExportVersion.Text) ? "1.0.0" : TextExportVersion.Text; // 重复任务检查 var loaderName = Lang.Text("Instance.Export.ExportTask.Prefix") + packName; foreach (var OngoingLoader in ModLoader.loaderTaskbar) { if ((OngoingLoader.name ?? "") != (loaderName ?? "")) continue; ModMain.frmMain.PageChange(FormMain.PageType.TaskManager); return; } // 确认导出位置 string packPath = null; if (!string.IsNullOrWhiteSpace(configPackPath) && !configPackPath.EndsWithF(@"\") && !configPackPath.EndsWithF("/")) try { Directory.CreateDirectory(ModBase.GetPathFromFullPath(configPackPath)); packPath = configPackPath; ModBase.Log($"[Export] 使用配置文件中指定的导出路径:{configPackPath}"); } catch (Exception ex) { ModBase.Log(ex, $"无法使用配置文件中指定的导出路径({configPackPath})"); if (ModMain.MyMsgBox( Lang.Text("Instance.Export.PackPathInvalid.WithDetail", configPackPath, ex.ToString()), Lang.Text("Instance.Export.PackPathInvalid.Title"), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) == 2) return; } if (packPath is null) { var extensions = new List(); if (CheckAdvancedModrinth.Checked == false) extensions.Add(Lang.Text("Instance.Export.ZipFilter")); if (CheckOptionsPcl.Checked == false) extensions.Add(Lang.Text("Instance.Export.MrpackFilter")); packPath = SystemDialogs.SelectSaveFile(Lang.Text("Instance.Export.SelectSaveLocation"), packName + (string.IsNullOrEmpty(TextExportVersion.Text) ? "" : " " + TextExportVersion.Text), extensions.Join("|")); ModBase.Log($"[Export] 手动指定的导出路径:{packPath}"); } if (string.IsNullOrEmpty(packPath)) return; // 缓存所需参数 var cacheFolder = ModMain.RequestTaskTempFolder(); var overridesFolder = Path.Combine(cacheFolder, "modpack", "overrides"); var mcInstance = PageInstanceLeft.McInstance; var pathIndie = mcInstance.PathIndie; var checkHostedAssets = (bool)!CheckAdvancedInclude.Checked; var modrinthUploadMode = (bool)CheckAdvancedModrinth.Checked; var includePCL = (bool)CheckOptionsPcl.Checked; var includePCLCustom = (bool)(includePCL ? CheckOptionsPclCustom.Checked : (bool?)false); var allRules = StandardizeLines(GetAllRules(), true).ToList(); var allExtraFiles = StandardizeLines(GetExtraFileLines(), false).ToList(); ModBase.Log($"[Export] 准备导出整合包,共有 {allRules.Count} 条规则,{allExtraFiles.Count} 条追加内容行"); // 构造步骤加载器 var loaders = new List(); #region 准备 PCL 文件 #if !RELEASE if (includePCL) loaders.Add(new ModLoader.LoaderTask(Lang.Text("Instance.Export.Task.DownloadPclRelease"), loader => { UpdateManager.DownloadLatestPCL(loader); ModBase.CopyFile(Path.Combine(ModBase.pathTemp, "CE-Latest.exe"), Path.Combine(cacheFolder, "Plain Craft Launcher.exe")); }) { ProgressWeight = 0.5d, block = false }); #endif #endregion #region 复制文件 loaders.Add(new ModLoader.LoaderTask>( Lang.Text("Instance.Export.Task.CopyContent"), loader => { loader.output = []; // 复制实例文件 var progress = 0; Action searchFolder = null; searchFolder = folder => { // 文件夹:进一步搜索 foreach (var SubFolder in folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) { // 跳过部分又没用文件又多的文件夹,加快搜索 if ((folder.FullName ?? "") == (pathIndie ?? "") && new[] { "assets", "versions", "libraries" }.Contains(SubFolder.Name)) continue; if (new[] { "structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache" }.Contains( SubFolder.Name)) continue; searchFolder(SubFolder); } // 文件:检查规则并复制 foreach (var Entry in folder.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) { var relativePath = Entry.FullName.AfterFirst(pathIndie); // 检查规则 var shouldKeep = false; foreach (var Rule in allRules) { var revert = Rule.StartsWith("!"); if (LikeString(relativePath, Rule.TrimStart('!'))) shouldKeep = !revert; } if (!shouldKeep) continue; var targetPath = Path.Combine(overridesFolder, relativePath); ModBase.CopyFile(Entry.FullName, targetPath); // 若为压缩包,考虑联网获取路径 if (checkHostedAssets && new[] { ".zip", ".rar", ".jar", ".disabled", ".old" }.Contains(Entry.Extension.ToLower()) && new[] { "mods", "packs", "openloader", "resource" }.Any(s => relativePath.Contains(s))) { var modFile = new ModLocalComp.LocalCompFile(targetPath); var unused = modFile.ModrinthHash; // 提前计算 Hash unused = modFile.CurseForgeHash.ToString(); loader.output.Add(modFile); } // 更新进度(进度并不准确,主要突出一个我还没似) progress += 1; if (progress == 25) { loader.Progress += (0.94d - loader.Progress) * 0.012d; progress = 0; } } }; searchFolder(new DirectoryInfo(pathIndie)); ModBase.Log($"[Export] 复制 overrides 文件完成,有 {loader.output.Count} 个文件需要联网检查"); loader.Progress = 0.95d; // 复制追加内容到根目录 var baseFolder = includePCL ? cacheFolder : Path.Combine(cacheFolder, "modpack"); foreach (var Line in allExtraFiles) if (Line.EndsWithF(@"\") || Line.EndsWithF("/")) { if (Directory.Exists(Line)) ModBase.CopyDirectory(Line, Path.Combine(baseFolder, ModBase.GetFolderNameFromPath(Line)) + @"\"); else HintService.Hint(Lang.Text("Instance.Export.Config.FolderNotFound", Line), HintType.Error); } else if (File.Exists(Line)) { ModBase.CopyFile(Line, Path.Combine(baseFolder, ModBase.GetFileNameFromPath(Line))); } else { HintService.Hint(Lang.Text("Instance.Export.Config.FileNotFound", Line), HintType.Error); } loader.Progress = 0.97d; // 复制 PCL 实例设置 ModBase.CopyDirectory(Path.Combine(mcInstance.PathInstance, "PCL"), Path.Combine(overridesFolder, "PCL")); #if RELEASE // 复制 PCL 本体 if (includePCL) ModBase.CopyFile(Basics.ExecutablePath, Path.Combine(cacheFolder, Basics.ExecutableName)); #endif // 复制 PCL 个性化内容 if (includePCLCustom) { if (Directory.Exists(Path.Combine(ModBase.exePath, "PCL", "Pictures"))) ModBase.CopyDirectory(Path.Combine(ModBase.exePath, "PCL", "Pictures"), Path.Combine(cacheFolder, "PCL", "Pictures")); if (Directory.Exists(Path.Combine(ModBase.exePath, "PCL", "Musics"))) ModBase.CopyDirectory(Path.Combine(ModBase.exePath, "PCL", "Musics"), Path.Combine(cacheFolder, "PCL", "Musics")); if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml"))) ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml"), Path.Combine(cacheFolder, "PCL", "Custom.xaml")); if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Setup.ini"))) ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Setup.ini"), Path.Combine(cacheFolder, "PCL", "Setup.ini")); if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "hints.txt"))) ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "hints.txt"), Path.Combine(cacheFolder, "PCL", "hints.txt")); if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Logo.png"))) ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Logo.png"), Path.Combine(cacheFolder, "PCL", "Logo.png")); } }) { ProgressWeight = 5d }); #endregion #region 联网检查 loaders.Add( new ModLoader.LoaderTask, Dictionary>>(Lang.Text("Instance.Export.Task.FetchFileInfo"), loader => { loader.output = new Dictionary>(); if (!checkHostedAssets) { ModBase.Log("[Export] 要求跳过联网获取步骤"); return; } if (!loader.input.Any()) { ModBase.Log("[Export] 没有需要联网检查的文件,跳过联网获取步骤"); return; } // 分平台获取下载地址 var endedThreadCount = 0; var failedExceptions = new List(); // 从 Modrinth 获取信息 // 查找对应的文件 // 写入下载地址 ModBase.RunInNewThread(() => { try { var modrinthHashes = loader.input.Select(m => m.ModrinthHash); var modrinthRaw = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.modrinth.com/v2/version_files", "POST", $"{{\"hashes\": [\"{modrinthHashes.Join("\",\"")}\"], \"algorithm\": \"sha1\"}}", "application/json")); foreach (var ModFile in loader.input) { if (!modrinthRaw.ContainsKey(ModFile.ModrinthHash)) continue; if ((string)modrinthRaw[ModFile.ModrinthHash]?["files"]?[0]["hashes"]?["sha1"] != ModFile.ModrinthHash) continue; loader.output.AddToList(ModFile, (string)modrinthRaw[ModFile.ModrinthHash]["files"][0]["url"]); } ModBase.Log($"[Export] 从 Modrinth 获取到 {modrinthRaw.Count} 个本地资源项的对应信息"); } catch (Exception ex) { ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败"); failedExceptions.Add(ex); } finally { endedThreadCount += 1; loader.Progress += 0.45d; } }, "Modrinth - " + loaderName); // 从 CurseForge 获取信息 // 查找对应的文件 // 写入下载地址 ModBase.RunInNewThread(() => { try { if (modrinthUploadMode) return; var curseForgeHashes = loader.input.Select(m => m.CurseForgeHash); var curseForgeRaw = (JsonNode)((JsonObject)ModBase.GetJson( ModDownload.DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", "POST", $"{{\"fingerprints\": [{curseForgeHashes.Join(",")}]}}", "application/json")))["data"][ "exactMatches"]; foreach (JsonObject ResultJson in curseForgeRaw.AsArray()) { if (!ResultJson.ContainsKey("file")) continue; var file = (JsonObject)ResultJson["file"]; if (string.IsNullOrEmpty((string)file["downloadUrl"])) continue; var modFile = loader.input.FirstOrDefault(m => m.CurseForgeHash == file["fileFingerprint"].ToObject()); if (modFile is null) continue; loader.output.AddToList(modFile, ModComp.CompFile.HandleCurseForgeDownloadUrls(file["downloadUrl"].ToString())); } ModBase.Log($"[Export] 从 CurseForge 获取到 {curseForgeRaw.AsArray().Count} 个本地资源项的对应信息"); } catch (Exception ex) { ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败"); failedExceptions.Add(ex); } finally { endedThreadCount += 1; loader.Progress += 0.45d; } }, "CurseForge - " + loaderName); // Modrinth 上传模式下,不能从 CurseForge 获取信息 // 等待线程结束 while (endedThreadCount != 2) { if (loader.IsAborted) return; Thread.Sleep(10); } // 若失败,确认是否继续 if (failedExceptions.Count == 1) { if (ModMain.MyMsgBox( Lang.Text("Instance.Export.NetCheckPartialFailed.Message"), Lang.Text("Instance.Export.NetCheckPartialFailed.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel")) == 2) throw failedExceptions.First(); } else if (failedExceptions.Count > 1) { if (ModMain.MyMsgBox( Lang.Text("Instance.Export.NetCheckAllFailed.Message"), Lang.Text("Instance.Export.NetCheckAllFailed.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel")) == 2) throw failedExceptions.First(); } }) { show = checkHostedAssets, ProgressWeight = checkHostedAssets ? 2d : 0.01d }); #endregion #region 生成压缩包 loaders.Add(new ModLoader.LoaderTask>, int>( Lang.Text("Instance.Export.Task.CreateArchive"), loader => { // 整理文件列表 var files = new JsonArray(); foreach (var Pair in loader.input) { var modFile = Pair.Key; files.Add(new JsonObject { { "path", Path.GetRelativePath(overridesFolder, modFile.path).Replace(@"\", "/") }, { "hashes", new JsonObject { { "sha1", modFile.ModrinthHash }, { "sha512", ModBase.GetFileSHA512(modFile.path) } } }, { "downloads", new JsonArray(Pair.Value.OrderByDescending(u => u.Contains("modrinth.com")).Select(s => (JsonNode)s).ToArray()) }, { "fileSize", new FileInfo(modFile.path).Length } }); File.Delete(modFile.path); } loader.Progress = 0.2d; // 导出最终 JSON 文件 var dependencies = new JsonObject { { "minecraft", mcInstance.Info.VanillaName } }; if (mcInstance.Info.HasForge) dependencies.Add("forge", mcInstance.Info.Forge); if (mcInstance.Info.HasFabric) dependencies.Add("fabric-loader", mcInstance.Info.Fabric); if (mcInstance.Info.HasNeoForge) dependencies.Add("neoforge", mcInstance.Info.NeoForge); var resultJson = new JsonObject { { "game", "minecraft" }, { "formatVersion", 1 }, { "versionId", packVersion }, { "name", packName }, { "summary", mcInstance.Desc }, { "files", files }, { "dependencies", dependencies } }; File.WriteAllText(Path.Combine(cacheFolder, "modpack", "modrinth.index.json"), resultJson.ToJsonString(new JsonSerializerOptions(JsonCompat.SerializerOptions) { WriteIndented = true })); // 打包 Directory.CreateDirectory(ModBase.GetPathFromFullPath(packPath)); if (File.Exists(packPath)) File.Delete(packPath); if (includePCL) { // 首次压缩整合包 ZipFile.CreateFromDirectory(Path.Combine(cacheFolder, "modpack"), Path.Combine(cacheFolder, "modpack.mrpack")); loader.Progress = 0.5d; Directory.Delete(Path.Combine(cacheFolder, "modpack"), true); loader.Progress = 0.6d; // 二次压缩整合包 ZipFile.CreateFromDirectory(cacheFolder, packPath); loader.Progress = 0.9d; } else { // 直接压缩整合包 ZipFile.CreateFromDirectory(Path.Combine(cacheFolder, "modpack"), packPath); loader.Progress = 0.8d; } Directory.Delete(cacheFolder, true); ModBase.OpenExplorer(packPath); }) { ProgressWeight = 6d }); #endregion // 启动 var mainLoader = new ModLoader.LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; mainLoader.Start(); ModLoader.LoaderTaskbarAdd(mainLoader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); ModMain.frmMain.PageChange(FormMain.PageType.TaskManager); } #endregion private static bool LikeString(string input, string pattern) { pattern = pattern.Replace("#", "[0-9]"); var options = new GlobOptions { Evaluation = { CaseInsensitive = true } }; var glob = Glob.Parse(pattern, options); return glob.IsMatch(input); } }