Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.cs
T

1227 lines
51 KiB
C#
Raw Normal View History

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; }
/// <summary>
/// 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。
/// 如果 ShowRules 也为空,则始终显示。
/// </summary>
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_" };
/// <summary>
/// 动态生成子文件夹下的选项,例如资源包、存档等。
/// </summary>
private void ReloadAllSubOptions()
{
ReloadSubOptions(PanOptionsResourcePacks, true, true, "resourcepacks", "texturepacks");
ReloadSubOptions(PanOptionsSaves, false, true, "saves");
ReloadSubOptions(PanOptionsShaderPacks, true, true, "shaderpacks");
ReloadOtherFolders();
}
/// <summary>
/// 扫描实例根目录下未被已有选项覆盖的文件夹,生成独立的复选框。
/// </summary>
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<string>(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 选项
/// <summary>
/// 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。
/// </summary>
private void RefreshAllOptionsUI()
{
// 预先归纳所有至多二级的文件/文件夹
var allEntries = new List<string>();
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;
}
}
}
/// <summary>
/// 对文本行进行标准化处理,以便使用 Like 进行匹配。
/// </summary>
private IEnumerable<string> StandardizeLines(IEnumerable<string> 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 ? "**" : "");
}
}
/// <summary>
/// 获取所有可作为选项的 CheckBox。
/// </summary>
private IEnumerable<MyCheckBox> 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;
}
}
}
/// <summary>
/// 获取该 CheckBox 对应的 ExportOption。
/// </summary>
private ExportOption GetExportOption(MyCheckBox checkBox)
{
return (ExportOption)checkBox.Tag;
}
#endregion
#region 配置文件
private const string sperator = "==============================================================";
// ================ 导出内容段 ================
/// <summary>
/// 从配置文件中读取的规则。
/// 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。
/// </summary>
private List<string> 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 });
}
}
}
/// <summary>
/// 获取当前实际生效的所有规则。
/// </summary>
private IEnumerable<string> 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<string> extraFiles;
/// <summary>
/// 获取当前实际生效的追加内容。
/// </summary>
private IEnumerable<string> 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 "";
}
}
// ================ 重置 ================
/// <summary>
/// 重置配置文件所带来的影响。
/// </summary>
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<string>();
// 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 配置文件核心读取逻辑
/// <summary>
/// 从指定路径读取配置文件(供按钮和拖放调用)
/// </summary>
/// <param name="configPath">配置文件路径</param>
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<string, string>();
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 拖放事件处理
/// <summary>
/// 文件拖入界面时触发:验证文件类型
/// </summary>
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;
}
/// <summary>
/// 文件放下时触发:读取配置文件
/// </summary>
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 导出
/// <summary>
/// 配置文件中指定的导出位置。
/// </summary>
private string configPackPath;
/// <summary>
/// 开始导出。
/// </summary>
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<string>();
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<ModLoader.LoaderBase>();
#region 准备 PCL 文件
#if !RELEASE
if (includePCL)
loaders.Add(new ModLoader.LoaderTask<int, int>(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<int, List<ModLocalComp.LocalCompFile>>(
Lang.Text("Instance.Export.Task.CopyContent"), loader =>
{
loader.output = [];
// 复制实例文件
var progress = 0;
Action<DirectoryInfo> 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<List<ModLocalComp.LocalCompFile>,
Dictionary<ModLocalComp.LocalCompFile, List<string>>>(Lang.Text("Instance.Export.Task.FetchFileInfo"),
loader =>
{
loader.output = new Dictionary<ModLocalComp.LocalCompFile, List<string>>();
if (!checkHostedAssets)
{
ModBase.Log("[Export] 要求跳过联网获取步骤");
return;
}
if (!loader.input.Any())
{
ModBase.Log("[Export] 没有需要联网检查的文件,跳过联网获取步骤");
return;
}
// 分平台获取下载地址
var endedThreadCount = 0;
var failedExceptions = new List<Exception>();
// 从 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<uint>());
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<Dictionary<ModLocalComp.LocalCompFile, List<string>>, 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<string>(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);
}
}