1227 lines
51 KiB
C#
1227 lines
51 KiB
C#
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);
|
|||
|
|
}
|
|||
|
|
}
|