Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

1184 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics.Eventing.Reader;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using FluentValidation;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.Minecraft.ResourceProject;
using PCL.Core.UI;
using PCL.Core.Utils;
using PCL.Core.Utils.Validate;
using PCL.Network;
using PCL.Network.Loaders;
using Control = System.Windows.Forms.Control;
namespace PCL;
public partial class PageDownloadCompDetail
{
// 资源下载;整合包另存为
public static Dictionary<ModComp.CompType, string> cachedFolder = new(); // 仅在本次缓存的下载文件夹
private MyCompItem _compItem;
private bool _isFirstInit = true;
private void Init()
{
ModAnimation.AniControlEnabled += 1;
_project = ModMain.frmMain.pageCurrent.additional.Value.CompProject;
PanBack.ScrollToHome();
// 重启加载器
if (_isFirstInit)
// 在 Me.Initialized 已经初始化了加载器,不再重复初始化
_isFirstInit = false;
else
PageLoaderRestart(isForceRestart: true);
// 放置当前工程
if (_compItem is not null)
PanIntro.Children.Remove(_compItem);
_compItem = _project.ToCompItem(true, true);
_compItem.CanInteraction = false;
_compItem.ShowFavoriteBtn = false;
_compItem.Margin = new Thickness(-7, -7, 0d, 8d);
PanIntro.Children.Insert(0, _compItem);
// 决定按钮显示
BtnIntroWeb.Text = _project.FromCurseForge ? "CurseForge" : "Modrinth";
BtnIntroWiki.Visibility = Lang.IsChineseMainland && _project.WikiId != 0
? Visibility.Visible
: Visibility.Collapsed;
BtnTranslate.Visibility = Lang.IsChineseMainland
? Visibility.Visible
: Visibility.Collapsed;
RefreshFavoriteButton();
ModAnimation.AniControlEnabled -= 1;
}
// 整合包安装
public void Install_Click(MyListItem sender, EventArgs e)
{
try
{
// 获取基本信息
var file = (ModComp.CompFile)sender.Tag;
var loaderName =
$"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} {Lang.Text("Download.Comp.Detail.ModpackDownload")}{_project.TranslatedName} ";
// 获取实例名
var packName = _project.TranslatedName.Replace(".zip", "").Replace(".rar", "").Replace(".mrpack", "")
.Replace(@"\", "").Replace("/", "").Replace("|", "").Replace(":", "").Replace("<", "")
.Replace(">", "").Replace("*", "").Replace("?", "").Replace("\"", "").Replace(" ", "");
var validate = new FolderNameValidator(ModFolder.mcFolderSelected + "versions");
if (!validate.Validate(packName).IsValid)
packName = "";
var instanceName = ModMain.MyMsgBoxInput(Lang.Text("Download.Comp.Detail.InputInstanceName"), "", packName, [validate]);
if (string.IsNullOrEmpty(instanceName))
return;
// 构造步骤加载器
var loaders = new List<ModLoader.LoaderBase>();
var target =
$@"{ModFolder.mcFolderSelected}versions\{instanceName}\原始整合包.{(_project.FromCurseForge ? "zip" : "mrpack")}";
var logoFileAddress = MyImage.GetTempPath(_compItem.Logo);
loaders.Add(new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadModpackFile"),
new List<DownloadFile> { file.ToNetFile(target, ModComp.DownloadReason.ModPack) })
{ ProgressWeight = 10d, block = true });
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.PrepareModpackInstall"),
_ => ModModpack.ModpackInstall(target, instanceName,
System.IO.File.Exists(logoFileAddress) ? logoFileAddress : null, file.ProjectId,
true)) { ProgressWeight = 0.1d });
// 启动
var loader = new ModLoader.LoaderCombo<string>(loaderName, loaders)
{
OnStateChanged = myLoader =>
{
switch (myLoader.State)
{
case ModBase.LoadState.Failed:
{
HintService.Hint(
Lang.Text("Download.Comp.Detail.Task.Failed", myLoader.name,
myLoader.Error.ToString()), HintType.Error);
break;
}
case ModBase.LoadState.Aborted:
{
HintService.Hint(Lang.Text("Download.Comp.Detail.Task.Cancelled", myLoader.name));
break;
}
case ModBase.LoadState.Loading:
{
return; // 不重新加载版本列表
}
}
ModDownloadLib.McInstallFailedClearFolder(myLoader);
}
};
loader.Start(Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName));
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
}
catch (Exception ex)
{
ModBase.Log(
ex,
"下载资源整合包失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
}
}
// 世界下载
public void InstallWorld_Click(MyListItem sender, EventArgs e)
{
try
{
// 获取基本信息
var file = (ModComp.CompFile)sender.Tag;
var loaderName = $"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} {Lang.Text("Download.Comp.Detail.WorldDownload")}{_project.TranslatedName} ";
// 确认默认保存位置
string defaultFolder = null;
var subFolder = @"saves\";
Func<McInstance, bool> isVersionSuitable = null;
// 获取资源所需的加载器
var allowedLoaders = new List<ModComp.CompLoaderType>();
if (file.ModLoaders.Any())
allowedLoaders = file.ModLoaders;
else if (_project.ModLoaders.Any()) allowedLoaders = _project.ModLoaders;
ModBase.Log("[Comp] 世界要求的加载器种类:" + (allowedLoaders.Any() ? allowedLoaders.Join(" / ") : "无要求"));
// 判断某个版本是否符合资源要求
isVersionSuitable = version =>
{
if (version is null)
return false;
if (!version.IsLoaded)
version.Load();
if (file.GameVersions.Any(v => v.Contains(".")) && !file.GameVersions.Any(v =>
v.Contains(".") && (v ?? "") == (version.Info.VanillaName ?? "")))
return false;
// 加载器
if (!allowedLoaders.Any())
return true; // 无要求
return false;
};
// 获取常规资源默认下载位置
if (cachedFolder.ContainsKey(file.Type) && !string.IsNullOrEmpty(cachedFolder[file.Type]))
{
defaultFolder = cachedFolder.GetOrDefault(file.Type,
ModInstanceList.McMcInstanceSelected?.PathIndie ?? ModBase.exePath);
ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{defaultFolder}");
}
else if (ModInstanceList.McMcInstanceSelected is not null && isVersionSuitable(ModInstanceList.McMcInstanceSelected))
{
defaultFolder = $"{ModInstanceList.McMcInstanceSelected.PathIndie}{subFolder}";
Directory.CreateDirectory(defaultFolder);
ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{defaultFolder}");
}
else
{
// 查找所有可能的实例
var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished;
if (needLoad)
{
HintService.Hint(Lang.Text("Download.Comp.Detail.FindingApplicableInstance"));
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true);
}
var suitableVersions = ModInstanceList.mcInstanceList.Values.SelectMany(l => l)
.Where(v => isVersionSuitable(v)).Select(v => new DirectoryInfo($"{v.PathIndie}{subFolder}"));
if (suitableVersions.Any())
{
var selectedVersion = suitableVersions
.OrderByDescending(dir => dir.Exists ? dir.LastWriteTimeUtc : DateTime.MinValue)
.ThenByDescending(dir => dir.Exists ? dir.GetFiles().Length : -1).First(); // 先按文件夹更改时间降序
// 再按文件夹中的文件数量降序
defaultFolder = selectedVersion.FullName;
Directory.CreateDirectory(defaultFolder);
ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{defaultFolder}");
}
else
{
defaultFolder = ModFolder.mcFolderSelected;
if (needLoad)
HintService.Hint(Lang.Text("Download.Comp.Detail.NoApplicableInstance"));
else
ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置");
}
}
var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Comp.Detail.SelectWorldInstallLocation"), file.FileName, Lang.Text("Download.Comp.Detail.WorldFile.Filter"),
defaultFolder);
if (string.IsNullOrEmpty(target))
return;
// 构造步骤加载器
var loaders = new List<ModLoader.LoaderBase>();
var targetPath = target.BeforeLast(@"\");
var logoFileAddress = MyImage.GetTempPath(_compItem.Logo);
loaders.Add(new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadWorldFile"),
new List<DownloadFile>
{
file.ToNetFile(target, ModComp.DownloadReason.Standalone, file.RawGameVersions.FirstOrDefault())
})
{ ProgressWeight = 10d, block = true });
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.InstallWorld"),
_ => ModBase.ExtractFile(target, targetPath, Encoding.UTF8)) { ProgressWeight = 0.1d, block = true });
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Download.Comp.Detail.CleanCache"),
_ => System.IO.File.Delete(target)));
// 启动
var loader = new ModLoader.LoaderCombo<int>(loaderName, loaders)
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
loader.Start();
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
}
catch (Exception ex)
{
ModBase.Log(
ex,
"下载世界资源失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
}
}
public void Save_Click(object sender, EventArgs e)
{
// 获取点击项关联的文件对象
var file = sender switch
{
FrameworkElement Element when Element.Tag is ModComp.CompFile CompFile => CompFile,
FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Tag is ModComp.CompFile CompFile => CompFile,
FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Parent is FrameworkElement GrandParent && GrandParent.Tag is ModComp.CompFile CompFile => CompFile,
_ => null
};
ModBase.RunInNewThread(() =>
{
try
{
var desc = file.Type switch
{
ModComp.CompType.ModPack => Lang.Text("Download.Comp.Type.Modpack"),
ModComp.CompType.Mod => Lang.Text("Download.Comp.Type.Mod"),
ModComp.CompType.ResourcePack => Lang.Text("Download.Comp.Type.ResourcePack"),
ModComp.CompType.Shader => Lang.Text("Download.Comp.Type.Shader"),
ModComp.CompType.DataPack => Lang.Text("Download.Comp.Type.DataPack"),
ModComp.CompType.World => Lang.Text("Download.Comp.Type.World"),
_ => ""
};
// 确认默认保存位置
string defaultFolder = null;
var allowedLoaders = new List<ModComp.CompLoaderType>();
if (file.Type != ModComp.CompType.ModPack)
{
var subFolder = "";
switch (file.Type)
{
case ModComp.CompType.Mod: subFolder = "mods\\"; break;
case ModComp.CompType.ResourcePack: subFolder = "resourcepacks\\"; break;
case ModComp.CompType.Shader: subFolder = "shaderpacks\\"; break;
case ModComp.CompType.World: subFolder = "saves\\"; break;
case ModComp.CompType.DataPack: subFolder = ""; break; // 导航到版本根目录
}
// 获取资源所需的加载器
if (file.ModLoaders.Any())
allowedLoaders = file.ModLoaders;
else if (_project.ModLoaders.Any()) allowedLoaders = _project.ModLoaders;
ModBase.Log(
$"[Comp] {desc}要求的加载器种类:{(allowedLoaders.Any() ? string.Join(" / ", allowedLoaders) : "无要求")}");
// 判断某个版本是否符合资源要求 (局部函数)
Func<McInstance, bool> isVersionSuitable = version =>
{
if (version is null) return false;
if (!version.IsLoaded) version.Load();
// 只对 Mod 和数据包进行版本检测
if (file.Type == ModComp.CompType.Mod || file.Type == ModComp.CompType.DataPack)
if (file.GameVersions.Any(v => v.Contains(".")) &&
!file.GameVersions.Any(v => v.Contains(".") && v == version.Info.VanillaName))
return false;
// 加载器判定
if (!allowedLoaders.Any()) return true; // 无要求
if (allowedLoaders.Contains(ModComp.CompLoaderType.Forge) && version.Info.HasForge) return true;
if (allowedLoaders.Contains(ModComp.CompLoaderType.Forge) && version.Info.HasCleanroom)
return true;
if (allowedLoaders.Contains(ModComp.CompLoaderType.Fabric) &&
(version.Info.HasFabric || version.Info.HasLegacyFabric)) return true;
if (allowedLoaders.Contains(ModComp.CompLoaderType.NeoForge) && version.Info.HasNeoForge)
return true;
if (allowedLoaders.Contains(ModComp.CompLoaderType.LiteLoader) && version.Info.HasLiteLoader)
return true;
return false;
};
// 获取常规资源默认下载位置逻辑
if (cachedFolder.ContainsKey(file.Type) && !string.IsNullOrEmpty(cachedFolder[file.Type]))
{
defaultFolder = cachedFolder.GetOrDefault(file.Type,
ModInstanceList.McMcInstanceSelected?.PathIndie ?? ModBase.exePath);
ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{defaultFolder}");
}
else if (ModInstanceList.McMcInstanceSelected is not null &&
isVersionSuitable(ModInstanceList.McMcInstanceSelected))
{
defaultFolder = $"{ModInstanceList.McMcInstanceSelected.PathIndie}{subFolder}";
Directory.CreateDirectory(defaultFolder);
ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{defaultFolder}");
}
else
{
// 查找所有可能的实例
var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished;
if (needLoad)
{
HintService.Hint(Lang.Text("Download.Comp.Detail.FindingApplicableInstance"));
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, "versions\\", true);
}
var suitableVersions = ModInstanceList.mcInstanceList.Values.SelectMany(l => l)
.Where(v => isVersionSuitable(v))
.Select(v => new DirectoryInfo($"{v.PathIndie}{subFolder}"));
if (suitableVersions.Any())
{
var selectedVersion = suitableVersions
.OrderByDescending(dir => dir.Exists ? dir.LastWriteTimeUtc : DateTime.MinValue)
.ThenByDescending(dir => dir.Exists ? dir.GetFiles().Length : -1)
.First();
defaultFolder = selectedVersion.FullName;
Directory.CreateDirectory(defaultFolder);
ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{defaultFolder}");
}
else
{
defaultFolder = ModFolder.mcFolderSelected;
if (needLoad)
HintService.Hint(Lang.Text("Download.Comp.Detail.NoApplicableInstance"));
else
ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置");
}
}
}
// 获取文件名并弹窗
var fileName = ModComp.CompFileNameGet(_project, file);
ModBase.RunInUi(() =>
{
var target = SystemDialogs.SelectSaveFile(Lang.Text("Download.Comp.Detail.SelectSaveLocation"),
fileName, Lang.Text("Download.Comp.Detail.ResourceFile.Filter", desc) + "|" +
(file.Type == ModComp.CompType.Mod
? file.FileName.EndsWith(".litemod") ? "*.litemod" : "*.jar"
: file.FileName.EndsWith(".mrpack")
? "*.mrpack"
: "*.zip"),
defaultFolder);
if (!target.Contains("\\")) return;
// 记录缓存路径
var targetDir = ModBase.GetPathFromFullPath(target);
if (target != defaultFolder)
{
if (cachedFolder.ContainsKey(file.Type))
cachedFolder[file.Type] = targetDir;
else
cachedFolder.Add(file.Type, targetDir);
}
if (file.Type == ModComp.CompType.Mod && Config.Download.Comp.AutoInstallDependencies &&
file.Dependencies.Any())
{
try
{
McInstance? targetInstance = null;
var knownInstances = new List<McInstance>();
if (ModInstanceList.McMcInstanceSelected is not null)
{
knownInstances.Add(ModInstanceList.McMcInstanceSelected);
}
knownInstances.AddRange(ModInstanceList.mcInstanceList.Values.SelectMany(list => list)
.Where(instance => instance is not null));
targetInstance = knownInstances
.Distinct()
.FirstOrDefault(instance =>
targetDir.StartsWith(instance.PathIndie, StringComparison.OrdinalIgnoreCase));
if (targetInstance is not null && !targetInstance.IsLoaded)
{
targetInstance.Load();
}
var mcVersion = targetInstance?.Info?.VanillaName
?? file.GameVersions.FirstOrDefault(version => version.Contains("."))
?? string.Empty;
var targetLoaders = new List<ModComp.CompLoaderType>();
if (targetInstance is not null)
{
if (targetInstance.Info.HasForge || targetInstance.Info.HasCleanroom)
targetLoaders.Add(ModComp.CompLoaderType.Forge);
if (targetInstance.Info.HasFabric || targetInstance.Info.HasLegacyFabric)
targetLoaders.Add(ModComp.CompLoaderType.Fabric);
if (targetInstance.Info.HasQuilt)
targetLoaders.Add(ModComp.CompLoaderType.Quilt);
if (targetInstance.Info.HasNeoForge)
targetLoaders.Add(ModComp.CompLoaderType.NeoForge);
if (targetInstance.Info.HasLiteLoader)
targetLoaders.Add(ModComp.CompLoaderType.LiteLoader);
}
if (!targetLoaders.Any())
{
targetLoaders = allowedLoaders.ToList();
}
ModBase.Log($"[CompDeps] 开始解析必需前置: {file.Dependencies.Count} 个依赖");
var request = ModCompDependency.BuildRequest(file, _project, mcVersion, targetLoaders,
targetDir);
var resolver = new ModDependencyResolver();
var result = resolver.Resolve(request);
void DownloadDependencies()
{
if (!result.ToInstall.Any())
{
ModBase.Log("[CompDeps] 所有前置均无法解析,仅下载 Mod 本体");
return;
}
ModBase.Log($"[CompDeps] 准备下载: {result.ToInstall.Count} 个前置");
var depDownloads = ModCompDependency.BuildDependencyDownloads(result, targetDir);
foreach (var (depFilename, downloadFile) in depDownloads)
{
var depLoaderName = Lang.Text("Download.Comp.Detail.DownloadResource", desc,
ModBase.GetFileNameWithoutExtentionFromPath(depFilename));
var depLoaders = new List<ModLoader.LoaderBase>
{
new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadFile"),
new List<DownloadFile> { downloadFile })
{
ProgressWeight = 6,
block = true
}
};
// 启动加载器
var depLoader = new ModLoader.LoaderCombo<int>(depLoaderName, depLoaders);
depLoader.OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly;
depLoader.Start(1);
ModLoader.LoaderTaskbarAdd(depLoader);
}
}
if (result.Unresolved.Any() || result.ToInstall.Any())
{
var installChoice = ModCompDependency.ConfirmDependencyInstall(result);
switch (installChoice)
{
case ModComp.CompDepsInstallTypes.Unresolved:
ModBase.Log("[CompDeps] 发现无法解析的前置");
DownloadDependencies();
break;
case ModComp.CompDepsInstallTypes.WithDeps:
DownloadDependencies();
break;
case ModComp.CompDepsInstallTypes.WithoutDeps:
ModBase.Log("[CompDeps] 用户选择仅下载 Mod 本体,跳过前置下载");
break;
case ModComp.CompDepsInstallTypes.Cancel:
ModBase.Log("[CompDeps] 用户取消安装");
return;
default:
ModBase.Log($"[CompDeps] 未知返回值: {installChoice} ,终止下载");
return;
}
}
else
{
ModBase.Log("[CompDeps] 已满足: 所有必需前置已安装");
}
}
catch (Exception depEx)
{
ModBase.Log(depEx, "[CompDeps] 依赖解析失败,跳过前置安装");
var message = ExceptionDetails.Compose(
Lang.Text("Download.Comp.Dependency.ResolveFailed.Message"),
depEx);
ModMain.MyMsgBox(
message,
Lang.Text("Download.Comp.Dependency.ResolveFailed.Title"),
Lang.Text("Download.Comp.Dependency.ResolveFailed.Continue"),
isWarn: true,
forceWait: true);
}
}
// 构造下载任务
var loaderName = Lang.Text("Download.Comp.Detail.DownloadResource", desc,
ModBase.GetFileNameWithoutExtentionFromPath(target));
var loaders = new List<ModLoader.LoaderBase>
{
new LoaderDownload(Lang.Text("Download.Comp.Detail.DownloadFile"),
new List<DownloadFile>
{
file.Type == ModComp.CompType.Mod
? file.ToNetFile(target)
: file.ToNetFile(target, ModComp.DownloadReason.Standalone, null)
})
{
ProgressWeight = 6,
block = true
}
};
// 启动加载器
var loader = new ModLoader.LoaderCombo<int>(loaderName, loaders);
loader.OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly;
loader.Start(1);
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
});
}
catch (Exception ex)
{
ModBase.Log(
ex,
"保存资源文件失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
}
}, "Download CompDetail Save");
}
private void BtnIntroWeb_Click(object sender, EventArgs e)
{
ModBase.OpenWebsite(_project.Website);
}
private void BtnIntroWiki_Click(object sender, EventArgs e)
{
ModBase.OpenWebsite("https://www.mcmod.cn/class/" + _project.WikiId + ".html");
}
private void BtnIntroCopy_Click(object sender, EventArgs e)
{
ModBase.ClipboardSet(_compItem.LabTitle.Text + _compItem.LabTitleRaw.Text);
}
private void BtnFavorites_Click(object sender, EventArgs e)
{
ModComp.CompFavorites.ShowMenu(_project, (UIElement)sender, RefreshFavoriteButton);
}
private void BtnIntroLinkCopy_Click(object sender, EventArgs e)
{
ModComp.CompClipboard.currentText = _project.Website;
ModBase.ClipboardSet(_project.Website);
}
// 翻译简介
private async void BtnTranslate_Click(object sender, EventArgs e)
{
HintService.Hint(Lang.Text("Download.Comp.Detail.DescriptionTranslating", _project.TranslatedName));
var chineseDescription = await _project.ChineseDescription;
if (chineseDescription is null)
return;
ModMain.MyMsgBox(Lang.Text("Download.Comp.Detail.DescriptionTranslationResult", _project.Description,
chineseDescription));
}
/// <summary>
/// 刷新收藏按钮的显示状态
/// </summary>
public void RefreshFavoriteButton()
{
try
{
if (_project is null) return;
var isFavourite = ModComp.CompFavorites.IsFavourite(_project.Id);
BtnFavorites.SvgIcon = isFavourite ? "lucide/heart-filled" : "lucide/heart";
// 刷新顶部的项目卡片收藏状态
if (_compItem is not null)
_compItem.RefreshFavoriteStatus();
}
catch (Exception ex)
{
ModBase.Log(ex, "刷新收藏按钮状态时出错");
}
}
#region 加载器
private readonly ModLoader.LoaderTask<int, List<ModComp.CompFile>> _compFileLoader;
public PageDownloadCompDetail()
{
_compFileLoader = new ModLoader.LoaderTask<int, List<ModComp.CompFile>>("Comp File", task =>
{
LoadTargetFromAdditional();
var result = ModComp.CompFilesGet(_project.Id, _project.FromCurseForge);
if (task.IsAborted)
return;
task.output = result;
});
Initialized += PageDownloadCompDetail_Inited;
Loaded += (_, _) => LoadTargetFromAdditional();
PageEnter += Init;
InitializeComponent();
Load.StateChanged += Load_State;
BtnIntroWeb.Click += BtnIntroWeb_Click;
BtnIntroWiki.Click += BtnIntroWiki_Click;
BtnIntroCopy.Click += BtnIntroCopy_Click;
BtnFavorites.Click += BtnFavorites_Click;
BtnIntroLinkCopy.Click += BtnIntroLinkCopy_Click;
BtnTranslate.Click += BtnTranslate_Click;
}
// 初始化加载器信息
private void PageDownloadCompDetail_Inited(object sender, EventArgs e)
{
LoadTargetFromAdditional();
PageLoaderInit(Load, PanLoad, PanMain, CardIntro, _compFileLoader, _ => Load_OnFinish());
}
public void LoadTargetFromAdditional()
{
var additional = ModMain.frmMain.pageCurrent.additional.Value;
_project = additional.CompProject;
_targetInstance = additional.TargetVersion;
_targetLoader = additional.TargetLoader;
_pageType = additional.ResourceType;
}
private ModComp.CompProject _project;
private string _targetInstance;
private ModComp.CompLoaderType _targetLoader;
/// <summary>
/// 当前页面应展示的内容类别。可能为 Any。
/// </summary>
private ModComp.CompType _pageType;
// 自动重试
private void Load_State(object sender, MyLoading.MyLoadingState state, MyLoading.MyLoadingState oldState)
{
switch (_compFileLoader.State)
{
case ModBase.LoadState.Failed:
{
var errorMessage = "";
if (_compFileLoader.Error is not null)
errorMessage = _compFileLoader.Error.Message;
if (errorMessage.Contains(Lang.Text("Common.Error.InvalidJson")))
{
ModBase.Log("[Comp] 下载的文件 Json 列表损坏,已自动重试", ModBase.LogLevel.Debug);
PageLoaderRestart();
}
break;
}
}
}
// 结果 UI 化
private class CardSorter : IComparer<string>
{
public readonly string topmost = "";
public CardSorter(string topmost = "")
{
this.topmost = topmost ?? "";
}
public int Compare(string x, string y)
{
// 相同
if ((x ?? "") == (y ?? ""))
return 0;
// 置顶
if ((x ?? "") == (topmost ?? ""))
return -1;
if ((y ?? "") == (topmost ?? ""))
return 1;
// 特殊版本
var isXSpecial = !x.Contains(".");
var isYSpecial = !y.Contains(".");
if (isXSpecial && isYSpecial)
return string.Compare(x, y, StringComparison.Ordinal);
if (isXSpecial)
return 1;
if (isYSpecial)
return -1;
// 比较版本号
var versionCodeSort = -McVersionComparer.CompareVersion(x.Replace(x.BeforeFirst(" ") + " ", ""),
y.Replace(y.BeforeFirst(" ") + " ", ""));
if (versionCodeSort != 0)
return versionCodeSort;
// 比较全部
return -McVersionComparer.CompareVersion(x, y);
}
}
private string? _instanceFilter;
private string? _modLoaderFilter;
private bool groupedDrop; // 是否按 Drop 筛选(1.21 / 1.20 / 1.19 / ...)而非小版本号(1.21.1 / 1.21 / 1.20.4 / ...
private bool groupedOld; // 是否折叠远古版本为一个选项
// 筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表)
private List<ModComp.CompFile> GetResults()
{
var results = _compFileLoader.output;
if (_pageType == ModComp.CompType.Any)
{
results = results.Where(r => r.Type != ModComp.CompType.Plugin).ToList();
}
else if (_pageType == ModComp.CompType.Shader || _pageType == ModComp.CompType.ResourcePack)
{
}
// 不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(Meloong-Git/#6473
else
{
results = results.Where(r => r.Type == _pageType).ToList();
}
return results;
}
private void Load_OnFinish()
{
var results = GetResults();
// 初始化筛选器
List<string> instanceFilters = null;
List<string> modLoaderFilters = null;
void updateFilters()
{
instanceFilters = results.SelectMany(v => v.GameVersions)
.Select(v => GetGroupedVersionName(v, groupedDrop, groupedOld)).Distinct()
.OrderByDescending(s => s, new McVersionComparer.VersionComparer()).ToList();
modLoaderFilters = results.SelectMany(v => v.ModLoaders).Select(l => l.ToString()).Distinct()
.OrderByDescending(s => s).ToList();
}
;
// 确定分组方式
groupedDrop = false;
groupedOld = false;
updateFilters();
if (instanceFilters.Count >= 9)
{
groupedDrop = true;
groupedOld = false;
updateFilters();
if (instanceFilters.Count >= 9)
{
groupedDrop = false;
groupedOld = true;
updateFilters();
if (instanceFilters.Count >= 9)
{
groupedDrop = true;
groupedOld = true;
updateFilters();
}
}
}
// UI 化筛选器
PanInstanceFilter.Children.Clear();
PanModLoaderFilter.Children.Clear();
if (_pageType == ModComp.CompType.Mod)
{
PanInstanceFilter.Margin = new Thickness(10d, 10d, 0d, 5d);
PanModLoaderFilter.Margin = new Thickness(10d, 5d, 0d, 10d);
}
else
{
PanInstanceFilter.Margin = new Thickness(10d, 10d, 0d, 10d);
PanModLoaderFilter.Margin = new Thickness(0d);
}
if (instanceFilters.Count < 2)
{
CardFilter.Visibility = Visibility.Collapsed;
_instanceFilter = null;
}
else
{
CardFilter.Visibility = Visibility.Visible;
// 插入标签
if (_pageType == ModComp.CompType.Mod)
{
var instanceTextBlock = new TextBlock
{
Text = Lang.Text("Download.Comp.Detail.InstanceFilter"),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(2d, 0d, 0d, 0d)
};
PanInstanceFilter.Children.Add(instanceTextBlock);
var modLoaderTextBlock = new TextBlock
{
Text = Lang.Text("Download.Comp.Detail.LoaderFilter"),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(2d, 0d, 0d, 0d)
};
PanModLoaderFilter.Children.Add(modLoaderTextBlock);
}
instanceFilters.Insert(0, Lang.Text("Common.Option.All"));
modLoaderFilters.Insert(0, Lang.Text("Common.Option.All"));
// 转化为按钮
foreach (var version in instanceFilters)
{
var newButton = new MyRadioButton
{
Text = version, Margin = new Thickness(2d, 0d, 2d, 0d),
ColorType = MyRadioButton.ColorState.Highlight
};
newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d);
newButton.Check += (sender, raiseByMouse) =>
{
_instanceFilter = sender.Text == Lang.Text("Common.Option.All") ? null : sender.Text;
UpdateFilterResult();
};
PanInstanceFilter.Children.Add(newButton);
}
if (_pageType == ModComp.CompType.Mod)
foreach (var loader in modLoaderFilters)
{
var newButton = new MyRadioButton
{
Text = loader,
Margin = new Thickness(2d, 0d, 2d, 0d),
ColorType = MyRadioButton.ColorState.Highlight
};
newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d);
newButton.Check += (sender, raiseByMouse) =>
{
_modLoaderFilter = sender.Text == Lang.Text("Common.Option.All") ? null : sender.Text;
UpdateFilterResult();
};
PanModLoaderFilter.Children.Add(newButton);
}
// 自动选择
MyRadioButton instanceToCheck = null;
MyRadioButton modLoaderToCheck = null;
if (!string.IsNullOrEmpty(_targetInstance))
{
var targetFile = results.FirstOrDefault(v => v.GameVersions.Contains(_targetInstance));
if (targetFile is not null)
{
var targetGroup = GetGroupedVersionName(_targetInstance, groupedDrop, groupedOld);
var children = _pageType == ModComp.CompType.Mod
? PanInstanceFilter.Children.Cast<UIElement>().Skip(1)
: PanInstanceFilter.Children.Cast<UIElement>();
foreach (MyRadioButton button in (IEnumerable)children)
{
if ((button.Text ?? "") != (targetGroup ?? ""))
continue;
instanceToCheck = button;
break;
}
}
}
if (_pageType == ModComp.CompType.Mod)
if (_targetLoader != ModComp.CompLoaderType.Any)
{
var targetFile = results.FirstOrDefault(v => v.ModLoaders.Contains(_targetLoader));
if (targetFile is not null)
{
var children = _pageType == ModComp.CompType.Mod
? PanInstanceFilter.Children.Cast<UIElement>().Skip(1)
: PanInstanceFilter.Children.Cast<UIElement>();
foreach (MyRadioButton button in (IEnumerable)children)
{
if ((button.Text ?? "") != (_targetLoader.ToString() ?? ""))
continue;
modLoaderToCheck = button;
break;
}
}
}
// 注意:在 Mod 下 index 0 是 TextBlock
var index = _pageType == ModComp.CompType.Mod ? 1 : 0;
if (instanceToCheck is null)
instanceToCheck = (MyRadioButton)PanInstanceFilter.Children[index];
if (modLoaderToCheck is null && (_pageType == ModComp.CompType.Mod))
modLoaderToCheck = (MyRadioButton)PanModLoaderFilter.Children[index];
instanceToCheck.Checked = true;
if (_pageType == ModComp.CompType.Mod)
modLoaderToCheck.Checked = true;
}
// 更新筛选结果(文件列表 UI 化)
UpdateFilterResult();
}
private void UpdateFilterResult()
{
var results = GetResults();
if (results is null)
return;
// 1. 预处理基础变量
var targetVersionText = _targetLoader != ModComp.CompLoaderType.Any ? _targetLoader + " " : "";
var targetCardName = !string.IsNullOrEmpty(_targetInstance) || _targetLoader != ModComp.CompLoaderType.Any
? Lang.Text("Download.Comp.Detail.SelectedVersion", targetVersionText, _targetInstance)
: "";
// 使用 HashSet 提高查询性能 O(1)
var supportedLoaders =
new HashSet<ModComp.CompLoaderType>(Enum.GetValues(typeof(ModComp.CompLoaderType))
.Cast<ModComp.CompLoaderType>());
var ignoreQuilt = Config.Download.Comp.IgnoreQuilt;
var hasMultipleLoaders = _project.ModLoaders.Count > 1;
// 2. 核心数据归类 (使用 Dictionary 配合 HashSet 去重)
var dict = new SortedDictionary<string, List<ModComp.CompFile>>(new CardSorter(targetCardName));
dict.Add(Lang.Text("Download.Comp.Detail.VersionGroup.Other"), new List<ModComp.CompFile>());
// 用于记录每个卡片内已存在的 version,防止 Contains(version) 的 O(n) 消耗
var versionDuplicateChecker = new Dictionary<string, HashSet<ModComp.CompFile>>();
foreach (var version in results)
{
// 处理普通卡片归类
foreach (var gameVersion in version.GameVersions)
{
// 筛选器预检查
var currentGroupedName = GetGroupedVersionName(gameVersion, groupedDrop, groupedOld);
if (_instanceFilter is not null && (currentGroupedName ?? "") != (_instanceFilter ?? ""))
continue;
var verName = GetGroupedVersionName(gameVersion, false, false);
var loaders = new List<string>();
// 判定 Loader 逻辑
if (hasMultipleLoaders && version.Type == ModComp.CompType.Mod &&
McInstanceInfo.IsFormatFit(verName))
{
foreach (var loader in version.ModLoaders)
{
if (loader == ModComp.CompLoaderType.Quilt && ignoreQuilt)
continue;
if (!supportedLoaders.Contains(loader))
continue;
// 模组加载器筛选器
if (_modLoaderFilter is not null && (loader.ToString() ?? "") != (_modLoaderFilter ?? ""))
continue;
loaders.Add(loader + " ");
}
if (loaders.Count == 0 && _modLoaderFilter is not null) continue;
}
if (loaders.Count == 0)
loaders.Add("");
// 填充数据
foreach (var loaderPrefix in loaders)
{
var targetKey = loaderPrefix + verName;
AddVersionToDict(dict, versionDuplicateChecker, targetKey, version);
}
}
// 处理“所选版本”卡片 (逻辑合并,减少二次循环)
if (!string.IsNullOrEmpty(targetCardName))
{
var isMatchFilter = _instanceFilter is null ||
GetGroupedVersionName(_targetInstance, groupedDrop, groupedOld)
.StartsWithF(_instanceFilter);
if (isMatchFilter && version.GameVersions.Contains(_targetInstance))
if (_targetLoader == ModComp.CompLoaderType.Any || version.ModLoaders.Contains(_targetLoader))
// 再次检查 version 是否符合筛选器(针对该文件的所有游戏版本)
if (_instanceFilter is null || version.GameVersions.Any(v =>
(GetGroupedVersionName(v, groupedDrop, groupedOld) ?? "") == (_instanceFilter ?? "")))
AddVersionToDict(dict, versionDuplicateChecker, targetCardName, version);
}
}
// 3. 渲染 UI
try
{
PanResults.Children.Clear();
var additional = ModMain.frmMain.pageCurrent.additional;
var additionalTitles = additional is not null
? additional.Value.ExpandedTitles
: new List<string>();
foreach (var pair in dict)
{
if (pair.Value.Count == 0)
continue;
// 创建卡片组件
var newCard = new MyCard
{
Title = pair.Key,
Margin = new Thickness(0d, 0d, 0d, 15d)
};
// 闭包引用:避免在 Sub 内做高耗时操作
var files = pair.Value;
var currentKey = pair.Key;
var newStack = new StackPanel
{
Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d),
VerticalAlignment = VerticalAlignment.Top,
Tag = files
};
newCard.Children.Add(newStack);
newCard.SwapControl = newStack;
// 延迟加载安装项的逻辑
newCard.InstallMethod = stack =>
{
var list = (List<ModComp.CompFile>)stack.Tag;
// 排序和去重检查
list.Sort((a, b) => b.ReleaseDate.CompareTo(a.ReleaseDate));
var distinctCount = list.Select(f => f.DisplayName).Distinct().Count();
var badDisplayName = distinctCount != list.Count;
// 批量添加子项
switch (_project.Type)
{
case ModComp.CompType.ModPack:
{
foreach (var item in list)
stack.Children.Add(item.ToListItem(
(sender, e) => ModMain.frmDownloadCompDetail.Install_Click((MyListItem)sender, e),
ModMain.frmDownloadCompDetail.Save_Click, badDisplayName));
break;
}
case ModComp.CompType.World:
{
foreach (var item in list)
stack.Children.Add(item.ToListItem(
(sender, e) =>
ModMain.frmDownloadCompDetail.InstallWorld_Click((MyListItem)sender, e),
ModMain.frmDownloadCompDetail.Save_Click, badDisplayName));
break;
}
default:
{
ModComp.CompFilesCardPreload(stack, list);
foreach (var item in list)
stack.Children.Add(item.ToListItem(ModMain.frmDownloadCompDetail.Save_Click,
badDisplayName: badDisplayName));
break;
}
}
};
PanResults.Children.Add(newCard);
// 展开逻辑
if ((currentKey ?? "") == (targetCardName ?? "") || additionalTitles.Contains(newCard.Title))
newCard.StackInstall();
else
newCard.IsSwapped = true;
// 特殊提示
if (currentKey == Lang.Text("Download.Comp.Detail.VersionGroup.Other"))
newStack.Children.Add(new MyHint
{
Text = Lang.Text("Download.Comp.Detail.VersionRecognitionDelayHint"),
Theme = MyHint.Themes.Yellow,
Margin = new Thickness(5d, 0d, 0d, 8d)
});
}
// 单卡片自动展开
if (PanResults.Children.Count == 1) ((MyCard)PanResults.Children[0]).IsSwapped = false;
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化工程下载列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Download.Comp.Error.OperationFailed"));
}
}
/// <summary>
/// 辅助方法:向字典添加数据并处理去重
/// </summary>
private void AddVersionToDict(SortedDictionary<string, List<ModComp.CompFile>> dict,
Dictionary<string, HashSet<ModComp.CompFile>> checker, string key, ModComp.CompFile version)
{
if (!dict.ContainsKey(key))
{
dict.Add(key, new List<ModComp.CompFile>());
checker.Add(key, new HashSet<ModComp.CompFile>());
}
// 使用 HashSet.Add 判断是否重复,比 List.Contains 快得多
if (checker[key].Add(version)) dict[key].Add(version);
}
private string GetGroupedVersionName(string name, bool groupedByDrop, bool foldOld)
{
if (name is null)
return Lang.Text("Download.Comp.Detail.VersionGroup.Other");
if (name.Contains('w'))
return Lang.Text("Download.Comp.Detail.VersionGroup.Snapshot");
if (foldOld && McInstanceInfo.VersionToDrop(name, true) < 120)
return Lang.Text("Download.Comp.Detail.VersionGroup.Old");
if (groupedByDrop)
{
var drop = McInstanceInfo.VersionToDrop(name, true);
if (drop >= 0)
return McInstanceInfo.DropToVersion(drop);
return name;
}
return name;
}
#endregion
}