初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashAnalysisContext(
|
||||
int processId,
|
||||
string tempFolder)
|
||||
{
|
||||
public int ProcessId { get; } = processId;
|
||||
|
||||
public string TempFolder { get; } = tempFolder;
|
||||
|
||||
public List<CrashLogEntry> RawFiles { get; } = [];
|
||||
|
||||
public List<string> OutputFiles { get; } = [];
|
||||
|
||||
public McInstance? Instance { get; set; }
|
||||
|
||||
public CrashLogEntry? DirectOpenFile { get; set; }
|
||||
|
||||
public CrashLogSet? PreparedLogs { get; set; }
|
||||
|
||||
public CrashAnalysisResult? Result { get; set; }
|
||||
|
||||
public string LogAll { get; set; } = string.Empty;
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashAnalysisResult
|
||||
{
|
||||
private readonly List<CrashFinding> _findings = [];
|
||||
|
||||
public IReadOnlyList<CrashFinding> Findings => _findings;
|
||||
|
||||
public bool HasFinding => _findings.Count > 0;
|
||||
|
||||
public bool Any => HasFinding;
|
||||
|
||||
public void Add(CrashFinding finding)
|
||||
{
|
||||
var existing = _findings.FirstOrDefault(item => item.Cause == finding.Cause);
|
||||
if (existing is null)
|
||||
{
|
||||
_findings.Add(finding);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var evidence in finding.Evidence)
|
||||
existing.AddEvidence(evidence);
|
||||
|
||||
existing.ShouldStop |= finding.ShouldStop;
|
||||
}
|
||||
|
||||
public void Add(
|
||||
CrashCause cause,
|
||||
CrashConfidence confidence,
|
||||
IEnumerable<string>? details = null,
|
||||
CrashLogKind? source = null,
|
||||
string? pattern = null,
|
||||
string? displayKind = null,
|
||||
bool shouldStop = false)
|
||||
{
|
||||
Add(new CrashFinding(cause, confidence, ToEvidence(details, source, pattern, displayKind))
|
||||
{
|
||||
ShouldStop = shouldStop
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashEvidence> ToEvidence(
|
||||
IEnumerable<string>? details,
|
||||
CrashLogKind? source,
|
||||
string? pattern,
|
||||
string? displayKind)
|
||||
{
|
||||
if (details is null)
|
||||
yield break;
|
||||
|
||||
foreach (var detail in details)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(detail))
|
||||
continue;
|
||||
|
||||
yield return new CrashEvidence
|
||||
{
|
||||
Value = detail.Trim(),
|
||||
Source = source,
|
||||
Pattern = pattern,
|
||||
DisplayKind = displayKind
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
using System.IO;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public sealed class CrashAnalyzer
|
||||
{
|
||||
private readonly CrashLogCollector _collector;
|
||||
private readonly CrashAnalysisContext _context;
|
||||
private readonly CrashDetector _detector;
|
||||
private readonly CrashDialogPresenter _dialogPresenter;
|
||||
private readonly CrashLogImporter _importer;
|
||||
private readonly CrashLogPreparer _preparer;
|
||||
|
||||
public CrashAnalyzer(int uUid)
|
||||
{
|
||||
var tempFolder = ModMain.RequestTaskTempFolder();
|
||||
Directory.CreateDirectory(Path.Combine(tempFolder, "Temp"));
|
||||
Directory.CreateDirectory(Path.Combine(tempFolder, "Report"));
|
||||
|
||||
_context = new CrashAnalysisContext(uUid, tempFolder);
|
||||
_collector = new CrashLogCollector(_context);
|
||||
_importer = new CrashLogImporter(_context);
|
||||
_preparer = new CrashLogPreparer(_context);
|
||||
_detector = new CrashDetector();
|
||||
_dialogPresenter = new CrashDialogPresenter(_context);
|
||||
|
||||
LogWrapper.Info("Crash", $"崩溃分析暂存文件夹:{tempFolder}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将可用于分析的日志存储到崩溃分析上下文。
|
||||
/// </summary>
|
||||
/// <param name="latestLog">从 PCL 捕获到的最后 200 行程序输出。</param>
|
||||
public void Collect(string versionPathIndie, IList<string>? latestLog = null)
|
||||
{
|
||||
_collector.Collect(versionPathIndie, latestLog);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件路径直接导入日志文件或崩溃报告压缩包。
|
||||
/// </summary>
|
||||
public void Import(string filePath)
|
||||
{
|
||||
_importer.Import(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从原始日志中提取实际有用的文本片段并整理可用于生成报告的文件。
|
||||
/// </summary>
|
||||
public bool Prepare()
|
||||
{
|
||||
return _preparer.Prepare();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据整理后的日志与可能的实例信息分析崩溃原因。
|
||||
/// </summary>
|
||||
public void Analyze(McInstance? version = null)
|
||||
{
|
||||
_context.Instance = version;
|
||||
var logs = _context.PreparedLogs ??
|
||||
throw new InvalidOperationException("Prepare must be called before Analyze.");
|
||||
_context.Result = _detector.Analyze(logs, version);
|
||||
_context.LogAll = logs.All;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出崩溃弹窗,并指导导出崩溃报告。
|
||||
/// </summary>
|
||||
public void Output(bool isHandAnalyze, List<string>? extraFiles = null)
|
||||
{
|
||||
_dialogPresenter.Output(isHandAnalyze, extraFiles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace PCL;
|
||||
|
||||
/// <summary>
|
||||
/// Stable internal crash cause identifiers.
|
||||
/// </summary>
|
||||
internal enum CrashCause
|
||||
{
|
||||
ExtractedModFile,
|
||||
MissingMixinBootstrap,
|
||||
OutOfMemory,
|
||||
UsingJdk,
|
||||
UnsupportedOpenGl,
|
||||
UsingOpenJ9,
|
||||
JavaTooNew,
|
||||
JavaIncompatible,
|
||||
InvalidModFileName,
|
||||
PixelFormatNotSupported,
|
||||
VeryShortOutput,
|
||||
IntelDriverAccessViolation,
|
||||
AmdDriverAccessViolation,
|
||||
NvidiaDriverAccessViolation,
|
||||
ManualDebugCrash,
|
||||
OpenGl1282,
|
||||
FileOrContentValidationFailed,
|
||||
ConfirmedModCrash,
|
||||
SuspectedModCrash,
|
||||
ModConfigCrash,
|
||||
ModMixinFailed,
|
||||
ModLoaderError,
|
||||
ModInitializationFailed,
|
||||
StackKeywordFound,
|
||||
StackModNameFound,
|
||||
OptiFineWorldLoadCrash,
|
||||
SpecificBlockCrash,
|
||||
SpecificEntityCrash,
|
||||
ResourcePackTooLarge,
|
||||
NoAnalyzableFile,
|
||||
X86JavaMemoryLimit,
|
||||
DuplicateMods,
|
||||
IncompatibleMods,
|
||||
OptiFineForgeIncompatible,
|
||||
FabricError,
|
||||
FabricSolutionProvided,
|
||||
ForgeError,
|
||||
OldForgeNewJavaIncompatible,
|
||||
MultipleForgeInInstanceJson,
|
||||
TooManyModsIdLimit,
|
||||
NightConfigBug,
|
||||
ShadersModWithOptiFine,
|
||||
IncompleteForgeInstallation,
|
||||
ModRequiresJava11,
|
||||
MissingDependencyOrWrongMcVersion
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashEvidence
|
||||
{
|
||||
public required string Value { get; init; }
|
||||
|
||||
public CrashLogKind? Source { get; init; }
|
||||
|
||||
public string? Pattern { get; init; }
|
||||
|
||||
public string? DisplayKind { get; init; }
|
||||
|
||||
public bool EqualsTo(CrashEvidence other)
|
||||
{
|
||||
return Source == other.Source &&
|
||||
string.Equals(Value, other.Value, StringComparison.Ordinal) &&
|
||||
string.Equals(DisplayKind, other.DisplayKind, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using PCL.Core.IO;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Codecs;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal static class CrashFileIo
|
||||
{
|
||||
public static byte[] ReadBytes(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 FileShare.ReadWrite 以读取正在被 Logger 写入的文件
|
||||
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var ms = new MemoryStream();
|
||||
fs.CopyTo(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash IO", "读取与游戏崩溃关联的日志文件时出错,");
|
||||
return Files.ReadAllBytesOrEmptyAsync(filePath).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
public static string ReadText(string filePath, Encoding? encoding = null)
|
||||
{
|
||||
var bytes = ReadBytes(filePath);
|
||||
return encoding is null
|
||||
? EncodingUtils.DecodeBytes(bytes)
|
||||
: encoding.GetString(bytes);
|
||||
}
|
||||
|
||||
public static void WriteText(string filePath, string text, Encoding? encoding = null)
|
||||
{
|
||||
Files.WriteFileAsync(filePath, text, encoding: encoding).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static void CopyFile(string fromPath, string toPath)
|
||||
{
|
||||
Files.CopyFileAsync(fromPath, toPath).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static void DeleteDirectory(string directoryPath)
|
||||
{
|
||||
Directories.DeleteDirectoryAsync(directoryPath, true).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static bool CanExtractArchive(string archivePath)
|
||||
{
|
||||
var fileName = Path.GetFileName(archivePath);
|
||||
return fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.EndsWith(".bz2", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static void ExtractFile(string archivePath, string destinationDirectory)
|
||||
{
|
||||
if (!CanExtractArchive(archivePath))
|
||||
throw new NotSupportedException("崩溃日志导入不支持该压缩格式。支持 zip、gz、bz2、tar、tgz 与 tar.gz。");
|
||||
|
||||
Files.ExtractFileAsync(archivePath, destinationDirectory).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashFinding
|
||||
{
|
||||
public CrashFinding(
|
||||
CrashCause cause,
|
||||
CrashConfidence confidence,
|
||||
IEnumerable<CrashEvidence>? evidence = null)
|
||||
{
|
||||
Cause = cause;
|
||||
Confidence = confidence;
|
||||
Evidence = evidence
|
||||
?.Where(item => !string.IsNullOrWhiteSpace(item.Value))
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
public CrashCause Cause { get; }
|
||||
|
||||
public CrashConfidence Confidence { get; }
|
||||
|
||||
public List<CrashEvidence> Evidence { get; }
|
||||
|
||||
public bool ShouldStop { get; set; }
|
||||
|
||||
public IReadOnlyList<string> Details => Evidence
|
||||
.Select(item => item.Value)
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
public void AddEvidence(CrashEvidence evidence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(evidence.Value))
|
||||
return;
|
||||
|
||||
if (!Evidence.Any(item => item.EqualsTo(evidence)))
|
||||
Evidence.Add(evidence);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal static class CrashRegex
|
||||
{
|
||||
public static string? First(
|
||||
string text,
|
||||
string pattern,
|
||||
RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = Regex.Match(text, pattern, options).Value;
|
||||
return string.IsNullOrEmpty(value)
|
||||
? null
|
||||
: value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "正则匹配第一项出错");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<string> All(
|
||||
string text,
|
||||
string pattern,
|
||||
RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Regex.Matches(text, pattern, options)
|
||||
.Select(match => match.Value)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "正则匹配全部项出错");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsMatch(
|
||||
string text,
|
||||
string pattern,
|
||||
RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Regex.IsMatch(text, pattern, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "正则检查出错");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace PCL;
|
||||
|
||||
internal static class CrashText
|
||||
{
|
||||
public static string BeforeFirst(
|
||||
string text,
|
||||
string marker,
|
||||
bool ignoreCase = false)
|
||||
{
|
||||
var pos = string.IsNullOrEmpty(marker)
|
||||
? -1
|
||||
: text.IndexOf(marker, _Comparison(ignoreCase));
|
||||
return pos >= 0
|
||||
? text[..pos]
|
||||
: text;
|
||||
}
|
||||
|
||||
public static string AfterFirst(
|
||||
string text,
|
||||
string marker,
|
||||
bool ignoreCase = false)
|
||||
{
|
||||
var pos = string.IsNullOrEmpty(marker)
|
||||
? -1
|
||||
: text.IndexOf(marker, _Comparison(ignoreCase));
|
||||
return pos >= 0
|
||||
? text[(pos + marker.Length)..]
|
||||
: text;
|
||||
}
|
||||
|
||||
public static string AfterLast(
|
||||
string text,
|
||||
string marker,
|
||||
bool ignoreCase = false)
|
||||
{
|
||||
var pos = string.IsNullOrEmpty(marker)
|
||||
? -1
|
||||
: text.LastIndexOf(marker, _Comparison(ignoreCase));
|
||||
return pos >= 0
|
||||
? text[(pos + marker.Length)..]
|
||||
: text;
|
||||
}
|
||||
|
||||
public static string Between(
|
||||
string text,
|
||||
string after,
|
||||
string before,
|
||||
bool ignoreCase = false)
|
||||
{
|
||||
var comparison = _Comparison(ignoreCase);
|
||||
var startPos = string.IsNullOrEmpty(after)
|
||||
? -1
|
||||
: text.LastIndexOf(after, comparison);
|
||||
startPos = startPos >= 0
|
||||
? startPos + after.Length
|
||||
: 0;
|
||||
|
||||
var endPos = string.IsNullOrEmpty(before)
|
||||
? -1
|
||||
: text.IndexOf(before, startPos, comparison);
|
||||
if (endPos >= 0)
|
||||
return text[startPos..endPos];
|
||||
|
||||
return startPos > 0
|
||||
? text[startPos..]
|
||||
: text;
|
||||
}
|
||||
|
||||
private static StringComparison _Comparison(bool ignoreCase)
|
||||
{
|
||||
return ignoreCase
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashDetector
|
||||
{
|
||||
private readonly CrashEvidenceExtractor _extractor = new();
|
||||
private readonly CrashStackAnalyzer _stackAnalyzer = new();
|
||||
|
||||
public CrashAnalysisResult Analyze(CrashLogSet logs, McInstance? instance)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因");
|
||||
var result = new CrashAnalysisResult();
|
||||
if (!logs.HasAnalyzableLog)
|
||||
{
|
||||
result.Add(new CrashFinding(CrashCause.NoAnalyzableFile, CrashConfidence.High) { ShouldStop = true });
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var normalizedLogs = _Normalize(logs);
|
||||
var modIndex = CrashModIndex.Create(normalizedLogs, instance);
|
||||
|
||||
_RunPhase(DetectionPhase.Fatal, normalizedLogs, modIndex, result);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_RunPhase(DetectionPhase.Primary, normalizedLogs, modIndex, result);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var stackFinding = _stackAnalyzer.Analyze(normalizedLogs, modIndex);
|
||||
if (stackFinding is not null)
|
||||
result.Add(stackFinding);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_RunPhase(DetectionPhase.Secondary, normalizedLogs, modIndex, result);
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CrashLogSet _Normalize(CrashLogSet logs)
|
||||
{
|
||||
var all = logs.All;
|
||||
if (all.Contains("quilt", StringComparison.OrdinalIgnoreCase) &&
|
||||
all.Contains("Mod Table Version", StringComparison.Ordinal))
|
||||
{
|
||||
LogWrapper.Info("Crash", "处理 Quilt Mod Table 后再继续分析");
|
||||
all = CrashText.BeforeFirst(all, "| Index") + CrashText.AfterFirst(all, "Mod Table Version:");
|
||||
}
|
||||
|
||||
return new CrashLogSet
|
||||
{
|
||||
Game = logs.Game,
|
||||
Debug = logs.Debug,
|
||||
CrashReport = logs.CrashReport,
|
||||
HsErr = logs.HsErr,
|
||||
All = all
|
||||
};
|
||||
}
|
||||
|
||||
private void _RunPhase(
|
||||
DetectionPhase phase,
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex,
|
||||
CrashAnalysisResult result)
|
||||
{
|
||||
var input = new CrashRuleInput { Logs = logs, ModIndex = modIndex };
|
||||
foreach (var rule in CrashRuleCatalog.Rules.Where(rule => rule.Phase == phase))
|
||||
{
|
||||
var finding = rule.Evaluate(input);
|
||||
if (finding is not null)
|
||||
_AddAndLog(result, finding);
|
||||
}
|
||||
|
||||
foreach (var finding in _extractor.Extract(phase, logs, modIndex))
|
||||
_AddAndLog(result, finding);
|
||||
}
|
||||
|
||||
private static void _AddAndLog(CrashAnalysisResult result, CrashFinding finding)
|
||||
{
|
||||
result.Add(finding);
|
||||
_LogFinding(finding);
|
||||
}
|
||||
|
||||
private static bool _ShouldStop(CrashAnalysisResult result)
|
||||
{
|
||||
return result.Findings.Any(finding => finding.ShouldStop);
|
||||
}
|
||||
|
||||
private static void _LogFinding(CrashFinding finding)
|
||||
{
|
||||
var evidence = string.Join(";", finding.Details);
|
||||
LogWrapper.Info(
|
||||
"Crash",
|
||||
$"可能的崩溃原因:{finding.Cause}{(string.IsNullOrEmpty(evidence) ? "" : "(" + evidence + ")")}");
|
||||
}
|
||||
|
||||
private static void _LogSummary(CrashAnalysisResult result)
|
||||
{
|
||||
if (!result.Any)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因完成,未找到可能的原因");
|
||||
return;
|
||||
}
|
||||
|
||||
LogWrapper.Info("Crash", $"步骤 3:分析崩溃原因完成,找到 {result.Findings.Count} 条可能的原因");
|
||||
foreach (var finding in result.Findings)
|
||||
_LogFinding(finding);
|
||||
}
|
||||
}
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashEvidenceExtractor
|
||||
{
|
||||
public IEnumerable<CrashFinding> Extract(
|
||||
DetectionPhase phase,
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
return phase switch
|
||||
{
|
||||
DetectionPhase.Fatal => _ExtractFatal(logs, modIndex),
|
||||
DetectionPhase.Primary => _ExtractPrimary(logs, modIndex),
|
||||
DetectionPhase.Secondary => _ExtractSecondary(logs, modIndex),
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFatal(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
foreach (var finding in _ExtractOutOfMemory(logs))
|
||||
yield return finding;
|
||||
|
||||
var memoryReservation = _ExtractJvmMemoryReservation(logs);
|
||||
if (memoryReservation is not null)
|
||||
yield return memoryReservation;
|
||||
|
||||
foreach (var finding in _ExtractAccessViolation(logs))
|
||||
yield return finding;
|
||||
|
||||
var signer = _ExtractSignerValidationFailure(logs);
|
||||
if (signer is not null)
|
||||
yield return signer;
|
||||
|
||||
var optifineMissingMods = _ExtractOptiFineMissingMods(logs);
|
||||
if (optifineMissingMods is not null)
|
||||
yield return optifineMissingMods;
|
||||
|
||||
foreach (var finding in _ExtractConfirmedModCrash(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractDuplicateMods(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractMissingDependency(logs))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractPrimary(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
foreach (var finding in _ExtractMixinFailure(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractForgeError(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractFabricSolution(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractFabricProvidedModCrash(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractSuspectedMods(logs, modIndex))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSecondary(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var shortOutput = _ExtractShortOutput(logs);
|
||||
if (shortOutput is not null)
|
||||
yield return shortOutput;
|
||||
|
||||
var modLoader = _ExtractModLoaderError(logs);
|
||||
if (modLoader is not null)
|
||||
yield return modLoader;
|
||||
|
||||
var modInitialization = _ExtractModInitializationFailure(logs, modIndex);
|
||||
if (modInitialization is not null)
|
||||
yield return modInitialization;
|
||||
|
||||
foreach (var finding in _ExtractSpecificBlockAndEntity(logs))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractOutOfMemory(CrashLogSet logs)
|
||||
{
|
||||
var evidence = _FindPatterns(
|
||||
logs,
|
||||
[
|
||||
(CrashLogKind.Game, "java.lang.OutOfMemoryError"),
|
||||
(CrashLogKind.Game, "an out of memory error"),
|
||||
(CrashLogKind.HsErr, "The system is out of physical RAM or swap space"),
|
||||
(CrashLogKind.HsErr, "Out of Memory Error"),
|
||||
(CrashLogKind.CrashReport, "java.lang.OutOfMemoryError")
|
||||
])
|
||||
.ToList();
|
||||
|
||||
if (evidence.Count == 0)
|
||||
yield break;
|
||||
|
||||
yield return new CrashFinding(
|
||||
CrashCause.OutOfMemory,
|
||||
CrashConfidence.High,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractJvmMemoryReservation(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Could not reserve enough space", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var cause = gameLog.Contains("for 1048576KB object heap", StringComparison.Ordinal)
|
||||
? CrashCause.X86JavaMemoryLimit
|
||||
: CrashCause.OutOfMemory;
|
||||
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
[
|
||||
_Evidence(
|
||||
"Could not reserve enough space",
|
||||
CrashLogKind.Game,
|
||||
"jvm-message")
|
||||
])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractAccessViolation(CrashLogSet logs)
|
||||
{
|
||||
var hsLog = logs.HsErr?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(hsLog) ||
|
||||
!hsLog.Contains("EXCEPTION_ACCESS_VIOLATION", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
if (hsLog.Contains("# C [ig", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.IntelDriverAccessViolation, "# C [ig");
|
||||
|
||||
if (hsLog.Contains("# C [atio", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.AmdDriverAccessViolation, "# C [atio");
|
||||
|
||||
if (hsLog.Contains("# C [nvoglv", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.NvidiaDriverAccessViolation, "# C [nvoglv");
|
||||
}
|
||||
|
||||
private static CrashFinding _DriverFinding(
|
||||
CrashCause cause,
|
||||
string pattern)
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
[_Evidence(pattern, CrashLogKind.HsErr, "driver-signature")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractSignerValidationFailure(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains(
|
||||
"signer information does not match signer information of other classes in the same package",
|
||||
StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var detail = (CrashRegex.First(gameLog, "(?<=class \")[^']+(?=\"'s signer information)") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.FileOrContentValidationFailed,
|
||||
CrashConfidence.High,
|
||||
[_Evidence(detail, CrashLogKind.Game, "class")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractOptiFineMissingMods(CrashLogSet logs)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash) ||
|
||||
!crash.Contains("has mods that were not found", StringComparison.Ordinal) ||
|
||||
!CrashRegex.IsMatch(crash, @"The Mod File [^\n]+optifine\\OptiFine[^\n]+ has mods that were not found"))
|
||||
return null;
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.OptiFineForgeIncompatible,
|
||||
CrashConfidence.High,
|
||||
[_Evidence("OptiFine", CrashLogKind.CrashReport, "mod")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractConfirmedModCrash(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (!string.IsNullOrEmpty(gameLog) &&
|
||||
gameLog.Contains("Caught exception from ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = CrashRegex.First(gameLog, @"(?<=Caught exception from )[^\n]+?")
|
||||
?.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint ?? ""]),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
var crash = logs.CrashReport?.Text;
|
||||
if (string.IsNullOrEmpty(crash))
|
||||
yield break;
|
||||
|
||||
if (crash.Contains("-- MOD ", StringComparison.Ordinal))
|
||||
{
|
||||
var modSection = CrashText.Between(crash, "-- MOD ", "Failure message:");
|
||||
|
||||
if (modSection.Contains(".jar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fileName = (CrashRegex.First(modSection, "(?<=Mod File: ).+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
[fileName],
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = (CrashRegex.First(crash, @"(?<=Failure message: )[\w\W]+?(?=\tMod)") ?? "")
|
||||
.Replace("\t", " ")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ModLoaderError,
|
||||
[message],
|
||||
CrashLogKind.CrashReport,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
}
|
||||
|
||||
if (crash.Contains("Multiple entries with same key: ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = (CrashRegex.First(crash, "(?<=Multiple entries with same key: )[^=]+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
if (crash.Contains("LoaderExceptionModCrash: Caught exception from ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = (CrashRegex.First(crash, @"(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
if (crash.Contains("Failed loading config file ", StringComparison.Ordinal))
|
||||
{
|
||||
var mod = (CrashRegex.First(crash, @"(?<=Failed loading config file .+ for modid )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
var config = (CrashRegex.First(crash, "(?<=Failed loading config file ).+(?= of type)") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
var resolved = modIndex.ResolveToDisplayNames([mod]);
|
||||
|
||||
var evidence = new List<CrashEvidence>();
|
||||
evidence.AddRange(resolved.Select(item => _Evidence(item, CrashLogKind.CrashReport, "mod")));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config))
|
||||
evidence.Add(_Evidence(config, CrashLogKind.CrashReport, "config"));
|
||||
|
||||
yield return new CrashFinding(
|
||||
CrashCause.ModConfigCrash,
|
||||
CrashConfidence.High,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractDuplicateMods(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
if (gameLog.Contains("DuplicateModsFoundException", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
gameLog,
|
||||
@"(?<=\n\t[\w]+ : [A-Z]:[^\n]+(/|\\))[^/\\\n]+?.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("Found a duplicate mod", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"Found a duplicate mod[^\n]+") ?? "",
|
||||
@"[^\\/]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("Found duplicate mods", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(gameLog, @"(?<=Mod ID: ')\w+?(?=' from mod files:)")
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("ModResolutionException: Duplicate", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"ModResolutionException: Duplicate[^\n]+") ?? "",
|
||||
@"[^\\/]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractMissingDependency(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
if (gameLog.Contains("Incompatible mods found!", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.IncompatibleMods,
|
||||
[CrashRegex.First(gameLog, @"(?<=Incompatible mods found![\s\S]+: )[\s\S]+?(?=\tat )") ?? ""],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
|
||||
if (gameLog.Contains("Missing or unsupported mandatory dependencies:", StringComparison.Ordinal))
|
||||
{
|
||||
var details = CrashRegex.All(
|
||||
gameLog,
|
||||
@"(?<=Missing or unsupported mandatory dependencies:)([\n\r]+\t(.*))+",
|
||||
RegexOptions.IgnoreCase)
|
||||
.Select(item => item.Trim('\r', '\n', '\t', ' '))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.MissingDependencyOrWrongMcVersion,
|
||||
details,
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractMixinFailure(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
return from source in new[] { CrashLogKind.Game, CrashLogKind.CrashReport }
|
||||
let text = logs.GetText(source)
|
||||
where !string.IsNullOrEmpty(text) && _LooksLikeMixinFailure(text)
|
||||
let modHints = _ExtractMixinModHints(text)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList()
|
||||
let resolved = modIndex.ResolveToDisplayNames(modHints)
|
||||
let hasResolved = modHints.Count > 0 && !modHints.SequenceEqual(resolved)
|
||||
let evidence = (resolved.Count != 0 ? resolved : modHints)
|
||||
.Select(item => _Evidence(item, source, hasResolved ? "mod" : "mod-hint"))
|
||||
select new CrashFinding(
|
||||
CrashCause.ModMixinFailed,
|
||||
hasResolved ? CrashConfidence.High : CrashConfidence.Medium,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool _LooksLikeMixinFailure(string text)
|
||||
{
|
||||
return text.Contains("Mixin prepare failed ", StringComparison.Ordinal) ||
|
||||
text.Contains("Mixin apply failed ", StringComparison.Ordinal) ||
|
||||
text.Contains("MixinApplyError", StringComparison.Ordinal) ||
|
||||
text.Contains("MixinTransformerError", StringComparison.Ordinal) ||
|
||||
text.Contains("mixin.injection.throwables.", StringComparison.Ordinal) ||
|
||||
text.Contains(".json] FAILED during )", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractMixinModHints(string text)
|
||||
{
|
||||
var patterns = new[]
|
||||
{
|
||||
new Regex(
|
||||
@"(?<=from mod )[^.\/ ]+(?=\] from)",
|
||||
RegexOptions.Compiled),
|
||||
|
||||
new Regex(
|
||||
@"(?<=for mod )[^.\/ ]+(?= failed)",
|
||||
RegexOptions.Compiled),
|
||||
|
||||
new Regex(
|
||||
@"(?<=^[^\t]+[ \[{(]{1})[^ \[{(]+\.[^ ]+(?=\.json)",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline)
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
foreach (Match match in pattern.Matches(text))
|
||||
{
|
||||
var value = match.Value
|
||||
.Replace("mixins", "mixin")
|
||||
.Replace(".mixin", "")
|
||||
.Replace("mixin.", "")
|
||||
.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractForgeError(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains(
|
||||
"An exception was thrown, the game will display an error screen and halt.",
|
||||
StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var message = CrashRegex.First(
|
||||
gameLog,
|
||||
@"(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")
|
||||
?.Trim('\r', '\n') ?? "";
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ForgeError,
|
||||
[message],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFabricSolution(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
var solution = "";
|
||||
|
||||
if (gameLog.Contains("A potential solution has been determined:", StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"(?<=A potential solution has been determined:\n)(\s+ - [^\n]+\n)+") ??
|
||||
"",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
else if (gameLog.Contains(
|
||||
"A potential solution has been determined, this may resolve your problem:",
|
||||
StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(
|
||||
gameLog,
|
||||
@"(?<=A potential solution has been determined, this may resolve your problem:\n)(\s+ - [^\n]+\n)+") ??
|
||||
"",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
else if (gameLog.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:", StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)(\s+ - [^\n]+\n)+") ?? "",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(solution))
|
||||
yield return _ModFinding(
|
||||
CrashCause.FabricSolutionProvided,
|
||||
[solution],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFabricProvidedModCrash(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("due to errors, provided by ", StringComparison.Ordinal) ||
|
||||
_LooksLikeMixinFailure(gameLog))
|
||||
yield break;
|
||||
|
||||
var hint = (CrashRegex.First(gameLog, "(?<=due to errors, provided by ')[^']+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSuspectedMods(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash) ||
|
||||
!crash.Contains("Suspected Mod", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var suspectsRaw = CrashText.Between(crash, "Suspected Mod", "Stacktrace");
|
||||
|
||||
if (suspectsRaw.StartsWith("s: None", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var suspects = CrashRegex.All(suspectsRaw, @"(?<=\n\t[^(\t]+\()[^)\n]+");
|
||||
|
||||
if (suspects.Count != 0)
|
||||
yield return _ModFinding(
|
||||
CrashCause.SuspectedModCrash,
|
||||
modIndex.ResolveToDisplayNames(suspects),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractShortOutput(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
logs.HsErr is not null ||
|
||||
logs.CrashReport is not null ||
|
||||
gameLog.Contains("at net.", StringComparison.Ordinal) ||
|
||||
gameLog.Contains("INFO]", StringComparison.Ordinal) ||
|
||||
gameLog.Length >= 100)
|
||||
return null;
|
||||
|
||||
return _ModFinding(
|
||||
CrashCause.VeryShortOutput,
|
||||
[gameLog],
|
||||
CrashLogKind.Game,
|
||||
false,
|
||||
"raw-output");
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractModLoaderError(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Mod resolution failed", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.ModLoaderError,
|
||||
CrashConfidence.High);
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractModInitializationFailure(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Failed to create mod instance.", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var hint = (CrashRegex.First(gameLog, "(?<=Failed to create mod instance. ModID: )[^,]+") ??
|
||||
CrashRegex.First(gameLog, @"(?<=Failed to create mod instance. ModId )[^\n]+(?= for )") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
return _ModFinding(
|
||||
CrashCause.ModInitializationFailed,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.Game,
|
||||
false);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSpecificBlockAndEntity(CrashLogSet logs)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash))
|
||||
yield break;
|
||||
|
||||
if (crash.Contains("\tBlock location: World: ", StringComparison.Ordinal))
|
||||
{
|
||||
var value =
|
||||
(CrashRegex.First(crash, @"(?<=\tBlock: Block\{)[^\}]+") ?? "") +
|
||||
" " +
|
||||
(CrashRegex.First(crash, @"(?<=\tBlock location: World: )\([^\)]+\)") ?? "");
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.SpecificBlockCrash,
|
||||
[value],
|
||||
CrashLogKind.CrashReport,
|
||||
false,
|
||||
"block");
|
||||
}
|
||||
|
||||
if (crash.Contains("\tEntity's Exact location: ", StringComparison.Ordinal))
|
||||
{
|
||||
var value =
|
||||
(CrashRegex.First(crash, @"(?<=\tEntity Type: )[^\n]+(?= \()") ?? "") +
|
||||
" (" +
|
||||
(CrashRegex.First(crash, @"(?<=\tEntity's Exact location: )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n') +
|
||||
")";
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.SpecificEntityCrash,
|
||||
[value],
|
||||
CrashLogKind.CrashReport,
|
||||
false,
|
||||
"entity");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashEvidence> _FindPatterns(
|
||||
CrashLogSet logs,
|
||||
IEnumerable<(CrashLogKind Source, string Pattern)> patterns)
|
||||
{
|
||||
foreach (var (source, pattern) in patterns)
|
||||
{
|
||||
var text = logs.GetText(source);
|
||||
|
||||
if (text?.Contains(pattern, StringComparison.Ordinal) == true)
|
||||
yield return _Evidence(pattern, source, "pattern", pattern);
|
||||
}
|
||||
}
|
||||
|
||||
private static CrashFinding _ModFinding(
|
||||
CrashCause cause,
|
||||
IEnumerable<string?> values,
|
||||
CrashLogKind source,
|
||||
bool shouldStop,
|
||||
string displayKind = "mod")
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => _Evidence(value!, source, displayKind)))
|
||||
{
|
||||
ShouldStop = shouldStop
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashEvidence _Evidence(
|
||||
string value,
|
||||
CrashLogKind source,
|
||||
string displayKind,
|
||||
string? pattern = null)
|
||||
{
|
||||
return new CrashEvidence
|
||||
{
|
||||
Value = value,
|
||||
Source = source,
|
||||
Pattern = pattern,
|
||||
DisplayKind = displayKind
|
||||
};
|
||||
}
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashModInfo
|
||||
{
|
||||
public string? ModId { get; init; }
|
||||
|
||||
public string? DisplayName { get; init; }
|
||||
|
||||
public string? FileName { get; init; }
|
||||
|
||||
public string? Version { get; init; }
|
||||
|
||||
public string Source { get; init; } = "";
|
||||
|
||||
public string DisplayNameOrFileName =>
|
||||
!string.IsNullOrWhiteSpace(DisplayName) ? DisplayName :
|
||||
!string.IsNullOrWhiteSpace(FileName) ? FileName :
|
||||
ModId ?? "";
|
||||
}
|
||||
|
||||
internal sealed class CrashModIndex
|
||||
{
|
||||
private readonly List<CrashModInfo> _mods = [];
|
||||
|
||||
public static CrashModIndex Create(CrashLogSet logs, McInstance? instance)
|
||||
{
|
||||
var index = new CrashModIndex();
|
||||
index._ReadCrashReport(logs.CrashReport?.Text);
|
||||
index._ReadForgeDebugLog(logs.Debug?.Text);
|
||||
index._ReadLoaderMessages(logs.Game?.Text);
|
||||
if (instance is not null)
|
||||
index._ReadInstanceModFiles(instance);
|
||||
index._NormalizeAndDeduplicate();
|
||||
return index;
|
||||
}
|
||||
|
||||
public IEnumerable<CrashModInfo> ResolveMany(IEnumerable<string> hints)
|
||||
{
|
||||
var found = new List<CrashModInfo>();
|
||||
foreach (var hint in hints)
|
||||
foreach (var mod in Resolve(hint))
|
||||
if (!found.Any(item => _SameDisplay(item, mod)))
|
||||
found.Add(mod);
|
||||
return found;
|
||||
}
|
||||
|
||||
public IEnumerable<CrashModInfo> Resolve(string? hint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hint))
|
||||
return [];
|
||||
|
||||
var normalizedHint = _Normalize(hint);
|
||||
if (string.IsNullOrWhiteSpace(normalizedHint))
|
||||
return [];
|
||||
|
||||
var exact = _mods
|
||||
.Where(mod =>
|
||||
string.Equals(_Normalize(mod.ModId), normalizedHint, StringComparison.Ordinal) ||
|
||||
string.Equals(_Normalize(mod.FileName), normalizedHint, StringComparison.Ordinal) ||
|
||||
string.Equals(_Normalize(mod.DisplayName), normalizedHint, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
if (exact.Count != 0)
|
||||
return exact;
|
||||
|
||||
return _mods
|
||||
.Where(mod =>
|
||||
_Normalize(mod.ModId).Contains(normalizedHint, StringComparison.Ordinal) ||
|
||||
_Normalize(mod.FileName).Contains(normalizedHint, StringComparison.Ordinal) ||
|
||||
_Normalize(mod.DisplayName).Contains(normalizedHint, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<string> ResolveToDisplayNames(IEnumerable<string> hints)
|
||||
{
|
||||
var hintList = hints
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(item => item.Trim())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var mods = ResolveMany(hintList)
|
||||
.Select(mod => mod.DisplayNameOrFileName)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return mods.Count != 0 ? mods : hintList;
|
||||
}
|
||||
|
||||
private void _ReadCrashReport(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) ||
|
||||
!text.Contains("A detailed walkthrough of the error", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var details = text.Replace("A detailed walkthrough of the error", "¨");
|
||||
var isFabric = details.Contains("Fabric Mods", StringComparison.Ordinal);
|
||||
if (isFabric)
|
||||
{
|
||||
details = details.Replace("Fabric Mods", "¨");
|
||||
LogWrapper.Info("Crash", "崩溃报告中检测到 Fabric Mod 信息格式");
|
||||
}
|
||||
|
||||
var isQuilt = details.Contains("quilt-loader", StringComparison.Ordinal);
|
||||
if (isQuilt)
|
||||
{
|
||||
details = details.Replace("Mod Table Version", "¨");
|
||||
LogWrapper.Info("Crash", "崩溃报告中检测到 Quilt Mod 信息格式");
|
||||
}
|
||||
|
||||
details = CrashText.AfterLast(details, "¨");
|
||||
foreach (var rawLine in details.Split('\n'))
|
||||
{
|
||||
var line = rawLine.Trim('\r', '\n');
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (isFabric && line.StartsWith(
|
||||
"\t\t",
|
||||
StringComparison.Ordinal) && !CrashRegex.IsMatch(line, @"\t\tfabric[\w-]*: Fabric"))
|
||||
{
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
ModId = CrashRegex.First(line, @"(?<=\t\t)[^:]+"),
|
||||
DisplayName = CrashRegex.First(line, @"(?<=: )[^\n]+(?= [^\n]+)"),
|
||||
Version = CrashRegex.First(line, @"(?<= )[\w\.-]+$"),
|
||||
Source = "crash-report-fabric"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Contains(".jar", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Length - line.Replace(".jar", "", StringComparison.OrdinalIgnoreCase).Length == 4)
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = CrashRegex.First(line,
|
||||
@"(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
DisplayName = CrashRegex.First(line, @"(?<=\t)[^\t\|]+(?=\s+\()"),
|
||||
ModId = CrashRegex.First(line, @"(?<=\| )[\w\.-]+(?= \|)"),
|
||||
Source = "crash-report-forge"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void _ReadForgeDebugLog(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
foreach (var line in CrashRegex.All(text, "(?<=valid mod file ).*", RegexOptions.Multiline))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = CrashRegex.First(line, ".*(?= with)"),
|
||||
ModId = CrashRegex.First(line, @"(?<=with \{)[^\}]+"),
|
||||
Version = CrashRegex.First(line, @"(?<=versions \{)[^\}]+"),
|
||||
Source = "debug-log"
|
||||
});
|
||||
}
|
||||
|
||||
private void _ReadLoaderMessages(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
foreach (var line in CrashRegex.All(text, @"(?<=ModID: )[^,\n]+|(?<=ModId )[^\n]+(?= for )",
|
||||
RegexOptions.IgnoreCase))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
ModId = line.Trim(),
|
||||
Source = "loader-message"
|
||||
});
|
||||
}
|
||||
|
||||
private void _ReadInstanceModFiles(McInstance instance)
|
||||
{
|
||||
foreach (var directory in _GetCandidateModDirectories(instance))
|
||||
try
|
||||
{
|
||||
var info = new DirectoryInfo(directory);
|
||||
if (!info.Exists)
|
||||
continue;
|
||||
|
||||
foreach (var file in info.EnumerateFiles("*.jar"))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = file.Name,
|
||||
DisplayName = Path.GetFileNameWithoutExtension(file.Name),
|
||||
Source = "instance-mods"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "读取实例 Mod 文件列表失败(" + directory + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _GetCandidateModDirectories(McInstance instance)
|
||||
{
|
||||
yield return Path.Combine(instance.PathInstance, "mods");
|
||||
string? pathIndie = null;
|
||||
try
|
||||
{
|
||||
pathIndie = instance.PathIndie;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "读取实例隔离路径失败");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pathIndie))
|
||||
yield return Path.Combine(pathIndie, "mods");
|
||||
}
|
||||
|
||||
private void _Add(CrashModInfo mod)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mod.ModId) &&
|
||||
string.IsNullOrWhiteSpace(mod.DisplayName) &&
|
||||
string.IsNullOrWhiteSpace(mod.FileName))
|
||||
return;
|
||||
_mods.Add(mod);
|
||||
}
|
||||
|
||||
private void _NormalizeAndDeduplicate()
|
||||
{
|
||||
var distinct = new List<CrashModInfo>();
|
||||
foreach (var mod in _mods
|
||||
.Where(mod => !distinct.Any(item => _SameDisplay(item, mod))))
|
||||
distinct.Add(mod);
|
||||
|
||||
_mods.Clear();
|
||||
_mods.AddRange(distinct);
|
||||
LogWrapper.Info("Crash", "构建 Mod 索引,找到 " + _mods.Count + " 个候选 Mod");
|
||||
}
|
||||
|
||||
private static bool _SameDisplay(CrashModInfo a, CrashModInfo b)
|
||||
{
|
||||
return string.Equals(_Normalize(a.ModId), _Normalize(b.ModId), StringComparison.Ordinal) &&
|
||||
string.Equals(_Normalize(a.FileName), _Normalize(b.FileName), StringComparison.Ordinal) &&
|
||||
string.Equals(_Normalize(a.DisplayName), _Normalize(b.DisplayName), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string _Normalize(string? value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.ToLowerInvariant()
|
||||
.Replace("_", "")
|
||||
.Replace("-", "")
|
||||
.Replace(" ", "")
|
||||
.Replace(".jar", "");
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashRule
|
||||
{
|
||||
public required CrashCause Cause { get; init; }
|
||||
|
||||
public required DetectionPhase Phase { get; init; }
|
||||
|
||||
public CrashConfidence Confidence { get; init; } = CrashConfidence.High;
|
||||
|
||||
public bool StopOnMatch { get; init; }
|
||||
|
||||
public required Func<CrashRuleInput, CrashFinding?> Evaluate { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class CrashRuleInput
|
||||
{
|
||||
public required CrashLogSet Logs { get; init; }
|
||||
|
||||
public required CrashModIndex ModIndex { get; init; }
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
namespace PCL;
|
||||
|
||||
internal static class CrashRuleCatalog
|
||||
{
|
||||
public static IReadOnlyList<CrashRule> Rules { get; } =
|
||||
[
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass",
|
||||
CrashCause.JavaTooNew),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Found multiple arguments for option fml.forgeVersion, but you asked for only one",
|
||||
CrashCause.MultipleForgeInInstanceJson),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"The driver does not appear to support OpenGL",
|
||||
CrashCause.UnsupportedOpenGl),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassCastException: java.base/jdk",
|
||||
CrashCause.UsingJdk),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassCastException: class jdk.",
|
||||
CrashCause.UsingJdk),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.<init>",
|
||||
"java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.<init>",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager",
|
||||
"java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize"
|
||||
],
|
||||
CrashCause.OptiFineForgeIncompatible),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"Open J9 is not supported",
|
||||
"OpenJ9 is incompatible",
|
||||
".J9VMInternals."
|
||||
],
|
||||
CrashCause.UsingOpenJ9),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"java.lang.NoSuchFieldException: ucp",
|
||||
"because module java.base does not export",
|
||||
"java.lang.ClassNotFoundException: jdk.nashorn.api.scripting.NashornScriptEngineFactory",
|
||||
"java.lang.ClassNotFoundException: java.lang.invoke.LambdaMetafactory"
|
||||
],
|
||||
CrashCause.JavaTooNew),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"The directories below appear to be extracted jar files. Fix this before you continue.",
|
||||
"Extracted mod jars found, loading will NOT continue"
|
||||
],
|
||||
CrashCause.ExtractedModFile),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker",
|
||||
CrashCause.MissingMixinBootstrap),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Couldn't set pixel format",
|
||||
CrashCause.PixelFormatNotSupported),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.",
|
||||
CrashCause.ShadersModWithOptiFine),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier",
|
||||
CrashCause.OldForgeNewJavaIncompatible),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"1282: Invalid operation",
|
||||
CrashCause.OpenGl1282),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Maybe try a lower resolution resourcepack?",
|
||||
CrashCause.ResourcePackTooLarge),
|
||||
_ContainsAll(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z",
|
||||
"OptiFine"
|
||||
],
|
||||
CrashCause.OptiFineWorldLoadCrash),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
["Unsupported class file major version", "Unsupported major.minor version"], CrashCause.JavaIncompatible),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"com.electronwill.nightconfig.core.io.ParsingException: Not enough data available",
|
||||
CrashCause.NightConfigBug),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Cannot find launch target fmlclient, unable to launch",
|
||||
CrashCause.IncompleteForgeInstallation),
|
||||
_ContainsAll(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"Invalid paths argument, contained no existing paths",
|
||||
@"libraries\net\minecraftforge\fmlcore"
|
||||
],
|
||||
CrashCause.IncompleteForgeInstallation),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Invalid module name: '' is not a Java identifier",
|
||||
CrashCause.InvalidModFileName),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to",
|
||||
"java.lang.RuntimeException: java.lang.NoSuchMethodException: no such method: sun.misc.Unsafe.defineAnonymousClass(Class,byte[],Object[])Class/invokeVirtual",
|
||||
"java.lang.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set. Level is not supported by the active JRE or ASM version"
|
||||
],
|
||||
CrashCause.ModRequiresJava11),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Invalid maximum heap size",
|
||||
CrashCause.X86JavaMemoryLimit),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"maximum id range exceeded",
|
||||
CrashCause.TooManyModsIdLimit),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Pixel format not accelerated",
|
||||
CrashCause.PixelFormatNotSupported),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Manually triggered debug crash",
|
||||
CrashCause.ManualDebugCrash)
|
||||
];
|
||||
|
||||
private static CrashRule _Contains(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
string pattern,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
return text?.Contains(pattern, StringComparison.Ordinal) != true
|
||||
? null
|
||||
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashRule _ContainsAny(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
IReadOnlyList<string> patterns,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return null;
|
||||
|
||||
var pattern = patterns.FirstOrDefault(item => text.Contains(item, StringComparison.Ordinal));
|
||||
return pattern is null
|
||||
? null
|
||||
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashRule _ContainsAll(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
IReadOnlyList<string> patterns,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
if (string.IsNullOrEmpty(text) ||
|
||||
patterns.Any(pattern => !text.Contains(pattern, StringComparison.Ordinal)))
|
||||
return null;
|
||||
|
||||
return _CreateFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
source,
|
||||
string.Join(" | ", patterns),
|
||||
stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding _CreateFinding(
|
||||
CrashCause cause,
|
||||
CrashConfidence confidence,
|
||||
CrashLogKind source,
|
||||
string pattern,
|
||||
bool stopOnMatch)
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
confidence,
|
||||
[
|
||||
new CrashEvidence
|
||||
{
|
||||
Source = source,
|
||||
Pattern = pattern,
|
||||
Value = pattern,
|
||||
DisplayKind = "pattern"
|
||||
}
|
||||
])
|
||||
{
|
||||
ShouldStop = stopOnMatch
|
||||
};
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed partial class CrashStackAnalyzer
|
||||
{
|
||||
private static readonly HashSet<string> IgnoredPrefixes = new(StringComparer.Ordinal)
|
||||
{
|
||||
"java", "sun", "javax", "jdk", "com.sun",
|
||||
"com.mojang", "net.minecraft", "MojangTricksIntelDriversForPerformance_javaw",
|
||||
"net.minecraftforge", "cpw.mods", "net.fabricmc", "org.quiltmc", "org.spongepowered", "com.mumfrey",
|
||||
"org.lwjgl", "paulscode.sound", "com.google", "org.apache", "com.electronwill.nightconfig", "it.unimi.dsi",
|
||||
"oolloo"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> IgnoredWords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"com", "org", "net", "top", "dev", "gitlab", "github",
|
||||
"sun", "lib", "nio", "api", "asm", "reflect", "internal", "microsoft",
|
||||
"mcp", "fml", "forge", "fabricmc", "neoforge", "neoforged", "minecraftforge", "minecraft", "mojang",
|
||||
"mod", "mods", "modapi", "jar", "loader", "launch", "preinit", "preload", "init", "setup", "plugin",
|
||||
"mixin", "mixins", "injection", "transformer", "transformers", "spongepowered",
|
||||
"game", "world", "server", "client", "common", "entity", "block", "item", "tile", "blockentity",
|
||||
"gui", "model", "render", "shader", "optifine",
|
||||
"config", "data", "file", "read", "recipe", "content", "general",
|
||||
"event", "events", "handler", "listeners", "assist", "override",
|
||||
"netty", "packet", "channel",
|
||||
"task", "pool", "scheduler", "systems", "system", "modules", "service", "platform",
|
||||
"core", "main", "base", "util", "impl", "done", "map", "load", "machine",
|
||||
"dsi", "unimi", "fastutil", "lwjgl", "oshi", "electronwill",
|
||||
"compat", "universal", "multipart"
|
||||
};
|
||||
|
||||
public CrashFinding? Analyze(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
if (!_ShouldAnalyzeStack(logs.All))
|
||||
{
|
||||
LogWrapper.Info("Crash", "可能并未安装 Mod,不进行堆栈分析");
|
||||
return null;
|
||||
}
|
||||
|
||||
var stackText = _CollectStackText(logs);
|
||||
var packages = _ExtractPackages(stackText).ToList();
|
||||
var keywords = _ExtractKeywords(packages).ToList();
|
||||
|
||||
if (keywords.Count is 0 or > 10)
|
||||
{
|
||||
if (keywords.Count > 10)
|
||||
LogWrapper.Info("Crash", "关键词过多,考虑匹配出错,不纳入考虑");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var mods = modIndex.ResolveMany(keywords).ToList();
|
||||
|
||||
if (mods.Count != 0)
|
||||
return new CrashFinding(
|
||||
CrashCause.StackModNameFound,
|
||||
CrashConfidence.Medium,
|
||||
mods.Select(mod => new CrashEvidence
|
||||
{
|
||||
Source = CrashLogKind.All,
|
||||
Value = mod.DisplayNameOrFileName,
|
||||
DisplayKind = "mod"
|
||||
}))
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.StackKeywordFound,
|
||||
CrashConfidence.Low,
|
||||
keywords.Select(keyword => new CrashEvidence
|
||||
{
|
||||
Source = CrashLogKind.All,
|
||||
Value = keyword,
|
||||
DisplayKind = "keyword"
|
||||
}))
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool _ShouldAnalyzeStack(string allLogs)
|
||||
{
|
||||
return allLogs.Contains("forge", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("fabric", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("quilt", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("liteloader", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string _CollectStackText(CrashLogSet logs)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (logs.CrashReport?.Text is not null)
|
||||
{
|
||||
LogWrapper.Info("Crash", "开始进行崩溃日志堆栈分析");
|
||||
result.Add(CrashText.BeforeFirst(logs.CrashReport.Text, "System Details"));
|
||||
}
|
||||
|
||||
if (logs.Game?.Text is not null)
|
||||
{
|
||||
var fatals = CrashRegex.All(logs.Game.Text, @"/FATAL] .+?(?=[\n]+\[)");
|
||||
|
||||
if (logs.Game.Text.Contains("Unreported exception thrown!", StringComparison.Ordinal))
|
||||
fatals.Add(
|
||||
CrashText.Between(logs.Game.Text,
|
||||
"Unreported exception thrown!",
|
||||
"at oolloo.jlw.Wrapper"));
|
||||
|
||||
LogWrapper.Info("Crash", "开始进行 Minecraft 日志堆栈分析,发现 " + fatals.Count + " 个报错项");
|
||||
result.AddRange(fatals);
|
||||
}
|
||||
|
||||
if (logs.HsErr?.Text is not null)
|
||||
{
|
||||
LogWrapper.Info("Crash", "开始进行虚拟机堆栈分析");
|
||||
result.Add(CrashText.Between(logs.HsErr.Text, "T H R E A D", "Registers:"));
|
||||
}
|
||||
|
||||
return "\n" + string.Join("\n", result) + "\n";
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractPackages(string stackText)
|
||||
{
|
||||
var packages = new List<string>();
|
||||
|
||||
packages.AddRange(
|
||||
PackageRegex()
|
||||
.Matches(stackText)
|
||||
.Select(match => match.Value));
|
||||
|
||||
packages.AddRange(
|
||||
MixinStackRegex()
|
||||
.Matches(stackText)
|
||||
.Select(match => match.Value.Replace("$", ".")));
|
||||
|
||||
var possibleStacks = packages
|
||||
.Select(item => item.Trim())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Where(item => !IgnoredPrefixes.Any(prefix => item.StartsWith(prefix, StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
|
||||
LogWrapper.Info("Crash", "找到 " + possibleStacks.Count + " 条可能的堆栈信息");
|
||||
|
||||
foreach (var stack in possibleStacks)
|
||||
LogWrapper.Info("Crash", " - " + stack);
|
||||
|
||||
return possibleStacks;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractKeywords(IEnumerable<string> packages)
|
||||
{
|
||||
var words = new List<string>();
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
var split = package.Split('.');
|
||||
|
||||
for (var i = 0; i <= Math.Min(3, split.Length - 1); i++)
|
||||
{
|
||||
var word = split[i].Trim();
|
||||
|
||||
if (word.Length <= 2 ||
|
||||
word.StartsWith("func_", StringComparison.Ordinal) ||
|
||||
IgnoredWords.Contains(word))
|
||||
continue;
|
||||
|
||||
words.Add(word);
|
||||
}
|
||||
}
|
||||
|
||||
var result = words
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
LogWrapper.Info("Crash", "从堆栈信息中找到 " + result.Count + " 个可能的 Mod ID 关键词");
|
||||
|
||||
if (result.Count != 0)
|
||||
LogWrapper.Info("Crash", " - " + string.Join(", ", result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[GeneratedRegex(
|
||||
@"(?<=\n[^{]+)[a-zA-Z_]+\w+\.[a-zA-Z_]+[\w\.]+(?=\.[\w\.$]+\.)",
|
||||
RegexOptions.Compiled)]
|
||||
private static partial Regex PackageRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
@"(?<=at [^(]+?\.\w+\$\w+\$)[\w\$]+?(?=\$\w+\()",
|
||||
RegexOptions.Compiled)]
|
||||
private static partial Regex MixinStackRegex();
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace PCL;
|
||||
|
||||
internal enum DetectionPhase
|
||||
{
|
||||
Fatal,
|
||||
Primary,
|
||||
Secondary
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Codecs;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Core.Utils.Secret;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashReportExporter
|
||||
{
|
||||
private const string ReportFolderName = "Report";
|
||||
|
||||
private const string LaunchScriptFileName = "启动脚本.bat";
|
||||
private const string RawOutputFileName = "游戏崩溃前的输出.txt";
|
||||
private const string LauncherLogFileName = "PCL CE 启动器日志.txt";
|
||||
private const string EnvironmentFileName = "环境与启动信息.txt";
|
||||
private const string ModInfoFileName = "模组列表.txt";
|
||||
|
||||
public void Export(
|
||||
CrashAnalysisContext context,
|
||||
string targetZipPath,
|
||||
IEnumerable<string>? extraFiles)
|
||||
{
|
||||
var targetFolder = Basics.GetParentPathOrEmpty(targetZipPath);
|
||||
Directory.CreateDirectory(targetFolder);
|
||||
|
||||
if (File.Exists(targetZipPath))
|
||||
File.Delete(targetZipPath);
|
||||
|
||||
ModBase.FeedbackInfo();
|
||||
|
||||
var reportFolder = Path.Combine(context.TempFolder, ReportFolderName);
|
||||
|
||||
if (Directory.Exists(reportFolder))
|
||||
CrashFileIo.DeleteDirectory(reportFolder);
|
||||
|
||||
Directory.CreateDirectory(reportFolder);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var outputFile in _CollectOutputFiles(context, extraFiles))
|
||||
_CopyFileToReport(reportFolder, outputFile);
|
||||
|
||||
_WriteEnvironmentInfo(reportFolder);
|
||||
_WriteModInfo(reportFolder, context.Instance);
|
||||
|
||||
ZipFile.CreateFromDirectory(reportFolder, targetZipPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CrashFileIo.DeleteDirectory(reportFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _CopyFileToReport(
|
||||
string reportFolder,
|
||||
string outputFile)
|
||||
{
|
||||
if (!File.Exists(outputFile))
|
||||
return;
|
||||
|
||||
var fileName = _GetExportFileName(outputFile, out var fileEncoding);
|
||||
|
||||
fileEncoding ??= EncodingDetector.DetectEncoding(CrashFileIo.ReadBytes(outputFile));
|
||||
|
||||
var fileContent = CrashFileIo.ReadText(outputFile, fileEncoding);
|
||||
fileContent = _SanitizeFileContent(fileContent, fileName);
|
||||
|
||||
CrashFileIo.WriteText(
|
||||
Path.Combine(reportFolder, fileName),
|
||||
fileContent,
|
||||
fileEncoding);
|
||||
|
||||
LogWrapper.Info("Crash", $"导出文件:{fileName},编码:{fileEncoding.HeaderName}");
|
||||
}
|
||||
|
||||
private static string _GetExportFileName(
|
||||
string outputFile,
|
||||
out Encoding? fileEncoding)
|
||||
{
|
||||
fileEncoding = null;
|
||||
|
||||
var fileName = Path.GetFileName(outputFile);
|
||||
|
||||
switch (fileName)
|
||||
{
|
||||
case "LatestLaunch.bat":
|
||||
return LaunchScriptFileName;
|
||||
|
||||
case "RawOutput.log":
|
||||
fileEncoding = Encoding.UTF8;
|
||||
return RawOutputFileName;
|
||||
}
|
||||
|
||||
var currentLogFile = LogWrapper.CurrentLogger.CurrentLogFiles.LastOrDefault();
|
||||
var currentLogFileName = currentLogFile is null ? null : CrashText.AfterLast(currentLogFile, @"\");
|
||||
|
||||
if (currentLogFileName != fileName) return fileName;
|
||||
|
||||
fileEncoding = Encoding.UTF8;
|
||||
return LauncherLogFileName;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> _CollectOutputFiles(
|
||||
CrashAnalysisContext context,
|
||||
IEnumerable<string>? extraFiles)
|
||||
{
|
||||
if (extraFiles is not null)
|
||||
context.OutputFiles.AddRange(extraFiles);
|
||||
|
||||
return context.OutputFiles;
|
||||
}
|
||||
|
||||
private static string _SanitizeFileContent(
|
||||
string fileContent,
|
||||
string fileName)
|
||||
{
|
||||
var tokenMask = fileName == LaunchScriptFileName ? 'F' : '*';
|
||||
|
||||
fileContent = McLogFilter.FilterAccessToken(fileContent, tokenMask);
|
||||
return McLogFilter.FilterUserName(fileContent, '*');
|
||||
}
|
||||
|
||||
private static void _WriteEnvironmentInfo(string reportFolder)
|
||||
{
|
||||
var launcherLog = CrashText.BeforeFirst(
|
||||
CrashText.AfterLast(_ReadReportFile(reportFolder, LauncherLogFileName), "[Launch] ~ 基础参数 ~"),
|
||||
"开始 Minecraft 日志监控");
|
||||
|
||||
var launchScript = _ReadReportFile(reportFolder, LaunchScriptFileName);
|
||||
|
||||
var envInfo = new StringBuilder();
|
||||
|
||||
_AppendLauncherInfo(envInfo);
|
||||
_AppendProfileInfo(envInfo, launcherLog);
|
||||
_AppendInstanceInfo(envInfo, launcherLog, launchScript);
|
||||
_AppendEnvironmentInfo(envInfo, launcherLog);
|
||||
|
||||
CrashFileIo.WriteText(
|
||||
Path.Combine(reportFolder, EnvironmentFileName),
|
||||
envInfo.ToString(),
|
||||
Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static void _AppendLauncherInfo(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherVersion", Basics.VersionName));
|
||||
builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherId", Identify.LauncherId));
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void _AppendProfileInfo(
|
||||
StringBuilder builder,
|
||||
string launcherLog)
|
||||
{
|
||||
builder.AppendLine(Lang.Text("Crash.Report.Environment.ProfileSection"));
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.ProfileName",
|
||||
_ExtractLauncherValue(launcherLog, "玩家用户名:"),
|
||||
_ExtractLauncherValue(launcherLog, "验证方式:")));
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void _AppendInstanceInfo(
|
||||
StringBuilder builder,
|
||||
string launcherLog,
|
||||
string launchScript)
|
||||
{
|
||||
builder.AppendLine(Lang.Text("Crash.Report.Environment.InstanceSection"));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.SelectedJava",
|
||||
_ExtractLauncherValue(launcherLog, "Java 信息:")));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.Log4j2NoLookups",
|
||||
!launchScript.Contains("-Dlog4j2.formatMsgNoLookups=false", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.MinecraftFolder",
|
||||
_ExtractLauncherValue(launcherLog, "MC 文件夹:")));
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void _AppendEnvironmentInfo(
|
||||
StringBuilder builder,
|
||||
string launcherLog)
|
||||
{
|
||||
builder.AppendLine(Lang.Text("Crash.Report.Environment.EnvironmentSection"));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.OperatingSystem",
|
||||
SystemInfo.OSInfo,
|
||||
!SystemInfo.Is32BitSystem,
|
||||
SystemInfo.IsArm64System));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.Cpu",
|
||||
HardwareInfo.CPUName));
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.MemoryAllocation",
|
||||
_ExtractLauncherValue(launcherLog, "分配的内存:"),
|
||||
Lang.Number(HardwareInfo.SystemMemorySize / 1024d, "N2"),
|
||||
Lang.Number(HardwareInfo.SystemMemorySize, "N0")));
|
||||
|
||||
for (var i = 0; i < HardwareInfo.GPUs.Count; i++)
|
||||
{
|
||||
var gpu = HardwareInfo.GPUs[i];
|
||||
|
||||
builder.AppendLine(
|
||||
Lang.Text(
|
||||
"Crash.Report.Environment.Gpu",
|
||||
i,
|
||||
gpu.Name,
|
||||
_FormatGpuMemory(gpu.Memory),
|
||||
gpu.DriverVersion));
|
||||
}
|
||||
}
|
||||
|
||||
private static string _ExtractLauncherValue(
|
||||
string launcherLog,
|
||||
string key)
|
||||
{
|
||||
return CrashText.Between(launcherLog, key, "[")
|
||||
.TrimEnd('[')
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static string _FormatGpuMemory(long memory)
|
||||
{
|
||||
return memory >= 4095L
|
||||
? ">= " + memory
|
||||
: memory.ToString();
|
||||
}
|
||||
|
||||
private static string _ReadReportFile(
|
||||
string reportFolder,
|
||||
string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(reportFolder, fileName);
|
||||
|
||||
return File.Exists(filePath)
|
||||
? CrashFileIo.ReadText(filePath)
|
||||
: "";
|
||||
}
|
||||
|
||||
private static void _WriteModInfo(string reportFolder, McInstance? instance)
|
||||
{
|
||||
if (instance is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var modsFolderName = ModLocalComp.GetPathNameByCompType(ModComp.CompType.Mod);
|
||||
var modsFolder = instance.Info.HasLabyMod
|
||||
? Path.Combine(instance.PathIndie, "labymod-neo", "fabric", instance.Info.VanillaName, modsFolderName)
|
||||
: Path.Combine(instance.PathIndie, modsFolderName);
|
||||
|
||||
if (!Directory.Exists(modsFolder))
|
||||
return;
|
||||
|
||||
// 老 Forge(Drop < 130)的启用 Mod 位于 mods/<版本名> 子目录,需一并扫描
|
||||
var scanFolders = new List<string> { modsFolder };
|
||||
if (instance.Info.HasForge && instance.Info.Drop < 130)
|
||||
{
|
||||
var versionSubFolder = Path.Combine(modsFolder, instance.Info.VanillaName);
|
||||
if (Directory.Exists(versionSubFolder))
|
||||
scanFolders.Add(versionSubFolder);
|
||||
}
|
||||
|
||||
var activeMods = new List<ModLocalComp.LocalCompFile>();
|
||||
foreach (var folder in scanFolders)
|
||||
foreach (var file in Directory.GetFiles(folder))
|
||||
{
|
||||
if (!ModLocalComp.LocalCompFile.IsModFile(file)
|
||||
|| file.EndsWith(".disabled", StringComparison.OrdinalIgnoreCase)
|
||||
|| file.EndsWith(".old", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
var mod = new ModLocalComp.LocalCompFile(file);
|
||||
mod.Load();
|
||||
if (mod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
|
||||
activeMods.Add(mod);
|
||||
}
|
||||
|
||||
activeMods = activeMods
|
||||
.OrderBy(m => m.Name ?? m.FileName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var modsByModId = new Dictionary<string, List<ModLocalComp.LocalCompFile>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var mod in activeMods)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mod.ModId))
|
||||
continue;
|
||||
if (!modsByModId.TryGetValue(mod.ModId, out var list))
|
||||
modsByModId[mod.ModId] = list = new List<ModLocalComp.LocalCompFile>();
|
||||
list.Add(mod);
|
||||
}
|
||||
|
||||
var duplicates = new List<string>();
|
||||
foreach (var host in activeMods)
|
||||
foreach (var embedded in _FlattenEmbedded(host.EmbeddedMods))
|
||||
{
|
||||
if (string.IsNullOrEmpty(embedded.ModId)
|
||||
|| !modsByModId.TryGetValue(embedded.ModId, out var matches))
|
||||
continue;
|
||||
foreach (var other in matches)
|
||||
{
|
||||
if (ReferenceEquals(other, host))
|
||||
continue;
|
||||
duplicates.Add(Lang.Text("Crash.Report.JarInJarMod.DuplicateEntry", other.Name, other.FileName, host.Name, embedded.Name ?? embedded.ModId));
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateSection"));
|
||||
if (duplicates.Count == 0)
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateNone"));
|
||||
else
|
||||
{
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateDescription"));
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateHeuristic"));
|
||||
foreach (var line in duplicates.Distinct())
|
||||
sb.AppendLine("\t|-> " + line);
|
||||
}
|
||||
|
||||
sb.AppendLine().AppendLine("----------------------------").AppendLine();
|
||||
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListSection"));
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListDirectory", modsFolder));
|
||||
sb.AppendLine("|-> mods");
|
||||
foreach (var mod in activeMods)
|
||||
{
|
||||
var line = "| |-> " + (mod.Name ?? mod.FileName);
|
||||
if (!string.IsNullOrWhiteSpace(mod.Version))
|
||||
line += $" ({mod.Version})";
|
||||
if (mod.Name != mod.FileName)
|
||||
line += $" [{mod.FileName}]";
|
||||
sb.AppendLine(line);
|
||||
}
|
||||
|
||||
sb.AppendLine().AppendLine("----------------------------").AppendLine();
|
||||
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarSection"));
|
||||
var hasJij = false;
|
||||
foreach (var mod in activeMods)
|
||||
{
|
||||
if (!mod.EmbeddedMods.Any())
|
||||
continue;
|
||||
hasJij = true;
|
||||
sb.AppendLine(mod.Name ?? mod.FileName);
|
||||
_AppendEmbeddedMods(sb, mod.EmbeddedMods, 1);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (!hasJij)
|
||||
sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarNone"));
|
||||
|
||||
CrashFileIo.WriteText(Path.Combine(reportFolder, ModInfoFileName), sb.ToString(), Encoding.UTF8);
|
||||
LogWrapper.Info("Crash", "已导出模组列表及 Jar-in-Jar 信息");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "导出模组信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ModLocalComp.LocalCompFile> _FlattenEmbedded(List<ModLocalComp.LocalCompFile> mods)
|
||||
{
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
yield return mod;
|
||||
if (mod.EmbeddedMods.Any())
|
||||
foreach (var child in _FlattenEmbedded(mod.EmbeddedMods))
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
|
||||
private static void _AppendEmbeddedMods(StringBuilder builder, List<ModLocalComp.LocalCompFile> mods, int depth)
|
||||
{
|
||||
var indent = new string('\t', depth);
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
var line = indent + "|-> " + (mod.Name ?? mod.ModId ?? "?");
|
||||
if (!string.IsNullOrWhiteSpace(mod.Version))
|
||||
line += $" ({mod.Version})";
|
||||
builder.AppendLine(line);
|
||||
if (mod.EmbeddedMods.Any())
|
||||
_AppendEmbeddedMods(builder, mod.EmbeddedMods, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace PCL;
|
||||
|
||||
internal enum CrashConfidence
|
||||
{
|
||||
High,
|
||||
Medium,
|
||||
Low
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
using System.IO;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogCollector(CrashAnalysisContext context)
|
||||
{
|
||||
public void Collect(string versionPathIndie, IList<string>? latestLog)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 1:收集日志文件");
|
||||
|
||||
var possibleLogs = _FindPossibleLogFiles(versionPathIndie)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var recentLogs = possibleLogs.Where(IsRecentNonEmptyFile).ToList();
|
||||
if (recentLogs.Count == 0)
|
||||
LogWrapper.Info("Crash", "未发现可能可用的日志文件");
|
||||
|
||||
foreach (var filePath in recentLogs)
|
||||
TryAddLogFile(filePath, "读取可能的崩溃日志文件失败");
|
||||
|
||||
AddCapturedOutput(latestLog);
|
||||
|
||||
LogWrapper.Info("Crash", "步骤 1:收集日志文件完成,收集到 " + context.RawFiles.Count + " 个文件");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _FindPossibleLogFiles(string versionPathIndie)
|
||||
{
|
||||
var possibleLogs = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(Path.Combine(versionPathIndie, "crash-reports"));
|
||||
if (dirInfo.Exists)
|
||||
possibleLogs.AddRange(dirInfo.EnumerateFiles().Select(file => file.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 崩溃日志文件夹下的日志失败");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var rootDirectory = new DirectoryInfo(versionPathIndie).Parent?.Parent;
|
||||
if (rootDirectory is not null && rootDirectory.Exists)
|
||||
possibleLogs.AddRange(
|
||||
rootDirectory.EnumerateFiles()
|
||||
.Where(file => file.Extension == ".log")
|
||||
.Select(file => file.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 主文件夹下的日志失败");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var instanceDirectory = new DirectoryInfo(versionPathIndie);
|
||||
if (instanceDirectory.Exists)
|
||||
possibleLogs.AddRange(
|
||||
instanceDirectory.EnumerateFiles()
|
||||
.Where(file => file.Extension == ".log")
|
||||
.Select(file => file.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "收集 Minecraft 隔离文件夹下的日志失败");
|
||||
}
|
||||
|
||||
possibleLogs.Add(Path.Combine(versionPathIndie, "logs", "latest.log"));
|
||||
var launchScript = CrashFileIo.ReadText(Path.Combine(Basics.ExecutableDirectory, "PCL", "LatestLaunch.bat"));
|
||||
if (launchScript.Contains("-Dlog4j2.formatMsgNoLookups=false", StringComparison.OrdinalIgnoreCase))
|
||||
possibleLogs.Add(Path.Combine(versionPathIndie, "logs", "debug.log"));
|
||||
|
||||
return possibleLogs;
|
||||
}
|
||||
|
||||
private static bool IsRecentNonEmptyFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(filePath);
|
||||
if (!info.Exists || info.Length <= 0L)
|
||||
return false;
|
||||
|
||||
var ageMinutes = Math.Abs((info.LastWriteTime - DateTime.Now).TotalMinutes);
|
||||
if (ageMinutes >= 3d)
|
||||
return false;
|
||||
|
||||
LogWrapper.Info("Crash", "可能可用的日志文件:" + filePath + "(" + Math.Round(ageMinutes, 1) + " 分钟)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "确认崩溃日志时间失败(" + filePath + ")");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAddLogFile(string filePath, string errorMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.RawFiles.Add(
|
||||
new CrashLogEntry(filePath, CrashFileIo.ReadText(filePath).Split("\r\n".ToCharArray())));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", errorMessage + "(" + filePath + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCapturedOutput(IList<string>? latestLog)
|
||||
{
|
||||
if (latestLog is null || !latestLog.Any())
|
||||
return;
|
||||
|
||||
var rawOutput = string.Join("\r\n", latestLog);
|
||||
LogWrapper.Info("Crash", "以下为游戏输出的最后一段内容:" + "\r\n" + rawOutput);
|
||||
var rawOutputPath = Path.Combine(context.TempFolder, "RawOutput.log");
|
||||
CrashFileIo.WriteText(rawOutputPath, rawOutput);
|
||||
context.RawFiles.Add(new CrashLogEntry(rawOutputPath, latestLog.ToArray()));
|
||||
latestLog.Clear();
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using System.IO;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogEntry(
|
||||
string fullPath,
|
||||
IReadOnlyList<string> lines)
|
||||
{
|
||||
public string FullPath { get; } = fullPath;
|
||||
|
||||
public IReadOnlyList<string> Lines { get; } = lines;
|
||||
|
||||
public string FileName => Path.GetFileName(FullPath);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
using System.IO;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogImporter(CrashAnalysisContext context)
|
||||
{
|
||||
public void Import(string filePath)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 1:自主导入日志文件");
|
||||
|
||||
if (!_TryExtractArchive(filePath))
|
||||
{
|
||||
CrashFileIo.CopyFile(filePath, Path.Combine(
|
||||
context.TempFolder,
|
||||
"Temp",
|
||||
Path.GetFileName(filePath)));
|
||||
LogWrapper.Info("Crash", "已复制导入的日志文件:" + filePath);
|
||||
}
|
||||
|
||||
foreach (
|
||||
var targetFile in new DirectoryInfo(Path.Combine(context.TempFolder, "Temp"))
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.ToList())
|
||||
try
|
||||
{
|
||||
if (!targetFile.Exists || targetFile.Length == 0L)
|
||||
continue;
|
||||
|
||||
var ext = targetFile.Extension.ToLowerInvariant();
|
||||
if (ext is ".log" or ".txt")
|
||||
context.RawFiles.Add(new CrashLogEntry(targetFile.FullName,
|
||||
CrashFileIo.ReadText(targetFile.FullName).Split("\r\n".ToCharArray())));
|
||||
else
|
||||
File.Delete(targetFile.FullName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "导入单个日志文件失败");
|
||||
}
|
||||
|
||||
LogWrapper.Info("Crash", "步骤 1:自主导入日志文件,收集到 " + context.RawFiles.Count + " 个文件");
|
||||
}
|
||||
|
||||
private bool _TryExtractArchive(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(filePath);
|
||||
if (!info.Exists || info.Length <= 0L)
|
||||
return false;
|
||||
|
||||
if (!CrashFileIo.CanExtractArchive(filePath))
|
||||
return false;
|
||||
|
||||
CrashFileIo.ExtractFile(filePath, Path.Combine(context.TempFolder, "Temp"));
|
||||
LogWrapper.Info("Crash", "已解压导入的日志文件:" + filePath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "尝试解压导入文件失败,将按普通文件处理(" + filePath + ")");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace PCL;
|
||||
|
||||
internal enum CrashLogKind
|
||||
{
|
||||
Game,
|
||||
Debug,
|
||||
CrashReport,
|
||||
HsErr,
|
||||
All
|
||||
}
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogPreparer(CrashAnalysisContext context)
|
||||
{
|
||||
private static readonly HashSet<string> KnownGameLogNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"latest.log",
|
||||
"latest log.txt",
|
||||
"游戏崩溃前的输出.txt",
|
||||
"rawoutput.log"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> KnownDebugLogNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"debug.log",
|
||||
"debug log.txt"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> KnownLauncherLogNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"启动器日志.txt",
|
||||
"PCL2 启动器日志.txt",
|
||||
"PCL 启动器日志.txt",
|
||||
"PCL CE 启动器日志.txt",
|
||||
"log1.txt",
|
||||
"log-ce1.log"
|
||||
};
|
||||
|
||||
public bool Prepare()
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 2:准备日志文本");
|
||||
|
||||
context.DirectOpenFile = null;
|
||||
context.PreparedLogs = null;
|
||||
context.OutputFiles.Clear();
|
||||
|
||||
var classifiedFiles = _ClassifyFiles();
|
||||
|
||||
var analyzable = classifiedFiles
|
||||
.Where(item => item.Kind is not null)
|
||||
.ToList();
|
||||
|
||||
context.DirectOpenFile = analyzable
|
||||
.OrderBy(item => _IsGeneratedLog(item.File))
|
||||
.ThenByDescending(item => _GetLastWriteTime(item.File))
|
||||
.Select(item => item.File)
|
||||
.FirstOrDefault();
|
||||
|
||||
var extraFiles = classifiedFiles
|
||||
.Where(item => item.Kind is null)
|
||||
.Select(item => item.File)
|
||||
.ToList();
|
||||
|
||||
if (analyzable.Count == 0 && extraFiles.Count > 0)
|
||||
{
|
||||
LogWrapper.Info("Crash", "由于仅发现了额外日志,将它们视作 Minecraft 日志进行分析");
|
||||
|
||||
analyzable = extraFiles
|
||||
.Select(file => new ClassifiedCrashLog(file, CrashLogKind.Game))
|
||||
.ToList();
|
||||
|
||||
extraFiles.Clear();
|
||||
}
|
||||
|
||||
var game = _SelectGameLog(analyzable);
|
||||
var debug = _SelectDebugLog(analyzable);
|
||||
var crashReport = _SelectNewest(analyzable, CrashLogKind.CrashReport, 300, 700);
|
||||
var hsErr = _SelectNewest(analyzable, CrashLogKind.HsErr, 200, 100);
|
||||
|
||||
foreach (var extraFile in extraFiles)
|
||||
{
|
||||
context.OutputFiles.Add(extraFile.FullPath);
|
||||
LogWrapper.Info("Crash", $"输出报告:{extraFile.FullPath},不用作分析");
|
||||
}
|
||||
|
||||
var logs = new CrashLogSet
|
||||
{
|
||||
Game = game,
|
||||
Debug = debug,
|
||||
CrashReport = crashReport,
|
||||
HsErr = hsErr,
|
||||
All = string.Concat(
|
||||
game?.Text ?? debug?.Text ?? string.Empty,
|
||||
hsErr?.Text ?? string.Empty,
|
||||
crashReport?.Text ?? string.Empty)
|
||||
};
|
||||
|
||||
context.PreparedLogs = logs;
|
||||
context.LogAll = logs.All;
|
||||
|
||||
if (logs.HasAnalyzableLog)
|
||||
LogWrapper.Info(
|
||||
"Crash",
|
||||
("步骤 2:准备日志文本完成,找到" +
|
||||
(game is null ? "" : "游戏日志、") +
|
||||
(debug is null ? "" : "游戏 Debug 日志、") +
|
||||
(hsErr is null ? "" : "虚拟机日志、") +
|
||||
(crashReport is null ? "" : "崩溃日志、"))
|
||||
.TrimEnd('、') + "用作分析");
|
||||
else
|
||||
LogWrapper.Info("Crash", "步骤 2:准备日志文本完成,没有任何可供分析的日志");
|
||||
|
||||
return logs.HasAnalyzableLog;
|
||||
}
|
||||
|
||||
private List<ClassifiedCrashLog> _ClassifyFiles()
|
||||
{
|
||||
var result = new List<ClassifiedCrashLog>();
|
||||
|
||||
foreach (var file in context.RawFiles)
|
||||
{
|
||||
var name = file.FileName.ToLowerInvariant();
|
||||
|
||||
if (!file.Lines.Any())
|
||||
{
|
||||
LogWrapper.Info("Crash", $"{name} 由于内容为空跳过");
|
||||
continue;
|
||||
}
|
||||
|
||||
var kind = _ClassifyLogKind(file);
|
||||
|
||||
if (kind is null && !_IsExtraLogLike(file))
|
||||
{
|
||||
LogWrapper.Info("Crash", $"{name} 分类为 Ignore");
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new ClassifiedCrashLog(file, kind));
|
||||
LogWrapper.Info("Crash", $"{name} 分类为 {kind?.ToString() ?? "Extra"}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CrashLogKind? _ClassifyLogKind(CrashLogEntry file)
|
||||
{
|
||||
var name = file.FileName.ToLowerInvariant();
|
||||
|
||||
if (name.StartsWith("hs_err", StringComparison.Ordinal))
|
||||
return CrashLogKind.HsErr;
|
||||
|
||||
if (name.StartsWith("crash-", StringComparison.Ordinal))
|
||||
return CrashLogKind.CrashReport;
|
||||
|
||||
if (KnownDebugLogNames.Contains(name))
|
||||
return CrashLogKind.Debug;
|
||||
|
||||
if (KnownGameLogNames.Contains(name))
|
||||
return CrashLogKind.Game;
|
||||
|
||||
if (KnownLauncherLogNames.Contains(name) &&
|
||||
file.Lines.Any(line => line.Contains("以下为游戏输出的最后一段内容")))
|
||||
return CrashLogKind.Game;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool _IsExtraLogLike(CrashLogEntry file)
|
||||
{
|
||||
var name = file.FileName.ToLowerInvariant();
|
||||
|
||||
return name.EndsWith(".log", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private CrashLogText? _SelectGameLog(IReadOnlyList<ClassifiedCrashLog> files)
|
||||
{
|
||||
var gameFiles = files
|
||||
.Where(item => item.Kind is CrashLogKind.Game or CrashLogKind.Debug)
|
||||
.Select(item => item.File)
|
||||
.ToList();
|
||||
|
||||
if (gameFiles.Count == 0)
|
||||
return null;
|
||||
|
||||
var byName = gameFiles
|
||||
.GroupBy(file => file.FileName.ToLowerInvariant())
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.OrderBy(_GetLastWriteTime).Last(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var text = "";
|
||||
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"rawoutput.log",
|
||||
"启动器日志.txt",
|
||||
"log1.txt",
|
||||
"log-ce1.log",
|
||||
"游戏崩溃前的输出.txt",
|
||||
"PCL CE 启动器日志.txt",
|
||||
"PCL2 启动器日志.txt",
|
||||
"PCL 启动器日志.txt"
|
||||
})
|
||||
{
|
||||
if (!byName.TryGetValue(name, out var currentLog))
|
||||
continue;
|
||||
|
||||
text += _ExtractLauncherGameOutput(currentLog);
|
||||
|
||||
context.OutputFiles.Add(currentLog.FullPath);
|
||||
LogWrapper.Info("Crash", $"导入分析:{currentLog.FullPath},作为启动器日志");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"latest.log",
|
||||
"latest log.txt",
|
||||
"debug.log",
|
||||
"debug log.txt"
|
||||
})
|
||||
{
|
||||
if (!byName.TryGetValue(name, out var currentLog))
|
||||
continue;
|
||||
|
||||
text += _GetHeadTailLines(currentLog.Lines, 1500, 500);
|
||||
|
||||
context.OutputFiles.Add(currentLog.FullPath);
|
||||
LogWrapper.Info("Crash", $"导入分析:{currentLog.FullPath},作为 Minecraft 日志");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
var fallback = gameFiles
|
||||
.OrderBy(_GetLastWriteTime)
|
||||
.Last();
|
||||
|
||||
text = _GetHeadTailLines(fallback.Lines, 1500, 500);
|
||||
|
||||
context.OutputFiles.Add(fallback.FullPath);
|
||||
LogWrapper.Info("Crash", $"导入分析:{fallback.FullPath},作为兜底日志");
|
||||
}
|
||||
|
||||
foreach (var file in gameFiles.Where(file => !context.OutputFiles.Contains(file.FullPath)))
|
||||
{
|
||||
context.OutputFiles.Add(file.FullPath);
|
||||
LogWrapper.Info("Crash", $"输出报告:{file.FullPath},作为 Minecraft 或启动器日志");
|
||||
}
|
||||
|
||||
return new CrashLogText
|
||||
{
|
||||
Kind = CrashLogKind.Game,
|
||||
Text = text.TrimEnd('\r', '\n'),
|
||||
FilePath = gameFiles.FirstOrDefault()?.FullPath
|
||||
};
|
||||
}
|
||||
|
||||
private CrashLogText? _SelectDebugLog(IReadOnlyList<ClassifiedCrashLog> files)
|
||||
{
|
||||
var debugFiles = files
|
||||
.Where(item => item.Kind == CrashLogKind.Debug)
|
||||
.Select(item => item.File)
|
||||
.ToList();
|
||||
|
||||
if (debugFiles.Count == 0)
|
||||
return null;
|
||||
|
||||
var selected = debugFiles
|
||||
.OrderBy(_GetLastWriteTime)
|
||||
.Last();
|
||||
|
||||
if (!context.OutputFiles.Contains(selected.FullPath))
|
||||
context.OutputFiles.Add(selected.FullPath);
|
||||
|
||||
LogWrapper.Info("Crash", $"导入分析:{selected.FullPath},作为 Minecraft Debug 日志");
|
||||
|
||||
return new CrashLogText
|
||||
{
|
||||
Kind = CrashLogKind.Debug,
|
||||
Text = _GetHeadTailLines(selected.Lines, 1000, 0),
|
||||
FilePath = selected.FullPath
|
||||
};
|
||||
}
|
||||
|
||||
private CrashLogText? _SelectNewest(
|
||||
IReadOnlyList<ClassifiedCrashLog> files,
|
||||
CrashLogKind kind,
|
||||
int head,
|
||||
int tail)
|
||||
{
|
||||
var selectedFiles = files
|
||||
.Where(item => item.Kind == kind)
|
||||
.Select(item => item.File)
|
||||
.ToList();
|
||||
|
||||
if (selectedFiles.Count == 0)
|
||||
return null;
|
||||
|
||||
var selected = selectedFiles
|
||||
.OrderBy(_GetLastWriteTime)
|
||||
.Last();
|
||||
|
||||
context.OutputFiles.Add(selected.FullPath);
|
||||
|
||||
LogWrapper.Info(
|
||||
"Crash",
|
||||
$"输出报告:{selected.FullPath}{(kind == CrashLogKind.HsErr ? ",作为虚拟机错误信息" : ",作为 Minecraft 崩溃报告")}");
|
||||
|
||||
LogWrapper.Info(
|
||||
"Crash",
|
||||
$"导入分析:{selected.FullPath}{(kind == CrashLogKind.HsErr ? ",作为虚拟机错误信息" : ",作为 Minecraft 崩溃报告")}");
|
||||
|
||||
return new CrashLogText
|
||||
{
|
||||
Kind = kind,
|
||||
Text = _GetHeadTailLines(selected.Lines, head, tail),
|
||||
FilePath = selected.FullPath
|
||||
};
|
||||
}
|
||||
|
||||
private static string _ExtractLauncherGameOutput(CrashLogEntry log)
|
||||
{
|
||||
var text = "";
|
||||
var hasLauncherMark = false;
|
||||
|
||||
foreach (var line in log.Lines)
|
||||
if (hasLauncherMark)
|
||||
{
|
||||
text += line + "\n";
|
||||
}
|
||||
else if (line.Contains("以下为游戏输出的最后一段内容"))
|
||||
{
|
||||
hasLauncherMark = true;
|
||||
LogWrapper.Info("Crash", "找到 PCL 输出的游戏实时日志头");
|
||||
}
|
||||
|
||||
return hasLauncherMark
|
||||
? text.TrimEnd('\r', '\n')
|
||||
: _GetHeadTailLines(log.Lines, 0, 500);
|
||||
}
|
||||
|
||||
private static DateTime _GetLastWriteTime(CrashLogEntry file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileInfo(file.FullPath).LastWriteTime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "获取日志文件修改时间失败");
|
||||
return new DateTime(1900, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _IsGeneratedLog(CrashLogEntry file) =>
|
||||
file.FullPath.StartsWith(context.TempFolder, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string _GetHeadTailLines(
|
||||
IReadOnlyList<string> raw,
|
||||
int headLines,
|
||||
int tailLines)
|
||||
{
|
||||
if (raw.Count <= headLines + tailLines)
|
||||
return string.Join("\n", raw.Distinct());
|
||||
|
||||
var lines = new List<string>();
|
||||
var seen = new HashSet<string>();
|
||||
var realHeadLines = 0;
|
||||
|
||||
int viewedLines;
|
||||
|
||||
for (viewedLines = 0; viewedLines <= raw.Count - 1; viewedLines++)
|
||||
{
|
||||
if (!seen.Add(raw[viewedLines]))
|
||||
continue;
|
||||
|
||||
realHeadLines += 1;
|
||||
lines.Add(raw[viewedLines]);
|
||||
|
||||
if (realHeadLines >= headLines)
|
||||
break;
|
||||
}
|
||||
|
||||
var realTailLines = 0;
|
||||
|
||||
for (var i = raw.Count - 1; i >= viewedLines; i -= 1)
|
||||
{
|
||||
if (!seen.Add(raw[i]))
|
||||
continue;
|
||||
|
||||
realTailLines += 1;
|
||||
lines.Insert(realHeadLines, raw[i]);
|
||||
|
||||
if (realTailLines >= tailLines)
|
||||
break;
|
||||
}
|
||||
|
||||
var result = new StringBuilder();
|
||||
|
||||
foreach (var line in lines.Where(line => !string.IsNullOrEmpty(line)))
|
||||
{
|
||||
result.Append(line);
|
||||
result.Append('\n');
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private sealed class ClassifiedCrashLog(
|
||||
CrashLogEntry file,
|
||||
CrashLogKind? kind)
|
||||
{
|
||||
public CrashLogEntry File { get; } = file;
|
||||
|
||||
public CrashLogKind? Kind { get; } = kind;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogSet
|
||||
{
|
||||
public CrashLogText? Game { get; init; }
|
||||
|
||||
public CrashLogText? Debug { get; init; }
|
||||
|
||||
public CrashLogText? CrashReport { get; init; }
|
||||
|
||||
public CrashLogText? HsErr { get; init; }
|
||||
|
||||
public string All { get; init; } = string.Empty;
|
||||
|
||||
public bool HasAnalyzableLog => Game is not null || CrashReport is not null || HsErr is not null;
|
||||
|
||||
public string? GetText(CrashLogKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
CrashLogKind.Game => Game?.Text,
|
||||
CrashLogKind.Debug => Debug?.Text,
|
||||
CrashLogKind.CrashReport => CrashReport?.Text,
|
||||
CrashLogKind.HsErr => HsErr?.Text,
|
||||
CrashLogKind.All => All,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashLogText
|
||||
{
|
||||
public required CrashLogKind Kind { get; init; }
|
||||
|
||||
public required string Text { get; init; }
|
||||
|
||||
public string? FilePath { get; init; }
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.UI;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashDialogPresenter(CrashAnalysisContext context)
|
||||
{
|
||||
private readonly CrashReportExporter _exporter = new();
|
||||
private readonly CrashResultFormatter _formatter = new();
|
||||
|
||||
public void Output(
|
||||
bool isHandAnalyze,
|
||||
List<string>? extraFiles)
|
||||
{
|
||||
ModMain.frmMain.ShowWindowToTop();
|
||||
|
||||
var crashContent = _formatter.Format(context, isHandAnalyze);
|
||||
var resultText = crashContent.Text;
|
||||
var directFile = context.DirectOpenFile;
|
||||
var openInstanceSettings =
|
||||
context.Instance is not null &&
|
||||
crashContent.SuggestedAction == CrashSuggestedAction.OpenInstanceSettings;
|
||||
|
||||
var title = isHandAnalyze
|
||||
? Lang.Text("Crash.Dialog.Title.Manual")
|
||||
: Lang.Text("Crash.Dialog.Title.Auto");
|
||||
|
||||
var secondButtonText = _GetSecondButtonText(
|
||||
isHandAnalyze,
|
||||
directFile,
|
||||
openInstanceSettings);
|
||||
|
||||
var thirdButtonText = isHandAnalyze
|
||||
? ""
|
||||
: Lang.Text("Crash.Dialog.Button.ExportReport");
|
||||
|
||||
var secondButtonAction = _GetSecondButtonAction(
|
||||
isHandAnalyze,
|
||||
directFile,
|
||||
openInstanceSettings);
|
||||
|
||||
var selectedButton = MsgBoxWrapper.ShowWithCustomButtons(
|
||||
resultText,
|
||||
title,
|
||||
MsgBoxTheme.Info,
|
||||
true,
|
||||
new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"), 1),
|
||||
new MsgBoxButtonInfo(secondButtonText, 2, secondButtonAction),
|
||||
new MsgBoxButtonInfo(thirdButtonText, 3));
|
||||
|
||||
switch (selectedButton)
|
||||
{
|
||||
case 2:
|
||||
if (openInstanceSettings)
|
||||
_OpenModLoaderInstallPage();
|
||||
else if (directFile is not null)
|
||||
_OpenDirectFile(directFile);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
_ExportReport(extraFiles);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _GetSecondButtonText(
|
||||
bool isHandAnalyze,
|
||||
CrashLogEntry? directFile,
|
||||
bool openInstanceSettings)
|
||||
{
|
||||
if (isHandAnalyze || directFile is null)
|
||||
return "";
|
||||
|
||||
return openInstanceSettings
|
||||
? Lang.Text("Crash.Dialog.Button.GoToModify")
|
||||
: Lang.Text("Crash.Dialog.Button.OpenLog");
|
||||
}
|
||||
|
||||
private static Action? _GetSecondButtonAction(
|
||||
bool isHandAnalyze,
|
||||
CrashLogEntry? directFile,
|
||||
bool openInstanceSettings)
|
||||
{
|
||||
if (isHandAnalyze ||
|
||||
directFile is null ||
|
||||
openInstanceSettings)
|
||||
return null;
|
||||
|
||||
return () => _OpenDirectFile(directFile);
|
||||
}
|
||||
|
||||
private void _OpenModLoaderInstallPage()
|
||||
{
|
||||
PageInstanceLeft.McInstance = context.Instance;
|
||||
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(
|
||||
FormMain.PageType.InstanceSetup,
|
||||
FormMain.PageSubType.VersionInstall));
|
||||
}
|
||||
|
||||
private static void _OpenDirectFile(CrashLogEntry directFile)
|
||||
{
|
||||
if (File.Exists(directFile.FullPath))
|
||||
{
|
||||
Basics.OpenPath(directFile.FullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(Paths.Temp, "Crash.txt");
|
||||
|
||||
CrashFileIo.WriteText(filePath, string.Join("\r\n", directFile.Lines));
|
||||
Basics.OpenPath(filePath);
|
||||
}
|
||||
|
||||
private void _ExportReport(List<string>? extraFiles)
|
||||
{
|
||||
string? fileAddress = null;
|
||||
|
||||
try
|
||||
{
|
||||
fileAddress = _SelectReportSavePath();
|
||||
|
||||
if (string.IsNullOrEmpty(fileAddress))
|
||||
return;
|
||||
|
||||
_exporter.Export(context, fileAddress, extraFiles);
|
||||
|
||||
HintWrapper.Show(
|
||||
Lang.Text("Crash.Report.Export.Success"),
|
||||
HintTheme.Success);
|
||||
|
||||
Basics.OpenPath(Path.GetDirectoryName(fileAddress) ?? fileAddress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Crash", "导出错误报告失败");
|
||||
|
||||
var message = _CreateExportFailureMessage(fileAddress, ex);
|
||||
MsgBoxWrapper.ShowWithCustomButtons(
|
||||
message,
|
||||
Lang.Text("Crash.Export.Failed.Title"),
|
||||
MsgBoxTheme.Error,
|
||||
false,
|
||||
new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"), 1),
|
||||
new MsgBoxButtonInfo(
|
||||
Lang.Text("Crash.Export.Failed.CopyDetails"),
|
||||
2,
|
||||
() => ModBase.ClipboardSet(message, false)));
|
||||
}
|
||||
}
|
||||
|
||||
private static string _CreateExportFailureMessage(
|
||||
string? targetZipPath,
|
||||
Exception exception)
|
||||
{
|
||||
var summary = string.IsNullOrWhiteSpace(targetZipPath)
|
||||
? Lang.Text("Crash.Export.Failed.MessageWithoutPath")
|
||||
: Lang.Text("Crash.Export.Failed.Message", targetZipPath);
|
||||
|
||||
return ExceptionDetails.Compose(summary, exception);
|
||||
}
|
||||
|
||||
private static string? _SelectReportSavePath()
|
||||
{
|
||||
string? fileAddress = null;
|
||||
|
||||
ModBase.RunInUiWait(() => fileAddress = SystemDialogs.SelectSaveFile(
|
||||
Lang.Text("Crash.Report.SaveDialog.Title"),
|
||||
_GetDefaultReportFileName(),
|
||||
Lang.Text("Crash.Report.SaveDialog.Filter")));
|
||||
|
||||
return fileAddress;
|
||||
}
|
||||
|
||||
private static string _GetDefaultReportFileName()
|
||||
{
|
||||
var time = DateTime.Now
|
||||
.ToString("G", CultureInfo.InvariantCulture)
|
||||
.Replace("/", "-")
|
||||
.Replace(":", ".")
|
||||
.Replace(" ", "_");
|
||||
|
||||
return Lang.Text("Crash.Report.SaveDialog.DefaultFileName", time);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
namespace PCL;
|
||||
|
||||
internal enum CrashSuggestedAction
|
||||
{
|
||||
None,
|
||||
OpenInstanceSettings
|
||||
}
|
||||
|
||||
internal sealed class CrashMessageSpec(
|
||||
string reasonKey,
|
||||
object?[] reasonArgs,
|
||||
string? suggestionKey,
|
||||
object?[] suggestionArgs,
|
||||
bool appendReportHint,
|
||||
bool appendHelpHint,
|
||||
CrashSuggestedAction suggestedAction = CrashSuggestedAction.None)
|
||||
{
|
||||
public string ReasonKey { get; } = reasonKey;
|
||||
|
||||
public object?[] ReasonArgs { get; } = reasonArgs;
|
||||
|
||||
public string? SuggestionKey { get; } = suggestionKey;
|
||||
|
||||
public object?[] SuggestionArgs { get; } = suggestionArgs;
|
||||
|
||||
public bool AppendReportHint { get; } = appendReportHint;
|
||||
|
||||
public bool AppendHelpHint { get; } = appendHelpHint;
|
||||
|
||||
public CrashSuggestedAction SuggestedAction { get; } = suggestedAction;
|
||||
}
|
||||
|
||||
internal sealed class CrashDialogContent(string text, CrashSuggestedAction suggestedAction)
|
||||
{
|
||||
public string Text { get; } = text;
|
||||
|
||||
public CrashSuggestedAction SuggestedAction { get; } = suggestedAction;
|
||||
}
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashResultFormatter
|
||||
{
|
||||
public CrashDialogContent Format(
|
||||
CrashAnalysisContext context,
|
||||
bool isHandAnalyze)
|
||||
{
|
||||
var result = context.Result ?? new CrashAnalysisResult();
|
||||
|
||||
if (!result.Any)
|
||||
return _CreateUnknownContent(isHandAnalyze);
|
||||
|
||||
var specs = result.Findings
|
||||
.Select(finding => _CreateMessageSpec(finding, context.LogAll))
|
||||
.ToList();
|
||||
|
||||
var text = _JoinParagraphs(specs.Select(_Render));
|
||||
if (!isHandAnalyze)
|
||||
text = _AppendFollowUps(text, specs);
|
||||
|
||||
return new CrashDialogContent(
|
||||
_NormalizeLineEndings(text).Trim('\r', '\n'),
|
||||
specs[0].SuggestedAction);
|
||||
}
|
||||
|
||||
private static CrashDialogContent _CreateUnknownContent(bool isHandAnalyze)
|
||||
{
|
||||
var spec = isHandAnalyze
|
||||
? _Spec("Crash.Reason.Unknown.Manual")
|
||||
: _Spec("Crash.Reason.Unknown.Auto", "Crash.Suggestion.ExportReport");
|
||||
|
||||
return new CrashDialogContent(
|
||||
_NormalizeLineEndings(_Render(spec)).Trim('\r', '\n'),
|
||||
CrashSuggestedAction.None);
|
||||
}
|
||||
|
||||
private static string _AppendFollowUps(
|
||||
string text,
|
||||
IReadOnlyCollection<CrashMessageSpec> specs)
|
||||
{
|
||||
var followUps = new List<string>();
|
||||
|
||||
if (specs.Any(spec => spec.AppendReportHint))
|
||||
followUps.Add(Lang.Text("Crash.Suggestion.ViewReport"));
|
||||
|
||||
if (specs.Any(spec => spec.AppendHelpHint))
|
||||
followUps.Add(Lang.Text("Crash.Suggestion.ExportReport"));
|
||||
|
||||
var launcherOutdatedSuggestion = _GetLauncherOutdatedSuggestion();
|
||||
if (!string.IsNullOrWhiteSpace(launcherOutdatedSuggestion))
|
||||
followUps.Add(launcherOutdatedSuggestion);
|
||||
|
||||
return followUps.Count == 0
|
||||
? text
|
||||
: Lang.Text(
|
||||
"Crash.Presentation.WithFollowUps",
|
||||
text,
|
||||
_JoinParagraphs(followUps));
|
||||
}
|
||||
|
||||
private static string _Render(CrashMessageSpec spec)
|
||||
{
|
||||
var reason = Lang.Text(spec.ReasonKey, spec.ReasonArgs);
|
||||
if (spec.SuggestionKey is null)
|
||||
return Lang.Text("Crash.Presentation.ReasonOnly", reason);
|
||||
|
||||
return Lang.Text(
|
||||
"Crash.Presentation.WithSuggestion",
|
||||
reason,
|
||||
Lang.Text(spec.SuggestionKey, spec.SuggestionArgs));
|
||||
}
|
||||
|
||||
private static CrashMessageSpec _CreateMessageSpec(
|
||||
CrashFinding finding,
|
||||
string combinedLogText)
|
||||
{
|
||||
var additional = finding.Details.ToList();
|
||||
|
||||
switch (finding.Cause)
|
||||
{
|
||||
case CrashCause.ExtractedModFile:
|
||||
return _Spec(
|
||||
"Crash.Reason.ExtractedModFile",
|
||||
"Crash.Suggestion.ExtractedModFile");
|
||||
|
||||
case CrashCause.OutOfMemory:
|
||||
return _Spec(
|
||||
"Crash.Reason.OutOfMemory",
|
||||
"Crash.Suggestion.OutOfMemory",
|
||||
help: true);
|
||||
|
||||
case CrashCause.UsingOpenJ9:
|
||||
return _Spec(
|
||||
"Crash.Reason.UsingOpenJ9",
|
||||
"Crash.Suggestion.UsingOpenJ9");
|
||||
|
||||
case CrashCause.UsingJdk:
|
||||
return _Spec(
|
||||
"Crash.Reason.UsingJdk",
|
||||
"Crash.Suggestion.UsingJdk");
|
||||
|
||||
case CrashCause.JavaTooNew:
|
||||
return _Spec(
|
||||
"Crash.Reason.JavaTooNew",
|
||||
"Crash.Suggestion.JavaTooNew");
|
||||
|
||||
case CrashCause.JavaIncompatible:
|
||||
return _Spec(
|
||||
"Crash.Reason.JavaIncompatible",
|
||||
"Crash.Suggestion.JavaIncompatible");
|
||||
|
||||
case CrashCause.InvalidModFileName:
|
||||
return _Spec(
|
||||
"Crash.Reason.InvalidModFileName",
|
||||
"Crash.Suggestion.InvalidModFileName");
|
||||
|
||||
case CrashCause.MissingMixinBootstrap:
|
||||
return _Spec(
|
||||
"Crash.Reason.MissingMixinBootstrap",
|
||||
"Crash.Suggestion.MissingMixinBootstrap");
|
||||
|
||||
case CrashCause.X86JavaMemoryLimit:
|
||||
return Environment.Is64BitOperatingSystem
|
||||
? _Spec(
|
||||
"Crash.Reason.X86JavaMemoryLimit.OnX64Os",
|
||||
"Crash.Suggestion.X86JavaMemoryLimit.OnX64Os")
|
||||
: _Spec(
|
||||
"Crash.Reason.X86JavaMemoryLimit.OnX86Os",
|
||||
"Crash.Suggestion.X86JavaMemoryLimit.OnX86Os",
|
||||
help: true);
|
||||
|
||||
case CrashCause.MissingDependencyOrWrongMcVersion:
|
||||
return _FormatMissingDependency(additional);
|
||||
|
||||
case CrashCause.StackKeywordFound:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.StackKeyword.Single",
|
||||
"Crash.Suggestion.StackKeyword", _Args(additional[0]),
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.StackKeyword.Multiple",
|
||||
"Crash.Suggestion.StackKeyword",
|
||||
_Args(_BulletList(additional)),
|
||||
help: true);
|
||||
|
||||
case CrashCause.StackModNameFound or CrashCause.SuspectedModCrash:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.SuspectedMod.Single",
|
||||
"Crash.Suggestion.DisableThisMod",
|
||||
_Args(additional[0]),
|
||||
report: true,
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.SuspectedMod.Multiple",
|
||||
"Crash.Suggestion.DisableTheseMods",
|
||||
_Args(_BulletList(additional)),
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.ConfirmedModCrash:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.ConfirmedMod.Single",
|
||||
"Crash.Suggestion.DisableThisMod",
|
||||
_Args(additional[0]),
|
||||
report: true,
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.ConfirmedMod.Multiple",
|
||||
"Crash.Suggestion.DisableTheseMods",
|
||||
_Args(_BulletList(additional)),
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.ModMixinFailed:
|
||||
if (additional.Count == 0)
|
||||
return _Spec(
|
||||
"Crash.Reason.ModMixinFailed.None",
|
||||
"Crash.Suggestion.ModMixinFailed",
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.ModMixinFailed.Single",
|
||||
"Crash.Suggestion.DisableThisMod",
|
||||
_Args(additional[0]),
|
||||
report: true,
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.ModMixinFailed.Multiple",
|
||||
"Crash.Suggestion.DisableTheseMods",
|
||||
_Args(_BulletList(additional)),
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.ModConfigCrash:
|
||||
return additional.Count > 1 && additional[1] is not null
|
||||
? _Spec(
|
||||
"Crash.Reason.ModConfigCrash.WithConfig",
|
||||
"Crash.Suggestion.ModConfigCrash",
|
||||
_Args(additional[0], additional[1]))
|
||||
: _Spec(
|
||||
"Crash.Reason.ModConfigCrash.Simple",
|
||||
"Crash.Suggestion.DisableThisMod",
|
||||
_Args(additional.FirstOrDefault() ?? string.Empty),
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.ModInitializationFailed:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.ModInitializationFailed.Single",
|
||||
"Crash.Suggestion.DisableThisMod",
|
||||
_Args(additional[0]),
|
||||
report: true,
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.ModInitializationFailed.Multiple",
|
||||
"Crash.Suggestion.DisableTheseMods",
|
||||
_Args(_BulletList(additional)),
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.SpecificBlockCrash:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.SpecificBlock.Single",
|
||||
"Crash.Suggestion.SpecificBlock.Single",
|
||||
_Args(additional[0]),
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.SpecificBlock.Multiple",
|
||||
"Crash.Suggestion.SpecificBlock.Multiple",
|
||||
help: true);
|
||||
|
||||
case CrashCause.DuplicateMods:
|
||||
return additional.Count >= 2
|
||||
? _Spec(
|
||||
"Crash.Reason.DuplicateMods.Known",
|
||||
"Crash.Suggestion.DuplicateMods",
|
||||
_Args(_BulletList(additional)))
|
||||
: _Spec(
|
||||
"Crash.Reason.DuplicateMods.Unknown",
|
||||
"Crash.Suggestion.DuplicateMods",
|
||||
report: true,
|
||||
help: true);
|
||||
|
||||
case CrashCause.SpecificEntityCrash:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.SpecificEntity.Single",
|
||||
"Crash.Suggestion.SpecificEntity.Single",
|
||||
_Args(additional[0]),
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.SpecificEntity.Multiple",
|
||||
"Crash.Suggestion.SpecificEntity.Multiple",
|
||||
help: true);
|
||||
|
||||
case CrashCause.OptiFineForgeIncompatible:
|
||||
return _Spec(
|
||||
"Crash.Reason.OptiFineForgeIncompatible",
|
||||
"Crash.Suggestion.OptiFineForgeIncompatible");
|
||||
|
||||
case CrashCause.ShadersModWithOptiFine:
|
||||
return _Spec(
|
||||
"Crash.Reason.ShadersModWithOptiFine",
|
||||
"Crash.Suggestion.ShadersModWithOptiFine");
|
||||
|
||||
case CrashCause.OldForgeNewJavaIncompatible:
|
||||
return _Spec(
|
||||
"Crash.Reason.OldForgeNewJavaIncompatible",
|
||||
"Crash.Suggestion.OldForgeNewJavaIncompatible");
|
||||
|
||||
case CrashCause.MultipleForgeInInstanceJson:
|
||||
return _Spec(
|
||||
"Crash.Reason.MultipleForgeInInstanceJson",
|
||||
"Crash.Suggestion.MultipleForgeInInstanceJson");
|
||||
|
||||
case CrashCause.ManualDebugCrash:
|
||||
return _Spec("Crash.Reason.ManualDebugCrash");
|
||||
|
||||
case CrashCause.ModRequiresJava11:
|
||||
return _Spec(
|
||||
"Crash.Reason.ModRequiresJava11",
|
||||
"Crash.Suggestion.ModRequiresJava11");
|
||||
|
||||
case CrashCause.VeryShortOutput:
|
||||
return _Spec(
|
||||
"Crash.Reason.VeryShortOutput",
|
||||
"Crash.Suggestion.ExportReport",
|
||||
_Args(additional.FirstOrDefault() ?? string.Empty));
|
||||
|
||||
case CrashCause.OptiFineWorldLoadCrash:
|
||||
return _Spec(
|
||||
"Crash.Reason.OptiFineWorldLoadCrash",
|
||||
"Crash.Suggestion.OptiFineWorldLoadCrash",
|
||||
help: true);
|
||||
|
||||
case CrashCause.PixelFormatNotSupported
|
||||
or CrashCause.IntelDriverAccessViolation
|
||||
or CrashCause.AmdDriverAccessViolation
|
||||
or CrashCause.NvidiaDriverAccessViolation
|
||||
or CrashCause.UnsupportedOpenGl:
|
||||
return combinedLogText.Contains("hd graphics ")
|
||||
? _Spec(
|
||||
"Crash.Reason.GraphicsDriver.IntelOrIntegrated",
|
||||
"Crash.Suggestion.GraphicsDriver.IntelOrIntegrated",
|
||||
help: true)
|
||||
: _Spec(
|
||||
"Crash.Reason.GraphicsDriver.Generic",
|
||||
"Crash.Suggestion.GraphicsDriver.Generic",
|
||||
help: true);
|
||||
|
||||
case CrashCause.ResourcePackTooLarge:
|
||||
return _Spec(
|
||||
"Crash.Reason.ResourcePackTooLarge",
|
||||
"Crash.Suggestion.ResourcePackTooLarge",
|
||||
help: true);
|
||||
|
||||
case CrashCause.NightConfigBug:
|
||||
return _Spec(
|
||||
"Crash.Reason.NightConfigBug",
|
||||
"Crash.Suggestion.NightConfigBug",
|
||||
help: true);
|
||||
|
||||
case CrashCause.OpenGl1282:
|
||||
return _Spec(
|
||||
"Crash.Reason.OpenGl1282",
|
||||
"Crash.Suggestion.OpenGl1282",
|
||||
help: true);
|
||||
|
||||
case CrashCause.TooManyModsIdLimit:
|
||||
return _Spec(
|
||||
"Crash.Reason.TooManyModsIdLimit",
|
||||
"Crash.Suggestion.TooManyModsIdLimit");
|
||||
|
||||
case CrashCause.FileOrContentValidationFailed:
|
||||
return _Spec(
|
||||
"Crash.Reason.FileOrContentValidationFailed",
|
||||
"Crash.Suggestion.FileOrContentValidationFailed",
|
||||
help: true);
|
||||
|
||||
case CrashCause.IncompleteForgeInstallation:
|
||||
return _Spec(
|
||||
"Crash.Reason.IncompleteForgeInstallation",
|
||||
"Crash.Suggestion.IncompleteForgeInstallation",
|
||||
help: true);
|
||||
|
||||
case CrashCause.FabricError:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.FabricError.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(additional[0]))
|
||||
: _Spec(
|
||||
"Crash.Reason.FabricError.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
case CrashCause.IncompatibleMods:
|
||||
return _FormatIncompatibleMods(additional);
|
||||
|
||||
case CrashCause.ModLoaderError:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.ModLoaderError.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(additional[0]))
|
||||
: _Spec(
|
||||
"Crash.Reason.ModLoaderError.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
case CrashCause.FabricSolutionProvided:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.FabricSolution.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(additional[0]))
|
||||
: _Spec(
|
||||
"Crash.Reason.FabricSolution.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
case CrashCause.ForgeError:
|
||||
return additional.Count == 1
|
||||
? _Spec(
|
||||
"Crash.Reason.ForgeError.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(additional[0]))
|
||||
: _Spec(
|
||||
"Crash.Reason.ForgeError.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
case CrashCause.NoAnalyzableFile:
|
||||
return _Spec(
|
||||
"Crash.Reason.NoAnalyzableFile",
|
||||
"Crash.Suggestion.ExportReport",
|
||||
help: true);
|
||||
|
||||
default:
|
||||
return _Spec(
|
||||
"Crash.Reason.UnknownFinding",
|
||||
"Crash.Suggestion.ExportReport",
|
||||
_Args(finding.Cause.ToString()),
|
||||
help: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static CrashMessageSpec _FormatMissingDependency(List<string> additional)
|
||||
{
|
||||
if (additional.Count == 0)
|
||||
return _Spec(
|
||||
"Crash.Reason.MissingDependency.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
var info = _BulletList(additional);
|
||||
|
||||
return info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)
|
||||
? _Spec(
|
||||
"Crash.Reason.ModLoaderIncompatible",
|
||||
"Crash.Suggestion.ModLoaderIncompatible",
|
||||
_Args(info),
|
||||
action: CrashSuggestedAction.OpenInstanceSettings)
|
||||
: _Spec(
|
||||
"Crash.Reason.MissingDependency.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(info));
|
||||
}
|
||||
|
||||
private static CrashMessageSpec _FormatIncompatibleMods(List<string> additional)
|
||||
{
|
||||
if (additional.Count != 1)
|
||||
return _Spec(
|
||||
"Crash.Reason.IncompatibleMods.Generic",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
help: true);
|
||||
|
||||
var info = additional[0];
|
||||
|
||||
return info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)
|
||||
? _Spec(
|
||||
"Crash.Reason.ModLoaderIncompatible",
|
||||
"Crash.Suggestion.ModLoaderIncompatible",
|
||||
_Args(info),
|
||||
action: CrashSuggestedAction.OpenInstanceSettings)
|
||||
: _Spec(
|
||||
"Crash.Reason.IncompatibleMods.WithDetail",
|
||||
"Crash.Suggestion.FollowLoaderInstructions",
|
||||
_Args(info));
|
||||
}
|
||||
|
||||
private static CrashMessageSpec _Spec(
|
||||
string reasonKey,
|
||||
string? suggestionKey = null,
|
||||
object?[]? reasonArgs = null,
|
||||
object?[]? suggestionArgs = null,
|
||||
bool report = false,
|
||||
bool help = false,
|
||||
CrashSuggestedAction action = CrashSuggestedAction.None)
|
||||
{
|
||||
return new CrashMessageSpec(
|
||||
reasonKey,
|
||||
reasonArgs ?? [],
|
||||
suggestionKey,
|
||||
suggestionArgs ?? [],
|
||||
report,
|
||||
help,
|
||||
action);
|
||||
}
|
||||
|
||||
private static object?[] _Args(params object?[] args)
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
private static string _BulletList(IEnumerable<string> items)
|
||||
{
|
||||
return string.Join(
|
||||
Environment.NewLine,
|
||||
items.Select(item => Lang.Text("Crash.Presentation.ListItem", item)));
|
||||
}
|
||||
|
||||
private static string _JoinParagraphs(IEnumerable<string> items)
|
||||
{
|
||||
return string.Join(
|
||||
Environment.NewLine + Environment.NewLine,
|
||||
items.Where(item => !string.IsNullOrWhiteSpace(item)));
|
||||
}
|
||||
|
||||
private static string _NormalizeLineEndings(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\r\n", "\r")
|
||||
.Replace("\n", "\r")
|
||||
.Replace("\r", "\r\n");
|
||||
}
|
||||
|
||||
private static string? _GetLauncherOutdatedSuggestion()
|
||||
{
|
||||
try
|
||||
{
|
||||
return UpdateManager.GetVersionStatus() == UpdateEnums.VersionStatus.Latest
|
||||
? null
|
||||
: Lang.Text("Crash.Suggestion.LauncherOutdated");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Crash", "确认启动器更新失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1063 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class McInstance
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示的描述文本。
|
||||
/// </summary>
|
||||
public string Desc = Lang.Text("Select.Instance.Description.NotLoaded");
|
||||
|
||||
/// <summary>
|
||||
/// 强制实例分类,0 为未启用,1 为隐藏,2 及以上为其他普通分类。
|
||||
/// </summary>
|
||||
public McInstanceCardType displayType = McInstanceCardType.Auto;
|
||||
|
||||
public bool IsLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已初始化从 JAR 中读取 version.json。
|
||||
/// </summary>
|
||||
private bool _jsonVersionInited;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为收藏的实例。
|
||||
/// </summary>
|
||||
public bool IsStar;
|
||||
|
||||
/// <summary>
|
||||
/// 显示的实例图标。
|
||||
/// </summary>
|
||||
public string Logo;
|
||||
|
||||
/// <summary>
|
||||
/// 实例的发布时间。
|
||||
/// </summary>
|
||||
public DateTime releaseTime = new(1970, 1, 1, 15, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的列表检查原始结果,不受自定义影响。
|
||||
/// </summary>
|
||||
public McInstanceState state = McInstanceState.Error;
|
||||
|
||||
/// <summary></summary>
|
||||
/// <param name="name">实例名,或实例文件夹的完整路径(不规定是否以 \ 结尾)。</param>
|
||||
public McInstance(string name)
|
||||
{
|
||||
PathInstance = (name.Contains(":") ? name : Path.Combine(ModFolder.mcFolderSelected, "versions", name)) + (name.EndsWithF(@"\") ? "" : @"\");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的实例文件夹,以“\”结尾。
|
||||
/// </summary>
|
||||
public string PathInstance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用版本隔离后,该实例所对应的 Minecraft 根文件夹,以“\”结尾。
|
||||
/// </summary>
|
||||
public string PathIndie
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Config.Instance.IndieV2Config.IsDefault(PathInstance))
|
||||
{
|
||||
if (!IsLoaded)
|
||||
Load();
|
||||
|
||||
// 决定该实例是否应该被隔离
|
||||
bool ShouldBeIndie()
|
||||
{
|
||||
// 从老的实例独立设置中迁移:-1 未决定,0 使用全局设置,1 手动开启,2 手动关闭
|
||||
if (!Config.Instance.IndieV1Config.IsDefault(PathInstance) && Config.Instance.IndieV1[PathInstance] > 0)
|
||||
{
|
||||
ModBase.Log($"[Minecraft] 版本隔离初始化({Name}):从老的实例独立设置中迁移");
|
||||
return Config.Instance.IndieV1[PathInstance] == 1;
|
||||
}
|
||||
|
||||
// 若实例文件夹下包含 mods 或 saves 文件夹,则自动开启版本隔离
|
||||
var modFolder = new DirectoryInfo(PathInstance + @"mods\");
|
||||
var saveFolder = new DirectoryInfo(PathInstance + @"saves\");
|
||||
if ((modFolder.Exists && modFolder.EnumerateFiles().Any()) ||
|
||||
(saveFolder.Exists && saveFolder.EnumerateDirectories().Any()))
|
||||
{
|
||||
ModBase.Log($"[Minecraft] 版本隔离初始化({Name}):实例文件夹下存在 mods 或 saves 文件夹,自动开启");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据全局的默认设置决定是否隔离
|
||||
var isRelease = state != McInstanceState.Fool && state != McInstanceState.Old &&
|
||||
state != McInstanceState.Snapshot;
|
||||
ModBase.Log(
|
||||
$"[Minecraft] 版本隔离初始化({Name}):从全局默认设置中({Config.Launch.IndieSolutionV2})判断,State {ModBase.GetStringFromEnum(state)},IsRelease {isRelease},Modable {Modable}");
|
||||
|
||||
return Config.Launch.IndieSolutionV2 switch
|
||||
{
|
||||
0 => false, // 关闭
|
||||
1 => Info.HasLabyMod || Modable, // 仅隔离可安装 Mod 的实例
|
||||
2 => !isRelease, // 仅隔离非正式版
|
||||
3 => Info.HasLabyMod || Modable || !isRelease, // 隔离非正式版与可安装 Mod 的实例
|
||||
_ => true // 隔离所有实例
|
||||
};
|
||||
}
|
||||
|
||||
Config.Instance.IndieV2[PathInstance] = ShouldBeIndie();
|
||||
}
|
||||
|
||||
return Config.Instance.IndieV2[PathInstance] ? PathInstance : ModFolder.mcFolderSelected;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的实例文件夹名称。
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null && !string.IsNullOrEmpty(PathInstance))
|
||||
field = ModBase.GetFolderNameFromPath(PathInstance);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否可以安装 Mod。
|
||||
/// </summary>
|
||||
public bool Modable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsLoaded)
|
||||
Load();
|
||||
return Info.HasFabric || Info.HasLegacyFabric || Info.HasQuilt || Info.HasForge || Info.HasLiteLoader ||
|
||||
Info.HasNeoForge || Info.HasCleanroom || displayType == McInstanceCardType.API; // #223
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例信息。
|
||||
/// </summary>
|
||||
public McInstanceInfo Info
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is not null)
|
||||
return field;
|
||||
field = new McInstanceInfo();
|
||||
|
||||
#region 获取游戏版本
|
||||
|
||||
try
|
||||
{
|
||||
// 获取发布时间并判断是否为老版本
|
||||
try
|
||||
{
|
||||
if (JsonObject["releaseTime"] is null)
|
||||
releaseTime = new DateTime(1970, 1, 1, 15, 0, 0); // 未知版本也可能显示为 1970 年
|
||||
else
|
||||
releaseTime = JsonObject["releaseTime"].ToObject<DateTime>();
|
||||
if (releaseTime.Year > 2000 && releaseTime.Year < 2013)
|
||||
{
|
||||
field.VanillaName = "Old";
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
releaseTime = new DateTime(1970, 1, 1, 15, 0, 0);
|
||||
}
|
||||
|
||||
// 实验性快照
|
||||
if ((string)(JsonObject["type"] ?? "") == "pending")
|
||||
{
|
||||
field.VanillaName = "pending";
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 PCL 下载的版本信息中获取版本号
|
||||
if (JsonObject["clientVersion"] is not null)
|
||||
{
|
||||
field.VanillaName = (string)JsonObject["clientVersion"];
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 HMCL 下载的版本信息中获取版本号
|
||||
if (JsonObject["patches"] is not null)
|
||||
foreach (var patchNode in JsonObject["patches"].AsArray()) { var patch = patchNode.AsObject();
|
||||
if ((patch["id"] ?? "").ToString() == "game" && patch["version"] is not null)
|
||||
{
|
||||
field.VanillaName = patch["version"].ToString();
|
||||
goto VersionSearchFinish;
|
||||
} }
|
||||
|
||||
// 从 Forge / NeoForge / LabyMod Arguments 中获取版本号
|
||||
if (JsonObject["arguments"] is not null)
|
||||
{
|
||||
if (JsonObject["arguments"]["game"] is not null)
|
||||
{
|
||||
var mark = false;
|
||||
foreach (var Argument in JsonObject["arguments"]["game"].AsArray())
|
||||
{
|
||||
if (mark)
|
||||
{
|
||||
field.VanillaName = Argument.ToString();
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
if (Argument.ToString() == "--fml.mcVersion")
|
||||
mark = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (JsonObject["arguments"]["jvm"] is not null)
|
||||
foreach (var Argument in JsonObject["arguments"]["jvm"].AsArray())
|
||||
{
|
||||
var regexArgument = Argument.ToString().RegexSeek(RegexPatterns.LabyModVersion);
|
||||
if (regexArgument is not null)
|
||||
{
|
||||
field.VanillaName = regexArgument;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从继承实例中获取版本号
|
||||
if (!string.IsNullOrEmpty(InheritInstanceName))
|
||||
{
|
||||
field.VanillaName = (JsonObject["jar"] ?? "").ToString(); // LiteLoader 优先使用 Jar
|
||||
if (string.IsNullOrEmpty(field.VanillaName))
|
||||
field.VanillaName = InheritInstanceName;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从下载地址中获取版本号
|
||||
var regex = (JsonObject["downloads"] ?? "").ToString()
|
||||
.RegexSeek(RegexPatterns.MinecraftDownloadUrlVersion);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 Forge 版本中获取版本号
|
||||
var librariesString = JsonObject["libraries"].ToString();
|
||||
regex = librariesString.RegexSeek(RegexPatterns.ForgeLibVersion);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 OptiFine 版本中获取版本号
|
||||
regex = librariesString.RegexSeek(RegexPatterns.OptiFineLibVersion);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 Fabric / Quilt / Legacy Fabric 版本中获取版本号
|
||||
regex = librariesString.RegexSeek(RegexPatterns.FabricLikeLibVersion);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 jar 项中获取版本号
|
||||
if (JsonObject["jar"] is not null)
|
||||
{
|
||||
field.VanillaName = JsonObject["jar"].ToString();
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 jar 文件的 version.json 中获取版本号
|
||||
if (JsonVersion?["name"] is not null)
|
||||
{
|
||||
var jsonVerName = JsonVersion["name"].ToString();
|
||||
if (jsonVerName.Length < 32) // 因为 wiki 说这玩意儿可能是个 hash,虽然我没发现
|
||||
{
|
||||
field.VanillaName = jsonVerName;
|
||||
ModBase.Log("[Minecraft] 从版本 jar 中的 version.json 获取到版本号:" + jsonVerName);
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
}
|
||||
|
||||
// 从 JSON 的 ID 中获取
|
||||
regex = ((string)JsonObject["id"]).RegexSeek(RegexPatterns.MinecraftJsonVersion,
|
||||
RegexOptions.IgnoreCase);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 非准确的版本判断警告
|
||||
ModBase.Log("[Minecraft] 无法完全确认 MC 版本号的版本:" + Name);
|
||||
field.Reliable = false;
|
||||
// 从文件夹名中获取
|
||||
regex = Name.RegexSeek(RegexPatterns.MinecraftJsonVersion, RegexOptions.IgnoreCase);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 从 JSON 出现的版本号中获取
|
||||
var jsonRaw = (JsonObject)JsonObject.DeepClone();
|
||||
jsonRaw.Remove("libraries");
|
||||
var jsonRawText = jsonRaw.ToString();
|
||||
regex = jsonRawText.RegexSeek(RegexPatterns.MinecraftJsonVersion, RegexOptions.IgnoreCase);
|
||||
if (regex is not null)
|
||||
{
|
||||
field.VanillaName = regex;
|
||||
goto VersionSearchFinish;
|
||||
}
|
||||
|
||||
// 无法获取
|
||||
field.VanillaName = "Unknown";
|
||||
Desc = Lang.Text("Select.Instance.Description.UnknownMcVersion");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "识别 Minecraft 版本时出错");
|
||||
field.VanillaName = "Unknown";
|
||||
Desc = Lang.Text("Minecraft.Error.Unrecognizable", ex.Message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
VersionSearchFinish: ;
|
||||
|
||||
if (field.VanillaName.StartsWithF("20.") || field.VanillaName.StartsWithF("21."))
|
||||
{
|
||||
field.VanillaName = "1." + field.VanillaName;
|
||||
}
|
||||
|
||||
field.VanillaName = field.VanillaName.Replace("_unobfuscated", "").Replace(" Unobfuscated", "");
|
||||
// 获取版本号
|
||||
if (field.VanillaName.StartsWithF("1."))
|
||||
{
|
||||
var segments = field.VanillaName.Split(" _-.".ToCharArray());
|
||||
field.vanilla = new Version((int)Math.Round(ModBase.Val(segments.Count() >= 2 ? segments[1] : "0")),
|
||||
0, (int)Math.Round(ModBase.Val(segments.Count() >= 3 ? segments[2] : "0")));
|
||||
}
|
||||
else if (field.VanillaName.RegexCheck(@"^[2-9][0-9]\."))
|
||||
{
|
||||
var segments = field.VanillaName.Split(" _-.".ToCharArray());
|
||||
field.vanilla = new Version((int)Math.Round(ModBase.Val(segments[0])),
|
||||
(int)Math.Round(ModBase.Val(segments.Count() >= 2 ? segments[1] : "0")),
|
||||
(int)Math.Round(ModBase.Val(segments.Count() >= 3 ? segments[2] : "0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
field.vanilla = new Version(9999, 0, 0);
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
set { field = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的 JSON 文本。
|
||||
/// </summary>
|
||||
public string JsonText
|
||||
{
|
||||
get
|
||||
{
|
||||
// 快速检查 JSON 是否以 { 开头、} 结尾;忽略空白字符
|
||||
bool FastJsonCheck(string json)
|
||||
{
|
||||
var trimedJson = json.Trim();
|
||||
return trimedJson.StartsWithF("{") && trimedJson.EndsWithF("}");
|
||||
}
|
||||
|
||||
;
|
||||
if (field is null)
|
||||
{
|
||||
var jsonPath = PathInstance + Name + ".json";
|
||||
if (!File.Exists(jsonPath))
|
||||
{
|
||||
// 如果文件夹下只有一个 JSON 文件,则将其作为实例 JSON
|
||||
var jsonFiles = Directory.GetFiles(PathInstance, "*.json");
|
||||
if (jsonFiles.Count() == 1)
|
||||
{
|
||||
jsonPath = jsonFiles[0];
|
||||
ModBase.Log("[Minecraft] 未找到同名实例 JSON,自动换用 " + jsonPath, ModBase.LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Error.InstanceJsonNotFound",
|
||||
$"{PathInstance}{Name}.json"));
|
||||
}
|
||||
}
|
||||
|
||||
field = ModBase.ReadFile(jsonPath);
|
||||
// 如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
|
||||
if (!FastJsonCheck(field))
|
||||
{
|
||||
if (ModBase.RunInUi())
|
||||
{
|
||||
ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将进行短暂重试({jsonPath})", ModBase.LogLevel.Debug);
|
||||
Thread.Sleep(200);
|
||||
field = ModBase.ReadFile(jsonPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({jsonPath})", ModBase.LogLevel.Debug);
|
||||
Thread.Sleep(2000);
|
||||
field = ModBase.ReadFile(jsonPath);
|
||||
}
|
||||
if (!FastJsonCheck(field))
|
||||
ModBase.GetJson(field);
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
set => field = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的 JSON 对象。
|
||||
/// 若 JSON 存在问题,在获取该属性时即会抛出异常。
|
||||
/// </summary>
|
||||
public JsonObject JsonObject
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
{
|
||||
var text = JsonText; // 触发 JsonText 的 Get 事件
|
||||
try
|
||||
{
|
||||
field = (JsonObject)ModBase.GetJson(text);
|
||||
// 转换 HMCL 关键项
|
||||
if (field.ContainsKey("patches") && !field.ContainsKey("time"))
|
||||
{
|
||||
IsHmclFormatJson = true;
|
||||
// 合并 JSON
|
||||
// Dim HasOptiFine As Boolean = False, HasForge As Boolean = False
|
||||
JsonObject currentObject = null;
|
||||
var subjsonList = new List<JsonObject>();
|
||||
foreach (var SubjsonNode in field["patches"].AsArray()) { var subjson = SubjsonNode.AsObject();
|
||||
subjsonList.Add(subjson); }
|
||||
subjsonList.Sort((left, right) =>
|
||||
{
|
||||
var leftVal = ModBase.Val((left["priority"] ?? "0").ToString());
|
||||
var rightVal = ModBase.Val((right["priority"] ?? "0").ToString());
|
||||
return leftVal.CompareTo(rightVal);
|
||||
});
|
||||
foreach (var Subjson in subjsonList)
|
||||
{
|
||||
var id = (string)Subjson["id"];
|
||||
if (id is not null)
|
||||
{
|
||||
// 合并 JSON
|
||||
ModBase.Log("[Minecraft] 合并 HMCL 分支项:" + id);
|
||||
if (currentObject is not null)
|
||||
currentObject.Merge(Subjson);
|
||||
else
|
||||
currentObject = Subjson;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[Minecraft] 存在为空的 HMCL 分支项");
|
||||
}
|
||||
}
|
||||
|
||||
field = currentObject;
|
||||
// 修改附加项
|
||||
field["id"] = Name;
|
||||
if (field.ContainsKey("inheritsFrom"))
|
||||
field.Remove("inheritsFrom");
|
||||
}
|
||||
|
||||
// 与继承实例合并
|
||||
object inheritInstanceName = null;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
inheritInstanceName = field["inheritsFrom"] is null
|
||||
? ""
|
||||
: field["inheritsFrom"].ToString();
|
||||
if (Equals(inheritInstanceName, Name))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 自引用的继承实例:" + Name, ModBase.LogLevel.Debug);
|
||||
inheritInstanceName = "";
|
||||
break;
|
||||
}
|
||||
|
||||
Recheck: ;
|
||||
|
||||
if (!Equals(inheritInstanceName, ""))
|
||||
{
|
||||
var inheritInstance = new McInstance(inheritInstanceName?.ToString() ?? "");
|
||||
// 继续循环
|
||||
if (Equals(inheritInstance.InheritInstanceName,
|
||||
inheritInstanceName))
|
||||
throw new Exception(Lang.Text("Minecraft.Error.DependencyRecursion",
|
||||
inheritInstanceName));
|
||||
inheritInstanceName = inheritInstance.InheritInstanceName;
|
||||
// 合并
|
||||
inheritInstance.JsonObject.Merge(field);
|
||||
field = inheritInstance.JsonObject;
|
||||
goto Recheck;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "合并实例依赖项 JSON 失败(" + (inheritInstanceName ?? "null") + ")");
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Error.InitInstanceJsonFailed", Name ?? "null"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
set => field = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为旧版 JSON 格式。
|
||||
/// </summary>
|
||||
public bool IsOldJson => JsonObject["minecraftArguments"] is not null &&
|
||||
(string)JsonObject["minecraftArguments"] != "";
|
||||
|
||||
/// <summary>
|
||||
/// JSON 是否为 HMCL 格式。
|
||||
/// </summary>
|
||||
public bool IsHmclFormatJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例 JAR 中的 version.json 文件对象。
|
||||
/// 若没有则返回 Nothing。
|
||||
/// </summary>
|
||||
public JsonObject JsonVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_jsonVersionInited)
|
||||
{
|
||||
_jsonVersionInited = true;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(PathInstance + Name + ".jar"))
|
||||
break;
|
||||
using (var jarArchive = new ZipArchive(new FileStream(PathInstance + Name + ".jar",
|
||||
FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
|
||||
{
|
||||
var versionJson = jarArchive.GetEntry("version.json");
|
||||
if (versionJson is not null)
|
||||
using (var versionJsonStream = new StreamReader(versionJson.Open()))
|
||||
{
|
||||
field = (JsonObject)ModBase.GetJson(versionJsonStream.ReadToEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"从实例 JAR 中读取 version.json 失败 ({PathInstance}{Name}.jar)");
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该实例的依赖实例。若无依赖实例则为空字符串。
|
||||
/// </summary>
|
||||
public string InheritInstanceName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
{
|
||||
field = (JsonObject["inheritsFrom"] ?? "").ToString();
|
||||
// 由于过老的 LiteLoader 中没有 Inherits(例如 1.5.2),需要手动判断以获取真实继承实例
|
||||
// 此外,由于这里的加载早于实例种类判断,所以需要手动判断是否为 LiteLoader
|
||||
// 如果实例提供了不同的 JAR,代表所需的 JAR 可能已被更改,则跳过 Inherit 替换
|
||||
if (JsonText.Contains("liteloader") && (Info.VanillaName ?? "") != (Name ?? "") &&
|
||||
!JsonText.Contains("logging"))
|
||||
if (((JsonObject["jar"] ?? Info.VanillaName).ToString() ?? "") == (Info.VanillaName ?? ""))
|
||||
field = Info.VanillaName;
|
||||
// HMCL 实例无 JSON
|
||||
if (IsHmclFormatJson)
|
||||
field = "";
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 Minecraft 版本,若检查通过 State 则为 Original 且返回 True。
|
||||
/// </summary>
|
||||
public bool Check()
|
||||
{
|
||||
// 检查文件夹
|
||||
if (!Directory.Exists(PathInstance))
|
||||
{
|
||||
state = McInstanceState.Error;
|
||||
Desc = Lang.Text("Select.Instance.Description.NotFound", Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(PathInstance + @"PCL\");
|
||||
ModBase.CheckPermissionWithException(PathInstance + @"PCL\");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state = McInstanceState.Error;
|
||||
Desc = Lang.Text("Select.Instance.Description.NoPermission");
|
||||
ModBase.Log(ex, "没有访问实例文件夹的权限");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确认 JSON 可用性
|
||||
try
|
||||
{
|
||||
var jsonObjCheck = JsonObject;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "实例 JSON 可用性检查失败(" + PathInstance + ")");
|
||||
JsonText = "";
|
||||
JsonObject = null;
|
||||
Desc = ex.Message;
|
||||
state = McInstanceState.Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查版本号获取
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(Info.VanillaName))
|
||||
throw new Exception(Lang.Text("Minecraft.Error.VersionNumberEmpty"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "版本号获取失败(" + Name + ")");
|
||||
state = McInstanceState.Error;
|
||||
Desc = Lang.Text("Minecraft.Error.VersionNumberFetchFailed", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查依赖实例
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(InheritInstanceName))
|
||||
if (!File.Exists(Path.Combine(ModBase.GetPathFromFullPath(PathInstance), InheritInstanceName, InheritInstanceName + ".json")))
|
||||
{
|
||||
state = McInstanceState.Error;
|
||||
Desc = Lang.Text("Select.Instance.Description.NeedInherit", InheritInstanceName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "依赖实例检查出错(" + Name + ")");
|
||||
state = McInstanceState.Error;
|
||||
Desc = Lang.Text("Select.Instance.Description.UnknownError") + ": " + ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
state = McInstanceState.Original;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Minecraft 实例的详细信息。不使用其缓存,且会更新缓存。
|
||||
/// </summary>
|
||||
public McInstance Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查实例,若出错则跳过数据确定阶段
|
||||
if (!Check())
|
||||
goto ExitDataLoad;
|
||||
|
||||
#region 确定实例分类
|
||||
|
||||
switch (Info.VanillaName ?? "") // 在获取 Version.Original 对象时会完成它的加载
|
||||
{
|
||||
case "Unknown":
|
||||
{
|
||||
state = McInstanceState.Error;
|
||||
break;
|
||||
}
|
||||
case "Old":
|
||||
{
|
||||
state = McInstanceState.Old; // 根据 API 进行筛选
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
var realJson = JsonObject is not null ? JsonObject.ToString() : JsonText;
|
||||
// 愚人节与快照版本
|
||||
if ((JsonObject["type"] ?? "").ToString() == "fool" ||
|
||||
!string.IsNullOrEmpty(McVersionClassifier.GetMcFoolName(Info.VanillaName)))
|
||||
state = McInstanceState.Fool;
|
||||
else if (IsSnapshot()) state = McInstanceState.Snapshot;
|
||||
// OptiFine
|
||||
if (realJson.Contains("optifine"))
|
||||
{
|
||||
state = McInstanceState.OptiFine;
|
||||
Info.HasOptiFine = true;
|
||||
Info.OptiFine = realJson.RegexSeek(RegexPatterns.OptiFineVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown");
|
||||
}
|
||||
|
||||
// LiteLoader
|
||||
if (realJson.Contains("liteloader"))
|
||||
{
|
||||
state = McInstanceState.LiteLoader;
|
||||
Info.HasLiteLoader = true;
|
||||
}
|
||||
|
||||
// Fabric、Forge、Quilt、LabyMod、Legacy Fabric
|
||||
if (realJson.Contains("labymod_data"))
|
||||
{
|
||||
state = McInstanceState.LabyMod;
|
||||
Info.HasLabyMod = true;
|
||||
Info.LabyMod = (string)JsonObject["labymod_data"]["version"];
|
||||
}
|
||||
else if (realJson.Contains("net.legacyfabric:intermediary"))
|
||||
{
|
||||
state = McInstanceState.LegacyFabric;
|
||||
Info.HasLegacyFabric = true;
|
||||
Info.LegacyFabric =
|
||||
(realJson.RegexSeek(RegexPatterns.LegacyFabricVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown"))
|
||||
.Replace("+build", "");
|
||||
}
|
||||
else if (realJson.Contains("net.fabricmc:fabric-loader"))
|
||||
{
|
||||
state = McInstanceState.Fabric;
|
||||
Info.HasFabric = true;
|
||||
Info.Fabric =
|
||||
(realJson.RegexSeek(RegexPatterns.FabricVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown")).Replace("+build", "");
|
||||
}
|
||||
else if (realJson.Contains("org.quiltmc:quilt-loader"))
|
||||
{
|
||||
state = McInstanceState.Quilt;
|
||||
Info.HasQuilt = true;
|
||||
Info.Quilt =
|
||||
(realJson.RegexSeek(RegexPatterns.QuiltVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown")).Replace("+build", "");
|
||||
}
|
||||
else if (realJson.Contains("com.cleanroommc:cleanroom:"))
|
||||
{
|
||||
state = McInstanceState.Cleanroom;
|
||||
Info.HasCleanroom = true;
|
||||
Info.Cleanroom =
|
||||
(realJson.RegexSeek(RegexPatterns.CleanroomVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown")).Replace("+build", "");
|
||||
}
|
||||
else if (realJson.Contains("minecraftforge") && !realJson.Contains("net.neoforge"))
|
||||
{
|
||||
state = McInstanceState.Forge;
|
||||
Info.HasForge = true;
|
||||
Info.Forge = realJson.RegexSeek(RegexPatterns.ForgeMainVersion) ??
|
||||
realJson.RegexSeek(RegexPatterns.ForgeLibVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown");
|
||||
}
|
||||
else if (realJson.Contains("net.neoforge"))
|
||||
{
|
||||
// 1.20.1 JSON 范例:"--fml.forgeVersion", "47.1.99"
|
||||
// 1.20.2+ JSON 范例:"--fml.neoForgeVersion", "20.6.119-beta"
|
||||
state = McInstanceState.NeoForge;
|
||||
Info.HasNeoForge = true;
|
||||
Info.NeoForge = realJson.RegexSeek(RegexPatterns.NeoForgeVersion) ??
|
||||
Lang.Text("Minecraft.Version.Unknown");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
ExitDataLoad: ;
|
||||
|
||||
// 确定实例图标
|
||||
Logo = States.Instance.LogoPath[PathInstance];
|
||||
if (string.IsNullOrEmpty(Logo) || !States.Instance.IsLogoCustom[PathInstance])
|
||||
switch (state)
|
||||
{
|
||||
case McInstanceState.Original:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Grass.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Snapshot:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/CommandBlock.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Old:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/CobbleStone.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Forge:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Anvil.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.NeoForge:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/NeoForge.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Cleanroom:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Cleanroom.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Fabric:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Fabric.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.LegacyFabric:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Fabric.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Quilt:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Quilt.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.OptiFine:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/GrassPath.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.LiteLoader:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/Egg.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Fool:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/GoldBlock.png";
|
||||
break;
|
||||
}
|
||||
case McInstanceState.LabyMod:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/LabyMod.png";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
Logo = ModBase.pathImage + "Blocks/RedstoneBlock.png";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定实例描述
|
||||
if (state == McInstanceState.Error)
|
||||
{
|
||||
Desc = Desc;
|
||||
}
|
||||
else
|
||||
{
|
||||
Desc = States.Instance.CustomInfo[PathInstance];
|
||||
if ((Desc ?? "") == (GetDefaultDescription() ?? ""))
|
||||
Desc = "";
|
||||
}
|
||||
|
||||
// 确定实例收藏状态
|
||||
IsStar = States.Instance.Starred[PathInstance];
|
||||
// 确定实例显示种类
|
||||
displayType = (McInstanceCardType)States.Instance.CardType[PathInstance];
|
||||
// 写入缓存
|
||||
if (Directory.Exists(PathInstance))
|
||||
{
|
||||
States.Instance.State[PathInstance] = (int)state;
|
||||
States.Instance.Info[PathInstance] = Desc;
|
||||
States.Instance.LogoPath[PathInstance] = Logo;
|
||||
}
|
||||
|
||||
if (state != McInstanceState.Error)
|
||||
{
|
||||
States.Instance.ReleaseTime[PathInstance] = releaseTime.ToString("yyyy'-'MM'-'dd HH':'mm", CultureInfo.InvariantCulture);
|
||||
States.Instance.FabricVersion[PathInstance] = Info.Fabric;
|
||||
States.Instance.LegacyFabricVersion[PathInstance] = Info.LegacyFabric;
|
||||
States.Instance.QuiltVersion[PathInstance] = Info.Quilt;
|
||||
States.Instance.LabyModVersion[PathInstance] = Info.LabyMod;
|
||||
States.Instance.OptiFineVersion[PathInstance] = Info.OptiFine;
|
||||
States.Instance.HasLiteLoader[PathInstance] = Info.HasLiteLoader;
|
||||
States.Instance.ForgeVersion[PathInstance] = Info.Forge;
|
||||
States.Instance.NeoForgeVersion[PathInstance] = Info.NeoForge;
|
||||
States.Instance.CleanroomVersion[PathInstance] = Info.Cleanroom;
|
||||
States.Instance.VanillaVersionName[PathInstance] = Info.VanillaName;
|
||||
States.Instance.VanillaVersion[PathInstance] = Info.vanilla.ToString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Desc = Lang.Text("Select.Instance.Description.UnknownError") + ": " + ex;
|
||||
Logo = ModBase.pathImage + "Blocks/RedstoneBlock.png";
|
||||
state = McInstanceState.Error;
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Instance.Error.Load", Name),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Select.Instance.Error.Load", Name));
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoaded = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private bool IsSnapshot()
|
||||
{
|
||||
return new[] { "w", "snapshot", "rc", "pre", "experimental", "-" }.Any(s =>
|
||||
Info.VanillaName.ContainsF(s, true)) || Name.ContainsF("combat", true) ||
|
||||
(JsonObject["type"] ?? "").ToString() == "snapshot" ||
|
||||
(JsonObject["type"] ?? "").ToString() == "pending";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实例的默认描述。
|
||||
/// </summary>
|
||||
public string GetDefaultDescription()
|
||||
{
|
||||
// Mod Loader 信息
|
||||
var modLoaderInfo = "";
|
||||
if (this.Info.HasForge)
|
||||
modLoaderInfo += ", Forge" + (this.Info.Forge == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.Forge);
|
||||
if (this.Info.HasNeoForge)
|
||||
modLoaderInfo += ", NeoForge" + (this.Info.NeoForge == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.NeoForge);
|
||||
if (this.Info.HasCleanroom)
|
||||
modLoaderInfo += ", Cleanroom" + (this.Info.Cleanroom == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.Cleanroom);
|
||||
if (this.Info.HasLabyMod)
|
||||
modLoaderInfo += ", LabyMod" + (this.Info.LabyMod == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.LabyMod);
|
||||
if (this.Info.HasFabric)
|
||||
modLoaderInfo += ", Fabric" + (this.Info.Fabric == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.Fabric);
|
||||
if (this.Info.HasQuilt)
|
||||
modLoaderInfo += ", Quilt" + (this.Info.Quilt == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.Quilt);
|
||||
if (this.Info.HasLegacyFabric)
|
||||
modLoaderInfo += ", Legacy Fabric" +
|
||||
(this.Info.LegacyFabric == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.LegacyFabric);
|
||||
if (this.Info.HasOptiFine)
|
||||
modLoaderInfo += ", OptiFine" + (this.Info.OptiFine == Lang.Text("Minecraft.Version.Unknown")
|
||||
? ""
|
||||
: " " + this.Info.OptiFine.Replace("-", " ").Replace("_", " "));
|
||||
if (this.Info.HasLiteLoader)
|
||||
modLoaderInfo += ", LiteLoader";
|
||||
// 基础信息
|
||||
string info;
|
||||
switch (state)
|
||||
{
|
||||
case McInstanceState.Snapshot:
|
||||
case McInstanceState.Original:
|
||||
case McInstanceState.Forge:
|
||||
case McInstanceState.NeoForge:
|
||||
case McInstanceState.Fabric:
|
||||
case McInstanceState.OptiFine:
|
||||
case McInstanceState.LiteLoader:
|
||||
{
|
||||
if (this.Info.VanillaName.ContainsF("pre", true))
|
||||
info = Lang.Text("Select.Instance.Description.PreRelease", this.Info.VanillaName);
|
||||
else if (this.Info.VanillaName.ContainsF("rc", true))
|
||||
info = Lang.Text("Select.Instance.Description.ReleaseCandidate", this.Info.VanillaName);
|
||||
else if (this.Info.VanillaName.Contains("experimental"))
|
||||
info = Lang.Text("Select.Instance.Description.ExperimentalSnapshot", this.Info.VanillaName);
|
||||
else if (this.Info.VanillaName == "pending")
|
||||
info = Lang.Text("Select.Instance.Description.ExperimentalSnapshot.Pending");
|
||||
else if (IsSnapshot())
|
||||
info = this.Info.Reliable ? Lang.Text("Select.Instance.Description.Snapshot", this.Info.VanillaName.Replace("-snapshot", "")) : Lang.Text("Select.Instance.Description.Snapshot.Unknown");
|
||||
else
|
||||
info = this.Info.Reliable ? Lang.Text("Select.Instance.Description.Release", this.Info.VanillaName) : Lang.Text("Select.Instance.Description.Release.Unknown");
|
||||
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Old:
|
||||
{
|
||||
info = Lang.Text("Select.Instance.Description.Old");
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Fool:
|
||||
{
|
||||
info = Lang.Text("Select.Instance.Description.AprilFools", this.Info.VanillaName);
|
||||
break;
|
||||
}
|
||||
case McInstanceState.Error:
|
||||
{
|
||||
return Desc; // 已有错误信息
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return Lang.Text("Select.Instance.Description.ReportUnknownError");
|
||||
}
|
||||
}
|
||||
|
||||
return (info + modLoaderInfo).Replace("_", "-");
|
||||
}
|
||||
|
||||
// 运算符支持
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var instance = obj as McInstance;
|
||||
return instance is not null && (PathInstance ?? "") == (instance.PathInstance ?? "");
|
||||
}
|
||||
|
||||
public static bool operator ==(McInstance? a, McInstance? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
return (a.PathInstance ?? "") == (b.PathInstance ?? "");
|
||||
}
|
||||
|
||||
public static bool operator !=(McInstance a, McInstance b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PCL;
|
||||
|
||||
public enum McInstanceCardType
|
||||
{
|
||||
Star = -1,
|
||||
Auto = 0, // 仅用于强制实例分类的自动
|
||||
Hidden = 1,
|
||||
API = 2,
|
||||
OriginalLike = 3,
|
||||
Rubbish = 4,
|
||||
Fool = 5,
|
||||
Error = 6
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
/// <summary>
|
||||
/// 某个 Minecraft 实例的版本名、附加组件信息。
|
||||
/// </summary>
|
||||
public class McInstanceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleanroom 版本号,如 0.2.4-alpha。
|
||||
/// </summary>
|
||||
public string Cleanroom = "";
|
||||
|
||||
/// <summary>
|
||||
/// Fabric 版本号,如 0.7.2.175。
|
||||
/// </summary>
|
||||
public string Fabric = "";
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本号,如 31.1.2、14.23.5.2847。
|
||||
/// </summary>
|
||||
public string Forge = "";
|
||||
|
||||
// Cleanroom
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 Cleanroom。
|
||||
/// </summary>
|
||||
public bool HasCleanroom;
|
||||
|
||||
// Fabric
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 Fabric。
|
||||
/// </summary>
|
||||
public bool HasFabric;
|
||||
|
||||
// Forge
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 Forge。
|
||||
/// </summary>
|
||||
public bool HasForge;
|
||||
|
||||
// LabyMod
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 LabyMod。
|
||||
/// </summary>
|
||||
public bool HasLabyMod;
|
||||
|
||||
// LegacyFabric
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 Fabric。
|
||||
/// </summary>
|
||||
public bool HasLegacyFabric;
|
||||
|
||||
// LiteLoader
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 LiteLoader。
|
||||
/// </summary>
|
||||
public bool HasLiteLoader;
|
||||
|
||||
// NeoForge
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 NeoForge。
|
||||
/// </summary>
|
||||
public bool HasNeoForge;
|
||||
|
||||
// OptiFine
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否通过 JSON 安装了 OptiFine。
|
||||
/// </summary>
|
||||
public bool HasOptiFine;
|
||||
|
||||
|
||||
// Quilt
|
||||
|
||||
/// <summary>
|
||||
/// 该实例是否安装了 Quilt。
|
||||
/// </summary>
|
||||
public bool HasQuilt;
|
||||
|
||||
/// <summary>
|
||||
/// LabyMod 版本号,如 4.2.59。
|
||||
/// </summary>
|
||||
public string LabyMod = "";
|
||||
|
||||
/// <summary>
|
||||
/// Fabric 版本号,如 0.7.2.175。
|
||||
/// </summary>
|
||||
public string LegacyFabric = "";
|
||||
|
||||
/// <summary>
|
||||
/// NeoForge 版本号,如 21.0.2-beta、47.1.79。
|
||||
/// </summary>
|
||||
public string NeoForge = "";
|
||||
|
||||
/// <summary>
|
||||
/// OptiFine 版本号,如 C8、C9_pre10。
|
||||
/// </summary>
|
||||
public string OptiFine = "";
|
||||
|
||||
/// <summary>
|
||||
/// Quilt 版本号,如 0.26.1-beta.1、0.26.0。
|
||||
/// </summary>
|
||||
public string Quilt = "";
|
||||
|
||||
/// <summary>
|
||||
/// 指示原版版本号是否可靠(不是通过猜测获取)。
|
||||
/// </summary>
|
||||
public bool Reliable = true;
|
||||
|
||||
/// <summary>
|
||||
/// 可比较的三段式原版版本号。
|
||||
/// 对老版本格式,例如 1.20.3,会被转换为 20.0.3。
|
||||
/// 若没有版本号,例如旧快照,则为 9999.0.0。
|
||||
/// </summary>
|
||||
public Version vanilla;
|
||||
|
||||
// 原版
|
||||
|
||||
/// <summary>
|
||||
/// 原版版本名。
|
||||
/// 如 26.1,26.1-snapshot-1,1.12.2,16w01a。
|
||||
/// </summary>
|
||||
public string VanillaName;
|
||||
|
||||
/// <summary>
|
||||
/// 原版版本号是否有效。
|
||||
/// </summary>
|
||||
public bool Valid => vanilla.Major < 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 可供比较的原版 Drop 序数。
|
||||
/// 例如 26.3.2 为 263,1.21.5 为 210。
|
||||
/// 若没有版本号,例如旧快照,则直接指定为 209。
|
||||
/// </summary>
|
||||
public int Drop => Valid ? vanilla.Major * 10 + vanilla.Minor : 209;
|
||||
|
||||
/// <summary>
|
||||
/// 可供比较的 OptiFine 版本序数。
|
||||
/// </summary>
|
||||
public int OptiFineCode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(OptiFine) || OptiFine == Lang.Text("Minecraft.Version.Unknown"))
|
||||
return 0;
|
||||
// 字母编号,如 G2 中的 G(7)
|
||||
var result = char.ToUpperInvariant(OptiFine.First()) - 'A' + 1;
|
||||
// 末尾数字,如 C5 beta4 中的 5
|
||||
result *= 100;
|
||||
result = (int)Math.Round(result +
|
||||
ModBase.Val(OptiFine[1..].RegexSeek("[0-9]+")));
|
||||
// 测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)
|
||||
result *= 100;
|
||||
if (OptiFine.ContainsF("pre", true))
|
||||
result += 50;
|
||||
if (OptiFine.ContainsF("pre", true) || OptiFine.ContainsF("beta", true))
|
||||
{
|
||||
var lastChar = OptiFine[^1..];
|
||||
if (ModBase.Val(lastChar) == 0d && lastChar != "0")
|
||||
result += 1; // 为 pre 或 beta 结尾,视作 1
|
||||
else
|
||||
result =
|
||||
(int)Math.Round(result +
|
||||
ModBase.Val(OptiFine.ToLower().RegexSeek("(?<=((pre)|(beta)))[0-9]+")));
|
||||
}
|
||||
else
|
||||
{
|
||||
result += 99;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Forgelike
|
||||
|
||||
/// <summary>
|
||||
/// 该版本是否安装了 Forgelike 加载器。
|
||||
/// </summary>
|
||||
public bool HasForgelike => HasForge || HasNeoForge || HasCleanroom;
|
||||
|
||||
/// <summary>
|
||||
/// 可供比较的类 Forge 版本序数。
|
||||
/// </summary>
|
||||
public int ForgelikeCode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasForgelike)
|
||||
return 0;
|
||||
if ((string.IsNullOrEmpty(Forge) || Forge == Lang.Text("Minecraft.Version.Unknown")) &&
|
||||
(string.IsNullOrEmpty(NeoForge) || NeoForge == Lang.Text("Minecraft.Version.Unknown")))
|
||||
return 0;
|
||||
var segments = (HasForge ? Forge : NeoForge).RegexSearch(@"\d+");
|
||||
switch (segments.Count)
|
||||
{
|
||||
case var @case when @case > 4:
|
||||
{
|
||||
return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d +
|
||||
ModBase.Val(segments[3]));
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d +
|
||||
ModBase.Val(segments[2]));
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d);
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fabriclike
|
||||
|
||||
/// <summary>
|
||||
/// 该版本是否安装了 Fabriclike 加载器。
|
||||
/// </summary>
|
||||
public bool HasFabriclike => HasFabric || HasQuilt || HasLegacyFabric;
|
||||
|
||||
// API
|
||||
|
||||
/// <summary>
|
||||
/// 生成对此实例信息的用户友好的描述性字符串。
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
string toStringRet = default;
|
||||
toStringRet = "";
|
||||
if (HasForge)
|
||||
toStringRet += ", Forge" + (Forge == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + Forge);
|
||||
if (HasNeoForge)
|
||||
toStringRet += ", NeoForge" +
|
||||
(NeoForge == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + NeoForge);
|
||||
if (HasCleanroom)
|
||||
toStringRet += ", Cleanroom" +
|
||||
(Cleanroom == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + Cleanroom);
|
||||
if (HasFabric)
|
||||
toStringRet += ", Fabric" + (Fabric == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + Fabric);
|
||||
if (HasLegacyFabric)
|
||||
toStringRet += ", LegacyFabric" +
|
||||
(LegacyFabric == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + LegacyFabric);
|
||||
if (HasQuilt)
|
||||
toStringRet += ", Quilt" + (Quilt == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + Quilt);
|
||||
if (HasLabyMod)
|
||||
toStringRet += ", LabyMod" + (LabyMod == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + LabyMod);
|
||||
if (HasOptiFine)
|
||||
toStringRet += ", OptiFine" +
|
||||
(OptiFine == Lang.Text("Minecraft.Version.Unknown") ? "" : " " + OptiFine);
|
||||
if (HasLiteLoader)
|
||||
toStringRet += ", LiteLoader";
|
||||
if (string.IsNullOrEmpty(toStringRet)) return Lang.Text("Minecraft.Version.Vanilla") + " " + VanillaName;
|
||||
|
||||
return VanillaName + toStringRet;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/// <summary>
|
||||
/// 版本字符串是否符合 Minecraft 原版格式,例如 1.x、26.x。
|
||||
/// </summary>
|
||||
public static bool IsFormatFit(string version)
|
||||
{
|
||||
if (version is null)
|
||||
return false;
|
||||
if (version.RegexCheck(@"^1\.\d"))
|
||||
return true;
|
||||
if (ModBase.Val(version.RegexSeek(@"^[2-9]\d\.\d+")) > 25d)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将版本字符串转换为 Drop 序数。
|
||||
/// 若无法转换则返回 -1。
|
||||
/// </summary>
|
||||
public static int VersionToDrop(string? version, bool allowSnapshot = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(version))
|
||||
return -1;
|
||||
|
||||
var lower = version.ToLowerInvariant();
|
||||
|
||||
if (lower.StartsWith("1.rv")) return 90; // 1.rv-pre1,约1.9
|
||||
if (lower.StartsWith("3d shareware")) return 140; // 3d shareware v1.34,约1.14
|
||||
|
||||
if (!allowSnapshot && lower.Contains('-'))
|
||||
return -1;
|
||||
|
||||
var baseVer = lower.BeforeFirst("-");
|
||||
var segments = baseVer.Split('.');
|
||||
|
||||
//XXwYY的快照
|
||||
// 按年份估算
|
||||
if (allowSnapshot && segments.Length == 1 && segments[0].Length >= 3
|
||||
&& segments[0][2] == 'w'
|
||||
&& char.IsDigit(segments[0][0]) && char.IsDigit(segments[0][1]))
|
||||
{
|
||||
var year = int.Parse(segments[0][..2]);
|
||||
if (year <= 16) return Math.Max(year * 10 - 70, 20);
|
||||
if (year == 17) return 120;
|
||||
return Math.Min(130 + (year - 18) * 10, 210);
|
||||
}
|
||||
|
||||
if (segments.Length < 2)
|
||||
return -1;
|
||||
var major = (int)Math.Round(ModBase.Val(segments[0]));
|
||||
var minor = (int)Math.Round(ModBase.Val(segments[1]));
|
||||
if (major == 1) return minor * 10;
|
||||
if (major >= 25) return major * 10 + minor;
|
||||
if (major == 2) return 50; //2.0愚人节
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Drop 序数转换为版本字符串。
|
||||
/// </summary>
|
||||
public static string DropToVersion(int drop)
|
||||
{
|
||||
if (drop >= 250) return $"{drop / 10}.{drop % 10}";
|
||||
|
||||
return $"1.{drop / 10}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PCL;
|
||||
|
||||
public enum McInstanceState
|
||||
{
|
||||
Error,
|
||||
Original,
|
||||
Snapshot,
|
||||
Fool,
|
||||
OptiFine,
|
||||
Old,
|
||||
Forge,
|
||||
NeoForge,
|
||||
LiteLoader,
|
||||
Fabric,
|
||||
LegacyFabric,
|
||||
Quilt,
|
||||
Cleanroom,
|
||||
LabyMod
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using PCL;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
public static class McLogFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 打码字符串中的 AccessToken。
|
||||
/// </summary>
|
||||
public static string FilterAccessToken(string raw, char filterChar)
|
||||
{
|
||||
// 打码 "accessToken " 后的内容
|
||||
if (raw.Contains("accessToken "))
|
||||
foreach (var Token in raw.RegexSearch("(?<=accessToken ([^ ]{5}))[^ ]+(?=[^ ]{5})"))
|
||||
raw = raw.Replace(Token, new string(filterChar, Token.Count()));
|
||||
// 打码当前登录的结果
|
||||
var accessToken = ModLaunch.mcLoginLoader.output.AccessToken;
|
||||
if (accessToken is not null && accessToken.Length >= 10 && raw.ContainsF(accessToken, true) &&
|
||||
(ModLaunch.mcLoginLoader.output.Uuid ?? "") !=
|
||||
(ModLaunch.mcLoginLoader.output.AccessToken ?? "")) // UUID 和 AccessToken 一样则不打码
|
||||
raw = raw.Replace(accessToken, accessToken[..5] + new string(filterChar, accessToken.Length - 10) +
|
||||
accessToken[^5..]);
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打码字符串中的 Windows 用户名。
|
||||
/// </summary>
|
||||
public static string FilterUserName(string raw, char filterChar)
|
||||
{
|
||||
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var userName = userProfile.Split(@"\").Last();
|
||||
var maskedProfile = userProfile.Replace(userName, new string(filterChar, userName.Length));
|
||||
return raw.Replace(userProfile, maskedProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class McVersionComparer
|
||||
{
|
||||
public const string UNKNOWN_VERSION_KEY = "UnknownVersion";
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个版本名;等同 Left >= Right。
|
||||
/// 无法比较两个预发布版的大小。
|
||||
/// 支持的格式:未知版本, 1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6
|
||||
/// </summary>
|
||||
public static bool CompareVersionGe(string left, string right)
|
||||
{
|
||||
return CompareVersion(left, right) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个版本名,若 Left 较新则返回 1,相同则返回 0,Right 较新则返回 -1;等同 Left - Right。
|
||||
/// 无法比较两个预发布版的大小。
|
||||
/// 支持的格式:未知版本, 26.1-snapshot-1,1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6
|
||||
/// </summary>
|
||||
public static int CompareVersion(string left, string right)
|
||||
{
|
||||
if (left == Lang.Text("Minecraft.Version.Unknown") || right == Lang.Text("Minecraft.Version.Unknown"))
|
||||
{
|
||||
if (left == Lang.Text("Minecraft.Version.Unknown") && right != Lang.Text("Minecraft.Version.Unknown"))
|
||||
return 1;
|
||||
if (left == Lang.Text("Minecraft.Version.Unknown") && right == Lang.Text("Minecraft.Version.Unknown"))
|
||||
return 0;
|
||||
if (left != Lang.Text("Minecraft.Version.Unknown") && right == Lang.Text("Minecraft.Version.Unknown"))
|
||||
return -1;
|
||||
}
|
||||
|
||||
left = left.ToLowerInvariant();
|
||||
right = right.ToLowerInvariant();
|
||||
var lefts = left.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+");
|
||||
var rights = right.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+");
|
||||
var i = 0;
|
||||
while (true)
|
||||
{
|
||||
// 两边均缺失,感觉是一个东西
|
||||
if (lefts.Count - 1 < i && rights.Count - 1 < i)
|
||||
{
|
||||
if (string.CompareOrdinal(left, right) > 0)
|
||||
return 1;
|
||||
if (string.CompareOrdinal(left, right) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 确定两边的数值
|
||||
var leftValue = lefts.Count - 1 < i ? "0" : lefts[i];
|
||||
var rightValue = rights.Count - 1 < i ? "0" : rights[i];
|
||||
if ((leftValue ?? "") == (rightValue ?? ""))
|
||||
goto NextEntry;
|
||||
if (leftValue == "rc")
|
||||
leftValue = (-1).ToString();
|
||||
if (leftValue == "pre")
|
||||
leftValue = (-2).ToString();
|
||||
if (leftValue == "snapshot")
|
||||
leftValue = (-3).ToString();
|
||||
if (leftValue == "experimental")
|
||||
leftValue = (-4).ToString();
|
||||
var leftValValue = ModBase.Val(leftValue);
|
||||
if (rightValue == "rc")
|
||||
rightValue = (-1).ToString();
|
||||
if (rightValue == "pre")
|
||||
rightValue = (-2).ToString();
|
||||
if (rightValue == "snapshot")
|
||||
rightValue = (-3).ToString();
|
||||
if (rightValue == "experimental")
|
||||
rightValue = (-4).ToString();
|
||||
var rightValValue = ModBase.Val(rightValue);
|
||||
if (leftValValue == 0d && rightValValue == 0d)
|
||||
{
|
||||
// 如果没有数值则直接比较字符串
|
||||
if (string.CompareOrdinal(leftValue, rightValue) > 0) return 1;
|
||||
|
||||
if (string.CompareOrdinal(leftValue, rightValue) < 0) return -1;
|
||||
}
|
||||
// 如果有数值则比较数值
|
||||
// 这会使得一边是数字一边是字母时数字方更大
|
||||
else if (leftValValue > rightValValue)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (leftValValue < rightValValue)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
NextEntry: ;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个版本名的排序器。
|
||||
/// </summary>
|
||||
public class VersionComparer : IComparer<string>
|
||||
{
|
||||
public int Compare(string x, string y)
|
||||
{
|
||||
return CompareVersion(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Nodes;
|
||||
using PCL;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
public static class ModAssets
|
||||
{
|
||||
// 获取索引
|
||||
/// <summary>
|
||||
/// 获取某实例资源文件索引的对应 Json 项,详见实例 Json 中的 assetIndex 项。失败会抛出异常。
|
||||
/// </summary>
|
||||
public static JsonNode McAssetsGetIndex(McInstance mcInstance, bool returnLegacyOnError = false,
|
||||
bool checkURLEmpty = false)
|
||||
{
|
||||
string assetsName;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var index = mcInstance.JsonObject["assetIndex"];
|
||||
if (index is not null && index["id"] is not null)
|
||||
return index;
|
||||
if (mcInstance.JsonObject["assets"] is not null)
|
||||
assetsName = mcInstance.JsonObject["assets"].ToString();
|
||||
if (checkURLEmpty && index["url"] is not null)
|
||||
return index;
|
||||
// 下一个实例
|
||||
if (string.IsNullOrEmpty(mcInstance.InheritInstanceName))
|
||||
break;
|
||||
mcInstance = new McInstance(Path.Combine(ModFolder.mcFolderSelected, "versions", mcInstance.InheritInstanceName));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// 无法获取到下载地址
|
||||
if (returnLegacyOnError)
|
||||
{
|
||||
// 返回 assets 文件名会由于没有下载地址导致全局失败
|
||||
// If AssetsName IsNot Nothing AndAlso AssetsName <> "legacy" Then
|
||||
// Log("[Minecraft] 无法获取资源文件索引下载地址,使用 assets 项提供的资源文件名:" & AssetsName)
|
||||
// Return GetJson("{""id"": """ & AssetsName & """}")
|
||||
// Else
|
||||
ModBase.Log("[Minecraft] 无法获取资源文件索引下载地址,使用默认的 legacy 下载地址");
|
||||
return (JsonNode)ModBase.GetJson(@"{
|
||||
""id"": ""legacy"",
|
||||
""sha1"": ""c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729"",
|
||||
""size"": 134284,
|
||||
""url"": ""https://launchermeta.mojang.com/mc-staging/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"",
|
||||
""totalSize"": 111220701
|
||||
}");
|
||||
}
|
||||
// End If
|
||||
|
||||
throw new Exception(Lang.Text("Minecraft.Error.NoAssetIndexInfo"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取某实例资源文件索引名,优先使用 assetIndex,其次使用 assets。失败会返回 legacy。
|
||||
/// </summary>
|
||||
public static string McAssetsGetIndexName(McInstance mcInstance)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (mcInstance.JsonObject["assetIndex"] is not null &&
|
||||
mcInstance.JsonObject["assetIndex"]["id"] is not null)
|
||||
return mcInstance.JsonObject["assetIndex"]["id"].ToString();
|
||||
if (mcInstance.JsonObject["assets"] is not null) return mcInstance.JsonObject["assets"].ToString();
|
||||
if (string.IsNullOrEmpty(mcInstance.InheritInstanceName))
|
||||
break;
|
||||
mcInstance = new McInstance(Path.Combine(ModFolder.mcFolderSelected, "versions", mcInstance.InheritInstanceName));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取资源文件索引名失败");
|
||||
}
|
||||
|
||||
return "legacy";
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
public struct McAssetsToken
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件的完整本地路径。
|
||||
/// </summary>
|
||||
public string localPath;
|
||||
|
||||
/// <summary>
|
||||
/// Json 中书写的源路径。例如 minecraft/sounds/mob/stray/death2.ogg 。
|
||||
/// </summary>
|
||||
public string sourcePath;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小。若无有效数据即为 0。
|
||||
/// </summary>
|
||||
public long size;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 Hash 校验码。
|
||||
/// </summary>
|
||||
public string hash;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ModBase.GetString(size) + " | " + localPath;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string McAssetsHashPrefix(string hash)
|
||||
{
|
||||
return hash[..2];
|
||||
}
|
||||
|
||||
internal static string McAssetsUrl(string hash)
|
||||
{
|
||||
return $"https://resources.download.minecraft.net/{McAssetsHashPrefix(hash)}/{hash}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Minecraft 的资源文件列表。失败会抛出异常。
|
||||
/// </summary>
|
||||
internal static List<McAssetsToken> McAssetsListGet(McInstance mcInstance)
|
||||
{
|
||||
var indexName = McAssetsGetIndexName(mcInstance);
|
||||
try
|
||||
{
|
||||
// 初始化
|
||||
if (!File.Exists($@"{ModFolder.mcFolderSelected}assets\indexes\{indexName}.json"))
|
||||
throw new FileNotFoundException(Lang.Text("Minecraft.Error.AssetIndexNotFound"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "assets", "indexes", indexName + ".json"));
|
||||
var result = new List<McAssetsToken>();
|
||||
var json = (JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile($@"{ModFolder.mcFolderSelected}assets\indexes\{indexName}.json"));
|
||||
|
||||
// 读取列表
|
||||
foreach (var file in json["objects"].AsObject())
|
||||
{
|
||||
string localPath;
|
||||
var hash = file.Value["hash"].ToString();
|
||||
if (json["map_to_resources"] is not null && json["map_to_resources"].ToObject<bool>())
|
||||
// Remap
|
||||
localPath = Path.Combine(mcInstance.PathIndie, "resources", file.Key.Replace("/", @"\"));
|
||||
else if (json["virtual"] is not null && json["virtual"].ToObject<bool>())
|
||||
// Virtual
|
||||
localPath = Path.Combine(ModFolder.mcFolderSelected, "assets", "virtual", "legacy", file.Key.Replace("/", @"\"));
|
||||
else
|
||||
{
|
||||
// 正常
|
||||
localPath = Path.Combine(ModFolder.mcFolderSelected, "assets", "objects", McAssetsHashPrefix(hash), hash);
|
||||
}
|
||||
result.Add(new McAssetsToken
|
||||
{
|
||||
localPath = localPath,
|
||||
sourcePath = file.Key,
|
||||
hash = hash,
|
||||
size = long.Parse(file.Value["size"].ToString())
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取资源文件列表失败:" + indexName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缺失列表
|
||||
/// <summary>
|
||||
/// 获取实例缺失的资源文件所对应的 NetTaskFile。
|
||||
/// </summary>
|
||||
public static List<DownloadFile> McAssetsFixList(McInstance mcInstance, bool checkHash,
|
||||
[Optional] ref ModLoader.LoaderBase progressFeed)
|
||||
{
|
||||
// 如果需要检查 Hash,则留到下载时处理,以借助多线程加快检查速度
|
||||
if (checkHash)
|
||||
return McAssetsListGet(mcInstance).Select(token =>
|
||||
{
|
||||
var hash = token.hash;
|
||||
return new DownloadFile(
|
||||
ModDownload.DlSourceAssetsGet(McAssetsUrl(hash)),
|
||||
token.localPath,
|
||||
new ModBase.FileChecker(actualSize: token.size == 0L ? -1 : token.size, hash: hash));
|
||||
}).ToList();
|
||||
// 如果不检查 Hash,则立即处理
|
||||
var result = new List<DownloadFile>();
|
||||
|
||||
List<McAssetsToken> assetsList;
|
||||
try
|
||||
{
|
||||
assetsList = McAssetsListGet(mcInstance);
|
||||
McAssetsToken token;
|
||||
if (progressFeed is not null)
|
||||
progressFeed.Progress = 0.04d;
|
||||
for (int i = 0, loopTo = assetsList.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
// 初始化
|
||||
token = assetsList[i];
|
||||
if (progressFeed is not null)
|
||||
progressFeed.Progress = 0.05d + 0.94d * i / assetsList.Count;
|
||||
// 检查文件是否存在
|
||||
var file = new FileInfo(token.localPath);
|
||||
if (file.Exists && (token.size == 0L || token.size == file.Length))
|
||||
continue;
|
||||
// 文件不存在,添加下载
|
||||
var hash = token.hash;
|
||||
result.Add(new DownloadFile(
|
||||
ModDownload.DlSourceAssetsGet(McAssetsUrl(hash)),
|
||||
token.localPath,
|
||||
new ModBase.FileChecker(actualSize: token.size == 0L ? -1 : token.size, hash: hash)));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取实例缺失的资源文件下载列表失败");
|
||||
}
|
||||
|
||||
if (progressFeed is not null)
|
||||
progressFeed.Progress = 0.99d;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3823 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using Dapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Hash;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
using ProtoBuf;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModComp
|
||||
{
|
||||
public enum CompLoaderType
|
||||
{
|
||||
// https://docs.curseforge.com/?http#tocS_ModLoaderType
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
Any = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
Forge = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
LiteLoader = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
Fabric = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
Quilt = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 模组加载器
|
||||
/// </summary>
|
||||
NeoForge = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 材质包
|
||||
/// </summary>
|
||||
Minecraft = 7,
|
||||
|
||||
/// <summary>
|
||||
/// 光影包
|
||||
/// </summary>
|
||||
Canvas = 8,
|
||||
|
||||
/// <summary>
|
||||
/// 光影包
|
||||
/// </summary>
|
||||
Iris = 9,
|
||||
|
||||
/// <summary>
|
||||
/// 光影包
|
||||
/// </summary>
|
||||
OptiFine = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 光影包
|
||||
/// </summary>
|
||||
Vanilla = 11,
|
||||
|
||||
/// <summary>
|
||||
/// LabyMod 客户端
|
||||
/// </summary>
|
||||
LabyMod = 12
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索结果排序方式
|
||||
/// </summary>
|
||||
public enum CompSortType
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
Default = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 相关性 (CurseForge Name (4) / Modrinth relevance)
|
||||
/// </summary>
|
||||
Relevance = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 下载量 (CurseForge TotalDownloads (6) / Modrinth downloads)
|
||||
/// </summary>
|
||||
Downloads = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 关注量 (CurseForge Popularity (2) / Modrinth follows)
|
||||
/// </summary>
|
||||
Follows = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 最新发布 (CurseForge ReleasedDate (11) / Modrinth newest)
|
||||
/// </summary>
|
||||
Newest = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 最近更新 (CurseForge LastUpdated (3) / Modrinth updated)
|
||||
/// </summary>
|
||||
Updated = 6
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CompSourceType
|
||||
{
|
||||
CurseForge = 1,
|
||||
Modrinth = 2,
|
||||
Any = CurseForge | Modrinth
|
||||
}
|
||||
|
||||
public enum CompType
|
||||
{
|
||||
/// <summary>
|
||||
/// 允许任意种类,或种类未知。
|
||||
/// </summary>
|
||||
Any = -1,
|
||||
|
||||
/// <summary>
|
||||
/// Mod。
|
||||
/// </summary>
|
||||
Mod = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 整合包。
|
||||
/// </summary>
|
||||
ModPack = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 资源包。
|
||||
/// </summary>
|
||||
ResourcePack = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 光影包。
|
||||
/// </summary>
|
||||
Shader = 3,
|
||||
|
||||
/// <summary>
|
||||
/// CurseForge:数据包。
|
||||
/// Modrinth:数据包,或数据包与 Mod 的混合。
|
||||
/// </summary>
|
||||
DataPack = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 服务端插件。
|
||||
/// </summary>
|
||||
Plugin = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 投影原理图。
|
||||
/// </summary>
|
||||
Schematic = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 世界。
|
||||
/// </summary>
|
||||
World = 7
|
||||
}
|
||||
|
||||
public enum CompDepsInstallTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// 无法解析依赖
|
||||
/// </summary>
|
||||
Unresolved = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 用户选择安装前置
|
||||
/// </summary>
|
||||
WithDeps = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 用户选择只安装本体,不安装前置
|
||||
/// </summary>
|
||||
WithoutDeps = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 用户取消安装
|
||||
/// </summary>
|
||||
Cancel = 3,
|
||||
}
|
||||
|
||||
public enum DownloadReason
|
||||
{
|
||||
Standalone,
|
||||
Dependency,
|
||||
ModPack,
|
||||
Update
|
||||
}
|
||||
|
||||
public static string GetCompTypeName(CompType type) => Lang.Text(type switch
|
||||
{
|
||||
CompType.Mod => "Download.Comp.Type.Mod",
|
||||
CompType.ModPack => "Download.Comp.Type.Modpack",
|
||||
CompType.ResourcePack => "Download.Comp.Type.ResourcePack",
|
||||
CompType.Shader => "Download.Comp.Type.Shader",
|
||||
CompType.DataPack => "Download.Comp.Type.DataPack",
|
||||
CompType.Plugin => "Download.Comp.Type.Plugin",
|
||||
CompType.World => "Download.Comp.Type.World",
|
||||
CompType.Schematic => "Download.Comp.Type.Schematic",
|
||||
_ => "Download.Comp.Type.Unknown"
|
||||
});
|
||||
|
||||
public static string GetCompLoadingName(CompType type) => Lang.Text(type switch
|
||||
{
|
||||
CompType.Mod => "Download.Comp.List.Loading.Mod",
|
||||
CompType.ModPack => "Download.Comp.List.Loading.Modpack",
|
||||
CompType.ResourcePack => "Download.Comp.List.Loading.ResourcePack",
|
||||
CompType.Shader => "Download.Comp.List.Loading.Shader",
|
||||
CompType.DataPack => "Download.Comp.List.Loading.DataPack",
|
||||
CompType.Plugin => "Download.Comp.List.Loading.Plugin",
|
||||
CompType.World => "Download.Comp.List.Loading.World",
|
||||
CompType.Schematic => "Download.Comp.List.Loading.Schematic",
|
||||
_ => "Download.Comp.List.Loading.Unknown"
|
||||
});
|
||||
|
||||
public static string GetCompSearchName(CompType type) => Lang.Text(type switch
|
||||
{
|
||||
CompType.Mod => "Download.Comp.List.Search.Mod",
|
||||
CompType.ModPack => "Download.Comp.List.Search.Modpack",
|
||||
CompType.ResourcePack => "Download.Comp.List.Search.ResourcePack",
|
||||
CompType.Shader => "Download.Comp.List.Search.Shader",
|
||||
CompType.DataPack => "Download.Comp.List.Search.DataPack",
|
||||
CompType.Plugin => "Download.Comp.List.Search.Plugin",
|
||||
CompType.World => "Download.Comp.List.Search.World",
|
||||
CompType.Schematic => "Download.Comp.List.Search.Schematic",
|
||||
_ => "Download.Comp.List.Search.Unknown"
|
||||
});
|
||||
|
||||
#region CompFavorites | 收藏
|
||||
|
||||
public class CompFavorites
|
||||
{
|
||||
private static List<FavData> _FavoritesList;
|
||||
|
||||
/// <summary>
|
||||
/// 收藏的工程列表
|
||||
/// </summary>
|
||||
public static List<FavData> FavoritesList
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_FavoritesList is null)
|
||||
{
|
||||
var rawData = States.Game.CompFavorites;
|
||||
List<FavData> rawList = null;
|
||||
// 尝试作为新格式解析
|
||||
try
|
||||
{
|
||||
rawList = JsonSerializer.Deserialize<List<FavData>>(rawData, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch (Exception ex1)
|
||||
{
|
||||
// 尝试作为旧格式(HashSet)迁移
|
||||
try
|
||||
{
|
||||
var migrate = JsonSerializer.Deserialize<HashSet<string>>(rawData, JsonCompat.SerializerOptions);
|
||||
if (migrate is not null) rawList = new List<FavData> { GetNewFav(Lang.Text("Download.Comp.Detail.Favorites.DefaultName"), migrate) };
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
// 两种都失败,使用默认
|
||||
}
|
||||
}
|
||||
|
||||
// 最终兜底:确保至少有一个收藏夹
|
||||
if (rawList is null || rawList.Count == 0) rawList = new List<FavData> { GetNewFav(Lang.Text("Download.Comp.Detail.Favorites.DefaultName"), null) };
|
||||
_FavoritesList = rawList;
|
||||
Save();
|
||||
}
|
||||
|
||||
return _FavoritesList;
|
||||
}
|
||||
set
|
||||
{
|
||||
_FavoritesList = value;
|
||||
foreach (var item in _FavoritesList)
|
||||
item.Notes = item.Notes.Where(n => !string.IsNullOrWhiteSpace(n.Value)).ToDictionary();
|
||||
var rawList = JsonSerializer.Serialize(_FavoritesList, JsonCompat.SerializerOptions);
|
||||
States.Game.CompFavorites = JsonSerializer.Serialize(_FavoritesList, JsonCompat.SerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetShareCode(HashSet<string> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(data, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[CompFavorites] 生成分享出错");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static HashSet<string> GetIdsByShareCode(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<HashSet<string>>(code, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[CompFavorites] 通过分享获取 ID 出错");
|
||||
}
|
||||
|
||||
return new HashSet<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示收藏菜单。
|
||||
/// </summary>
|
||||
/// <param name="project"></param>
|
||||
/// <param name="pos"></param>
|
||||
public static void ShowMenu(CompProject project, UIElement pos, Action closedCallBack = null)
|
||||
{
|
||||
var body = new ContextMenu();
|
||||
foreach (var i in FavoritesList)
|
||||
{
|
||||
var item = new MyMenuItem();
|
||||
item.MaxWidth = 240d;
|
||||
var hasFavs = i.Favs.Contains(project.Id);
|
||||
if (hasFavs)
|
||||
{
|
||||
item.Header = Lang.Text("Download.Comp.Detail.Favorites.UnfavoriteContextMenu", i.Name);
|
||||
item.SvgIcon = "lucide/heart-filled";
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Header = Lang.Text("Download.Comp.Detail.Favorites.FavoriteContextMenu", i.Name);
|
||||
item.SvgIcon = "lucide/heart";
|
||||
}
|
||||
|
||||
item.Click += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (hasFavs)
|
||||
{
|
||||
i.Favs.Remove(project.Id);
|
||||
HintService.Hint(Lang.Text("Download.Comp.Detail.Favorites.Remove", project.TranslatedName, i.Name), HintType.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
i.Favs.Add(project.Id);
|
||||
HintService.Hint(Lang.Text("Download.Comp.Detail.Favorites.Add", project.TranslatedName, i.Name), HintType.Success);
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[CompFavorites] 改变收藏项出错");
|
||||
}
|
||||
};
|
||||
body.Items.Add(item);
|
||||
}
|
||||
|
||||
body.Closed += (_, _) => closedCallBack?.Invoke();
|
||||
body.Placement = PlacementMode.Bottom;
|
||||
body.PlacementTarget = pos;
|
||||
body.IsOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示收藏菜单。
|
||||
/// </summary>
|
||||
public static void ShowMenu(List<CompProject> project, UIElement pos, Action closedCallBack = null)
|
||||
{
|
||||
var body = new ContextMenu();
|
||||
foreach (var i in FavoritesList)
|
||||
{
|
||||
var item = new MyMenuItem
|
||||
{
|
||||
MaxWidth = 240d,
|
||||
Header = Lang.Text("Download.Comp.Detail.Favorites.FavoriteContextMenu", i.Name),
|
||||
SvgIcon = "lucide/heart"
|
||||
};
|
||||
item.Click += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = i.Favs.Count;
|
||||
project.Select(p => p.Id).ToList().ForEach(x => i.Favs.Add(x));
|
||||
Save();
|
||||
var successCount = i.Favs.Count - count;
|
||||
var failedCount = project.Count - successCount;
|
||||
HintService.Hint(
|
||||
Lang.Text(failedCount > 0
|
||||
? "Download.Comp.Detail.Favorites.BulkAddWithFailures"
|
||||
: "Download.Comp.Detail.Favorites.BulkAdd", successCount, i.Name, failedCount),
|
||||
HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[CompFavorites] 改变收藏项出错");
|
||||
}
|
||||
};
|
||||
body.Items.Add(item);
|
||||
}
|
||||
|
||||
body.Closed += (_, _) => closedCallBack?.Invoke();
|
||||
body.Placement = PlacementMode.Bottom;
|
||||
body.PlacementTarget = pos;
|
||||
body.IsOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存收藏夹数据
|
||||
/// </summary>
|
||||
public static void Save()
|
||||
{
|
||||
FavoritesList = _FavoritesList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个新的收藏夹
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="favList">没有传 Nothing</param>
|
||||
/// <returns></returns>
|
||||
public static FavData GetNewFav(string name, HashSet<string> favList)
|
||||
{
|
||||
var res = new FavData { Name = name, Id = Guid.NewGuid().ToString() };
|
||||
if (favList is null)
|
||||
res.Favs = new HashSet<string>();
|
||||
else
|
||||
res.Favs = favList;
|
||||
return res;
|
||||
}
|
||||
|
||||
public static bool IsFavourite(string id)
|
||||
{
|
||||
if (FavoritesList is null)
|
||||
return false;
|
||||
foreach (var i in FavoritesList)
|
||||
if (i.Favs.Contains(id))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public class FavData
|
||||
{
|
||||
/// <summary>
|
||||
/// 收藏夹名称
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JsonPropertyName("Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Guid
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JsonPropertyName("Id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收藏的工程 ID 列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JsonPropertyName("Favs")]
|
||||
public HashSet<string> Favs { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[JsonPropertyName("Notes")]
|
||||
public Dictionary<string, string> Notes { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompProject | 项目信息
|
||||
|
||||
public class CompRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 通过项目 Id 判断是否来自 CurseForge
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsFromCurseForge(string id)
|
||||
{
|
||||
var res = 0;
|
||||
return int.TryParse(id, out res); // CurseForge 数字 ID Modrinth 乱序 ID
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过一堆 ID 从 Modrinth 那获取项目信息
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<List<CompProject>> GetListByIdsFromModrinthAsync(List<string> ids)
|
||||
{
|
||||
var res = new List<CompProject>();
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var rawProjectsData =
|
||||
ModDownload.DlModRequest<JsonArray>($"https://api.modrinth.com/v2/projects?ids=[\"{ids.Join("\",\"")}\"]");
|
||||
foreach (var rawData in (IEnumerable)rawProjectsData)
|
||||
res.Add(new CompProject((JsonObject)rawData));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "从 Modrinth 获取数据失败");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过一堆 ID 从 CurseForge 那获取项目信息
|
||||
/// </summary>
|
||||
/// <param name="Ids"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<List<CompProject>> GetListByIdsFromCurseforgeAsync(List<string> ids)
|
||||
{
|
||||
var res = new List<CompProject>();
|
||||
try
|
||||
{
|
||||
// 使用 Task.Run 将同步的 DlModRequest 包装为异步
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// 构建请求 Body,建议使用 string.Join
|
||||
var jsonBody = "{\"modIds\": [" + string.Join(",", ids) + "]}";
|
||||
|
||||
// DlModRequest 返回 object,先强转 JsonObject,再获取 "data" 并强转为 JsonArray
|
||||
var response = ModDownload.DlModRequest<JsonObject>(
|
||||
"https://api.curseforge.com/v1/mods",
|
||||
"POST",
|
||||
jsonBody,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
var rawProjectsData = (JsonArray)response["data"];
|
||||
|
||||
// 2. 使用 LINQ 快速转换并填充列表
|
||||
if (rawProjectsData is not null)
|
||||
{
|
||||
var projectList = rawProjectsData
|
||||
.Cast<JsonObject>()
|
||||
.Select(data => new CompProject(data))
|
||||
.ToList();
|
||||
|
||||
res.AddRange(projectList);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "Failed to get project data from CurseForge");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public static List<CompProject> GetCompProjectsByIds(List<string> input)
|
||||
{
|
||||
return GetCompProjectsByIdsAsync(input).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static async Task<List<CompProject>> GetCompProjectsByIdsAsync(List<string> input)
|
||||
{
|
||||
if (input?.Any() == false)
|
||||
return new List<CompProject>();
|
||||
|
||||
var modrinthIds = new List<string>();
|
||||
var curseForgeIds = new List<string>();
|
||||
foreach (var id in input)
|
||||
if (IsFromCurseForge(id))
|
||||
curseForgeIds.Add(id);
|
||||
else
|
||||
modrinthIds.Add(id);
|
||||
|
||||
var tasks = new List<Task<List<CompProject>>>();
|
||||
if (curseForgeIds.Any()) tasks.Add(GetListByIdsFromCurseforgeAsync(curseForgeIds));
|
||||
if (modrinthIds.Any()) tasks.Add(GetListByIdsFromModrinthAsync(modrinthIds));
|
||||
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
var result = new List<CompProject>();
|
||||
foreach (var task in tasks)
|
||||
result.AddRange(task.Result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompClipboard | 剪贴板识别
|
||||
|
||||
public class CompClipboard
|
||||
{
|
||||
// 剪贴板已读取内容
|
||||
public static string? currentText;
|
||||
|
||||
// 识别剪贴板内容
|
||||
public static void GetClipboardResource()
|
||||
{
|
||||
string? text = null;
|
||||
ModBase.RunInUiWait(() => text = Clipboard.GetText());
|
||||
|
||||
if (string.IsNullOrEmpty(text) || text == currentText) return;
|
||||
currentText = text;
|
||||
|
||||
// 在新线程中处理网络请求
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var projectId = ResolveLinkToProjectId(text);
|
||||
|
||||
if (string.IsNullOrEmpty(projectId)) return;
|
||||
ModBase.Log($"[Clipboard] Found ProjectId: {projectId}");
|
||||
|
||||
// 3. UI 交互:跳转到详情页
|
||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||||
{
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Download.Comp.Detail.Clipboard.Detected.Message"),
|
||||
Lang.Text("Download.Comp.Detail.Clipboard.Detected.Title"),
|
||||
Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel"),
|
||||
forceWait: true) == 1)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.Detail.Clipboard.Fetching"));
|
||||
|
||||
var ids = new List<string> { projectId };
|
||||
var compProjects = await CompRequest.GetCompProjectsByIdsAsync(ids);
|
||||
|
||||
if (compProjects.Count == 0)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.Detail.Clipboard.InvalidContent"),
|
||||
HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModMain.frmMain.PageChange(new FormMain.PageStackData
|
||||
{
|
||||
page = FormMain.PageType.CompDetail,
|
||||
additional = (compProjects.First(), new List<string>(), string.Empty, CompLoaderType.Any,
|
||||
CompType.Any, null)
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "Error processing clipboard resource");
|
||||
}
|
||||
}, "Clipboard Resource Processing");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompDatabase | Mod 数据库
|
||||
|
||||
private static readonly Lazy<string> _dbInitializer = new(InitializeModDbAndGetConnectionString);
|
||||
|
||||
private static string CompDBConnectionString => _dbInitializer.Value;
|
||||
|
||||
private static string InitializeModDbAndGetConnectionString()
|
||||
{
|
||||
ModBase.Log("[DB] 解压 ModData (SQLite) 中");
|
||||
using (var compressedDbData = ModBase.GetResourceStream("Resources/mcmod.buf"))
|
||||
{
|
||||
using (var trueDbFile = new GZipStream(compressedDbData, CompressionMode.Decompress))
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
// 这里提取文件资源
|
||||
trueDbFile.CopyTo(ms);
|
||||
ms.Seek(0L, SeekOrigin.Begin);
|
||||
var fileHash = ModBase.GetHexString(SHA1Provider.Instance.ComputeHash(ms));
|
||||
var dbDir = Path.Combine(ModBase.pathTemp, "Cache");
|
||||
var dbPath = Path.Combine(dbDir, $"ModData{fileHash}.sqlite");
|
||||
|
||||
if (File.Exists(dbPath) && !IsDatabaseValid(dbPath))
|
||||
{
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(dbPath))
|
||||
{
|
||||
ms.Seek(0L, SeekOrigin.Begin);
|
||||
var entries = Serializer.Deserialize<List<CompDatabaseEntry>>(ms);
|
||||
|
||||
Directory.CreateDirectory(dbDir);
|
||||
|
||||
var tempPath = dbPath + ".tmp";
|
||||
if (File.Exists(tempPath)) File.Delete(tempPath);
|
||||
|
||||
using (var buildDbConnection = new SqliteConnection($"Data Source=\"{tempPath}\";Pooling=False"))
|
||||
{
|
||||
buildDbConnection.Open();
|
||||
|
||||
// 不用事务的话构建会非常慢
|
||||
using (var transaction = buildDbConnection.BeginTransaction())
|
||||
{
|
||||
buildDbConnection.Execute(@"
|
||||
CREATE TABLE ModTranslation (
|
||||
WikiId INTEGER,
|
||||
ChineseName TEXT,
|
||||
CurseForgeSlug TEXT,
|
||||
ModrinthSlug TEXT
|
||||
);
|
||||
CREATE INDEX idx_curseforge ON ModTranslation (CurseForgeSlug);
|
||||
CREATE INDEX idx_modrinth ON ModTranslation (ModrinthSlug);
|
||||
CREATE INDEX idx_chinesename ON ModTranslation (ChineseName);
|
||||
");
|
||||
|
||||
var insertSql =
|
||||
@"INSERT INTO ModTranslation (WikiId, ChineseName, CurseForgeSlug, ModrinthSlug)
|
||||
VALUES (@WikiId, @ChineseName, @CurseForgeSlug, @ModrinthSlug)";
|
||||
|
||||
foreach (var entry in entries)
|
||||
buildDbConnection.Execute(insertSql, entry, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完成的文件移入缓存位
|
||||
File.Move(tempPath, dbPath, true);
|
||||
}
|
||||
|
||||
return $"Data Source=\"{dbPath}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 SQLite 数据库文件是否包含预期的表且非空
|
||||
/// </summary>
|
||||
private static bool IsDatabaseValid(string dbPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var conn = new SqliteConnection($"Data Source=\"{dbPath}\";Pooling=False;Mode=ReadOnly"))
|
||||
{
|
||||
conn.Open();
|
||||
// 检查表是否存在
|
||||
var tableCheck = conn.ExecuteScalar<int>(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ModTranslation'");
|
||||
if (tableCheck == 0) return false;
|
||||
// 检查表中是否有数据
|
||||
var rowCount = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM ModTranslation");
|
||||
return rowCount > 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "检查模组翻译数据库有效性失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static SqliteConnection CompDB
|
||||
{
|
||||
get
|
||||
{
|
||||
var conn = new SqliteConnection(CompDBConnectionString);
|
||||
conn.Open();
|
||||
return conn;
|
||||
}
|
||||
}
|
||||
|
||||
private static CompDatabaseEntry GetCompWikiEntryBySlug(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var conn = CompDB)
|
||||
{
|
||||
return conn.QueryFirstOrDefault<CompDatabaseEntry>(
|
||||
"SELECT * FROM ModTranslation WHERE CurseForgeSlug = @s OR ModrinthSlug = @s LIMIT 1",
|
||||
new { s = slug });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"获取模组翻译信息失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Comp.Error.OperationFailed"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
private class CompDatabaseEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// McMod 的对应 ID。
|
||||
/// </summary>
|
||||
[ProtoMember(1)]
|
||||
public int WikiId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 中文译名。空字符串代表没有翻译。
|
||||
/// </summary>
|
||||
[ProtoMember(2)]
|
||||
public string ChineseName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// CurseForge Slug(例如 advanced-solar-panels)。
|
||||
/// </summary>
|
||||
[ProtoMember(3)]
|
||||
public string CurseForgeSlug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Modrinth Slug(例如 advanced-solar-panels)。
|
||||
/// </summary>
|
||||
[ProtoMember(4)]
|
||||
public string ModrinthSlug { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (CurseForgeSlug ?? "") + "&" + (ModrinthSlug ?? "") + "|" + WikiId + "|" + ChineseName;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompProject | 工程信息
|
||||
|
||||
// 类定义
|
||||
|
||||
public class CompProject
|
||||
{
|
||||
/// <summary>
|
||||
/// CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
|
||||
/// </summary>
|
||||
public readonly List<int> CurseForgeFileIds;
|
||||
|
||||
/// <summary>
|
||||
/// 英文描述。
|
||||
/// </summary>
|
||||
public readonly string Description;
|
||||
|
||||
/// <summary>
|
||||
/// 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
|
||||
/// </summary>
|
||||
public readonly int DownloadCount;
|
||||
|
||||
/// <summary>
|
||||
/// 支持的 Drop 编号,从高到低排序,不为 Nothing。
|
||||
/// 例如:261(26.1.x)、180(1.18.x)。
|
||||
/// </summary>
|
||||
public readonly List<int> Drops;
|
||||
|
||||
// 源信息
|
||||
|
||||
/// <summary>
|
||||
/// 该工程信息来自 CurseForge 还是 Modrinth。
|
||||
/// </summary>
|
||||
public readonly bool FromCurseForge;
|
||||
|
||||
/// <summary>
|
||||
/// CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
|
||||
/// </summary>
|
||||
public readonly string Id;
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次更新的时间。可能为 Nothing。
|
||||
/// </summary>
|
||||
public readonly DateTime? LastUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// 支持的 Mod 加载器列表。可能为空。
|
||||
/// </summary>
|
||||
public readonly List<CompLoaderType> ModLoaders;
|
||||
|
||||
// 描述性信息
|
||||
|
||||
/// <summary>
|
||||
/// 原始的英文名称。
|
||||
/// </summary>
|
||||
public readonly string RawName;
|
||||
|
||||
/// <summary>
|
||||
/// 工程的短名。例如 technical-enchant。
|
||||
/// </summary>
|
||||
public readonly string Slug;
|
||||
|
||||
/// <summary>
|
||||
/// 描述性标签的内容。已转换为中文。
|
||||
/// </summary>
|
||||
public readonly List<string> Tags;
|
||||
|
||||
/// <summary>
|
||||
/// 工程的种类。
|
||||
/// 由于 Modrinth 混合使用 Mod 和数据包,结果不一定准确。
|
||||
/// </summary>
|
||||
public readonly CompType Type;
|
||||
|
||||
/// <summary>
|
||||
/// 来源网站的工程页面网址。确保格式一定标准。
|
||||
/// CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
|
||||
/// Modrinth:https://modrinth.com/mod/technical-enchant
|
||||
/// </summary>
|
||||
public readonly string Website;
|
||||
|
||||
private CompDatabaseEntry _DatabaseEntry;
|
||||
|
||||
// 数据库信息
|
||||
|
||||
private bool loadedDatabase;
|
||||
|
||||
/// <summary>
|
||||
/// Logo 图片的下载地址。
|
||||
/// 若为 Nothing 则没有,保证不为空字符串。
|
||||
/// </summary>
|
||||
public string LogoUrl;
|
||||
|
||||
// 实例化
|
||||
|
||||
/// <summary>
|
||||
/// 从工程 Json 中初始化实例。若出错会抛出异常。
|
||||
/// </summary>
|
||||
public CompProject(JsonObject data)
|
||||
{
|
||||
var result = data.ContainsKey("Tags")
|
||||
? _BuildFromCompJson(data)
|
||||
: data.ContainsKey("summary")
|
||||
? _BuildFromCurseForge(data)
|
||||
: _BuildFromModrinth(data);
|
||||
|
||||
if (!data.ContainsKey("Tags"))
|
||||
{
|
||||
if (result.Tags.Count == 0)
|
||||
result.Tags.Add(Lang.Text("Download.Comp.Category.Other"));
|
||||
|
||||
result.Tags = result.Tags.Distinct().ToList();
|
||||
result.Tags.Sort();
|
||||
|
||||
result.ModLoaders = result.ModLoaders
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
FromCurseForge = result.FromCurseForge;
|
||||
Type = result.Type;
|
||||
Slug = result.Slug;
|
||||
Id = result.Id;
|
||||
CurseForgeFileIds = result.CurseForgeFileIds;
|
||||
RawName = result.RawName;
|
||||
Description = result.Description;
|
||||
Website = result.Website;
|
||||
LastUpdate = result.LastUpdate;
|
||||
DownloadCount = result.DownloadCount;
|
||||
ModLoaders = result.ModLoaders;
|
||||
Tags = result.Tags;
|
||||
LogoUrl = result.LogoUrl;
|
||||
Drops = result.Drops;
|
||||
|
||||
// 保存缓存
|
||||
compProjectCache[Id] = this;
|
||||
}
|
||||
|
||||
private sealed class CompProjectBuildResult
|
||||
{
|
||||
public bool FromCurseForge;
|
||||
public CompType Type;
|
||||
public string Slug;
|
||||
public string Id;
|
||||
public List<int> CurseForgeFileIds = [];
|
||||
public string RawName;
|
||||
public string Description;
|
||||
public string Website;
|
||||
public DateTime? LastUpdate;
|
||||
public int DownloadCount;
|
||||
public List<CompLoaderType> ModLoaders = [];
|
||||
public List<string> Tags = [];
|
||||
public string LogoUrl;
|
||||
public List<int> Drops = [];
|
||||
}
|
||||
|
||||
private static CompProjectBuildResult _BuildFromCompJson(JsonObject data)
|
||||
{
|
||||
var result = new CompProjectBuildResult
|
||||
{
|
||||
FromCurseForge = (string)data["DataSource"] == "CurseForge",
|
||||
Type = (CompType)data["Type"].ToObject<int>(),
|
||||
Slug = (string)data["Slug"],
|
||||
Id = (string)data["Id"],
|
||||
RawName = (string)data["RawName"],
|
||||
Description = (string)data["Description"],
|
||||
Website = (string)data["Website"],
|
||||
DownloadCount = (int)data["DownloadCount"],
|
||||
Tags = ((JsonArray)data["Tags"]).Select(t => t.ToString()).ToList()
|
||||
};
|
||||
|
||||
if (data.TryGetPropertyValue("CurseForgeFileIds", out var id))
|
||||
result.CurseForgeFileIds = ((JsonArray)id).Select(t => t.ToObject<int>()).ToList();
|
||||
|
||||
if (data.TryGetPropertyValue("LastUpdate", out var last))
|
||||
result.LastUpdate = last?.ToObject<DateTime>();
|
||||
|
||||
if (data.TryGetPropertyValue("ModLoaders", out var loaders))
|
||||
result.ModLoaders = ((JsonArray)loaders).Select(t => (CompLoaderType)t.ToObject<int>())
|
||||
.ToList();
|
||||
|
||||
if (data.TryGetPropertyValue("LogoUrl", out var url))
|
||||
result.LogoUrl = (string)url;
|
||||
|
||||
if (data.TryGetPropertyValue("Drops", out var drops))
|
||||
result.Drops = ((JsonArray)drops).Select(t => t.ToObject<int>()).ToList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CompProjectBuildResult _BuildFromCurseForge(JsonObject data)
|
||||
{
|
||||
var result = new CompProjectBuildResult
|
||||
{
|
||||
FromCurseForge = true,
|
||||
|
||||
// 简单信息
|
||||
Id = data["id"].ToString(),
|
||||
Slug = (string)data["slug"],
|
||||
RawName = (string)data["name"],
|
||||
Description = (string)data["summary"],
|
||||
Website = (data["links"]?["websiteUrl"]?.ToString() ?? "").TrimEnd('/'),
|
||||
LastUpdate = data["dateReleased"]?.ToObject<DateTime>(), // #1194
|
||||
DownloadCount = (int)data["downloadCount"]
|
||||
};
|
||||
|
||||
if (data["logo"] is JsonObject { Count: > 0 } logo)
|
||||
result.LogoUrl = string.IsNullOrEmpty((string)logo["thumbnailUrl"])
|
||||
? (string)logo["url"]
|
||||
: (string)logo["thumbnailUrl"];
|
||||
|
||||
if (string.IsNullOrEmpty(result.LogoUrl))
|
||||
result.LogoUrl = null;
|
||||
|
||||
// Type
|
||||
result.Type = _GetCurseForgeTypeByWebsite(result.Website);
|
||||
|
||||
// FileIndexes / VanillaMajorVersions / ModLoaders
|
||||
var files = new List<KeyValuePair<int, List<string>>>(); // FileId, GameVersions
|
||||
|
||||
foreach (var file in (data["latestFiles"] as JsonArray) ?? [])
|
||||
{
|
||||
var newFile = new CompFile((JsonObject)file, result.Type);
|
||||
if (!newFile.Available)
|
||||
continue;
|
||||
|
||||
result.ModLoaders.AddRange(newFile.ModLoaders);
|
||||
|
||||
var gameVersions = file["gameVersions"]?.ToObject<List<string>>() ?? [];
|
||||
if (!gameVersions.Any(McInstanceInfo.IsFormatFit))
|
||||
continue;
|
||||
|
||||
files.Add(new KeyValuePair<int, List<string>>((int)file["id"], gameVersions));
|
||||
}
|
||||
|
||||
files.AddRange(
|
||||
from File in (data["latestFilesIndexes"] as JsonArray) ?? []
|
||||
let GameVersion = File["gameVersion"]?.ToString() ?? ""
|
||||
where McInstanceInfo.IsFormatFit(GameVersion)
|
||||
select new KeyValuePair<int, List<string>>((int)File["fileId"], new[] { GameVersion }.ToList())
|
||||
);
|
||||
|
||||
result.CurseForgeFileIds = files
|
||||
.Select(f => f.Key)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
result.Drops = files
|
||||
.SelectMany(f => f.Value)
|
||||
.Select(v => McInstanceInfo.VersionToDrop(v))
|
||||
.Where(v => v > 0)
|
||||
.Distinct()
|
||||
.OrderByDescending(v => v)
|
||||
.ToList();
|
||||
|
||||
result.ModLoaders = result.ModLoaders
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
|
||||
// Tags
|
||||
var categories = ((data["categories"] as JsonArray) ?? [])
|
||||
.Select(t => t["id"]?.ToObject<int?>())
|
||||
.Where(t => t.HasValue)
|
||||
.Select(t => t.Value)
|
||||
.Distinct()
|
||||
.OrderByDescending(t => t);
|
||||
|
||||
foreach (var category in categories)
|
||||
if (curseForgeCategoryLangKeys.TryGetValue(category, out var langKey))
|
||||
_AddTag(result.Tags, langKey);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CompProjectBuildResult _BuildFromModrinth(JsonObject data)
|
||||
{
|
||||
var projectType = data["project_type"]?.ToString() ?? "";
|
||||
var slug = (string)data["slug"];
|
||||
|
||||
var result = new CompProjectBuildResult
|
||||
{
|
||||
FromCurseForge = false,
|
||||
|
||||
// 简单信息
|
||||
Id = (string)(data["project_id"] ?? data["id"]), // 两个 API 会返回的 key 不一样
|
||||
Slug = slug,
|
||||
RawName = (string)data["title"],
|
||||
Description = (string)data["description"],
|
||||
LastUpdate = data["date_modified"]?.ToObject<DateTime>(),
|
||||
DownloadCount = (int)data["downloads"],
|
||||
LogoUrl = (string)data["icon_url"],
|
||||
Website = $"https://modrinth.com/{projectType}/{slug}",
|
||||
|
||||
// Type
|
||||
Type = projectType switch
|
||||
{
|
||||
"modpack" => CompType.ModPack,
|
||||
"resourcepack" => CompType.ResourcePack,
|
||||
"shader" => CompType.Shader,
|
||||
_ => CompType.Mod // Modrinth 将数据包标为 Mod
|
||||
}
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(result.LogoUrl))
|
||||
result.LogoUrl = null;
|
||||
|
||||
// GameVersions
|
||||
// 搜索结果的键为 versions,获取特定工程的键为 game_versions
|
||||
result.Drops = ((data["game_versions"] ?? data["versions"]) as JsonArray ?? [])
|
||||
.Select(v => McInstanceInfo.VersionToDrop((string)v))
|
||||
.Where(v => v > 0)
|
||||
.Distinct()
|
||||
.OrderByDescending(v => v)
|
||||
.ToList();
|
||||
|
||||
// Tags & ModLoaders
|
||||
foreach (var category in (data["loaders"] as JsonArray)?.Select(t => t.ToString()) ?? [])
|
||||
if (modrinthLoaderTypes.TryGetValue(category ?? "", out var loader))
|
||||
result.ModLoaders.Add(loader);
|
||||
|
||||
foreach (var category in (data["categories"] as JsonArray)?.Select(t => t.ToString()) ?? [])
|
||||
{
|
||||
if (string.IsNullOrEmpty(category)) continue;
|
||||
// 加载器
|
||||
if (modrinthLoaderTypes.TryGetValue(category, out var loader))
|
||||
{
|
||||
result.ModLoaders.Add(loader);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Modrinth 将数据包标为 Mod,若包含数据包版本,则优先标为 DataPack
|
||||
if (category.Equals("datapack", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Type = CompType.DataPack;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 这些分类在资源包中不显示
|
||||
if (resourcePackHiddenCategoryLangKeys.TryGetValue(category, out var hiddenLangKey))
|
||||
{
|
||||
if (result.Type != CompType.ResourcePack)
|
||||
_AddTag(result.Tags, hiddenLangKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (modrinthCategoryLangKeys.TryGetValue(category, out var langKey))
|
||||
_AddTag(result.Tags, langKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CompType _GetCurseForgeTypeByWebsite(string website)
|
||||
{
|
||||
var websiteLower = (website ?? "").ToLowerInvariant();
|
||||
|
||||
if (websiteLower.Contains("/mc-mods/") || websiteLower.Contains("/mod/"))
|
||||
return CompType.Mod;
|
||||
if (websiteLower.Contains("/modpacks/"))
|
||||
return CompType.ModPack;
|
||||
if (websiteLower.Contains("/resourcepacks/") || websiteLower.Contains("/texture-packs/"))
|
||||
return CompType.ResourcePack;
|
||||
if (websiteLower.Contains("/shaders/"))
|
||||
return CompType.Shader;
|
||||
if (websiteLower.Contains("/worlds/"))
|
||||
return CompType.World;
|
||||
|
||||
return CompType.DataPack;
|
||||
}
|
||||
|
||||
private static void _AddTag(List<string> tags, string langKey)
|
||||
{
|
||||
var tag = Lang.Text(langKey);
|
||||
|
||||
if (!tags.Contains(tag))
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, CompLoaderType> modrinthLoaderTypes =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["forge"] = CompLoaderType.Forge,
|
||||
["fabric"] = CompLoaderType.Fabric,
|
||||
["quilt"] = CompLoaderType.Quilt,
|
||||
["neoforge"] = CompLoaderType.NeoForge
|
||||
};
|
||||
|
||||
private static readonly Dictionary<int, string> curseForgeCategoryLangKeys =
|
||||
new()
|
||||
{
|
||||
// Mod
|
||||
[406] = "Download.Comp.Category.WorldGen",
|
||||
[407] = "Download.Comp.Category.Biomes",
|
||||
[410] = "Download.Comp.Category.Dimensions",
|
||||
[408] = "Download.Comp.Category.OresResources",
|
||||
[409] = "Download.Comp.Category.Structures",
|
||||
[412] = "Download.Comp.Category.Technology",
|
||||
[415] = "Download.Comp.Category.PipesLogistics",
|
||||
[4843] = "Download.Comp.Category.Automation",
|
||||
[417] = "Download.Comp.Category.Energy",
|
||||
[4558] = "Download.Comp.Category.Redstone",
|
||||
[436] = "Download.Comp.Category.FoodCooking",
|
||||
[416] = "Download.Comp.Category.Farming",
|
||||
[414] = "Download.Comp.Category.Transportation",
|
||||
[420] = "Download.Comp.Category.Storage",
|
||||
[419] = "Download.Comp.Category.Magic",
|
||||
[422] = "Download.Comp.Category.Adventure",
|
||||
[424] = "Download.Comp.Category.Decoration",
|
||||
[411] = "Download.Comp.Category.Mobs",
|
||||
[434] = "Download.Comp.Category.Equipment",
|
||||
[6814] = "Download.Comp.Category.Optimization",
|
||||
[9026] = "Download.Comp.Category.Creative",
|
||||
[423] = "Download.Comp.Category.Display",
|
||||
[435] = "Download.Comp.Category.Server",
|
||||
[5191] = "Download.Comp.Category.Tweaks",
|
||||
[421] = "Download.Comp.Category.Library",
|
||||
|
||||
// 整合包
|
||||
[4484] = "Download.Comp.Category.Multiplayer",
|
||||
[4479] = "Download.Comp.Category.Modpack.Hardcore",
|
||||
[4483] = "Download.Comp.Category.Combat",
|
||||
[4478] = "Download.Comp.Category.Modpack.Quests",
|
||||
[4472] = "Download.Comp.Category.Technology",
|
||||
[4473] = "Download.Comp.Category.Magic",
|
||||
[4475] = "Download.Comp.Category.Adventure",
|
||||
[4476] = "Download.Comp.Category.Modpack.Exploration",
|
||||
[4477] = "Download.Comp.Category.Modpack.MiniGame",
|
||||
[4471] = "Download.Comp.Category.Modpack.SciFi",
|
||||
[4736] = "Download.Comp.Category.Modpack.Skyblock",
|
||||
[5128] = "Download.Comp.Category.Modpack.VanillaPlus",
|
||||
[4487] = "Download.Comp.Category.Modpack.Ftb",
|
||||
[4480] = "Download.Comp.Category.Modpack.MapBased",
|
||||
[4481] = "Download.Comp.Category.Modpack.SmallLight",
|
||||
[4482] = "Download.Comp.Category.Modpack.ExtraLarge",
|
||||
|
||||
// 资源包
|
||||
[403] = "Download.Comp.Category.VanillaLike",
|
||||
[400] = "Download.Comp.Category.Realistic",
|
||||
[401] = "Download.Comp.Category.Modern",
|
||||
[402] = "Download.Comp.Category.Medieval",
|
||||
[399] = "Download.Comp.Category.Steampunk",
|
||||
[5244] = "Download.Comp.Category.Fonts",
|
||||
[404] = "Download.Comp.Category.Animated",
|
||||
[4465] = "Download.Comp.Category.ModSupport",
|
||||
[393] = "Download.Comp.Category.ResourcePack.Resolution16x",
|
||||
[394] = "Download.Comp.Category.ResourcePack.Resolution32x",
|
||||
[395] = "Download.Comp.Category.ResourcePack.Resolution64x",
|
||||
[396] = "Download.Comp.Category.ResourcePack.Resolution128x",
|
||||
[397] = "Download.Comp.Category.ResourcePack.Resolution256x",
|
||||
[398] = "Download.Comp.Category.ResourcePack.Resolution512xOrHigher",
|
||||
[5193] = "Download.Comp.Type.DataPack", // 有这个 Tag 的项会从资源包请求中被移除
|
||||
|
||||
// 光影包
|
||||
[6553] = "Download.Comp.Category.Realistic",
|
||||
[6554] = "Download.Comp.Category.Fantasy",
|
||||
[6555] = "Download.Comp.Category.VanillaLike",
|
||||
|
||||
// 数据包
|
||||
[6948] = "Download.Comp.Category.Adventure",
|
||||
[6949] = "Download.Comp.Category.DataPack.Fantasy",
|
||||
[6950] = "Download.Comp.Category.Library",
|
||||
[6952] = "Download.Comp.Category.Magic",
|
||||
[6946] = "Download.Comp.Category.Mod.ModRelated",
|
||||
[6951] = "Download.Comp.Category.Technology",
|
||||
[6953] = "Download.Comp.Category.Utility",
|
||||
|
||||
// 世界
|
||||
[248] = "Download.Comp.Category.Adventure",
|
||||
[249] = "Download.Comp.Category.World.Creative",
|
||||
[250] = "Download.Comp.Category.Modpack.MiniGame",
|
||||
[251] = "Download.Comp.Category.World.Parkour",
|
||||
[252] = "Download.Comp.Category.World.Puzzle",
|
||||
[253] = "Download.Comp.Category.World.Survival",
|
||||
[4464] = "Download.Comp.Category.World.ModWorld"
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> resourcePackHiddenCategoryLangKeys =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["decoration"] = "Download.Comp.Category.Decoration",
|
||||
["mobs"] = "Download.Comp.Category.Mobs",
|
||||
["equipment"] = "Download.Comp.Category.Equipment"
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> modrinthCategoryLangKeys =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 共用
|
||||
["technology"] = "Download.Comp.Category.Technology",
|
||||
["magic"] = "Download.Comp.Category.Magic",
|
||||
["adventure"] = "Download.Comp.Category.Adventure",
|
||||
["utility"] = "Download.Comp.Category.Utility",
|
||||
["optimization"] = "Download.Comp.Category.Optimization",
|
||||
["vanilla-like"] = "Download.Comp.Category.VanillaLike",
|
||||
["realistic"] = "Download.Comp.Category.Realistic",
|
||||
|
||||
// Mod / 数据包
|
||||
["worldgen"] = "Download.Comp.Category.WorldGen",
|
||||
["food"] = "Download.Comp.Category.FoodCooking",
|
||||
["game-mechanics"] = "Download.Comp.Category.GameMechanics",
|
||||
["transportation"] = "Download.Comp.Category.Transportation",
|
||||
["storage"] = "Download.Comp.Category.Storage",
|
||||
["social"] = "Download.Comp.Category.Server",
|
||||
["library"] = "Download.Comp.Category.Library",
|
||||
|
||||
// 整合包
|
||||
["multiplayer"] = "Download.Comp.Category.Multiplayer",
|
||||
["challenging"] = "Download.Comp.Category.Modpack.Hardcore",
|
||||
["combat"] = "Download.Comp.Category.Combat",
|
||||
["quests"] = "Download.Comp.Category.Modpack.Quests",
|
||||
["kitchen-sink"] = "Download.Comp.Category.Modpack.KitchenSink",
|
||||
["lightweight"] = "Download.Comp.Category.Modpack.SmallLight",
|
||||
|
||||
// 资源包
|
||||
["simplistic"] = "Download.Comp.Category.Simplistic",
|
||||
["tweaks"] = "Download.Comp.Category.Tweaks",
|
||||
["8x-"] = "Download.Comp.Category.ResourcePack.Resolution8xOrLower",
|
||||
["16x"] = "Download.Comp.Category.ResourcePack.Resolution16x",
|
||||
["32x"] = "Download.Comp.Category.ResourcePack.Resolution32x",
|
||||
["48x"] = "Download.Comp.Category.ResourcePack.Resolution48x",
|
||||
["64x"] = "Download.Comp.Category.ResourcePack.Resolution64x",
|
||||
["128x"] = "Download.Comp.Category.ResourcePack.Resolution128x",
|
||||
["256x"] = "Download.Comp.Category.ResourcePack.Resolution256x",
|
||||
["512x+"] = "Download.Comp.Category.ResourcePack.Resolution512xOrHigher",
|
||||
["audio"] = "Download.Comp.Category.Audio",
|
||||
["fonts"] = "Download.Comp.Category.Fonts",
|
||||
["models"] = "Download.Comp.Category.Models",
|
||||
["gui"] = "Download.Comp.Category.Gui",
|
||||
["locale"] = "Download.Comp.Category.Locale",
|
||||
["core-shaders"] = "Download.Comp.Category.CoreShaders",
|
||||
["modded"] = "Download.Comp.Category.ModSupport",
|
||||
|
||||
// 光影包
|
||||
["fantasy"] = "Download.Comp.Category.Fantasy",
|
||||
["semi-realistic"] = "Download.Comp.Category.SemiRealistic",
|
||||
["cartoon"] = "Download.Comp.Category.Cartoon",
|
||||
// 暂时不添加性能负荷 Tag:
|
||||
// potato / low / medium / high
|
||||
["colored-lighting"] = "Download.Comp.Category.ColoredLighting",
|
||||
["path-tracing"] = "Download.Comp.Category.PathTracing",
|
||||
["pbr"] = "Download.Comp.Category.Pbr",
|
||||
["reflections"] = "Download.Comp.Category.Reflections",
|
||||
["iris"] = "Download.Comp.Category.Iris",
|
||||
["optifine"] = "Download.Comp.Category.Optifine",
|
||||
["vanilla"] = "Download.Comp.Filter.Loader.VanillaAvailable"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 关联的数据库条目。若为 Nothing 则没有。
|
||||
/// </summary>
|
||||
private CompDatabaseEntry DatabaseEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!loadedDatabase)
|
||||
{
|
||||
loadedDatabase = true;
|
||||
if (Type == CompType.Mod || Type == CompType.DataPack)
|
||||
_DatabaseEntry = GetCompWikiEntryBySlug(Slug);
|
||||
}
|
||||
|
||||
return _DatabaseEntry;
|
||||
}
|
||||
set
|
||||
{
|
||||
loadedDatabase = true;
|
||||
_DatabaseEntry = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MC 百科的页面 ID。若为 0 则没有。
|
||||
/// </summary>
|
||||
public int WikiId => DatabaseEntry is null ? 0 : DatabaseEntry.WikiId;
|
||||
|
||||
/// <summary>
|
||||
/// 翻译后的中文名。若数据库没有则等同于 RawName。
|
||||
/// </summary>
|
||||
public string TranslatedName =>
|
||||
Lang.IsChineseMainland && DatabaseEntry?.ChineseName is { Length: > 0 } cn
|
||||
? cn
|
||||
: RawName;
|
||||
|
||||
/// <summary>
|
||||
/// 中文描述。若为 Nothing 则没有。
|
||||
/// </summary>
|
||||
public Task<string> ChineseDescription => GetChineseDescriptionAsync();
|
||||
|
||||
private async Task<string> GetChineseDescriptionAsync()
|
||||
{
|
||||
var from = FromCurseForge ? "curseforge" : "modrinth";
|
||||
var para = FromCurseForge ? "modId" : "project_id";
|
||||
string result = null;
|
||||
|
||||
var descHash = $"{Id}{ModBase.GetStringMD5(Description)}";
|
||||
var cacheFilePath = $@"{ModBase.pathTemp}Cache\CompTranslation.ini";
|
||||
var cacheTranslation = ModBase.ReadIni(cacheFilePath, descHash);
|
||||
if (!string.IsNullOrWhiteSpace(cacheTranslation))
|
||||
{
|
||||
result = ModBase.Base64Decode(cacheTranslation);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonObject = (JsonObject)await
|
||||
Requester.FetchJsonAsync($"https://mod.mcimirror.top/translate/{from}/{Id}");
|
||||
if (jsonObject.ContainsKey("translated"))
|
||||
{
|
||||
result = jsonObject["translated"].ToString();
|
||||
ModBase.WriteIni(cacheFilePath, descHash, ModBase.Base64Encode(result));
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (ex.Message.Contains("404"))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Download.Comp.Detail.DescriptionNoTranslation"), Lang.Text("Download.Comp.Detail.DescriptionTranslationFailed"), Lang.Text("Download.Comp.Detail.KnownButton"));
|
||||
return null;
|
||||
}
|
||||
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"获取中文描述时出现错误",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Comp.Error.OperationFailed"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"获取中文描述时出现错误",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Comp.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前实例转为可用于保存缓存的 Json。
|
||||
/// </summary>
|
||||
public JsonObject ToJson()
|
||||
{
|
||||
var json = new JsonObject();
|
||||
json["DataSource"] = FromCurseForge ? "CurseForge" : "Modrinth";
|
||||
json["Type"] = (int)Type;
|
||||
json["Slug"] = Slug;
|
||||
json["Id"] = Id;
|
||||
if (CurseForgeFileIds is not null)
|
||||
json["CurseForgeFileIds"] = new JsonArray(CurseForgeFileIds.Select(i => (JsonNode)i).ToArray());
|
||||
json["RawName"] = RawName;
|
||||
json["Description"] = Description;
|
||||
json["Website"] = Website;
|
||||
if (LastUpdate is not null)
|
||||
json["LastUpdate"] = LastUpdate;
|
||||
json["DownloadCount"] = DownloadCount;
|
||||
if (ModLoaders is not null && ModLoaders.Any())
|
||||
json["ModLoaders"] = new JsonArray(ModLoaders.Select(m => (JsonNode)(int)m).ToArray());
|
||||
json["Tags"] = new JsonArray(Tags.Select(s => (JsonNode)s).ToArray());
|
||||
if (LogoUrl is not null)
|
||||
json["LogoUrl"] = LogoUrl;
|
||||
if (Drops.Any())
|
||||
json["Drops"] = new JsonArray(Drops.Select(i => (JsonNode)i).ToArray());
|
||||
json["CacheTime"] = DateTime.Now; // 用于检查缓存时间
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前工程信息实例化为控件。
|
||||
/// </summary>
|
||||
/// <param name="showQuickDownload">是否在卡片右侧显示快速下载按钮(仅搜索结果页应传 true)。</param>
|
||||
public MyVirtualizingElement<MyCompItem> ToCompItem(bool showMcVersionDesc, bool showLoaderDesc,
|
||||
bool showQuickDownload = false)
|
||||
{
|
||||
// --- 1. 获取版本描述 (核心算法优化) ---
|
||||
string gameVersionDescription;
|
||||
if (Drops is null || !Drops.Any())
|
||||
{
|
||||
gameVersionDescription = Lang.Text("Download.Comp.Detail.CompItem.SnapshotOnly");
|
||||
}
|
||||
else
|
||||
{
|
||||
var segments = new List<string>();
|
||||
var isOld = false;
|
||||
|
||||
for (var i = 0; i < Drops.Count; i++)
|
||||
{
|
||||
int startDrop = Drops[i], endDrop = Drops[i];
|
||||
|
||||
if (startDrop < 100)
|
||||
{
|
||||
if (segments.Any() && !isOld) break;
|
||||
isOld = true;
|
||||
}
|
||||
|
||||
// 查找连续的版本段
|
||||
for (var ii = i + 1; ii < Drops.Count; ii++)
|
||||
{
|
||||
if (ModDownload.AllDrops is null || ModDownload.AllDrops.IndexOf(Drops[ii]) !=
|
||||
ModDownload.AllDrops.IndexOf(endDrop) + 1) break;
|
||||
endDrop = Drops[ii];
|
||||
i = ii;
|
||||
}
|
||||
|
||||
// 将段转为文本的逻辑
|
||||
var startName = McInstanceInfo.DropToVersion(startDrop);
|
||||
var endName = McInstanceInfo.DropToVersion(endDrop);
|
||||
|
||||
if (startDrop == endDrop)
|
||||
{
|
||||
segments.Add(startName);
|
||||
}
|
||||
else if (ModDownload.AllDrops?.Any() == true && startDrop >= ModDownload.AllDrops.First())
|
||||
{
|
||||
if (endDrop < 100)
|
||||
{
|
||||
segments.Clear();
|
||||
segments.Add(Lang.Text("Download.Comp.Detail.CompItem.AllVersions"));
|
||||
break;
|
||||
}
|
||||
|
||||
segments.Add(endName + "+");
|
||||
}
|
||||
else if (endDrop < 100)
|
||||
{
|
||||
segments.Add(startName + "-");
|
||||
break;
|
||||
}
|
||||
else if (ModDownload.AllDrops is null ||
|
||||
ModDownload.AllDrops.IndexOf(endDrop) - ModDownload.AllDrops.IndexOf(startDrop) == 1)
|
||||
{
|
||||
segments.Add($"{startName}, {endName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
segments.Add($"{startName}~{endName}");
|
||||
}
|
||||
}
|
||||
|
||||
gameVersionDescription = string.Join(", ", segments);
|
||||
}
|
||||
|
||||
// --- 2. 获取 Mod 加载器描述 (使用 Switch 表达式) ---
|
||||
var modLoadersForDesc = ModLoaders.ToList();
|
||||
if (Config.Download.Comp.IgnoreQuilt) modLoadersForDesc.Remove(CompLoaderType.Quilt);
|
||||
|
||||
var (fullDesc, partDesc) = modLoadersForDesc.Count switch
|
||||
{
|
||||
0 => ModLoaders.Count == 1 ? (Lang.Text("Download.Comp.Type.Only", ModLoaders.Single().ToString()), ModLoaders.Single().ToString()) : (Lang.Text("Download.Comp.Type.Unknown"), ""),
|
||||
1 => (Lang.Text("Download.Comp.Type.Only", modLoadersForDesc.Single().ToString()), modLoadersForDesc.Single().ToString()),
|
||||
_ => GetMultiLoaderDesc()
|
||||
};
|
||||
|
||||
// 局部函数处理复杂的“任意”判断逻辑
|
||||
(string, string) GetMultiLoaderDesc()
|
||||
{
|
||||
var newestDrop = Drops?.FirstOrDefault() ?? 9999;
|
||||
var isAny = ModLoaders.Contains(CompLoaderType.Forge) &&
|
||||
(newestDrop < 140 || ModLoaders.Contains(CompLoaderType.Fabric)) &&
|
||||
(newestDrop < 200 || ModLoaders.Contains(CompLoaderType.NeoForge)) &&
|
||||
(newestDrop < 140 || ModLoaders.Contains(CompLoaderType.Quilt) ||
|
||||
Config.Download.Comp.IgnoreQuilt);
|
||||
|
||||
var joined = string.Join(" / ", modLoadersForDesc);
|
||||
return isAny ? (Lang.Text("Download.Comp.Type.Any"), "") : (joined, joined);
|
||||
}
|
||||
|
||||
// --- 3. 实例化 UI (精简布局逻辑) ---
|
||||
return new MyVirtualizingElement<MyCompItem>(() =>
|
||||
{
|
||||
var newItem = new MyCompItem { Tag = this };
|
||||
ApplyLogoToMyImage(newItem.PathLogo);
|
||||
|
||||
var title = GetControlTitle(true);
|
||||
newItem.Title = title.Key;
|
||||
|
||||
if (string.IsNullOrEmpty(title.Value))
|
||||
((StackPanel)newItem.LabTitleRaw.Parent).Children.Remove(newItem.LabTitleRaw);
|
||||
else
|
||||
newItem.SubTitle = title.Value;
|
||||
|
||||
newItem.Tags = Tags;
|
||||
newItem.Description = Description.Replace("\r", "").Replace("\n", "");
|
||||
newItem.ShowDownloadBtn = showQuickDownload;
|
||||
|
||||
// 下边栏逻辑切换
|
||||
newItem.LabVersion.Text = (showMcVersionDesc, showLoaderDesc) switch
|
||||
{
|
||||
(true, true) =>
|
||||
$"{(string.IsNullOrEmpty(partDesc) ? "" : partDesc + " ")}{gameVersionDescription}",
|
||||
(true, false) => gameVersionDescription,
|
||||
(false, true) => fullDesc,
|
||||
_ => "" // 处理隐藏逻辑见下
|
||||
};
|
||||
|
||||
if (!showMcVersionDesc && !showLoaderDesc)
|
||||
{
|
||||
((Grid)newItem.SvgIconVersion.Parent).Children.Remove(newItem.SvgIconVersion);
|
||||
((Grid)newItem.LabVersion.Parent).Children.Remove(newItem.LabVersion);
|
||||
newItem.ColumnVersion1.Width = new GridLength(0);
|
||||
newItem.ColumnVersion2.MaxWidth = 0;
|
||||
newItem.ColumnVersion3.Width = new GridLength(0);
|
||||
}
|
||||
|
||||
newItem.LabSource.Text = FromCurseForge ? "CurseForge" : "Modrinth";
|
||||
|
||||
if (LastUpdate is not null)
|
||||
{
|
||||
newItem.LabTime.Text = Lang.TimeSpan(LastUpdate.Value - DateTime.Now, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
newItem.LabTime.Visibility = Visibility.Collapsed;
|
||||
newItem.ColumnTime1.Width =
|
||||
newItem.ColumnTime2.Width = newItem.ColumnTime3.Width = new GridLength(0);
|
||||
}
|
||||
|
||||
// 下载量数值缩写
|
||||
newItem.LabDownload.Text = Lang.CompactNumber(DownloadCount);
|
||||
|
||||
return newItem;
|
||||
})
|
||||
{ Height = 64 };
|
||||
}
|
||||
|
||||
public MyListItem ToListItem()
|
||||
{
|
||||
var result = new MyListItem
|
||||
{
|
||||
Title = TranslatedName,
|
||||
Info = Description.Replace("\r", "").Replace("\n", ""),
|
||||
Logo = string.IsNullOrEmpty(LogoUrl) ? $"{ModBase.pathImage}Icons/NoIcon.png" : LogoUrl,
|
||||
Tags = Tags,
|
||||
Tag = this,
|
||||
LogoCornerRadius = new CornerRadius(6)
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ApplyLogoToMyImage(MyImage img)
|
||||
{
|
||||
if (string.IsNullOrEmpty(LogoUrl))
|
||||
{
|
||||
img.Source = ModBase.pathImage + "Icons/NoIcon.png";
|
||||
}
|
||||
else
|
||||
{
|
||||
img.Source = LogoUrl;
|
||||
img.FallbackSource = ModDownload.DlSourceModGet(LogoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyValuePair<string, string> GetControlTitle(bool hasModLoaderDescription)
|
||||
{
|
||||
// 参考 #1567 测试例
|
||||
var title = RawName;
|
||||
List<string> subtitleList = new();
|
||||
|
||||
if (TranslatedName == RawName)
|
||||
{
|
||||
// --- 场景 A: 没有中文翻译 ---
|
||||
var nameLists = TranslatedName.Split(new[] { " | ", " - ", "(", ")", "[", "]", "{", "}" },
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim(' ', '/', '\\', '"'))
|
||||
.Where(w => !string.IsNullOrEmpty(w))
|
||||
.ToList();
|
||||
|
||||
if (nameLists.Count <= 1) return BuildResult(title, "");
|
||||
|
||||
var normalNameList = new List<string>();
|
||||
foreach (var name in nameLists)
|
||||
{
|
||||
var lowerName = name.ToLower();
|
||||
// 匹配缩写 (全大写且不是特定词)
|
||||
if (name.ToUpper() == name && name != "FPS" && name != "HUD")
|
||||
subtitleList.Add(name);
|
||||
// 匹配加载器标记 (Forge/Fabric/Quilt 且去掉后不含其他字母)
|
||||
else if (IsModLoaderMarker(lowerName))
|
||||
subtitleList.Add(name);
|
||||
else
|
||||
normalNameList.Add(name);
|
||||
}
|
||||
|
||||
if (!normalNameList.Any() || !subtitleList.Any())
|
||||
return BuildResult(title, "");
|
||||
|
||||
title = string.Join(" - ", normalNameList);
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- 场景 B: 有中文翻译 ---
|
||||
// 尝试拆分:Title (EnglishName) - Suffix
|
||||
title = TranslatedName.BeforeFirst(" (").BeforeFirst(" - ");
|
||||
|
||||
var suffix = "";
|
||||
if (TranslatedName.AfterLast(")").Contains(" - "))
|
||||
suffix = TranslatedName.AfterLast(")").AfterLast(" - ");
|
||||
|
||||
var englishName = TranslatedName;
|
||||
if (!string.IsNullOrEmpty(suffix))
|
||||
englishName = englishName.Replace(" - " + suffix, "");
|
||||
|
||||
englishName = englishName.Replace(title, "").Trim('(', ')', ' ');
|
||||
|
||||
subtitleList = englishName.Split(new[] { " | ", " - ", "(", ")", "[", "]", "{", "}" },
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim(' ', '/'))
|
||||
.Where(w => !string.IsNullOrEmpty(w))
|
||||
.ToList();
|
||||
|
||||
// 特殊逻辑:如果看起来不像版本标记或特定缩写,则保持原名
|
||||
if (subtitleList.Count > 1 &&
|
||||
!subtitleList.Any(s => IsModLoaderMarker(s.ToLower())) &&
|
||||
!(subtitleList.Count == 2 && subtitleList.Last().ToUpper() == subtitleList.Last()))
|
||||
subtitleList = new List<string> { englishName };
|
||||
|
||||
if (!string.IsNullOrEmpty(suffix)) subtitleList.Add(suffix);
|
||||
}
|
||||
|
||||
// --- 后处理: 构建 Subtitle 字符串 ---
|
||||
var finalSubtitles = new List<string>();
|
||||
foreach (var rawEx in subtitleList.Distinct())
|
||||
{
|
||||
var ex = rawEx;
|
||||
var lowerEx = ex.ToLower();
|
||||
var isModLoader = lowerEx.Contains("forge") || lowerEx.Contains("fabric") || lowerEx.Contains("quilt");
|
||||
|
||||
if (!hasModLoaderDescription && isModLoader) continue;
|
||||
if (ex.Length < 16 && lowerEx.Contains("fabric") && lowerEx.Contains("forge")) continue;
|
||||
|
||||
if (isModLoader && !ex.Contains("版") &&
|
||||
lowerEx.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3)
|
||||
ex = ex.Replace("Edition", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("edition", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Trim().Capitalize() + Lang.Text("Download.Comp.Detail.CompItem.EditionSuffix");
|
||||
|
||||
// 规范化名称大小写
|
||||
ex = ex.Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric")
|
||||
.Replace("quilt", "Quilt");
|
||||
finalSubtitles.Add(ex.Trim());
|
||||
}
|
||||
|
||||
var subtitleResult = finalSubtitles.Any() ? " | " + string.Join(" | ", finalSubtitles) : "";
|
||||
return BuildResult(title, subtitleResult);
|
||||
|
||||
bool IsModLoaderMarker(string input)
|
||||
{
|
||||
return (input.Contains("forge") || input.Contains("fabric") || input.Contains("quilt")) &&
|
||||
!input.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").RegexCheck("[a-z]+");
|
||||
}
|
||||
|
||||
KeyValuePair<string, string> BuildResult(string t, string s)
|
||||
{
|
||||
return new KeyValuePair<string, string>(t, s);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否与某个 Project 是相同的工程,只是在不同的网站。
|
||||
/// </summary>
|
||||
public bool IsLike(CompProject project)
|
||||
{
|
||||
if ((Id ?? "") == (project.Id ?? ""))
|
||||
return true; // 相同实例
|
||||
|
||||
// 提取字符串中的字母和数字
|
||||
string GetRaw(string data)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach (var r in data.Where(c => char.IsLetterOrDigit(c)))
|
||||
result.Append(r);
|
||||
return result.ToString().ToLower();
|
||||
}
|
||||
|
||||
;
|
||||
// 来自不同的网站
|
||||
if (FromCurseForge == project.FromCurseForge)
|
||||
return false;
|
||||
// Mod 加载器一致
|
||||
if (ModLoaders.Count != project.ModLoaders.Count || ModLoaders.Except(project.ModLoaders).Any())
|
||||
return false;
|
||||
// 若不为光影,则要求 MC 版本一致
|
||||
if (Type != CompType.Shader && (Drops.Count != project.Drops.Count || Drops.Except(project.Drops).Any()))
|
||||
return false;
|
||||
// 最近更新时间差距在一周以内
|
||||
if (LastUpdate is not null && project.LastUpdate is not null &&
|
||||
Math.Abs((LastUpdate - project.LastUpdate).Value.TotalDays) > 7d)
|
||||
return false;
|
||||
// MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
|
||||
if ((TranslatedName ?? "") == (project.TranslatedName ?? "") ||
|
||||
(RawName ?? "") == (project.RawName ?? "") || (Description ?? "") == (project.Description ?? "") ||
|
||||
(GetRaw(Slug) ?? "") == (GetRaw(project.Slug) ?? ""))
|
||||
{
|
||||
ModBase.Log($"[Comp] 将 {RawName} ({Slug}) 与 {project.RawName} ({project.Slug}) 认定为相似工程");
|
||||
// 如果只有一个有 DatabaseEntry,设置给另外一个
|
||||
if (DatabaseEntry is null && project.DatabaseEntry is not null)
|
||||
DatabaseEntry = project.DatabaseEntry;
|
||||
if (DatabaseEntry is not null && project.DatabaseEntry is null)
|
||||
project.DatabaseEntry = DatabaseEntry;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Id} ({Slug}): {RawName}";
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var project = obj as CompProject;
|
||||
return project is not null && (Id ?? "") == (project.Id ?? "");
|
||||
}
|
||||
|
||||
public static bool operator ==(CompProject left, CompProject right)
|
||||
{
|
||||
return EqualityComparer<CompProject>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(CompProject left, CompProject right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入与输出
|
||||
|
||||
public class CompProjectRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 筛选 MC 版本。
|
||||
/// </summary>
|
||||
public string gameVersion = null;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选 Mod 加载器类别。
|
||||
/// </summary>
|
||||
public CompLoaderType modLoader = CompLoaderType.Any;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索的文本内容。
|
||||
/// </summary>
|
||||
public string searchText;
|
||||
|
||||
/// <summary>
|
||||
/// 在进行中文搜索时,CurseForge 的替代搜索文本。
|
||||
/// 由于 CurseForge API 在有任意关键词未匹配的时候就不显示结果,所以不能使用与 Modrinth 相同的算法。
|
||||
/// </summary>
|
||||
public string curseForgeAltSearchText;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索结果排序方式。
|
||||
/// </summary>
|
||||
public CompSortType sort = CompSortType.Default;
|
||||
|
||||
/// <summary>
|
||||
/// 允许的来源。
|
||||
/// </summary>
|
||||
public CompSourceType source = CompSourceType.Any;
|
||||
|
||||
// 结果要求
|
||||
|
||||
/// <summary>
|
||||
/// 加载后应输出到的结果存储器。
|
||||
/// </summary>
|
||||
public CompProjectStorage storage;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
|
||||
/// </summary>
|
||||
public string tag = "";
|
||||
|
||||
/// <summary>
|
||||
/// 应当尽量达成的结果数量。
|
||||
/// </summary>
|
||||
public int targetResultCount;
|
||||
|
||||
// 输入内容
|
||||
|
||||
/// <summary>
|
||||
/// 筛选资源种类。
|
||||
/// </summary>
|
||||
public CompType type;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
public CompProjectRequest(CompType type, CompProjectStorage storage, int targetResultCount)
|
||||
{
|
||||
this.type = type;
|
||||
this.storage = storage;
|
||||
this.targetResultCount = targetResultCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据加载位置记录,是否还可以继续获取内容。
|
||||
/// </summary>
|
||||
public bool CanContinue
|
||||
{
|
||||
get
|
||||
{
|
||||
if (tag.StartsWithF("/") || !source.HasFlag(CompSourceType.CurseForge))
|
||||
storage.curseForgeTotal = 0;
|
||||
if (tag.EndsWithF("/") || !source.HasFlag(CompSourceType.Modrinth))
|
||||
storage.modrinthTotal = 0;
|
||||
if (storage.curseForgeTotal == -1 || storage.modrinthTotal == -1)
|
||||
return true;
|
||||
return storage.curseForgeOffset < storage.curseForgeTotal ||
|
||||
storage.modrinthOffset < storage.modrinthTotal;
|
||||
}
|
||||
}
|
||||
|
||||
// 构造请求
|
||||
|
||||
/// <summary>
|
||||
/// 获取对应的 CurseForge API 请求链接。若返回 Nothing 则为不进行 CurseForge 请求。
|
||||
/// </summary>
|
||||
public string GetCurseForgeAddress()
|
||||
{
|
||||
if (!source.HasFlag(CompSourceType.CurseForge))
|
||||
return null;
|
||||
if (tag.StartsWithF("/"))
|
||||
storage.curseForgeTotal = 0;
|
||||
if (storage.curseForgeTotal > -1 && storage.curseForgeTotal <= storage.curseForgeOffset)
|
||||
return null;
|
||||
// 应用筛选参数
|
||||
var address =
|
||||
new StringBuilder(
|
||||
$"https://api.curseforge.com/v1/mods/search?gameId=432&sortOrder=desc&pageSize={compPageSize}");
|
||||
switch (type)
|
||||
{
|
||||
case CompType.Mod:
|
||||
{
|
||||
address.Append("&classId=6");
|
||||
break;
|
||||
}
|
||||
case CompType.ModPack:
|
||||
{
|
||||
address.Append("&classId=4471");
|
||||
break;
|
||||
}
|
||||
case CompType.DataPack:
|
||||
{
|
||||
address.Append("&classId=6945");
|
||||
break;
|
||||
}
|
||||
case CompType.Shader:
|
||||
{
|
||||
address.Append("&classId=6552");
|
||||
break;
|
||||
}
|
||||
case CompType.ResourcePack:
|
||||
{
|
||||
address.Append("&classId=12");
|
||||
break;
|
||||
}
|
||||
case CompType.World:
|
||||
{
|
||||
address.Append("&classId=17");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tag)) address.Append($"&categoryId={tag.BeforeFirst("/")}");
|
||||
if (modLoader != CompLoaderType.Any)
|
||||
address.Append("&modLoaderType=").Append(((int)modLoader).ToString());
|
||||
if (!string.IsNullOrEmpty(gameVersion))
|
||||
address.Append("&gameVersion=").Append(gameVersion);
|
||||
if (!string.IsNullOrEmpty(curseForgeAltSearchText ?? searchText))
|
||||
address.Append("&searchFilter=").Append(WebUtility.UrlEncode(curseForgeAltSearchText ?? searchText));
|
||||
if (storage.curseForgeOffset > 0)
|
||||
address.Append("&index=").Append(storage.curseForgeOffset);
|
||||
switch (sort)
|
||||
{
|
||||
case CompSortType.Relevance:
|
||||
{
|
||||
address.Append("&sortField=4");
|
||||
break;
|
||||
}
|
||||
case CompSortType.Downloads:
|
||||
{
|
||||
address.Append("&sortField=6");
|
||||
break;
|
||||
}
|
||||
case CompSortType.Follows:
|
||||
{
|
||||
address.Append("&sortField=2");
|
||||
break;
|
||||
}
|
||||
case CompSortType.Newest:
|
||||
{
|
||||
address.Append("&sortField=11");
|
||||
break;
|
||||
}
|
||||
case CompSortType.Updated:
|
||||
{
|
||||
address.Append("&sortField=3");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
address.Append("&sortField=2");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return address.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对应的 Modrinth API 请求链接。若返回 Nothing 则为不进行 Modrinth 请求。
|
||||
/// </summary>
|
||||
public string GetModrinthAddress()
|
||||
{
|
||||
if (!source.HasFlag(CompSourceType.Modrinth))
|
||||
return null;
|
||||
if (tag.EndsWithF("/"))
|
||||
storage.modrinthTotal = 0;
|
||||
if (storage.modrinthTotal > -1 && storage.modrinthTotal <= storage.modrinthOffset)
|
||||
return null;
|
||||
// 应用筛选参数
|
||||
var address = $"https://api.modrinth.com/v2/search?limit={compPageSize}";
|
||||
switch (sort)
|
||||
{
|
||||
case CompSortType.Relevance:
|
||||
{
|
||||
address += "&index=relevance";
|
||||
break;
|
||||
}
|
||||
case CompSortType.Downloads:
|
||||
{
|
||||
address += "&index=downloads";
|
||||
break;
|
||||
}
|
||||
case CompSortType.Follows:
|
||||
{
|
||||
address += "&index=follows";
|
||||
break;
|
||||
}
|
||||
case CompSortType.Newest:
|
||||
{
|
||||
address += "&index=newest";
|
||||
break;
|
||||
}
|
||||
case CompSortType.Updated:
|
||||
{
|
||||
address += "&index=updated";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
address += "&index=relevance";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(searchText))
|
||||
address += "&query=" + WebUtility.UrlEncode(searchText);
|
||||
if (storage.modrinthOffset > 0)
|
||||
address += "&offset=" + storage.modrinthOffset;
|
||||
// facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
|
||||
var facets = new List<string>();
|
||||
facets.Add($"[\"project_type:{ModBase.GetStringFromEnum(type).ToLower()}\"]");
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
facets.Add($"[\"categories:'{tag.AfterLast("/")}'\"]");
|
||||
if (modLoader != CompLoaderType.Any)
|
||||
facets.Add($"[\"categories:'{ModBase.GetStringFromEnum(modLoader).ToLower()}'\"]");
|
||||
if (!string.IsNullOrEmpty(gameVersion))
|
||||
facets.Add($"[\"versions:'{gameVersion}'\"]");
|
||||
address += "&facets=[" + string.Join(",", facets) + "]";
|
||||
return address;
|
||||
}
|
||||
|
||||
// 相同判断
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var request = obj as CompProjectRequest;
|
||||
return request is not null && type == request.type && targetResultCount == request.targetResultCount &&
|
||||
(tag ?? "") == (request.tag ?? "") && modLoader == request.modLoader && source == request.source &&
|
||||
(gameVersion ?? "") == (request.gameVersion ?? "") &&
|
||||
(searchText ?? "") == (request.searchText ?? "") && sort == request.sort;
|
||||
}
|
||||
|
||||
public static bool operator ==(CompProjectRequest left, CompProjectRequest right)
|
||||
{
|
||||
return EqualityComparer<CompProjectRequest>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(CompProjectRequest left, CompProjectRequest right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProjectStorage
|
||||
{
|
||||
// 加载位置记录
|
||||
|
||||
public int curseForgeOffset;
|
||||
public int curseForgeTotal = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 当前的错误信息。如果没有则为 Nothing。
|
||||
/// </summary>
|
||||
public string errorMessage = null;
|
||||
|
||||
public int modrinthOffset;
|
||||
public int modrinthTotal = -1;
|
||||
|
||||
// 结果列表
|
||||
|
||||
/// <summary>
|
||||
/// 可供展示的所有工程的列表。
|
||||
/// </summary>
|
||||
public List<CompProject> results = new();
|
||||
}
|
||||
|
||||
// 实际的获取
|
||||
|
||||
private const int compPageSize = 40;
|
||||
|
||||
/// <summary>
|
||||
/// 已知工程信息的缓存。
|
||||
/// </summary>
|
||||
public static ConcurrentDictionary<string, CompProject> compProjectCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// CurseForge 分类 URL 段 → classId 映射。提为 static 避免每次解析重新分配。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> curseForgeCategoryClassIds = new()
|
||||
{
|
||||
{ "mc-mods", "6" },
|
||||
{ "modpacks", "4471" },
|
||||
{ "texture-packs", "12" },
|
||||
{ "shaders", "6552" }
|
||||
};
|
||||
|
||||
private enum ResourceSite { None, CurseForge, Modrinth }
|
||||
|
||||
/// <summary>
|
||||
/// 用 Uri 解析单个 token 是否为受支持的 CurseForge/Modrinth 资源链接。
|
||||
/// 成功时输出站点、分类段与 slug(query、fragment 会被自动丢弃)。
|
||||
/// </summary>
|
||||
private static bool TryParseResourceLink(string token, out ResourceSite site, out string category, out string slug)
|
||||
{
|
||||
site = ResourceSite.None;
|
||||
category = string.Empty;
|
||||
slug = string.Empty;
|
||||
|
||||
// 容忍无协议前缀(如直接粘贴 www.curseforge.com/...):原样试,再补 https:// 试
|
||||
if (!Uri.TryCreate(token, UriKind.Absolute, out var uri) &&
|
||||
!Uri.TryCreate($"https://{token}", UriKind.Absolute, out uri)) return false;
|
||||
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) return false;
|
||||
|
||||
// 仅取 path 段,天然忽略 ?query 与 #fragment
|
||||
var segments = uri.AbsolutePath.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// CurseForge: /minecraft/{category}/{slug}
|
||||
if (IsHostOf(uri.Host, "curseforge.com"))
|
||||
{
|
||||
if (segments.Length < 3 || !segments[0].Equals("minecraft", StringComparison.OrdinalIgnoreCase)) return false;
|
||||
site = ResourceSite.CurseForge;
|
||||
category = segments[1];
|
||||
slug = segments[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Modrinth: /{type}/{slug}
|
||||
if (IsHostOf(uri.Host, "modrinth.com"))
|
||||
{
|
||||
if (segments.Length < 2) return false;
|
||||
site = ResourceSite.Modrinth;
|
||||
category = segments[0]; // 类型段,当前解析未使用
|
||||
slug = segments[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>host 等于 domain 或为其子域,避免 evilcurseforge.com 之类的伪装域名。</summary>
|
||||
private static bool IsHostOf(string host, string domain) =>
|
||||
host.Equals(domain, StringComparison.OrdinalIgnoreCase) ||
|
||||
host.EndsWith($".{domain}", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>从文本中找出首个可识别的资源链接 token,找不到返回 null。</summary>
|
||||
private static string? FindFirstResourceLinkToken(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
foreach (var token in text.Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
|
||||
if (TryParseResourceLink(token, out _, out _, out _)) return token;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从单条 CurseForge / Modrinth 资源链接解析出 projectId。无法识别或获取失败时返回 null。
|
||||
/// </summary>
|
||||
public static string? ResolveLinkToProjectId(string url)
|
||||
{
|
||||
// 纯链接(搜索框已抽出的单 token)直接命中;含周围文本(如剪贴板整段)时回退取首个链接
|
||||
if (!TryParseResourceLink(url, out var site, out var category, out var slug))
|
||||
{
|
||||
var token = FindFirstResourceLinkToken(url);
|
||||
if (token is null || !TryParseResourceLink(token, out site, out category, out slug)) return null;
|
||||
}
|
||||
|
||||
if (site == ResourceSite.CurseForge)
|
||||
{
|
||||
var encodedSlug = WebUtility.UrlEncode(slug);
|
||||
var json = ModDownload.DlModRequest<JsonObject>(
|
||||
$"https://api.curseforge.com/v1/mods/search?gameId=432&slug={encodedSlug}");
|
||||
var dataArray = (JsonArray)json["data"];
|
||||
if (!dataArray.Any()) return null;
|
||||
|
||||
var receivedClassId = ((JsonObject)dataArray[0])["classId"]?.ToString();
|
||||
if (!curseForgeCategoryClassIds.TryGetValue(category, out var targetClassId) ||
|
||||
receivedClassId == targetClassId)
|
||||
return dataArray[0]["id"]?.ToString();
|
||||
|
||||
// 分类不符:带 classId 重搜。结果与首次查询语义不同,使用独立变量
|
||||
var filteredJson = ModDownload.DlModRequest<JsonObject>(
|
||||
$"https://api.curseforge.com/v1/mods/search?gameId=432&slug={encodedSlug}&classId={targetClassId}");
|
||||
var filteredDatas = (JsonArray)filteredJson["data"];
|
||||
return filteredDatas.Any() ? filteredDatas[0]["id"]?.ToString() : null;
|
||||
}
|
||||
|
||||
// Modrinth:slug 进 path,用 EscapeDataString
|
||||
var mr = ModDownload.DlModRequest<JsonObject>(
|
||||
$"https://api.modrinth.com/v2/project/{Uri.EscapeDataString(slug)}");
|
||||
return mr["id"]?.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 若输入文本中恰好含 1 条 CurseForge/Modrinth 资源链接,返回该链接;含 0 条或 ≥2 条时返回 null。
|
||||
/// </summary>
|
||||
public static string? TryExtractSingleResourceLink(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
var tokens = text.Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
string? found = null;
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (!TryParseResourceLink(token, out _, out _, out _)) continue;
|
||||
if (found is not null) return null; // ≥2 条链接,一条也不识别
|
||||
found = token;
|
||||
}
|
||||
return found; // 恰好 1 条返回该链接;0 条返回 null
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据搜索请求获取一系列的工程列表。需要基于加载器运行。
|
||||
/// </summary>
|
||||
public static void CompProjectsGet(ModLoader.LoaderTask<CompProjectRequest, int> task)
|
||||
{
|
||||
var request = task.input;
|
||||
var storage = request.storage;
|
||||
|
||||
// === Issue #2942: 搜索框单条资源链接识别 ===
|
||||
var singleLink = TryExtractSingleResourceLink(request.searchText);
|
||||
if (singleLink is not null)
|
||||
{
|
||||
// 已得到结果则直接结束(幂等,避免重复获取)
|
||||
if (storage.results.Any()) return;
|
||||
|
||||
CompProject? project;
|
||||
try
|
||||
{
|
||||
var projectId = ResolveLinkToProjectId(singleLink);
|
||||
project = string.IsNullOrEmpty(projectId)
|
||||
? null
|
||||
: CompRequest.GetCompProjectsByIds(new List<string> { projectId }).FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Comp] 解析资源链接失败");
|
||||
throw new Exception(Lang.Text("Download.Comp.Link.ResolveFailed"));
|
||||
}
|
||||
|
||||
// 解析或获取失败 → 提示
|
||||
if (project is null)
|
||||
throw new Exception(Lang.Text("Download.Comp.Link.ResolveFailed"));
|
||||
|
||||
// 类型与当前页不符 → 按无结果处理(与既有"无匹配结果"一致)
|
||||
if (request.type != CompType.Any && project.Type != request.type)
|
||||
throw new Exception(Lang.Text("Download.Comp.List.NoMatchingResults"));
|
||||
|
||||
// 命中:单条结果,隐藏分页
|
||||
storage.results.Add(project);
|
||||
storage.curseForgeTotal = 0;
|
||||
storage.modrinthTotal = 0;
|
||||
return;
|
||||
}
|
||||
// === /Issue #2942 ===
|
||||
|
||||
#region 状态与版本初步检查
|
||||
|
||||
if (storage.results.Count >= request.targetResultCount)
|
||||
{
|
||||
LogWrapper.Info($"[Comp] 已有 {storage.results.Count} 个结果,多于所需的 {request.targetResultCount} 个结果,结束处理");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!request.CanContinue)
|
||||
{
|
||||
if (!storage.results.Any()) throw new Exception(Lang.Text("Download.Comp.List.NoMatchingResults"));
|
||||
LogWrapper.Info(
|
||||
$"[Comp] 已有 {storage.results.Count} 个结果,少于所需的 {request.targetResultCount} 个结果,但无法继续获取,结束处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 拒绝不支持的版本
|
||||
if (request.modLoader == CompLoaderType.Quilt &&
|
||||
McVersionComparer.CompareVersion(request.gameVersion ?? "1.15", "1.14") == -1)
|
||||
throw new Exception(Lang.Text("Minecraft.Error.QuiltUnsupported", request.gameVersion));
|
||||
|
||||
#endregion
|
||||
|
||||
#region 处理搜索文本 (内嵌关键词转换逻辑)
|
||||
|
||||
var rawFilter = (request.searchText ?? "").Trim();
|
||||
request.searchText = rawFilter;
|
||||
var rawFilterLower = rawFilter.ToLower();
|
||||
LogWrapper.Info("[Comp] 工程列表搜索原始文本:" + rawFilter);
|
||||
|
||||
// 中文请求关键字处理
|
||||
var isChineseSearch = Lang.IsChineseMainland &&
|
||||
RegexPatterns.HasChineseChar.IsMatch(rawFilter) &&
|
||||
!string.IsNullOrEmpty(rawFilter);
|
||||
if (isChineseSearch && request.type is CompType.Mod or CompType.DataPack)
|
||||
{
|
||||
var searchEntries = new List<ModBase.SearchEntry<CompDatabaseEntry>>();
|
||||
using (var conn = CompDB)
|
||||
{
|
||||
var likeEscaped = rawFilter.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
|
||||
var searchRes = conn.Query<CompDatabaseEntry>(
|
||||
"SELECT * FROM ModTranslation WHERE ChineseName LIKE @p ESCAPE '\\' OR CurseForgeSlug LIKE @p ESCAPE '\\' OR ModrinthSlug LIKE @p ESCAPE '\\'",
|
||||
new { p = $"%{likeEscaped}%" }).ToList();
|
||||
foreach (var searchItem in searchRes)
|
||||
{
|
||||
if (searchItem.ChineseName.Contains("动态的树")) continue;
|
||||
searchEntries.Add(new ModBase.SearchEntry<CompDatabaseEntry>
|
||||
{
|
||||
item = searchItem,
|
||||
searchSource = new List<ModBase.SearchSource>
|
||||
{
|
||||
new(searchItem.ChineseName.BeforeFirst(" (").Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries), 1),
|
||||
new(searchItem.ChineseName.AfterFirst(" (") + (searchItem.CurseForgeSlug ?? "") + (searchItem.ModrinthSlug ?? ""), 0.5)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var searchResults = ModBase.Search(searchEntries, request.searchText, 40, 0.2);
|
||||
if (!searchResults.Any()) throw new Exception(Lang.Text("Download.Comp.List.NoResults"));
|
||||
|
||||
string[] ExtractWords(ModBase.SearchEntry<CompDatabaseEntry> result)
|
||||
{
|
||||
var word = "";
|
||||
if (result.item.CurseForgeSlug is not null)
|
||||
word += result.item.CurseForgeSlug.Replace("-", " ").Replace("/", " ") + " ";
|
||||
if (result.item.ModrinthSlug is not null)
|
||||
word += result.item.ModrinthSlug.Replace("-", " ").Replace("/", " ") + " ";
|
||||
word += result.item.ChineseName.AfterLast(" (").TrimEnd(')', ' ').BeforeFirst(" - ")
|
||||
.Replace(":", "").Replace("(", "").Replace(")", "").ToLower().Replace("/", " ").Replace("-", " ");
|
||||
var words = word.ToLower().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
words = words.Select(w => w.TrimStart('{', '[', '(').TrimEnd('}', ']', ')')).Where(
|
||||
w =>
|
||||
{
|
||||
if (w.Length <= 1) return false;
|
||||
if (!w.Any(char.IsLetterOrDigit)) return false;
|
||||
if (new[] { "the", "of", "for", "mod", "and", "forge", "fabric", "quilt", "neoforge" }.Contains(w)) return false;
|
||||
if (ModBase.Val(w) > 0) return false;
|
||||
if (w.Split(' ').Length > 3 && w.Contains("ftb")) return false;
|
||||
return true;
|
||||
}).Distinct().ToArray();
|
||||
return words;
|
||||
}
|
||||
|
||||
var wordModCount = new Dictionary<string, HashSet<int>>();
|
||||
foreach (var result in searchResults)
|
||||
foreach (var word in ExtractWords(result))
|
||||
{
|
||||
if (!wordModCount.TryGetValue(word, out var mods))
|
||||
wordModCount[word] = mods = new HashSet<int>();
|
||||
mods.Add(result.item.WikiId);
|
||||
}
|
||||
|
||||
if (wordModCount.Count == 0) throw new Exception(Lang.Text("Download.Comp.List.NoResults"));
|
||||
|
||||
static string NormalizeName(string s) =>
|
||||
new string(s.Where(c => !char.IsWhiteSpace(c) && !char.IsSurrogate(c)).ToArray());
|
||||
string CanonName(string name) => NormalizeName(name.BeforeFirst(" ("));
|
||||
var normalizedQuery = NormalizeName(rawFilter);
|
||||
var exactNameEntries = searchResults
|
||||
.Where(r => CanonName(r.item.ChineseName) == normalizedQuery).ToList();
|
||||
var exactNameMods = exactNameEntries.Select(r => r.item.WikiId).Distinct().Count();
|
||||
|
||||
if (exactNameMods == 1)
|
||||
{
|
||||
var canonicalEntry = exactNameEntries
|
||||
.OrderByDescending(r => r.absoluteRight)
|
||||
.ThenByDescending(r => r.similarity)
|
||||
.ThenBy(r => (r.item.CurseForgeSlug ?? r.item.ModrinthSlug ?? r.item.ChineseName).Length)
|
||||
.First();
|
||||
var canonicalWords = ExtractWords(canonicalEntry);
|
||||
request.searchText = canonicalWords.Any()
|
||||
? string.Join(" ", canonicalWords)
|
||||
: string.Join(" ", wordModCount.OrderByDescending(w => w.Value.Count).Take(2).Select(w => w.Key));
|
||||
var cfSlugs = exactNameEntries.Select(r => r.item.CurseForgeSlug)
|
||||
.Where(s => s is not null).Distinct().ToList();
|
||||
if (cfSlugs.Count == 1)
|
||||
request.curseForgeAltSearchText = cfSlugs[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxCount = wordModCount.Values.Max(mods => mods.Count);
|
||||
if (maxCount <= 1)
|
||||
{
|
||||
var best = searchResults
|
||||
.OrderByDescending(r => r.absoluteRight)
|
||||
.ThenByDescending(r => r.similarity)
|
||||
.ThenBy(r => (r.item.CurseForgeSlug ?? r.item.ModrinthSlug ?? r.item.ChineseName).Length)
|
||||
.First();
|
||||
request.searchText = string.Join(" ", ExtractWords(best));
|
||||
}
|
||||
else
|
||||
{
|
||||
var tied = wordModCount
|
||||
.Where(w => w.Value.Count == maxCount)
|
||||
.OrderBy(w => w.Key.Length)
|
||||
.ToList();
|
||||
var anchorMods = tied[0].Value;
|
||||
request.searchText = string.Join(" ", tied
|
||||
.Where(w => w.Value.Overlaps(anchorMods))
|
||||
.Take(3)
|
||||
.Select(w => w.Key));
|
||||
}
|
||||
}
|
||||
LogWrapper.Debug("[Comp] 中文搜索基础关键词:" + request.searchText);
|
||||
}
|
||||
|
||||
// 最终处理关键字:分割、去重
|
||||
void processKeywords(ref string text)
|
||||
{
|
||||
if (text is null) return;
|
||||
text = text.ToLowerInvariant();
|
||||
var words = new List<string>();
|
||||
foreach (var keyword in text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var cleanKeyword = keyword.Trim('[', ']');
|
||||
if (string.IsNullOrEmpty(cleanKeyword)) continue;
|
||||
if (new[] { "forge", "fabric", "for", "mod", "quilt" }.Contains(cleanKeyword))
|
||||
{
|
||||
LogWrapper.Debug("[Comp] 已跳过搜索关键词:" + cleanKeyword);
|
||||
continue;
|
||||
}
|
||||
|
||||
words.Add(cleanKeyword);
|
||||
}
|
||||
|
||||
if (rawFilter.Length > 0 && !words.Any())
|
||||
text = rawFilter;
|
||||
else
|
||||
text = string.Join(" ", words.Distinct());
|
||||
|
||||
// 例外项:OptiForge、OptiFabric(拆词后因为包含 Forge/Fabric 导致无法搜到实际的 Mod)
|
||||
if (rawFilter.Replace(" ", "").ContainsF("optiforge", true)) text = "optiforge";
|
||||
if (rawFilter.Replace(" ", "").ContainsF("optifabric", true)) text = "optifabric";
|
||||
}
|
||||
|
||||
if (request.curseForgeAltSearchText is not null)
|
||||
{
|
||||
processKeywords(ref request.curseForgeAltSearchText);
|
||||
LogWrapper.Debug("[Comp] 工程列表搜索最终文本(CurseForge):" + request.curseForgeAltSearchText);
|
||||
}
|
||||
|
||||
processKeywords(ref request.searchText);
|
||||
LogWrapper.Debug("[Comp] 工程列表搜索最终文本:" + request.searchText);
|
||||
task.Progress = 0.1;
|
||||
|
||||
#endregion
|
||||
|
||||
var realResults = new List<CompProject>();
|
||||
|
||||
#region 网络请求与结果获取 (Retry 循环)
|
||||
|
||||
while (true)
|
||||
{
|
||||
var rawResults = new List<CompProject>();
|
||||
Exception lastError = null;
|
||||
var resultsLock = new object();
|
||||
|
||||
// 1.14 以下 Forge 筛选处理
|
||||
var isOldForgeRequest = request.modLoader == CompLoaderType.Forge &&
|
||||
McInstanceInfo.VersionToDrop(request.gameVersion, true) < 140;
|
||||
if (isOldForgeRequest) request.modLoader = CompLoaderType.Any;
|
||||
var curseForgeUrl = request.GetCurseForgeAddress();
|
||||
var modrinthUrl = request.GetModrinthAddress();
|
||||
if (isOldForgeRequest) request.modLoader = CompLoaderType.Forge;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
// CurseForge 线程内嵌
|
||||
if (curseForgeUrl is not null)
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
LogWrapper.Info("[Comp] 开始从 CurseForge 获取列表:" + curseForgeUrl);
|
||||
var json = ModDownload.DlModRequest<JsonObject>(curseForgeUrl);
|
||||
var projects = json["data"].AsArray().Select(j => new CompProject((JsonObject)j))
|
||||
.Where(p => !(request.type == CompType.ResourcePack && p.Tags.Contains(Lang.Text("Download.Comp.Type.DataPack"))))
|
||||
.ToList();
|
||||
lock (resultsLock)
|
||||
{
|
||||
rawResults.AddRange(projects);
|
||||
}
|
||||
|
||||
storage.curseForgeOffset += projects.Count;
|
||||
storage.curseForgeTotal = json["pagination"]["totalCount"].ToObject<int>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
LogWrapper.Error(ex, "CurseForge 获取失败");
|
||||
}
|
||||
}));
|
||||
|
||||
// Modrinth 线程内嵌
|
||||
if (modrinthUrl is not null)
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
LogWrapper.Info("[Comp] 开始从 Modrinth 获取列表:" + modrinthUrl);
|
||||
var json = ModDownload.DlModRequest<JsonObject>(modrinthUrl);
|
||||
var projects = json["hits"].AsArray().Select(j => new CompProject((JsonObject)j)).ToList();
|
||||
lock (resultsLock)
|
||||
{
|
||||
rawResults.AddRange(projects);
|
||||
}
|
||||
|
||||
storage.modrinthOffset += projects.Count;
|
||||
storage.modrinthTotal = json["total_hits"].ToObject<int>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
LogWrapper.Error(ex, "Modrinth 获取失败");
|
||||
}
|
||||
}));
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
task.Progress += 0.4;
|
||||
if (task.IsAborted) return;
|
||||
|
||||
// 过滤老版本 Forge
|
||||
if (isOldForgeRequest)
|
||||
rawResults = rawResults.Where(p => !p.ModLoaders.Any() || p.ModLoaders.Contains(CompLoaderType.Forge))
|
||||
.ToList();
|
||||
|
||||
// 错误检查与空结果处理
|
||||
if (!rawResults.Any())
|
||||
{
|
||||
if (lastError is not null) throw lastError;
|
||||
// 处理各平台不兼容报错... (此处省略具体 Exception 文本以保持简略)
|
||||
throw new Exception(Lang.Text("Download.Comp.List.NoResultsSimple"));
|
||||
}
|
||||
|
||||
#region 去重与分页判断
|
||||
|
||||
// 优先保留 Modrinth 顺序并去重
|
||||
var processedResults = rawResults.OrderBy(x => x.FromCurseForge)
|
||||
.Where(r => !realResults.Any(b => r.IsLike(b)) && !storage.results.Any(b => r.IsLike(b)))
|
||||
.ToList();
|
||||
|
||||
realResults.AddRange(processedResults);
|
||||
LogWrapper.Info($"[Comp] 去重、筛选后累计新增结果 {processedResults.Count} 个(目前已有结果 {storage.results.Count} 个)");
|
||||
|
||||
if (realResults.Count + storage.results.Count < request.targetResultCount && request.CanContinue &&
|
||||
lastError is null)
|
||||
{
|
||||
LogWrapper.Info("[Comp] 数量不足,继续加载下一页");
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 排序与最终输出
|
||||
|
||||
if (request.sort != CompSortType.Default)
|
||||
{
|
||||
if (task.IsAborted) throw new ThreadInterruptedException();
|
||||
storage.results.AddRange(realResults); // 遵从API返回顺序
|
||||
return;
|
||||
}
|
||||
|
||||
var scores = new Dictionary<CompProject, double>();
|
||||
Func<CompProject, double> getDownloadCountMult = p =>
|
||||
{
|
||||
switch (request.type)
|
||||
{
|
||||
case CompType.Mod:
|
||||
case CompType.ModPack: return p.FromCurseForge ? 1 : 7;
|
||||
case CompType.DataPack: return p.FromCurseForge ? 10 : 1;
|
||||
case CompType.ResourcePack:
|
||||
case CompType.Shader: return p.FromCurseForge ? 1 : 5;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(rawFilter))
|
||||
{
|
||||
foreach (var res in realResults) scores.Add(res, res.DownloadCount * getDownloadCountMult(res));
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchEntries = new List<ModBase.SearchEntry<CompProject>>();
|
||||
foreach (var res in realResults)
|
||||
{
|
||||
scores.Add(res,
|
||||
(Lang.IsChineseMainland && res.WikiId > 0 ? 0.2 : 0) +
|
||||
Math.Log10(Math.Max(res.DownloadCount, 1) * getDownloadCountMult(res)) / 9);
|
||||
searchEntries.Add(new ModBase.SearchEntry<CompProject>
|
||||
{
|
||||
item = res,
|
||||
searchSource = new List<ModBase.SearchSource>
|
||||
{
|
||||
new((isChineseSearch ? res.TranslatedName : res.RawName).Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries), 1),
|
||||
new(res.Description, 0.05)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var searchRes = ModBase.Search(searchEntries, rawFilter, 101, -1);
|
||||
foreach (var item in searchRes)
|
||||
scores[item.item] +=
|
||||
(item.absoluteRight ? 10 : item.similarity) /
|
||||
(searchRes.First().absoluteRight ? 10 : searchRes.First().similarity);
|
||||
}
|
||||
|
||||
if (task.IsAborted) throw new ThreadInterruptedException();
|
||||
storage.results.AddRange(scores.OrderByDescending(s => s.Value).Select(s => s.Key));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompFile | 文件信息
|
||||
|
||||
// 类定义
|
||||
|
||||
public enum CompFileStatus
|
||||
{
|
||||
Release = 1, // 枚举值来源:https://docs.curseforge.com/#tocS_FileReleaseType
|
||||
Beta = 2,
|
||||
Alpha = 3
|
||||
}
|
||||
|
||||
public class CompFile
|
||||
{
|
||||
/// <summary>
|
||||
/// 该文件的所有必要依赖工程的 Project.Id。
|
||||
/// </summary>
|
||||
public readonly List<string> Dependencies = new();
|
||||
|
||||
/// <summary>
|
||||
/// 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量,且 CurseForge 可能错误地返回 0。
|
||||
/// </summary>
|
||||
public readonly int DownloadCount;
|
||||
|
||||
/// <summary>
|
||||
/// 下载的文件名。
|
||||
/// </summary>
|
||||
public readonly string FileName;
|
||||
|
||||
/// <summary>
|
||||
/// 该文件来自 CurseForge 还是 Modrinth。
|
||||
/// </summary>
|
||||
public readonly bool FromCurseForge;
|
||||
|
||||
// <summary>
|
||||
// 未经处理的支持的游戏版本列表。
|
||||
// </summary>
|
||||
public readonly List<string> RawGameVersions = new();
|
||||
/// <summary>
|
||||
/// 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
|
||||
/// </summary>
|
||||
public readonly List<string> GameVersions = new();
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 SHA1 或 MD5。
|
||||
/// </summary>
|
||||
public readonly string Hash;
|
||||
|
||||
/// <summary>
|
||||
/// 用于唯一性鉴别该文件的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
|
||||
/// </summary>
|
||||
public readonly string Id;
|
||||
|
||||
/// <summary>
|
||||
/// 支持的 Mod 加载器列表。可能为空。
|
||||
/// </summary>
|
||||
public readonly List<CompLoaderType> ModLoaders = new();
|
||||
|
||||
/// <summary>
|
||||
/// 该文件的所有可选依赖工程的 Project.Id。
|
||||
/// </summary>
|
||||
public readonly List<string> OptionalDependencies = new();
|
||||
|
||||
/// <summary>
|
||||
/// 该文件所属项目的 ID。
|
||||
/// </summary>
|
||||
public readonly string ProjectId;
|
||||
|
||||
/// <summary>
|
||||
/// 该文件的所有必要依赖工程的原始 ID。
|
||||
/// 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
|
||||
/// </summary>
|
||||
public readonly List<string> RawDependencies = new();
|
||||
|
||||
/// <summary>
|
||||
/// 该文件的所有可选依赖工程的原始 ID。
|
||||
/// 这些 ID 可能没有加载,在加载后会添加到 OptionalDependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
|
||||
/// </summary>
|
||||
public readonly List<string> RawOptionalDependencies = new();
|
||||
|
||||
/// <summary>
|
||||
/// 发布时间。
|
||||
/// </summary>
|
||||
public readonly DateTime ReleaseDate;
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态:Release/Beta/Alpha。
|
||||
/// </summary>
|
||||
public readonly CompFileStatus Status;
|
||||
|
||||
// 源信息
|
||||
|
||||
/// <summary>
|
||||
/// 文件的种类。
|
||||
/// </summary>
|
||||
public readonly CompType Type;
|
||||
|
||||
// 描述性信息
|
||||
|
||||
/// <summary>
|
||||
/// 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。
|
||||
/// </summary>
|
||||
public string DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// 文件所有可能的下载源。
|
||||
/// </summary>
|
||||
public List<string> DownloadUrls;
|
||||
|
||||
/// <summary>
|
||||
/// Mod 版本号。
|
||||
/// 不一定是标准格式。CurseForge 上默认为 Nothing。
|
||||
/// </summary>
|
||||
public string Version;
|
||||
|
||||
// 实例化
|
||||
|
||||
/// <summary>
|
||||
/// 从文件 Json 中初始化实例。若出错会抛出异常。
|
||||
/// </summary>
|
||||
public CompFile(JsonObject data, CompType defaultType)
|
||||
{
|
||||
Type = defaultType;
|
||||
if (data.ContainsKey("FromCurseForge"))
|
||||
{
|
||||
#region CompJson
|
||||
|
||||
FromCurseForge = data["FromCurseForge"].ToObject<bool>();
|
||||
Id = data["Id"].ToString();
|
||||
DisplayName = data["DisplayName"].ToString();
|
||||
if (data.ContainsKey("Version"))
|
||||
Version = data["Version"].ToString();
|
||||
ReleaseDate = data["ReleaseDate"].ToObject<DateTime>();
|
||||
DownloadCount = data["DownloadCount"].ToObject<int>();
|
||||
Status = (CompFileStatus)data["Status"].ToObject<int>();
|
||||
if (data.ContainsKey("FileName"))
|
||||
FileName = data["FileName"].ToString();
|
||||
if (data.ContainsKey("DownloadUrls"))
|
||||
DownloadUrls = data["DownloadUrls"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("ModLoaders"))
|
||||
ModLoaders = data["ModLoaders"].ToObject<List<CompLoaderType>>();
|
||||
if (data.ContainsKey("Hash"))
|
||||
Hash = data["Hash"].ToString();
|
||||
if (data.ContainsKey("RawGameVersions"))
|
||||
RawGameVersions = data["RawGameVersions"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("GameVersions"))
|
||||
GameVersions = data["GameVersions"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("RawDependencies"))
|
||||
RawDependencies = data["RawDependencies"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("Dependencies"))
|
||||
Dependencies = data["Dependencies"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("RawOptionalDependencies"))
|
||||
RawOptionalDependencies = data["RawOptionalDependencies"].ToObject<List<string>>();
|
||||
if (data.ContainsKey("OptionalDependencies"))
|
||||
OptionalDependencies = data["OptionalDependencies"].ToObject<List<string>>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
else
|
||||
{
|
||||
FromCurseForge = data.ContainsKey("gameId");
|
||||
if (FromCurseForge)
|
||||
{
|
||||
#region CurseForge
|
||||
|
||||
// 简单信息
|
||||
Id = data["id"].ToString();
|
||||
ProjectId = data["modId"].ToString();
|
||||
DisplayName = data["displayName"].ToString().Replace(" ", "").Trim(' ');
|
||||
Version = null;
|
||||
ReleaseDate = data["fileDate"].ToObject<DateTime>();
|
||||
Status = (CompFileStatus)data["releaseType"].ToObject<int>();
|
||||
DownloadCount = (int)data["downloadCount"];
|
||||
FileName = (string)data["fileName"];
|
||||
Hash =
|
||||
(string)((JsonArray)data["hashes"]).ToList().FirstOrDefault(s => s["algo"].ToObject<int>() == 1)?[
|
||||
"value"];
|
||||
if (Hash is null)
|
||||
Hash = (string)((JsonArray)data["hashes"]).ToList()
|
||||
.FirstOrDefault(s => s["algo"].ToObject<int>() == 2)?["value"];
|
||||
// DownloadAddress
|
||||
var url = data["downloadUrl"]?.ToString() ?? "";
|
||||
// TODO: 移除龙猫写的直接下载,换用提醒用户手动下载相关模组
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
url =
|
||||
$"https://edge.forgecdn.net/files/{int.Parse(Id[..4])}/{int.Parse(Id[4..])}/{FileName}";
|
||||
url = url.Replace(FileName, WebUtility.UrlEncode(FileName)); // 对文件名进行编码
|
||||
url = url.Replace("+", "%20"); // 修正被编码成 + 的空格,CurseForge 会对 + 号也进行编码
|
||||
DownloadUrls = ModDownload.DlSourceModDownloadGet(HandleCurseForgeDownloadUrls(url)); // 添加镜像源
|
||||
// Dependencies
|
||||
if (data.ContainsKey("dependencies"))
|
||||
{
|
||||
RawDependencies = data["dependencies"].AsArray()
|
||||
.Where(d => d["relationType"].ToObject<int>() == 3 &&
|
||||
d["modId"].ToObject<int>() != 306612 && d["modId"].ToObject<int>() != 634179)
|
||||
.Select(d => d["modId"].ToString()).ToList(); // 种类为必要依赖
|
||||
// 排除 Fabric API 和 Quilt API
|
||||
RawOptionalDependencies = data["dependencies"].AsArray()
|
||||
.Where(d => d["relationType"].ToObject<int>() == 2 &&
|
||||
d["modId"].ToObject<int>() != 306612 && d["modId"].ToObject<int>() != 634179)
|
||||
.Select(d => d["modId"].ToString()).ToList(); // 种类为可选依赖
|
||||
// 排除 Fabric API 和 Quilt API
|
||||
}
|
||||
|
||||
// GameVersions
|
||||
RawGameVersions = data["gameVersions"].AsArray().Select(t => t.ToString().Trim().ToLower()).ToList();
|
||||
GameVersions = RawGameVersions.Where(v => McInstanceInfo.IsFormatFit(v))
|
||||
.Select(v => v.Replace("-snapshot", Lang.Text("Download.Comp.Detail.CompItem.PreviewSuffix"))).Distinct().ToList();
|
||||
if (GameVersions.Count > 1)
|
||||
{
|
||||
GameVersions = GameVersions.Sort(McVersionComparer.CompareVersionGe).ToList();
|
||||
if (Type == CompType.ModPack)
|
||||
GameVersions = new List<string> { GameVersions[0] }; // 整合包理应只 "支持" 一个版本
|
||||
}
|
||||
else if (GameVersions.Count == 1)
|
||||
{
|
||||
GameVersions = GameVersions.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameVersions = new List<string> { Lang.Text("Download.Comp.Detail.CompItem.UnknownVersion") };
|
||||
}
|
||||
|
||||
// ModLoaders
|
||||
ModLoaders = new List<CompLoaderType>();
|
||||
if (RawGameVersions.Contains("forge"))
|
||||
ModLoaders.Add(CompLoaderType.Forge);
|
||||
if (RawGameVersions.Contains("fabric"))
|
||||
ModLoaders.Add(CompLoaderType.Fabric);
|
||||
if (RawGameVersions.Contains("quilt"))
|
||||
ModLoaders.Add(CompLoaderType.Quilt);
|
||||
if (RawGameVersions.Contains("neoforge"))
|
||||
ModLoaders.Add(CompLoaderType.NeoForge);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
else
|
||||
{
|
||||
#region Modrinth
|
||||
|
||||
// 简单信息
|
||||
Id = (string)data["id"];
|
||||
ProjectId = (string)data["project_id"];
|
||||
DisplayName = data["name"].ToString().Replace(" ", "").Trim(' ');
|
||||
Version = (string)data["version_number"];
|
||||
ReleaseDate = data["date_published"].ToObject<DateTime>();
|
||||
Status = data["version_type"].ToString() == "release" ? CompFileStatus.Release :
|
||||
data["version_type"].ToString() == "beta" ? CompFileStatus.Beta : CompFileStatus.Alpha;
|
||||
DownloadCount = (int)data["downloads"];
|
||||
if (((JsonArray)data["files"]).Any()) // 可能为空
|
||||
{
|
||||
var file = data["files"][0];
|
||||
FileName = (string)file["filename"];
|
||||
DownloadUrls = ModDownload.DlSourceModDownloadGet(file["url"].ToString()); // 同时添加了镜像源
|
||||
Hash = (string)file["hashes"]["sha1"];
|
||||
}
|
||||
|
||||
// ModLoaders
|
||||
// 结果可能混杂着 Mod、数据包和服务端插件
|
||||
var rawLoaders = data["loaders"].AsArray().Select(v => v.ToString()).ToList();
|
||||
ModLoaders = new List<CompLoaderType>();
|
||||
if (Type == CompType.Mod) // 以尽量宽容的方式检测加载器,以免同时兼容两种的项被删除
|
||||
{
|
||||
if (rawLoaders.Intersect(new[] { "bukkit", "folia", "paper", "purpur", "spigot" }).Any())
|
||||
Type = CompType.Plugin; // Veinminer Enchantment 同时支持服务端与 Fabric
|
||||
if (rawLoaders.Contains("datapack"))
|
||||
Type = CompType.DataPack;
|
||||
if (rawLoaders.Contains("forge"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Forge);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("neoforge"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.NeoForge);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("fabric"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Fabric);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("quilt"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Quilt);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
}
|
||||
else if (Type == CompType.DataPack)
|
||||
{
|
||||
if (rawLoaders.Intersect(new[] { "bukkit", "folia", "paper", "purpur", "spigot" }).Any())
|
||||
Type = CompType.Plugin;
|
||||
if (rawLoaders.Contains("forge"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Forge);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("neoforge"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.NeoForge);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("fabric"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Fabric);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("quilt"))
|
||||
{
|
||||
ModLoaders.Add(CompLoaderType.Quilt);
|
||||
Type = CompType.Mod;
|
||||
}
|
||||
|
||||
if (rawLoaders.Contains("datapack"))
|
||||
Type = CompType.DataPack;
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (data.ContainsKey("dependencies"))
|
||||
{
|
||||
RawDependencies = data["dependencies"].AsArray()
|
||||
.Where(d => (string)d["dependency_type"] == "required" &&
|
||||
d["project_id"] is not null &&
|
||||
(string)d["project_id"] != "P7dR8mSH" &&
|
||||
(string)d["project_id"] != "qvIfYCYJ" && d["project_id"] is not null)
|
||||
.Select(d => d["project_id"].ToString()).ToList(); // 种类为必要依赖
|
||||
// 排除 Fabric API 和 Quilt API
|
||||
// 有时候真的会空……
|
||||
RawOptionalDependencies = data["dependencies"].AsArray()
|
||||
.Where(d => (string)d["dependency_type"] == "optional" &&
|
||||
d["project_id"] is not null &&
|
||||
(string)d["project_id"] != "P7dR8mSH" &&
|
||||
(string)d["project_id"] != "qvIfYCYJ" && d["project_id"] is not null)
|
||||
.Select(d => d["project_id"].ToString()).ToList(); // 种类为可选依赖
|
||||
// 排除 Fabric API 和 Quilt API
|
||||
// 有时候真的会空……
|
||||
}
|
||||
|
||||
// GameVersions
|
||||
RawGameVersions = data["game_versions"].AsArray().Select(t => t.ToString().Trim().ToLower()).ToList();
|
||||
GameVersions = RawGameVersions.Where(v => v.Contains(".")).Select(v =>
|
||||
v.Contains("-") ? v.BeforeFirst("-") + Lang.Text("Download.Comp.Detail.CompItem.PreviewSuffix") : v.StartsWithF("b1.") ? Lang.Text("Download.Comp.Detail.CompItem.AncientVersion") : v).Distinct().ToList();
|
||||
if (GameVersions.Count > 1)
|
||||
{
|
||||
GameVersions = GameVersions.Sort(McVersionComparer.CompareVersionGe).ToList();
|
||||
if (Type == CompType.ModPack)
|
||||
GameVersions = new List<string> { GameVersions[0] }; // 整合包理应只 “支持” 一个版本
|
||||
}
|
||||
else if (GameVersions.Count == 1)
|
||||
{
|
||||
}
|
||||
// 无需处理
|
||||
else if (RawGameVersions.Any(v => v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")))
|
||||
{
|
||||
GameVersions = RawGameVersions.Where(v => v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameVersions = new List<string> { Lang.Text("Download.Comp.Detail.CompItem.UnknownVersion") };
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布状态的友好描述。例如:"正式版","Beta 版"。
|
||||
/// </summary>
|
||||
public string StatusDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case CompFileStatus.Release:
|
||||
{
|
||||
return Lang.Text("Download.Comp.Detail.FileList.ReleaseType.Release");
|
||||
}
|
||||
case CompFileStatus.Beta:
|
||||
{
|
||||
return Lang.Text("Download.Comp.Detail.FileList.ReleaseType.Beta");
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return Lang.Text("Download.Comp.Detail.FileList.ReleaseType.Alpha");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下载信息
|
||||
/// <summary>
|
||||
/// 下载信息是否可用。
|
||||
/// </summary>
|
||||
public bool Available => FileName is not null && DownloadUrls is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取下载信息。
|
||||
/// </summary>
|
||||
/// <param name="localAddress">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
|
||||
/// <param name="reason">下载原因。</param>
|
||||
/// <returns>下载信息。</returns>
|
||||
public DownloadFile ToNetFile(string localAddress, DownloadReason reason = DownloadReason.Standalone)
|
||||
{
|
||||
return ToNetFile(localAddress, reason, RawGameVersions.FirstOrDefault(), ModLoaders.FirstOrDefault());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取下载信息。
|
||||
/// </summary>
|
||||
/// <param name="localAddress">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
|
||||
/// <param name="reason">下载原因。</param>
|
||||
/// <param name="version">实例版本。</param>
|
||||
/// <param name="modLoader">实例的模组加载器。</param>
|
||||
/// <returns>下载信息。</returns>
|
||||
public DownloadFile ToNetFile(
|
||||
string localAddress,
|
||||
DownloadReason reason,
|
||||
string? version,
|
||||
CompLoaderType modLoader = CompLoaderType.Any)
|
||||
{
|
||||
if (DownloadUrls is null)
|
||||
{
|
||||
throw new InvalidCastException("DownloadUrls 为空");
|
||||
}
|
||||
|
||||
return new DownloadFile(HandleModrinthDownloadUrls(DownloadUrls, reason, version, modLoader),
|
||||
localAddress + (localAddress.EndsWithF(@"\") ? CompFileNameSanitize(FileName) : ""),
|
||||
new ModBase.FileChecker(hash: Hash), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对之前错误的 CurseForge 的下载地址进行修正。
|
||||
/// </summary>
|
||||
public static string HandleCurseForgeDownloadUrls(string url)
|
||||
{
|
||||
return url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://media.", "://edge.")
|
||||
.Replace("://mediafilez.", "://edge.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对 Modrinth 下载链接添加上报参数。
|
||||
/// </summary>
|
||||
/// <param name="urls">下载链接。</param>
|
||||
/// <param name="reason">下载原因。</param>
|
||||
/// <param name="version">实例版本。</param>
|
||||
/// <param name="modLoader">实例的模组加载器。</param>
|
||||
/// <returns>处理后的下载链接。</returns>
|
||||
public static IEnumerable<string> HandleModrinthDownloadUrls(
|
||||
IEnumerable<string> urls,
|
||||
DownloadReason reason = DownloadReason.Standalone,
|
||||
string? version = null,
|
||||
CompLoaderType modLoader = CompLoaderType.Any)
|
||||
{
|
||||
foreach (var url in urls)
|
||||
{
|
||||
if (!url.Contains("modrinth", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
yield return url;
|
||||
continue;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(url);
|
||||
|
||||
sb.Append(url.Contains('?') ? "&mr_download_reason=" : "?mr_download_reason=");
|
||||
sb.Append(reason.ToString().ToLowerInvariant());
|
||||
|
||||
if (version is not null)
|
||||
{
|
||||
sb.Append("&mr_game_version=");
|
||||
sb.Append(version);
|
||||
}
|
||||
|
||||
if (modLoader != CompLoaderType.Any)
|
||||
{
|
||||
sb.Append("&mr_loader=");
|
||||
sb.Append(modLoader.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
yield return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前实例转为可用于保存缓存的 Json。
|
||||
/// </summary>
|
||||
public JsonObject ToJson()
|
||||
{
|
||||
var json = new JsonObject();
|
||||
json.Add("FromCurseForge", FromCurseForge);
|
||||
json.Add("Id", Id);
|
||||
if (Version is not null)
|
||||
json.Add("Version", Version);
|
||||
json.Add("DisplayName", DisplayName);
|
||||
json.Add("ReleaseDate", ReleaseDate);
|
||||
json.Add("DownloadCount", DownloadCount);
|
||||
json.Add("ModLoaders", new JsonArray(ModLoaders.Select(m => (JsonNode)(int)m).ToArray()));
|
||||
json.Add("RawGameVersions", new JsonArray(RawGameVersions.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("GameVersions", new JsonArray(GameVersions.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("Status", (int)Status);
|
||||
if (FileName is not null)
|
||||
json.Add("FileName", FileName);
|
||||
if (DownloadUrls is not null)
|
||||
json.Add("DownloadUrls", new JsonArray(DownloadUrls.Select(s => (JsonNode)s).ToArray()));
|
||||
if (Hash is not null)
|
||||
json.Add("Hash", Hash);
|
||||
json.Add("RawDependencies", new JsonArray(RawDependencies.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("RawOptionalDependencies", new JsonArray(RawOptionalDependencies.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("Dependencies", new JsonArray(Dependencies.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("OptionalDependencies", new JsonArray(OptionalDependencies.Select(s => (JsonNode)s).ToArray()));
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前文件信息实例化为控件。
|
||||
/// </summary>
|
||||
public MyVirtualizingElement<MyListItem> ToListItem(MyListItem.ClickEventHandler onClick,
|
||||
MyIconButton.ClickEventHandler? onSaveClick = null,
|
||||
bool badDisplayName = false)
|
||||
{
|
||||
return new MyVirtualizingElement<MyListItem>(() =>
|
||||
{
|
||||
// 1. 获取基础描述信息
|
||||
var title = badDisplayName ? FileName : DisplayName;
|
||||
var info = new List<string>();
|
||||
|
||||
// 2. 填充信息列表
|
||||
if (title != FileName.BeforeLast("."))
|
||||
info.Add(FileName.BeforeLast("."));
|
||||
|
||||
if (Dependencies.Any())
|
||||
info.Add(Lang.Text("Download.Comp.Detail.FileList.DependencyCount", Dependencies.Count()));
|
||||
|
||||
// 简化后的游戏版本逻辑喵
|
||||
var snapshotKeywords = new[] { "w", "snapshot", "rc", "pre", "experimental", "-" };
|
||||
if (GameVersions.All(ver =>
|
||||
!ver.Contains('.') || snapshotKeywords.Any(s => ver.ContainsF(s, true))))
|
||||
info.Add(Lang.Text("Download.Comp.Detail.FileList.GameVersion", string.Join("、", GameVersions)));
|
||||
|
||||
if (DownloadCount > 0)
|
||||
info.Add(Lang.Text("Common.Format.DownloadCount", Lang.CompactNumber(DownloadCount)));
|
||||
|
||||
info.Add(Lang.Text("Download.Comp.Detail.FileList.Updated", Lang.TimeSpan(ReleaseDate - DateTime.Now)));
|
||||
|
||||
if (Status != CompFileStatus.Release)
|
||||
info.Add(StatusDescription);
|
||||
|
||||
// 3. 建立控件
|
||||
var newItem = new MyListItem
|
||||
{
|
||||
Title = title,
|
||||
SnapsToDevicePixels = true,
|
||||
Height = 42,
|
||||
Type = MyListItem.CheckType.Clickable,
|
||||
Tag = this,
|
||||
Info = string.Join(" | ", info),
|
||||
// 使用 switch 表达式精简 Logo 选择喵!
|
||||
Logo = Status switch
|
||||
{
|
||||
CompFileStatus.Release => ModBase.pathImage + "Icons/R.png",
|
||||
CompFileStatus.Beta => ModBase.pathImage + "Icons/B.png",
|
||||
_ => ModBase.pathImage + "Icons/A.png"
|
||||
}
|
||||
};
|
||||
newItem.Click += onClick;
|
||||
|
||||
// 4. 建立另存为按钮
|
||||
if (onSaveClick is not null)
|
||||
{
|
||||
var btnSave = new MyIconButton { SvgIcon = "lucide/save", ToolTip = Lang.Text("Download.Version.SaveAs") };
|
||||
ToolTipService.SetPlacement(btnSave, PlacementMode.Center);
|
||||
ToolTipService.SetVerticalOffset(btnSave, 30);
|
||||
ToolTipService.SetHorizontalOffset(btnSave, 2);
|
||||
btnSave.Click += onSaveClick;
|
||||
newItem.Buttons = new[] { btnSave };
|
||||
}
|
||||
|
||||
return newItem;
|
||||
})
|
||||
{ Height = 42 };
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Id}: {FileName}";
|
||||
}
|
||||
}
|
||||
|
||||
// 获取
|
||||
|
||||
/// <summary>
|
||||
/// 已知文件信息的缓存。
|
||||
/// </summary>
|
||||
public static ConcurrentDictionary<string, List<CompFile>> compFilesCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个工程下的全部文件列表。
|
||||
/// 必须在工作线程执行,失败会抛出异常。
|
||||
/// </summary>
|
||||
public static List<CompFile> CompFilesGet(string projectId, bool fromCurseForge)
|
||||
{
|
||||
// 1. 获取工程对象(使用 TryGetValue 提高效率并防止并发异常)
|
||||
CompProject targetProject = null;
|
||||
if (!compProjectCache.TryGetValue(projectId, out targetProject))
|
||||
{
|
||||
var url = fromCurseForge
|
||||
? $"https://api.curseforge.com/v1/mods/{projectId}"
|
||||
: $"https://api.modrinth.com/v2/project/{projectId}";
|
||||
if (fromCurseForge)
|
||||
{
|
||||
var json = ModDownload.DlModRequest<JsonObject>(url);
|
||||
targetProject = new CompProject((JsonObject)json["data"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetProject = new CompProject(ModDownload.DlModRequest<JsonObject>(url));
|
||||
}
|
||||
// 假设 CompProject 构造函数内已处理缓存,否则此处应添加缓存逻辑
|
||||
}
|
||||
|
||||
// 2. 获取并缓存文件列表
|
||||
if (!compFilesCache.ContainsKey(projectId))
|
||||
{
|
||||
ModBase.Log("[Comp] 开始获取文件列表:" + projectId);
|
||||
JsonArray resultJsonArray;
|
||||
if (fromCurseForge)
|
||||
{
|
||||
// 注意:若 pageSize=10000 失效,需考虑分页逻辑
|
||||
var response = ModDownload.DlModRequest<JsonObject>(
|
||||
$"https://api.curseforge.com/v1/mods/{projectId}/files?pageSize=10000"
|
||||
);
|
||||
|
||||
resultJsonArray = (JsonArray)response["data"];
|
||||
}
|
||||
else
|
||||
{
|
||||
resultJsonArray =
|
||||
ModDownload.DlModRequest<JsonArray>($"https://api.modrinth.com/v2/project/{projectId}/version?include_changelog=false");
|
||||
}
|
||||
|
||||
compFilesCache[projectId] = resultJsonArray.Select(a => new CompFile((JsonObject)a, targetProject.Type))
|
||||
.Where(a => a.Available).GroupBy(a => a.Id).Select(g => g.First())
|
||||
.ToList(); // 使用 GroupBy 实现更高效的 Distinct
|
||||
}
|
||||
|
||||
var currentFiles = compFilesCache[projectId];
|
||||
|
||||
// 3. 提取所有需要获取信息的前置 ID(合并必要和可选)
|
||||
var allRawDeps = currentFiles.SelectMany(f => f.RawDependencies.Concat(f.RawOptionalDependencies)).Distinct()
|
||||
.ToList();
|
||||
var undoneDeps = allRawDeps.Where(id => !compProjectCache.ContainsKey(id)).ToList();
|
||||
|
||||
// 4. 批量请求缺失的前置工程信息
|
||||
if (undoneDeps.Any())
|
||||
{
|
||||
ModBase.Log($"[Comp] {projectId} 需要补全信息的依赖项共 {undoneDeps.Count} 个");
|
||||
JsonArray projects;
|
||||
if (fromCurseForge)
|
||||
{
|
||||
// 1. 获取响应并转为 JsonObject
|
||||
var response = ModDownload.DlModRequest<JsonObject>(
|
||||
"https://api.curseforge.com/v1/mods",
|
||||
"POST",
|
||||
"{\"modIds\": [" + string.Join(",", undoneDeps) + "]}",
|
||||
"application/json"
|
||||
);
|
||||
|
||||
// 2. 提取 data 数组
|
||||
projects = (JsonArray)response["data"];
|
||||
}
|
||||
else
|
||||
{
|
||||
projects = ModDownload.DlModRequest<JsonArray>(
|
||||
$"https://api.modrinth.com/v2/projects?ids=[\"{undoneDeps.Join("\",\"")}\"]");
|
||||
}
|
||||
|
||||
foreach (var project in projects)
|
||||
new CompProject((JsonObject)project);
|
||||
}
|
||||
|
||||
// 5. 建立文件与依赖工程的关联映射
|
||||
// 优化:预先筛选出存在于缓存中的依赖工程,避免在多层循环中重复查询字典
|
||||
var availableDeps = allRawDeps.Where(id => compProjectCache.ContainsKey(id) && (id ?? "") != (projectId ?? ""))
|
||||
.Select(id => compProjectCache[id]).ToList();
|
||||
|
||||
foreach (var file in currentFiles)
|
||||
foreach (var dep in availableDeps)
|
||||
{
|
||||
// 处理必要依赖
|
||||
if (file.RawDependencies.Contains(dep.Id))
|
||||
if (!file.Dependencies.Contains(dep.Id))
|
||||
file.Dependencies.Add(dep.Id);
|
||||
|
||||
// 处理可选依赖
|
||||
if (file.RawOptionalDependencies.Contains(dep.Id))
|
||||
if (!file.OptionalDependencies.Contains(dep.Id))
|
||||
file.OptionalDependencies.Add(dep.Id);
|
||||
}
|
||||
|
||||
return compFilesCache[projectId];
|
||||
}
|
||||
|
||||
public static string CompFileNameGet(CompProject proj, CompFile file)
|
||||
{
|
||||
string fileName;
|
||||
if ((proj.TranslatedName ?? "") == (proj.RawName ?? ""))
|
||||
{
|
||||
fileName = file.FileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
var chineseName = proj.TranslatedName.BeforeFirst(" (").BeforeFirst(" - ").Replace(@"\", "\")
|
||||
.Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">")
|
||||
.Replace("*", "*").Replace("?", "?").Replace("\"", "").Replace(": ", ":");
|
||||
fileName = Config.Download.Comp.NameFormatV2 switch
|
||||
{
|
||||
0 => $"【{chineseName}】{file.FileName}",
|
||||
1 => $"[{chineseName}] {file.FileName}",
|
||||
2 => $"{chineseName}-{file.FileName}",
|
||||
3 => $"{file.FileName}-{chineseName}",
|
||||
_ => file.FileName
|
||||
};
|
||||
}
|
||||
|
||||
if (file.Type == CompType.Mod)
|
||||
fileName = fileName.Replace("~", "-"); // ~ 会导致 Mixin 加载失败
|
||||
return CompFileNameSanitize(fileName);
|
||||
}
|
||||
|
||||
public static string CompFileNameSanitize(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return "download";
|
||||
|
||||
var sanitized = new StringBuilder(fileName.Length);
|
||||
foreach (var c in fileName)
|
||||
{
|
||||
sanitized.Append(c switch
|
||||
{
|
||||
'\\' => '\',
|
||||
'/' => '/',
|
||||
':' => ':',
|
||||
'*' => '*',
|
||||
'?' => '?',
|
||||
'"' => '"',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'|' => '|',
|
||||
_ when char.IsControl(c) => '_',
|
||||
_ => c
|
||||
});
|
||||
}
|
||||
|
||||
var result = sanitized.ToString().Trim();
|
||||
return result is "" or "." or ".." ? "download" : result;
|
||||
}
|
||||
|
||||
#region 快速下载(资源卡片下载按钮)
|
||||
|
||||
/// <summary>
|
||||
/// 资源卡片的快速下载入口:按 <see cref="Config.Download.Comp.QuickDownloadBehavior"/> 指定的行为,
|
||||
/// 下载该资源最新(优先 Release、其次最新发布)的兼容版本到目标实例或文件夹。
|
||||
/// 由 <see cref="MyCompItem"/> 上的快速下载按钮调用。快速下载不会自动安装前置。
|
||||
/// </summary>
|
||||
public static void QuickDownload(CompProject project)
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.Loading"), HintType.Info);
|
||||
var files = FilterFilesByType(
|
||||
CompFilesGet(project.Id, project.FromCurseForge).Where(f => f.Available).ToList(),
|
||||
project.Type);
|
||||
if (files.Count == 0)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoFile"), HintType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var behavior = Config.Download.Comp.QuickDownloadBehavior;
|
||||
if (behavior == 0)
|
||||
{
|
||||
// 总是询问:弹「方式选择」
|
||||
int? choice = ModBase.RunInUiWait(() =>
|
||||
{
|
||||
var options = new List<IMyRadio>
|
||||
{
|
||||
new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.CurrentInstance") },
|
||||
new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.AskInstance") },
|
||||
new MyRadioBox { Text = Lang.Text("Download.Comp.QuickDownload.ChooseMethod.AskPath") }
|
||||
};
|
||||
return ModMain.MyMsgBoxSelect(options,
|
||||
Lang.Text("Download.Comp.QuickDownload.ChooseMethod.Title"),
|
||||
button1: Lang.Text("Common.Action.Continue"),
|
||||
button2: Lang.Text("Common.Action.Cancel"));
|
||||
});
|
||||
if (choice is null) return; // 用户取消
|
||||
behavior = choice.Value + 1; // 0→1 当前实例, 1→2 选实例, 2→3 选路径
|
||||
}
|
||||
|
||||
switch (behavior)
|
||||
{
|
||||
case 1: // 下载到当前选中实例
|
||||
_QuickDownloadToInstance(project, files, ModInstanceList.McMcInstanceSelected);
|
||||
break;
|
||||
case 2: // 询问并下载到选择的实例
|
||||
{
|
||||
var instance = _QuickDownloadPickInstance(project, files);
|
||||
if (instance is null) return;
|
||||
_QuickDownloadToInstance(project, files, instance);
|
||||
break;
|
||||
}
|
||||
case 3: // 询问并下载到一个路径
|
||||
_QuickDownloadToFolder(project, files);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"[Comp] 快速下载失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Comp.Error.OperationFailed"));
|
||||
}
|
||||
}, "Comp QuickDownload");
|
||||
}
|
||||
|
||||
/// <summary>下载到指定实例的最新兼容版本。</summary>
|
||||
private static void _QuickDownloadToInstance(CompProject project, List<CompFile> files, McInstance? instance)
|
||||
{
|
||||
if (instance is null)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoInstance"), HintType.Info);
|
||||
return;
|
||||
}
|
||||
if (!instance.IsLoaded) instance.Load();
|
||||
var compatible = files
|
||||
.Where(f => IsInstanceSuitableForFile(instance, f, _ResolveLoaders(f, project)))
|
||||
.ToList();
|
||||
var file = _PickLatestFile(compatible);
|
||||
if (file is null)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoCompatibleFile"), HintType.Info);
|
||||
return;
|
||||
}
|
||||
var folder = instance.PathIndie + _GetSubFolder(project.Type);
|
||||
Directory.CreateDirectory(folder);
|
||||
var target = Path.Combine(folder, CompFileNameGet(project, file));
|
||||
_StartQuickDownload(file, target);
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.DownloadStarted", project.RawName), HintType.Success);
|
||||
}
|
||||
|
||||
/// <summary>弹实例列表让用户选择,返回选中的实例(兼容者优先、当前选中实例居首);取消或无兼容实例返回 null。</summary>
|
||||
private static McInstance? _QuickDownloadPickInstance(CompProject project, List<CompFile> files)
|
||||
{
|
||||
var needLoad = ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished;
|
||||
if (needLoad)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.Loading"), HintType.Info);
|
||||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||||
ModLoader.LoaderFolderRunType.ForceRun, 1, "versions\\", true);
|
||||
}
|
||||
var compatible = ModInstanceList.mcInstanceList.Values
|
||||
.SelectMany(l => l)
|
||||
.Where(v => v is not null && files.Any(f => IsInstanceSuitableForFile(v, f, _ResolveLoaders(f, project))))
|
||||
.ToList();
|
||||
if (compatible.Count == 0)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoCompatibleInstance"), HintType.Info);
|
||||
return null;
|
||||
}
|
||||
// 当前选中实例排首位(呼应 issue「第一行应为当前所选实例」)
|
||||
var current = ModInstanceList.McMcInstanceSelected;
|
||||
if (current is not null)
|
||||
compatible = compatible
|
||||
.OrderBy(v => v == current ? 0 : 1)
|
||||
.ThenBy(v => v.Name)
|
||||
.ToList();
|
||||
int? idx = ModBase.RunInUiWait(() =>
|
||||
{
|
||||
var options = compatible
|
||||
.Select(v => (IMyRadio)new MyRadioBox { Text = v.Name })
|
||||
.ToList();
|
||||
return ModMain.MyMsgBoxSelect(options,
|
||||
Lang.Text("Download.Comp.QuickDownload.ChooseInstance.Title"),
|
||||
button1: Lang.Text("Common.Action.Continue"),
|
||||
button2: Lang.Text("Common.Action.Cancel"));
|
||||
});
|
||||
if (idx is null) return null;
|
||||
return compatible[idx.Value];
|
||||
}
|
||||
|
||||
/// <summary>下载最新版本到用户选择的文件夹。</summary>
|
||||
private static void _QuickDownloadToFolder(CompProject project, List<CompFile> files)
|
||||
{
|
||||
var file = _PickLatestFile(files);
|
||||
if (file is null)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.NoFile"), HintType.Info);
|
||||
return;
|
||||
}
|
||||
var saveFolder = ModBase.RunInUiWait(() =>
|
||||
SystemDialogs.SelectFolder(Lang.Text("Download.Comp.QuickDownload.Hint.SelectFolder")));
|
||||
if (string.IsNullOrWhiteSpace(saveFolder)) return; // 取消
|
||||
var target = Path.Combine(saveFolder, CompFileNameGet(project, file));
|
||||
_StartQuickDownload(file, target);
|
||||
HintService.Hint(Lang.Text("Download.Comp.QuickDownload.Hint.DownloadStarted", project.RawName), HintType.Success);
|
||||
}
|
||||
|
||||
/// <summary>构造并启动单文件下载任务(与详情页 Save_Click 末段一致)。</summary>
|
||||
private static void _StartQuickDownload(CompFile file, string target)
|
||||
{
|
||||
var desc = file.Type switch
|
||||
{
|
||||
CompType.Mod => Lang.Text("Download.Comp.Type.Mod"),
|
||||
CompType.ResourcePack => Lang.Text("Download.Comp.Type.ResourcePack"),
|
||||
CompType.Shader => Lang.Text("Download.Comp.Type.Shader"),
|
||||
CompType.DataPack => Lang.Text("Download.Comp.Type.DataPack"),
|
||||
CompType.World => Lang.Text("Download.Comp.Type.World"),
|
||||
_ => Lang.Text("Download.Comp.Type.Mod")
|
||||
};
|
||||
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 == CompType.Mod
|
||||
? file.ToNetFile(target)
|
||||
: file.ToNetFile(target, DownloadReason.Standalone, null)
|
||||
})
|
||||
{
|
||||
ProgressWeight = 6,
|
||||
block = true
|
||||
}
|
||||
};
|
||||
if (file.Type == CompType.World)
|
||||
{
|
||||
var extractDir = Path.GetDirectoryName(target);
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(
|
||||
Lang.Text("Download.Comp.Detail.InstallWorld"),
|
||||
_ => ModBase.ExtractFile(target, extractDir, 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(1);
|
||||
ModLoader.LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||||
}
|
||||
|
||||
/// <summary>根据资源类型返回实例内的目标子文件夹(与 Save_Click 一致)。</summary>
|
||||
private static string _GetSubFolder(CompType type) => type switch
|
||||
{
|
||||
CompType.Mod => "mods\\",
|
||||
CompType.ResourcePack => "resourcepacks\\",
|
||||
CompType.Shader => "shaderpacks\\",
|
||||
CompType.World => "saves\\",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
/// <summary>取文件自身声明的加载器,缺失时回退到工程的加载器。</summary>
|
||||
private static List<CompLoaderType> _ResolveLoaders(CompFile file, CompProject project)
|
||||
=> file.ModLoaders.Count > 0 ? file.ModLoaders : project.ModLoaders;
|
||||
|
||||
/// <summary>
|
||||
/// 按资源类型筛选文件,与详情页 GetResults 一致:Modrinth 会返回 Mod / 服务端插件 / 数据包混合的列表,
|
||||
/// 需过滤回当前类型,避免快速下载到另一种产物。光影与资源包不筛(原版光影以资源包格式发布)。
|
||||
/// </summary>
|
||||
private static List<CompFile> FilterFilesByType(List<CompFile> files, CompType type)
|
||||
{
|
||||
if (type == CompType.Shader || type == CompType.ResourcePack)
|
||||
return files;
|
||||
return files.Where(f => f.Type == type).ToList();
|
||||
}
|
||||
|
||||
/// <summary>判断某实例是否兼容该文件(基于 Save_Click 的 isVersionSuitable,补全了 Quilt 判定)。</summary>
|
||||
public static bool IsInstanceSuitableForFile(McInstance? version, CompFile file, List<CompLoaderType> allowedLoaders)
|
||||
{
|
||||
if (version is null) return false;
|
||||
if (!version.IsLoaded) version.Load();
|
||||
|
||||
// 只对 Mod 和数据包进行版本检测
|
||||
if (file.Type == CompType.Mod || file.Type == CompType.DataPack)
|
||||
if (file.GameVersions.Any(v => v.Contains(".")) &&
|
||||
!file.GameVersions.Any(v => v.Contains(".") && v == version.Info.VanillaName))
|
||||
return false;
|
||||
|
||||
// 加载器判定
|
||||
if (allowedLoaders.Count == 0) return true; // 无要求
|
||||
if (allowedLoaders.Contains(CompLoaderType.Forge) && version.Info.HasForge) return true;
|
||||
if (allowedLoaders.Contains(CompLoaderType.Forge) && version.Info.HasCleanroom)
|
||||
return true;
|
||||
if (allowedLoaders.Contains(CompLoaderType.Fabric) &&
|
||||
(version.Info.HasFabric || version.Info.HasLegacyFabric)) return true;
|
||||
if (allowedLoaders.Contains(CompLoaderType.NeoForge) && version.Info.HasNeoForge) return true;
|
||||
if (allowedLoaders.Contains(CompLoaderType.Quilt) && version.Info.HasQuilt) return true;
|
||||
if (allowedLoaders.Contains(CompLoaderType.LiteLoader) && version.Info.HasLiteLoader) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>挑选最新文件:优先 Release,其次按发布日期最新。compatFilter 为空时不做兼容过滤。</summary>
|
||||
private static CompFile? _PickLatestFile(List<CompFile> files, Func<CompFile, bool>? compatFilter = null)
|
||||
{
|
||||
var candidates = (compatFilter is null ? files : files.Where(compatFilter)).ToList();
|
||||
if (candidates.Count == 0) return null;
|
||||
return candidates
|
||||
.OrderByDescending(f => f.Status == CompFileStatus.Release)
|
||||
.ThenByDescending(f => f.ReleaseDate)
|
||||
.First();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 预载包含大量 CompFile 的卡片,添加必要的元素和前置列表。
|
||||
/// 前置列表(必要 / 可选)会被放入可折叠栏:必要前置默认展开,可选前置默认收起。
|
||||
/// </summary>
|
||||
public static void CompFilesCardPreload(StackPanel stack, List<CompFile> files)
|
||||
{
|
||||
// 获取卡片对应的前置 ID
|
||||
// 如果为整合包就不会有 Dependencies 信息,所以不用管
|
||||
var deps = files.SelectMany(f => f.Dependencies).Distinct().ToList();
|
||||
var optionalDeps = files.SelectMany(f => f.OptionalDependencies).Distinct().ToList();
|
||||
if (!deps.Any() && !optionalDeps.Any())
|
||||
return;
|
||||
|
||||
// 必要前置:默认展开
|
||||
_AddDependencyBar(stack, deps,
|
||||
Lang.Text("Download.Comp.Detail.FileList.RequiredDependencies"), collapsed: false);
|
||||
// 可选前置:默认收起(库 Mod 可能有大量可选前置,参见 Issue #2873)
|
||||
_AddDependencyBar(stack, optionalDeps,
|
||||
Lang.Text("Download.Comp.Detail.FileList.OptionalDependencies"), collapsed: true);
|
||||
|
||||
// 添加结尾间隔(版本列表标题)
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Lang.Text("Download.Comp.Detail.FileList.VersionList"), FontSize = 14d,
|
||||
HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(6d, 12d, 0d, 5d)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一组前置依赖(按工程 ID)渲染为一个可折叠栏并加入 <paramref name="stack"/>。
|
||||
/// 仅保留在 compProjectCache 中有信息的前置;若过滤后为空则不添加任何折叠栏。
|
||||
/// </summary>
|
||||
/// <param name="collapsed">是否默认收起。前置 item 全部加入即可,靠 MyVirtualizingElement 在可见时才实例化。</param>
|
||||
private static void _AddDependencyBar(StackPanel stack, List<string> depIds, string title, bool collapsed)
|
||||
{
|
||||
if (depIds is null || !depIds.Any())
|
||||
return;
|
||||
|
||||
depIds.Sort();
|
||||
var projects = new List<CompProject>();
|
||||
foreach (var dep in depIds)
|
||||
{
|
||||
if (compProjectCache.TryGetValue(dep, out var project))
|
||||
projects.Add(project);
|
||||
else
|
||||
ModBase.Log($"[Comp] 未找到 ID {dep} 的前置信息", ModBase.LogLevel.Debug);
|
||||
}
|
||||
if (!projects.Any())
|
||||
return;
|
||||
|
||||
var bar = new MyCollapseBar
|
||||
{
|
||||
Title = $"{title} ({projects.Count})",
|
||||
IsCollapsed = collapsed
|
||||
};
|
||||
foreach (var project in projects)
|
||||
bar.ContentPanel.Children.Add(project.ToCompItem(false, false));
|
||||
|
||||
stack.Children.Add(bar);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.IO;
|
||||
using CompFile = PCL.ModComp.CompFile;
|
||||
using CompFileStatus = PCL.ModComp.CompFileStatus;
|
||||
using CompLoaderType = PCL.ModComp.CompLoaderType;
|
||||
using CompProject = PCL.ModComp.CompProject;
|
||||
using LocalCompFile = PCL.ModLocalComp.LocalCompFile;
|
||||
using PCL.Core.Minecraft.ResourceProject;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModCompDependency
|
||||
{
|
||||
public static ModDependencyRequest BuildRequest(
|
||||
CompFile file,
|
||||
CompProject project,
|
||||
string targetMinecraftVersion,
|
||||
List<CompLoaderType> targetLoaders,
|
||||
string targetModsFolder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(file);
|
||||
ArgumentNullException.ThrowIfNull(project);
|
||||
targetLoaders ??= new List<CompLoaderType>();
|
||||
|
||||
var source = GetSource(project.FromCurseForge);
|
||||
var dependencies = file.Dependencies
|
||||
.Where(static dependencyId => !string.IsNullOrWhiteSpace(dependencyId))
|
||||
.Select(dependencyId => new ModDependencyReference
|
||||
{
|
||||
ProjectId = dependencyId,
|
||||
Source = source,
|
||||
IsRequired = true,
|
||||
})
|
||||
.Concat(file.OptionalDependencies
|
||||
.Where(static dependencyId => !string.IsNullOrWhiteSpace(dependencyId))
|
||||
.Select(dependencyId => new ModDependencyReference
|
||||
{
|
||||
ProjectId = dependencyId,
|
||||
Source = source,
|
||||
IsRequired = false,
|
||||
}))
|
||||
.ToList();
|
||||
|
||||
return new ModDependencyRequest
|
||||
{
|
||||
TargetMinecraftVersion = targetMinecraftVersion ?? string.Empty,
|
||||
TargetLoaders = ToLoaderNames(targetLoaders),
|
||||
RequiredDependencies = dependencies,
|
||||
InstalledMods = ScanInstalledMods(targetModsFolder),
|
||||
ProjectResolver = ResolveProjectFiles,
|
||||
};
|
||||
}
|
||||
|
||||
public static List<InstalledModIdentity> ScanInstalledMods(string targetModsFolder)
|
||||
{
|
||||
var result = new List<InstalledModIdentity>();
|
||||
if (string.IsNullOrWhiteSpace(targetModsFolder) || !Directory.Exists(targetModsFolder))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var path in Directory.GetFiles(targetModsFolder))
|
||||
{
|
||||
if (!LocalCompFile.IsModFile(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var localFile = new LocalCompFile(path);
|
||||
localFile.Load();
|
||||
|
||||
var source = localFile.Comp is null ? null : GetSource(localFile.Comp.FromCurseForge);
|
||||
var gameVersions = localFile.compFile?.GameVersions?.Where(static version => !string.IsNullOrWhiteSpace(version)).ToList()
|
||||
?? new List<string>();
|
||||
var loaders = ToLoaderNames(localFile.compFile?.ModLoaders);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(localFile.Comp?.Id) && !string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
result.Add(new InstalledModIdentity
|
||||
{
|
||||
SourceProjectId = localFile.Comp.Id,
|
||||
Source = source,
|
||||
ModId = localFile.ModId,
|
||||
GameVersions = gameVersions,
|
||||
Loaders = loaders,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(localFile.compFile?.ProjectId))
|
||||
{
|
||||
var fileSource = GetSource(localFile.compFile.FromCurseForge);
|
||||
result.Add(new InstalledModIdentity
|
||||
{
|
||||
SourceProjectId = localFile.compFile.ProjectId,
|
||||
Source = fileSource,
|
||||
ModId = localFile.ModId,
|
||||
GameVersions = gameVersions,
|
||||
Loaders = loaders,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new InstalledModIdentity
|
||||
{
|
||||
SourceProjectId = null,
|
||||
Source = null,
|
||||
ModId = localFile.ModId,
|
||||
GameVersions = gameVersions,
|
||||
Loaders = loaders,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ModDependencyProject? ResolveProjectFiles(string source, string projectId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fromCurseForge = string.Equals(source, "CurseForge", StringComparison.OrdinalIgnoreCase);
|
||||
var files = ModComp.CompFilesGet(projectId, fromCurseForge);
|
||||
if (!ModComp.compProjectCache.TryGetValue(projectId, out var compProject))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compProject.FromCurseForge != fromCurseForge)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ModDependencyProject
|
||||
{
|
||||
ProjectId = compProject.Id,
|
||||
Source = source,
|
||||
ProjectName = compProject.TranslatedName ?? compProject.RawName,
|
||||
Files = files.Select(compFile => new ModDependencyFile
|
||||
{
|
||||
Id = compFile.Id,
|
||||
DisplayName = compFile.DisplayName,
|
||||
Version = compFile.Version,
|
||||
GameVersions = compFile.GameVersions?.Where(static version => !string.IsNullOrWhiteSpace(version)).ToList()
|
||||
?? new List<string>(),
|
||||
Loaders = ToLoaderNames(compFile.ModLoaders),
|
||||
ReleaseType = MapReleaseType(compFile.Status),
|
||||
ReleaseDate = compFile.ReleaseDate,
|
||||
RequiredDependencies = compFile.Dependencies
|
||||
.Where(static dependencyId => !string.IsNullOrWhiteSpace(dependencyId))
|
||||
.Select(dependencyId => new ModDependencyReference
|
||||
{
|
||||
ProjectId = dependencyId,
|
||||
Source = source,
|
||||
IsRequired = true,
|
||||
})
|
||||
.ToList(),
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
public static ModDependencyFile? SelectCompatibleDependencyFile(
|
||||
ModDependencyResolutionResult result,
|
||||
string projectId,
|
||||
string source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
return result.ToInstall
|
||||
.FirstOrDefault(install =>
|
||||
string.Equals(install.ProjectId, projectId, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(install.Source, source, StringComparison.OrdinalIgnoreCase))
|
||||
?.File;
|
||||
}
|
||||
|
||||
public static List<(string Filename, DownloadFile File)> BuildDependencyDownloads(
|
||||
ModDependencyResolutionResult result,
|
||||
string targetModsFolder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
var downloads = new List<(string, DownloadFile)>();
|
||||
foreach (var install in result.ToInstall.AsEnumerable().Reverse())
|
||||
{
|
||||
if (!ModComp.compProjectCache.TryGetValue(install.ProjectId, out var depProject))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fromCurseForge = string.Equals(install.Source, "CurseForge", StringComparison.OrdinalIgnoreCase);
|
||||
if (depProject.FromCurseForge != fromCurseForge)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var depCompFile = ModComp.CompFilesGet(install.ProjectId, fromCurseForge)
|
||||
.FirstOrDefault(file => string.Equals(file.Id, install.File.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (depCompFile is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var depFileName = ModComp.CompFileNameGet(depProject, depCompFile);
|
||||
var targetPath = Path.Combine(targetModsFolder ?? string.Empty, depFileName);
|
||||
downloads.Add((depFileName, depCompFile.ToNetFile(targetPath, ModComp.DownloadReason.Dependency)));
|
||||
}
|
||||
|
||||
return downloads;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows confirmation dialog for required dependency installs.
|
||||
/// Returns: CompDepsInstallTypes.WithDeps if user chooses to install with deps,
|
||||
/// CompDepsInstallTypes.WithoutDeps if user chooses to install without deps,
|
||||
/// CompDepsInstallTypes.Cancel if user cancels,
|
||||
/// CompDepsInstallTypes.Unresolved if there are unresolved required deps.
|
||||
/// </summary>
|
||||
public static ModComp.CompDepsInstallTypes ConfirmDependencyInstall(ModDependencyResolutionResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (result.Unresolved is { Count: > 0 })
|
||||
{
|
||||
ModBase.Log($"[CompDeps] 无法解析: {result.Unresolved.Count} 个必需前置");
|
||||
var dependencies = string.Join(
|
||||
Environment.NewLine,
|
||||
result.Unresolved.Select(dep => Lang.Text(
|
||||
"Download.Comp.Dependency.Unresolved.ListItem",
|
||||
dep.Source,
|
||||
dep.ProjectId,
|
||||
dep.Reason)));
|
||||
var message = Lang.Text("Download.Comp.Dependency.Unresolved.Message", dependencies);
|
||||
var selectedButton = ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("Download.Comp.Dependency.Unresolved.Title"),
|
||||
Lang.Text("Download.Comp.Dependency.Unresolved.ContinueWithoutDependencies"),
|
||||
Lang.Text("Common.Action.Cancel"),
|
||||
isWarn: true,
|
||||
forceWait: true);
|
||||
|
||||
return selectedButton == 1
|
||||
? ModComp.CompDepsInstallTypes.Unresolved
|
||||
: ModComp.CompDepsInstallTypes.Cancel;
|
||||
}
|
||||
|
||||
if (result.ToInstall is { Count: > 0 })
|
||||
{
|
||||
var dependencies = string.Join(
|
||||
Environment.NewLine,
|
||||
result.ToInstall.Select(install => Lang.Text(
|
||||
"Download.Comp.Dependency.Install.ListItem",
|
||||
install.ProjectName,
|
||||
install.Source,
|
||||
install.File.DisplayName,
|
||||
install.File.Version)));
|
||||
var message = Lang.Text("Download.Comp.Dependency.Install.Message", dependencies);
|
||||
var dialogResult = ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("Download.Comp.Dependency.Install.Title"),
|
||||
Lang.Text("Download.Comp.Dependency.Install.WithDependencies"),
|
||||
Lang.Text("Download.Comp.Dependency.Install.WithoutDependencies"),
|
||||
Lang.Text("Download.Comp.Dependency.Install.Cancel"),
|
||||
forceWait: true);
|
||||
|
||||
return dialogResult switch
|
||||
{
|
||||
1 => ModComp.CompDepsInstallTypes.WithDeps,
|
||||
2 => ModComp.CompDepsInstallTypes.WithoutDeps,
|
||||
3 => ModComp.CompDepsInstallTypes.Cancel,
|
||||
_ => ModComp.CompDepsInstallTypes.Cancel
|
||||
};
|
||||
}
|
||||
|
||||
return ModComp.CompDepsInstallTypes.WithDeps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows abort message when dependency resolution was cancelled by user or failed.
|
||||
/// </summary>
|
||||
public static void ShowDependencyAbortMessage(string reason)
|
||||
{
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Download.Comp.Dependency.Abort.Message", reason),
|
||||
Lang.Text("Download.Comp.Dependency.Abort.Title"),
|
||||
Lang.Text("Common.Action.Confirm"),
|
||||
isWarn: false,
|
||||
forceWait: true);
|
||||
}
|
||||
|
||||
private static string GetSource(bool fromCurseForge)
|
||||
{
|
||||
return fromCurseForge ? "CurseForge" : "Modrinth";
|
||||
}
|
||||
|
||||
private static List<string> ToLoaderNames(IEnumerable<CompLoaderType>? loaders)
|
||||
{
|
||||
if (loaders is null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
return loaders
|
||||
.Where(static loader => loader != CompLoaderType.Any)
|
||||
.Select(static loader => loader.ToString())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int MapReleaseType(CompFileStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
CompFileStatus.Release => 1,
|
||||
CompFileStatus.Beta => 2,
|
||||
CompFileStatus.Alpha => 3,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2502 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModDownload
|
||||
{
|
||||
#region DlClient* | Minecraft 客户端
|
||||
|
||||
/// <summary>
|
||||
/// 返回某 Minecraft 版本对应的原版主 Jar 文件的下载信息,要求对应依赖实例已存在。
|
||||
/// 失败则抛出异常,不需要下载则返回 Nothing。
|
||||
/// </summary>
|
||||
public static DownloadFile DlClientJarGet(McInstance version, bool returnNothingOnFileUseable)
|
||||
{
|
||||
// 获取底层继承实例
|
||||
try
|
||||
{
|
||||
while (!string.IsNullOrEmpty(version.InheritInstanceName))
|
||||
version = new McInstance(version.InheritInstanceName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取底层继承实例失败");
|
||||
}
|
||||
|
||||
// 检查 Json 是否标准
|
||||
if (version.JsonObject["downloads"] is null || version.JsonObject["downloads"]["client"] is null ||
|
||||
version.JsonObject["downloads"]["client"]["url"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NoJarDownloadInfo", version.Name));
|
||||
// 检查文件
|
||||
var checker = new ModBase.FileChecker(1024L, (long)(version.JsonObject["downloads"]["client"]["size"] ?? -1),
|
||||
(string)version.JsonObject["downloads"]["client"]["sha1"]);
|
||||
if (returnNothingOnFileUseable && checker.Check(version.PathInstance + version.Name + ".jar") is null)
|
||||
return null; // 通过校验
|
||||
// 返回下载信息
|
||||
var jarUrl = (string)version.JsonObject["downloads"]["client"]["url"];
|
||||
return new DownloadFile(DlSourceLauncherOrMetaGet(jarUrl), version.PathInstance + version.Name + ".jar",
|
||||
checker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回某 Minecraft 版本对应的原版主 AssetIndex 文件的下载信息,要求对应依赖实例已存在。
|
||||
/// 若未找到,则会返回 Legacy 资源文件或 Nothing。
|
||||
/// </summary>
|
||||
public static DownloadFile DlClientAssetIndexGet(McInstance version)
|
||||
{
|
||||
// 获取底层继承实例
|
||||
while (!string.IsNullOrEmpty(version.InheritInstanceName))
|
||||
version = new McInstance(version.InheritInstanceName);
|
||||
// 获取信息
|
||||
var indexInfo = ModAssets.McAssetsGetIndex(version, true, true);
|
||||
var indexAddress = Path.Combine(ModFolder.mcFolderSelected, "assets", "indexes", indexInfo["id"] + ".json");
|
||||
ModBase.Log("[Download] 实例 " + version.Name + " 对应的资源文件索引为 " + indexInfo["id"]);
|
||||
var indexUrl = (string)(indexInfo["url"] ?? "");
|
||||
if (string.IsNullOrEmpty(indexUrl)) return null;
|
||||
|
||||
return new DownloadFile(DlSourceLauncherOrMetaGet(indexUrl), indexAddress,
|
||||
new ModBase.FileChecker(canUseExistsFile: false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造补全某 Minecraft 版本的所有文件的加载器列表。失败会抛出异常。
|
||||
/// </summary>
|
||||
public static List<ModLoader.LoaderBase> DlClientFix(McInstance version, bool checkAssetsHash,
|
||||
AssetsIndexExistsBehaviour assetsIndexBehaviour)
|
||||
{
|
||||
var loaders = new List<ModLoader.LoaderBase>();
|
||||
|
||||
#region 下载支持库文件
|
||||
|
||||
if (ModLibrary.ShouldIgnoreFileCheck(version))
|
||||
{
|
||||
ModBase.Log("[Download] 已跳过所有 Libraries 检查");
|
||||
}
|
||||
else
|
||||
{
|
||||
var loadersLib = new List<ModLoader.LoaderBase>
|
||||
{
|
||||
new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Download.Stage.AnalyzeMissingLibraries"),
|
||||
task => task.output = ModLibrary.McLibNetFilesFromInstance(version)) { ProgressWeight = 1d },
|
||||
new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadLibraries"), new List<DownloadFile>())
|
||||
{ ProgressWeight = 15d }
|
||||
};
|
||||
// 构造加载器
|
||||
loaders.Add(
|
||||
new ModLoader.LoaderCombo<string>(Lang.Text("Minecraft.Download.Stage.DownloadLibraries.MainLoader"),
|
||||
loadersLib)
|
||||
{ block = false, show = false, ProgressWeight = 16d });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 下载资源文件
|
||||
|
||||
if (ModLibrary.ShouldIgnoreFileCheck(version))
|
||||
{
|
||||
ModBase.Log("[Download] 已跳过所有 Assets 检查");
|
||||
}
|
||||
else
|
||||
{
|
||||
var loadersAssets = new List<ModLoader.LoaderBase>();
|
||||
// 获取资源文件索引地址
|
||||
loadersAssets.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Download.Stage.AnalyzeAssetsIndex"), task =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var indexFile = DlClientAssetIndexGet(version);
|
||||
var indexFileInfo = new FileInfo(indexFile.LocalPath);
|
||||
if (assetsIndexBehaviour != AssetsIndexExistsBehaviour.AlwaysDownload &&
|
||||
indexFile.Check.Check(indexFile.LocalPath) is null)
|
||||
task.output = new List<DownloadFile>();
|
||||
else
|
||||
task.output = new List<DownloadFile> { indexFile };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.AssetIndexAnalysisFailed"), ex);
|
||||
}
|
||||
}) { ProgressWeight = 0.5d, show = false });
|
||||
// 下载资源文件索引
|
||||
loadersAssets.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssetsIndex"),
|
||||
new List<DownloadFile>())
|
||||
{ ProgressWeight = 2d });
|
||||
// 要求独立更新索引
|
||||
if (assetsIndexBehaviour == AssetsIndexExistsBehaviour.DownloadInBackground)
|
||||
{
|
||||
var loadersAssetsUpdate = new List<ModLoader.LoaderBase>();
|
||||
string tempAddress = null;
|
||||
string realAddress = null;
|
||||
loadersAssetsUpdate.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Download.Stage.AnalyzeAssetsIndex.Background"), task =>
|
||||
{
|
||||
var backAssetsFile = DlClientAssetIndexGet(version);
|
||||
realAddress = backAssetsFile.LocalPath;
|
||||
tempAddress = ModBase.pathTemp + @"Cache\" + backAssetsFile.LocalName;
|
||||
backAssetsFile.LocalPath = tempAddress;
|
||||
task.output = new List<DownloadFile> { backAssetsFile };
|
||||
// 检查是否需要更新:每天只更新一次
|
||||
if (File.Exists(realAddress) &&
|
||||
Math.Abs((File.GetLastWriteTime(realAddress).Date - DateTime.Now.Date).TotalDays) < 1d)
|
||||
{
|
||||
ModBase.Log("[Download] 无需更新资源文件索引,取消");
|
||||
task.Abort();
|
||||
}
|
||||
}));
|
||||
loadersAssetsUpdate.Add(new LoaderDownload(
|
||||
Lang.Text("Minecraft.Download.Stage.DownloadAssetsIndex.Background"), new List<DownloadFile>()));
|
||||
loadersAssetsUpdate.Add(new ModLoader.LoaderTask<List<DownloadFile>, string>(
|
||||
Lang.Text("Minecraft.Download.Stage.CopyAssetsIndex.Background"), task =>
|
||||
{
|
||||
ModBase.CopyFile(tempAddress, realAddress);
|
||||
ModLaunch.McLaunchLog("后台更新资源文件索引成功:" + tempAddress);
|
||||
}));
|
||||
var updater = new ModLoader.LoaderCombo<string>(
|
||||
Lang.Text("Minecraft.Download.Stage.UpdateAssetsIndex.Background"), loadersAssetsUpdate);
|
||||
ModBase.Log("[Download] 开始后台检查资源文件索引");
|
||||
updater.Start();
|
||||
}
|
||||
|
||||
// 获取资源文件地址
|
||||
loadersAssets.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Download.Stage.AnalyzeMissingAssets"), task =>
|
||||
{
|
||||
ModLoader.LoaderBase argprogressFeed = task;
|
||||
task.output = ModAssets.McAssetsFixList(version, checkAssetsHash, ref argprogressFeed);
|
||||
task = (ModLoader.LoaderTask<string, List<DownloadFile>>)argprogressFeed;
|
||||
})
|
||||
{
|
||||
ProgressWeight = 3d
|
||||
});
|
||||
// 下载资源文件
|
||||
loadersAssets.Add(
|
||||
new LoaderDownload(Lang.Text("Minecraft.Download.Stage.DownloadAssets"), new List<DownloadFile>())
|
||||
{ ProgressWeight = 25d });
|
||||
// 构造加载器
|
||||
loaders.Add(
|
||||
new ModLoader.LoaderCombo<string>(Lang.Text("Minecraft.Download.Stage.DownloadAssets.MainLoader"),
|
||||
loadersAssets)
|
||||
{ block = false, show = false, ProgressWeight = 30.5d });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return loaders;
|
||||
}
|
||||
|
||||
public enum AssetsIndexExistsBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 如果文件存在,则不进行下载。
|
||||
/// </summary>
|
||||
DontDownload,
|
||||
|
||||
/// <summary>
|
||||
/// 如果文件存在,则启动新的下载加载器进行独立的更新。
|
||||
/// </summary>
|
||||
DownloadInBackground,
|
||||
|
||||
/// <summary>
|
||||
/// 如果文件存在,也同样进行下载。
|
||||
/// </summary>
|
||||
AlwaysDownload
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlClientList | Minecraft 客户端 版本列表
|
||||
|
||||
/// <summary>
|
||||
/// 所有正式版的 Minecraft Drop 序数。
|
||||
/// 若从未完成过获取,返回 Nothing;否则必定存在元素,且从高到低排列。
|
||||
/// </summary>
|
||||
public static List<int> AllDrops
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_allDropsLock)
|
||||
{
|
||||
if (field is null)
|
||||
{
|
||||
var rawData = States.Game.Drops;
|
||||
if (string.IsNullOrEmpty(rawData))
|
||||
field = new List<int>();
|
||||
else
|
||||
field = rawData.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(d => (int)Math.Round(ModBase.Val(d))).ToList();
|
||||
}
|
||||
|
||||
return field.Count != 0 ? field : null;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (_allDropsLock)
|
||||
{
|
||||
field = value;
|
||||
States.Game.Drops = value.Join(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object _allDropsLock = new();
|
||||
|
||||
// 主加载器
|
||||
public struct DlClientListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Mojang”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string SourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool IsOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的 Json 数据。
|
||||
/// </summary>
|
||||
public JsonObject Value;
|
||||
// ''' <summary>
|
||||
// ''' 官方源的失败原因。若没有则为 Nothing。
|
||||
// ''' </summary>
|
||||
// Public OfficialError As Exception
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft 客户端 版本列表,主加载器。
|
||||
/// 若要求镜像源必须包含某个版本,则将该版本 ID 作为输入(#5195)。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<string, DlClientListResult> dlClientListLoader =
|
||||
new("DlClientList Main", DlClientListMain);
|
||||
|
||||
private static void DlClientListMain(ModLoader.LoaderTask<string, DlClientListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, DlClientListResult>, int>>
|
||||
{ new(dlClientListBmclapiLoader, 30), new(dlClientListMojangLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, DlClientListResult>, int>>
|
||||
{ new(dlClientListMojangLoader, 5), new(dlClientListBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, DlClientListResult>, int>>
|
||||
{ new(dlClientListMojangLoader, 60), new(dlClientListBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取所有 Drop 序数
|
||||
var drops = new List<int>();
|
||||
foreach (JsonObject version in loader.output.Value["versions"].AsArray())
|
||||
drops.Add(McInstanceInfo.VersionToDrop((string)version["id"]));
|
||||
AllDrops = drops.Distinct().OrderByDescending(d => d).ToList();
|
||||
}
|
||||
|
||||
// 各个下载源的分加载器
|
||||
/// <summary>
|
||||
/// Minecraft 客户端 版本列表,Mojang 官方源加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<string, DlClientListResult> dlClientListMojangLoader =
|
||||
new("DlClientList Mojang", DlClientListMojangMain);
|
||||
|
||||
private static bool isNewClientVersionHinted = false;
|
||||
|
||||
// MC 更新提示
|
||||
private static bool _DlClientListMojangMain_IsHinted;
|
||||
|
||||
private static void DlClientListMojangMain(ModLoader.LoaderTask<string, DlClientListResult> loader)
|
||||
{
|
||||
var startTime = TimeUtils.GetTimeTick();
|
||||
var json = (JsonObject)Requester.FetchJson("https://launchermeta.mojang.com/mc/game/version_manifest.json");
|
||||
try
|
||||
{
|
||||
var versions = (JsonArray)json["versions"];
|
||||
if (versions.Count < 200)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Mojang", json));
|
||||
// 添加 UVMC 项
|
||||
var cacheFilePath = ModBase.pathTemp + @"Cache\uvmc-download.json";
|
||||
if (!File.Exists(cacheFilePath))
|
||||
try
|
||||
{
|
||||
var unlistedJson = (JsonObject)Requester.FetchJson(
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto/version_manifest.json");
|
||||
File.WriteAllText(cacheFilePath, unlistedJson.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log("[Download] 未列出的版本官方源下载失败: " + ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cachedJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(cacheFilePath));
|
||||
versions.Merge(cachedJson["versions"]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Download] UVMC 列表加载失败,忽略列表内容");
|
||||
}
|
||||
|
||||
// 确定官方源是否可用
|
||||
if (!dlPreferMojang)
|
||||
{
|
||||
var deltaTime = TimeUtils.GetTimeTick() - startTime;
|
||||
dlPreferMojang = deltaTime < 4000;
|
||||
ModBase.Log($"[Download] Mojang 官方源加载耗时:{deltaTime}ms,{(dlPreferMojang ? "可优先使用官方源" : "不优先使用官方源")}");
|
||||
}
|
||||
|
||||
// 添加 PCL 特供项
|
||||
// 这个社区版下不了
|
||||
// If File.Exists(PathTemp & "Cache\download.json") Then Versions.Merge(GetJson(ReadFile(PathTemp & "Cache\download.json")))
|
||||
// 返回
|
||||
loader.output = new DlClientListResult
|
||||
{ IsOfficial = true, SourceName = Lang.Text("Download.Source.MojangOfficial"), Value = json };
|
||||
string version;
|
||||
// 快照版
|
||||
version = (string)json["latest"]["snapshot"];
|
||||
if (Config.Tool.SnapshotNotification &&
|
||||
States.Tool.LastSnapshot != "" &&
|
||||
States.Tool.LastSnapshot != version &&
|
||||
!_DlClientListMojangMain_IsHinted)
|
||||
{
|
||||
_DlClientListMojangMain_IsHinted = true;
|
||||
McDownloadClientUpdateHint(version, json);
|
||||
}
|
||||
|
||||
States.Tool.LastSnapshot = version ?? "Nothing";
|
||||
// 正式版
|
||||
version = (string)json["latest"]["release"];
|
||||
if (Config.Tool.ReleaseNotification &&
|
||||
States.Tool.LastRelease != "" &&
|
||||
States.Tool.LastRelease != version &&
|
||||
!_DlClientListMojangMain_IsHinted)
|
||||
{
|
||||
_DlClientListMojangMain_IsHinted = true;
|
||||
McDownloadClientUpdateHint(version, json);
|
||||
}
|
||||
|
||||
States.Tool.LastRelease = version;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Mojang", ""), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft 客户端 版本列表,BMCLAPI 源加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<string, DlClientListResult> dlClientListBmclapiLoader =
|
||||
new("DlClientList Bmclapi", DlClientListBmclapiMain);
|
||||
|
||||
private static void DlClientListBmclapiMain(ModLoader.LoaderTask<string, DlClientListResult> loader)
|
||||
{
|
||||
var json = (JsonObject)Requester.FetchJson(
|
||||
"https://bmclapi2.bangbang93.com/mc/game/version_manifest.json");
|
||||
try
|
||||
{
|
||||
var versions = (JsonArray)json["versions"];
|
||||
if (versions.Count < 200)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "BMCLAPI", json));
|
||||
// 添加 UVMC 项
|
||||
var cacheFilePath = ModBase.pathTemp + @"Cache\uvmc-download.json";
|
||||
if (!File.Exists(cacheFilePath))
|
||||
try
|
||||
{
|
||||
var unlistedJson = (JsonObject)Requester.FetchJson(
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto/version_manifest.json");
|
||||
File.WriteAllText(cacheFilePath, unlistedJson.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log("[Download] 未列出的版本镜像源下载失败: " + ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cachedJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(cacheFilePath));
|
||||
versions.Merge(cachedJson["versions"]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Download] UVMC 列表加载失败,忽略列表内容");
|
||||
}
|
||||
|
||||
// 检查是否有要求的版本(#5195)
|
||||
if (!string.IsNullOrEmpty(loader.input))
|
||||
{
|
||||
var id = loader.input;
|
||||
if (dlClientListLoader.output.Value is not null &&
|
||||
!dlClientListLoader.output.Value["versions"].AsArray().Any(v => (string)v["id"] == id))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.BmclapiMissingTargetVersion", id));
|
||||
}
|
||||
|
||||
// 返回
|
||||
loader.output = new DlClientListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = json };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "BMCLAPI", json), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个版本的 Json 下载地址,若失败则返回 Nothing。必须在工作线程执行。
|
||||
/// </summary>
|
||||
public static object DlClientListGet(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确认版本格式标准
|
||||
id = id.Replace("_", "-"); // 1.7.10_pre4 在版本列表中显示为 1.7.10-pre4
|
||||
if (id != "1.0" && id.EndsWithF(".0"))
|
||||
id = id.Substring(0, id.Length - 2); // OptiFine 1.8 的下载会触发此问题,显示版本为 1.8.0
|
||||
// 获取 Minecraft 版本列表
|
||||
switch (dlClientListLoader.State)
|
||||
{
|
||||
case ModBase.LoadState.Finished:
|
||||
{
|
||||
// 从当前的结果获取目标版本…
|
||||
foreach (JsonObject Version in dlClientListLoader.output.Value["versions"].AsArray())
|
||||
if ((string)Version["id"] == id)
|
||||
return Version["url"].ToString();
|
||||
// …如果没有,则重新尝试获取(在版本刚更新时可能出现这种情况,#5195)
|
||||
dlClientListLoader.WaitForExit(id, isForceRestart: true);
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
dlClientListLoader.WaitForExit(id);
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Failed:
|
||||
case ModBase.LoadState.Aborted:
|
||||
case ModBase.LoadState.Waiting:
|
||||
{
|
||||
dlClientListLoader.WaitForExit(id, isForceRestart: true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新查找版本
|
||||
foreach (JsonObject Version in dlClientListLoader.output.Value["versions"].AsArray())
|
||||
if ((string)Version["id"] == id)
|
||||
return Version["url"].ToString();
|
||||
ModBase.Log($"未发现版本 {id} 的 json 下载地址,版本列表返回为:{"\r\n"}{dlClientListLoader.output.Value}",
|
||||
ModBase.LogLevel.Debug);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"获取版本 {id} 的 json 下载地址失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlOptiFineList | OptiFine 版本列表
|
||||
|
||||
public struct DlOptiFineListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public List<DlOptiFineListEntry> Value;
|
||||
}
|
||||
|
||||
public class DlOptiFineListEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示名称,已去除 HD_U 字样,如“1.12.2 C8”。
|
||||
/// </summary>
|
||||
public string DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为测试版。
|
||||
/// </summary>
|
||||
public bool IsPreview;
|
||||
|
||||
/// <summary>
|
||||
/// 原始文件名称,如“preview_OptiFine_1.11_HD_U_E1_pre.jar”。
|
||||
/// </summary>
|
||||
public string NameFile;
|
||||
|
||||
/// <summary>
|
||||
/// 对应的版本名称,如“1.13.2-OptiFine_HD_U_E6”。
|
||||
/// </summary>
|
||||
public string NameVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 发布时间,格式为“yyyy/mm/dd”。OptiFine 源无此数据。
|
||||
/// </summary>
|
||||
public string ReleaseTime;
|
||||
|
||||
/// <summary>
|
||||
/// 需要的最低 Forge 版本。空字符串为无限制,Nothing 为不兼容,“28.1.56” 表示版本号,“1161” 表示版本号的最后一位。
|
||||
/// </summary>
|
||||
public string RequiredForgeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 对应的 Minecraft 版本,如“1.12.2”。
|
||||
/// </summary>
|
||||
public string Inherit
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value.EndsWithF(".0"))
|
||||
value = value.Substring(0, value.Length - 2);
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OptiFine 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlOptiFineListResult> dlOptiFineListLoader =
|
||||
new("DlOptiFineList Main", DlOptiFineListMain);
|
||||
|
||||
private static void DlOptiFineListMain(ModLoader.LoaderTask<int, DlOptiFineListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlOptiFineListResult>, int>>
|
||||
{ new(dlOptiFineListBmclapiLoader, 30), new(dlOptiFineListOfficialLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlOptiFineListResult>, int>>
|
||||
{ new(dlOptiFineListOfficialLoader, 5), new(dlOptiFineListBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlOptiFineListResult>, int>>
|
||||
{ new(dlOptiFineListOfficialLoader, 60), new(dlOptiFineListBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OptiFine 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlOptiFineListResult> dlOptiFineListOfficialLoader =
|
||||
new("DlOptiFineList Official", DlOptiFineListOfficialMain);
|
||||
|
||||
private static void DlOptiFineListOfficialMain(ModLoader.LoaderTask<int, DlOptiFineListResult> loader)
|
||||
{
|
||||
string result = "";
|
||||
using var resp = HttpRequest
|
||||
.Create("https://optifine.net/downloads")
|
||||
.WithHeader("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
.WithHeader("Accept-Language", "en-US,en;q=0.5")
|
||||
.WithHeader("X-Requested-With", "XMLHttpRequest")
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
resp.EnsureSuccessStatusCode();
|
||||
result = resp.AsString();
|
||||
if (result.Length < 200)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "OptiFine", result));
|
||||
try
|
||||
{
|
||||
var forge = result.RegexSearch("(?<=colForge'>)[^<]*");
|
||||
var releaseTime = result.RegexSearch("(?<=colDate'>)[^<]+");
|
||||
var name = result.RegexSearch("(?<=OptiFine_)[0-9A-Za-z_.]+(?=.jar\")");
|
||||
if (releaseTime.Count != name.Count)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.OptiFineTimeDataMismatch"));
|
||||
if (forge.Count != name.Count)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.OptiFineForgeCompatMismatch"));
|
||||
if (releaseTime.Count < 10)
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "OptiFine", result));
|
||||
// 转化为列表输出
|
||||
var versions = new List<DlOptiFineListEntry>();
|
||||
for (int i = 0, loopTo = releaseTime.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
name[i] = name[i].Replace("_", " ");
|
||||
var releaseDate = DateTime.ParseExact(releaseTime[i],
|
||||
["d.M.yyyy", "dd.M.yyyy", "d.MM.yyyy", "dd.MM.yyyy"],
|
||||
CultureInfo.InvariantCulture, DateTimeStyles.None);
|
||||
var entry = new DlOptiFineListEntry
|
||||
{
|
||||
DisplayName = name[i].Replace("HD U ", "").Replace(".0 ", " "),
|
||||
ReleaseTime = Lang.Date(releaseDate, "d"),
|
||||
IsPreview = name[i].ContainsF("pre", true),
|
||||
Inherit = name[i].Split(" ")[0],
|
||||
NameFile = (name[i].ContainsF("pre", true) ? "preview_" : "") + "OptiFine_" +
|
||||
name[i].Replace(" ", "_") + ".jar",
|
||||
RequiredForgeVersion = forge[i].Replace("Forge ", "").Replace("#", "")
|
||||
};
|
||||
if (entry.RequiredForgeVersion.Contains("N/A"))
|
||||
entry.RequiredForgeVersion = null;
|
||||
entry.NameVersion = entry.Inherit + "-OptiFine_" +
|
||||
name[i].Replace(" ", "_").Replace(entry.Inherit + "_", "");
|
||||
versions.Add(entry);
|
||||
}
|
||||
|
||||
loader.output = new DlOptiFineListResult
|
||||
{ isOfficial = true, sourceName = Lang.Text("Download.Source.OptiFineOfficial"), Value = versions };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "OptiFine", result),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OptiFine 版本列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlOptiFineListResult> dlOptiFineListBmclapiLoader =
|
||||
new("DlOptiFineList Bmclapi", DlOptiFineListBmclapiMain);
|
||||
|
||||
private static void DlOptiFineListBmclapiMain(ModLoader.LoaderTask<int, DlOptiFineListResult> loader)
|
||||
{
|
||||
var json = (JsonArray)Requester.FetchJson("https://bmclapi2.bangbang93.com/optifine/versionList");
|
||||
try
|
||||
{
|
||||
var versions = new List<DlOptiFineListEntry>();
|
||||
foreach (JsonObject Token in json)
|
||||
{
|
||||
var entry = new DlOptiFineListEntry
|
||||
{
|
||||
DisplayName =
|
||||
(Token["mcversion"] + Token["type"].ToString().Replace("HD_U", "").Replace("_", " ") + " " +
|
||||
Token["patch"]).Replace(".0 ", " "),
|
||||
ReleaseTime = "",
|
||||
IsPreview = Token["patch"].ToString().ContainsF("pre", true),
|
||||
Inherit = Token["mcversion"].ToString(),
|
||||
NameFile = Token["filename"].ToString(),
|
||||
RequiredForgeVersion = (Token["forge"] ?? "").ToString().Replace("Forge ", "").Replace("#", "")
|
||||
};
|
||||
if (entry.RequiredForgeVersion.Contains("N/A"))
|
||||
entry.RequiredForgeVersion = null;
|
||||
entry.NameVersion = entry.Inherit + "-OptiFine_" + (Token["type"] + " " + Token["patch"])
|
||||
.Replace(".0 ", " ").Replace(" ", "_").Replace(entry.Inherit + "_", "");
|
||||
versions.Add(entry);
|
||||
}
|
||||
|
||||
loader.output = new DlOptiFineListResult { isOfficial = false, sourceName = "BMCLAPI", Value = versions };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "OptiFine BMCLAPI", json), ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlForgeList | Forge Minecraft 版本列表
|
||||
|
||||
public struct DlForgeListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public List<string> Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlForgeListResult> dlForgeListLoader =
|
||||
new("DlForgeList Main", DlForgeListMain);
|
||||
|
||||
private static void DlForgeListMain(ModLoader.LoaderTask<int, DlForgeListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlForgeListResult>, int>>
|
||||
{ new(dlForgeListBmclapiLoader, 30), new(dlForgeListOfficialLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlForgeListResult>, int>>
|
||||
{ new(dlForgeListOfficialLoader, 5), new(dlForgeListBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlForgeListResult>, int>>
|
||||
{ new(dlForgeListOfficialLoader, 60), new(dlForgeListBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlForgeListResult> dlForgeListOfficialLoader =
|
||||
new("DlForgeList Official", DlForgeListOfficialMain);
|
||||
|
||||
private static void DlForgeListOfficialMain(ModLoader.LoaderTask<int, DlForgeListResult> loader)
|
||||
{
|
||||
var result = Requester.FetchString(
|
||||
"https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.2.4.html", new RequestParam
|
||||
{
|
||||
Encoding = Encoding.Default,
|
||||
UseBrowserUserAgent = true
|
||||
});
|
||||
if (result.Length < 200)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge", result));
|
||||
// 获取所有版本信息
|
||||
var names = result.RegexSearch("(?<=a href=\"index_)[0-9.]+(_pre[0-9]?)?(?=.html)");
|
||||
names.Add("1.2.4"); // 1.2.4 不会被匹配上
|
||||
if (names.Count < 10)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge", result));
|
||||
loader.output = new DlForgeListResult
|
||||
{ isOfficial = true, sourceName = Lang.Text("Download.Source.ForgeOfficial"), Value = names };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlForgeListResult> dlForgeListBmclapiLoader =
|
||||
new("DlForgeList Bmclapi", DlForgeListBmclapiMain);
|
||||
|
||||
private static void DlForgeListBmclapiMain(ModLoader.LoaderTask<int, DlForgeListResult> loader)
|
||||
{
|
||||
var result =
|
||||
Requester.FetchJson("https://bmclapi2.bangbang93.com/forge/minecraft",
|
||||
new RequestParam
|
||||
{
|
||||
Encoding = Encoding.Default,
|
||||
})?.ToString() ?? "";
|
||||
if (result.Length < 200)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge BMCLAPI",
|
||||
result));
|
||||
// 获取所有版本信息
|
||||
var names = result.RegexSearch("[0-9.]+(_pre[0-9]?)?");
|
||||
if (names.Count < 10)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge BMCLAPI",
|
||||
result));
|
||||
loader.output = new DlForgeListResult { isOfficial = false, sourceName = "BMCLAPI", Value = names };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlForgeVersion | Forge 版本列表
|
||||
|
||||
public abstract class DlForgelikeEntry : IComparable<DlForgelikeEntry>
|
||||
{
|
||||
public enum ForgelikeType
|
||||
{
|
||||
Forge,
|
||||
NeoForge,
|
||||
Cleanroom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forgelike 种类。Forge、NeoForge、Cleanroom。
|
||||
/// </summary>
|
||||
public ForgelikeType forgeType;
|
||||
|
||||
/// <summary>
|
||||
/// 对应的 Minecraft 版本,如“1.12.2”。
|
||||
/// </summary>
|
||||
public string Inherit;
|
||||
|
||||
/// <summary>
|
||||
/// 标准化后的版本号,仅可用于比较与排序。
|
||||
/// 格式:Major.Minor.Build.Revision
|
||||
/// Forge:如 “50.1.9.0”(最后一位固定为 0)、“14.22.1.2478”(Legacy)。
|
||||
/// NeoForge:如 “20.4.30.0”(最后一位固定为 0)、“19.47.1.99”(Legacy:第一位固定为 19)。
|
||||
/// Cleanroom:如 “0.2.4.1”(Alpha:最后一位固定为 1)。
|
||||
/// </summary>
|
||||
public Version version;
|
||||
|
||||
/// <summary>
|
||||
/// 可对玩家显示的非格式化版本名。
|
||||
/// Forge:如 “50.1.9”、“14.22.1.2478”(Legacy)。
|
||||
/// NeoForge:如 “20.4.30-beta”、“47.1.99”(Legacy)。
|
||||
/// Cleanroom:如 “0.2.4-alpha”。
|
||||
/// </summary>
|
||||
public string VersionName;
|
||||
|
||||
/// <summary>
|
||||
/// 加载器名称。Forge / NeoForge / Cleanroom。
|
||||
/// </summary>
|
||||
public string LoaderName => forgeType.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 文件扩展名。不以小数点开头。
|
||||
/// </summary>
|
||||
public string FileExtension
|
||||
{
|
||||
get
|
||||
{
|
||||
if (forgeType == 0) return ((DlForgeVersionEntry)this).Category == "installer" ? "jar" : "zip";
|
||||
|
||||
return "jar";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge:MC 版本是否小于 1.13。
|
||||
/// NeoForge:MC 版本是否为 1.20.1。
|
||||
/// Cleanroom:固定为 False。
|
||||
/// </summary>
|
||||
public bool IsLegacy
|
||||
{
|
||||
get
|
||||
{
|
||||
// Cleanroom 始终为 False
|
||||
if ((int)forgeType == 2)
|
||||
return false;
|
||||
// 虽然很抽象,但确实可以这样判断
|
||||
// Forge:1.13+ 的版本号首位都大于 20
|
||||
// NeoForge:1.20.1 的版本号首位人为规定为 19 开头
|
||||
return version.Major < 20;
|
||||
}
|
||||
}
|
||||
|
||||
public int CompareTo(DlForgelikeEntry other)
|
||||
{
|
||||
if (version != other.version) return version.CompareTo(other.version);
|
||||
|
||||
return McVersionComparer.CompareVersion(VersionName, other.VersionName);
|
||||
}
|
||||
}
|
||||
|
||||
public class DlForgeVersionEntry : DlForgelikeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 安装类型。有 installer、client、universal 三种。
|
||||
/// </summary>
|
||||
public string Category;
|
||||
|
||||
/// <summary>
|
||||
/// 用于下载的文件版本名。可能在 Version 的基础上添加了分支。
|
||||
/// </summary>
|
||||
public string FileVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 MD5 或 SHA1(BMCLAPI 的老版本是 MD5,新版本是 SHA1;官方源总是 MD5)。
|
||||
/// </summary>
|
||||
public string Hash;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为推荐版本。
|
||||
/// </summary>
|
||||
public bool IsRecommended;
|
||||
|
||||
/// <summary>
|
||||
/// 发布时间,格式为“yyyy/MM/dd HH:mm”。
|
||||
/// </summary>
|
||||
public string ReleaseTime;
|
||||
|
||||
public DlForgeVersionEntry(string version, string branch, string inherit)
|
||||
{
|
||||
// 司马版本的特殊处理
|
||||
if (version == "11.15.1.2318" || version == "11.15.1.1902" || version == "11.15.1.1890")
|
||||
branch = "1.8.9";
|
||||
if (branch is null && inherit == "1.7.10" && double.Parse(version.Split(".")[3]) >= 1300d)
|
||||
branch = "1.7.10";
|
||||
// 为 DlForgelikeEntry 提供所有信息
|
||||
forgeType = ForgelikeType.Forge;
|
||||
VersionName = version;
|
||||
this.version = new Version(version);
|
||||
this.Inherit = inherit;
|
||||
FileVersion = version + (branch is null ? "" : "-" + branch);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static void DlForgeVersionMain(ModLoader.LoaderTask<string, List<DlForgeVersionEntry>> loader)
|
||||
{
|
||||
var dlForgeVersionOfficialLoader =
|
||||
new ModLoader.LoaderTask<string, List<DlForgeVersionEntry>>("DlForgeVersion Official",
|
||||
DlForgeVersionOfficialMain);
|
||||
var dlForgeVersionBmclapiLoader =
|
||||
new ModLoader.LoaderTask<string, List<DlForgeVersionEntry>>("DlForgeVersion Bmclapi",
|
||||
DlForgeVersionBmclapiMain);
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, List<DlForgeVersionEntry>>, int>>
|
||||
{ new(dlForgeVersionBmclapiLoader, 30), new(dlForgeVersionOfficialLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, List<DlForgeVersionEntry>>, int>>
|
||||
{ new(dlForgeVersionOfficialLoader, 5), new(dlForgeVersionBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<string, List<DlForgeVersionEntry>>, int>>
|
||||
{ new(dlForgeVersionOfficialLoader, 60), new(dlForgeVersionBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static void DlForgeVersionOfficialMain(ModLoader.LoaderTask<string, List<DlForgeVersionEntry>> loader)
|
||||
{
|
||||
string result;
|
||||
try
|
||||
{
|
||||
result = Requester.FetchString(
|
||||
"https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" +
|
||||
loader.input.Replace("-", "_") + ".html", new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}); // 兼容 Forge 1.7.10-pre4,#4057
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message.Contains("(404)")) throw new Exception(Lang.Text("Minecraft.Download.Error.NotFound"));
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
if (result.Length < 1000)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge", result));
|
||||
var versions = new List<DlForgeVersionEntry>();
|
||||
try
|
||||
{
|
||||
// 分割版本信息
|
||||
var versionCodes = result.Substring(0, result.LastIndexOfF("</table>"))
|
||||
.Split("<td class=\"download-version");
|
||||
// 获取所有版本信息
|
||||
for (int i = 1, loopTo = versionCodes.Count() - 1; i <= loopTo; i++)
|
||||
{
|
||||
var versionCode = versionCodes[i];
|
||||
try
|
||||
{
|
||||
// 基础信息获取
|
||||
var name = versionCode.RegexSeek(@"(?<=[^(0-9)]+)[0-9\.]+");
|
||||
var isRecommended = versionCode.Contains("fa promo-recommended");
|
||||
var inherit = loader.input;
|
||||
// 分支获取
|
||||
var branch = versionCode.RegexSeek($"(?<=-{name}-)[^-\"]+(?=-[a-z]+.[a-z]{{3}})");
|
||||
if (string.IsNullOrWhiteSpace(branch))
|
||||
branch = null;
|
||||
// 发布时间获取
|
||||
var releaseTimeOriginal = versionCode.RegexSeek("(?<=\"download-time\" title=\")[^\"]+");
|
||||
// Dim ReleaseTimeSplit = ReleaseTimeOriginal.Split(" -:".ToCharArray) '原格式:"2021-02-15 03:24:02"
|
||||
var releaseDate =
|
||||
DateTime.Parse(releaseTimeOriginal, null, DateTimeStyles.AssumeUniversal); // 以 UTC 时间作为标准
|
||||
var releaseTime = Lang.Date(releaseDate.ToLocalTime(), "g"); // 时区与格式转换
|
||||
// 分类与 MD5 获取
|
||||
string mD5;
|
||||
string category;
|
||||
if (versionCode.Contains("classifier-installer\""))
|
||||
{
|
||||
// 类型为 installer.jar,支持范围 ~753 (~ 1.6.1 部分), 738~684 (1.5.2 全部)
|
||||
versionCode = versionCode.Substring(versionCode.IndexOfF("installer.jar"));
|
||||
mD5 = versionCode.RegexSeek("(?<=MD5:</strong> )[^<]+");
|
||||
category = "installer";
|
||||
}
|
||||
else if (versionCode.Contains("classifier-universal\""))
|
||||
{
|
||||
// 类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分)
|
||||
versionCode = versionCode.Substring(versionCode.IndexOfF("universal.zip"));
|
||||
mD5 = versionCode.RegexSeek("(?<=MD5:</strong> )[^<]+");
|
||||
category = "universal";
|
||||
}
|
||||
else if (versionCode.Contains("client.zip"))
|
||||
{
|
||||
// 类型为 client.zip,支持范围 182~ (1.3.2 部分 ~)
|
||||
versionCode = versionCode.Substring(versionCode.IndexOfF("client.zip"));
|
||||
mD5 = versionCode.RegexSeek("(?<=MD5:</strong> )[^<]+");
|
||||
category = "client";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有任何下载(1.6.4 有一部分这种情况)
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加进列表
|
||||
versions.Add(new DlForgeVersionEntry(name, branch, inherit)
|
||||
{
|
||||
Category = category, IsRecommended = isRecommended,
|
||||
Hash = mD5.Trim('\r', '\n'),
|
||||
ReleaseTime = releaseTime
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge", versionCode), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge", result), ex);
|
||||
}
|
||||
|
||||
if (!versions.Any())
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NotFound"));
|
||||
loader.output = versions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forge 版本列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static void DlForgeVersionBmclapiMain(ModLoader.LoaderTask<string, List<DlForgeVersionEntry>> loader)
|
||||
{
|
||||
var json = (JsonArray)Requester.FetchJson(
|
||||
"https://bmclapi2.bangbang93.com/forge/minecraft/" +
|
||||
loader.input.Replace("-", "_")); // 兼容 Forge 1.7.10-pre4,#4057
|
||||
var versions = new List<DlForgeVersionEntry>();
|
||||
try
|
||||
{
|
||||
var recommended = ModDownloadLib.McDownloadForgeRecommendedGet(loader.input);
|
||||
foreach (JsonObject Token in json)
|
||||
{
|
||||
// 分类与 Hash 获取
|
||||
string hash = null;
|
||||
var category = "unknown";
|
||||
var proi = -1;
|
||||
foreach (JsonObject File in Token["files"].AsArray())
|
||||
switch (File["category"].ToString() ?? "")
|
||||
{
|
||||
case "installer":
|
||||
{
|
||||
if (File["format"].ToString() == "jar")
|
||||
{
|
||||
// 类型为 installer.jar,支持范围 ~753 (~ 1.6.1 部分), 738~684 (1.5.2 全部)
|
||||
hash = (string)File["hash"];
|
||||
category = "installer";
|
||||
proi = 2;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "universal":
|
||||
{
|
||||
if (proi <= 1 && File["format"].ToString() == "zip")
|
||||
{
|
||||
// 类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分)
|
||||
hash = (string)File["hash"];
|
||||
category = "universal";
|
||||
proi = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "client":
|
||||
{
|
||||
if (proi <= 0 && File["format"].ToString() == "zip")
|
||||
{
|
||||
// 类型为 client.zip,支持范围 182~ (1.3.2 部分 ~)
|
||||
hash = (string)File["hash"];
|
||||
category = "client";
|
||||
proi = 0;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Entry
|
||||
var branch = (string)Token["branch"];
|
||||
var name = (string)Token["version"];
|
||||
// 基础信息获取
|
||||
var entry = new DlForgeVersionEntry(name, branch, loader.input)
|
||||
{ Hash = hash, Category = category, IsRecommended = (recommended ?? "") == (name ?? "") };
|
||||
var timeSplit = Token["modified"].ToString().Split('-', 'T', ':', '.', ' ', '/');
|
||||
entry.ReleaseTime = Lang.Date(Token["modified"].ToObject<DateTime>().ToLocalTime(), "g");
|
||||
// 添加项
|
||||
versions.Add(entry);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Forge BMCLAPI", json),
|
||||
ex);
|
||||
}
|
||||
|
||||
if (!versions.Any())
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NotFound"));
|
||||
loader.output = versions;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlNeoForgeList | NeoForge 版本列表
|
||||
|
||||
public struct DlNeoForgeListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 所有版本的列表。已经按从新到老排序。
|
||||
/// </summary>
|
||||
public List<DlNeoForgeListEntry> Value;
|
||||
}
|
||||
|
||||
public class DlNeoForgeListEntry : DlForgelikeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// API 使用的原始版本字符串,如 “20.4.30-beta”、“1.20.1-47.1.99”(Legacy)。
|
||||
/// </summary>
|
||||
public string ApiName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否是 Beta 版。
|
||||
/// </summary>
|
||||
public bool IsBeta;
|
||||
|
||||
public DlNeoForgeListEntry(string apiName)
|
||||
{
|
||||
forgeType = ForgelikeType.NeoForge;
|
||||
this.ApiName = apiName;
|
||||
IsBeta = apiName.Contains("beta") || apiName.Contains("alpha");
|
||||
if (apiName.Contains("1.20.1")) // 1.20.1-47.1.99
|
||||
{
|
||||
VersionName = apiName.Replace("1.20.1-", "");
|
||||
version = new Version("19." + VersionName);
|
||||
Inherit = "1.20.1";
|
||||
}
|
||||
else if (apiName.StartsWith("0.")) // 0.25w14craftmine.3-beta
|
||||
{
|
||||
VersionName = apiName;
|
||||
var segments = apiName.BeforeFirst("-").Split('.');
|
||||
version = new Version(0, 0, int.Parse(segments.Last()));
|
||||
Inherit = segments[1];
|
||||
}
|
||||
else // 20.4.30-beta;26.1.0.0-alpha.1+snapshot-1
|
||||
{
|
||||
VersionName = apiName;
|
||||
version = new Version(apiName.BeforeFirst("-"));
|
||||
if (version.Major >= 24)
|
||||
Inherit = $"{version.Major}.{version.Minor}{(version.Build > 0 ? $".{version.Build}" : "")}";
|
||||
else
|
||||
Inherit = "1." + version.Major + (version.Minor > 0 ? "." + version.Minor : "");
|
||||
if (VersionName.Contains("+"))
|
||||
Inherit += "-" + VersionName.AfterFirst("+");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件在官网的基础地址,不包含后缀。
|
||||
/// </summary>
|
||||
public string UrlBase
|
||||
{
|
||||
get
|
||||
{
|
||||
var packageName = IsLegacy ? "forge" : "neoforge";
|
||||
return
|
||||
$"https://maven.neoforged.net/releases/net/neoforged/{packageName}/{ApiName}/{packageName}-{ApiName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NeoForge 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlNeoForgeListResult> dlNeoForgeListLoader =
|
||||
new("DlNeoForgeList Main", DlNeoForgeListMain);
|
||||
|
||||
private static void DlNeoForgeListMain(ModLoader.LoaderTask<int, DlNeoForgeListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlNeoForgeListResult>, int>>
|
||||
{ new(dlNeoForgeListBmclapiLoader, 30), new(dlNeoForgeListOfficialLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlNeoForgeListResult>, int>>
|
||||
{ new(dlNeoForgeListOfficialLoader, 5), new(dlNeoForgeListBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlNeoForgeListResult>, int>>
|
||||
{ new(dlNeoForgeListOfficialLoader, 60), new(dlNeoForgeListBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NeoForge 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlNeoForgeListResult> dlNeoForgeListOfficialLoader =
|
||||
new("DlNeoForgeList Official", DlNeoForgeListOfficialMain);
|
||||
|
||||
private static void DlNeoForgeListOfficialMain(ModLoader.LoaderTask<int, DlNeoForgeListResult> loader)
|
||||
{
|
||||
// 获取版本列表 JSON
|
||||
var resultLatest = Requester.FetchJson(
|
||||
"https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge",
|
||||
new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}).ToString();
|
||||
var resultLegacy = Requester.FetchJson(
|
||||
"https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge",
|
||||
new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}).ToString();
|
||||
if (resultLatest.Length < 100 || resultLegacy.Length < 100)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "NeoForge",
|
||||
resultLatest + "\r\n\r\n" + resultLegacy));
|
||||
// 解析
|
||||
try
|
||||
{
|
||||
loader.output = new DlNeoForgeListResult
|
||||
{
|
||||
isOfficial = true,
|
||||
sourceName = Lang.Text("Download.Source.NeoForgeOfficial"),
|
||||
Value = GetNeoForgeEntries(resultLatest, resultLegacy)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "NeoForge",
|
||||
resultLatest + "\r\n\r\n" + resultLegacy), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NeoForge 版本列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlNeoForgeListResult> dlNeoForgeListBmclapiLoader =
|
||||
new("DlNeoForgeList Bmclapi", DlNeoForgeListBmclapiMain);
|
||||
|
||||
public static void DlNeoForgeListBmclapiMain(ModLoader.LoaderTask<int, DlNeoForgeListResult> loader)
|
||||
{
|
||||
// 获取版本列表 JSON
|
||||
var resultLatest = Requester.FetchJson(
|
||||
"https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge",
|
||||
new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}).ToString();
|
||||
var resultLegacy = Requester.FetchJson(
|
||||
"https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge",
|
||||
new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}).ToString();
|
||||
if (resultLatest.Length < 100 || resultLegacy.Length < 100)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "NeoForge BMCLAPI",
|
||||
resultLatest + "\r\n\r\n" + resultLegacy));
|
||||
// 解析
|
||||
try
|
||||
{
|
||||
loader.output = new DlNeoForgeListResult
|
||||
{
|
||||
isOfficial = true,
|
||||
sourceName = "BMCLAPI",
|
||||
Value = GetNeoForgeEntries(resultLatest, resultLegacy)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "NeoForge BMCLAPI",
|
||||
resultLatest + "\r\n\r\n" + resultLegacy),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<DlNeoForgeListEntry> GetNeoForgeEntries(string latestJson, string latestLegacyJson)
|
||||
{
|
||||
var versionNames = ModBase.RegexSearch(latestLegacyJson + latestJson, RegexPatterns.DlNeoForgeVersion);
|
||||
var versions = versionNames.Where(name => name != "47.1.82").Select(name => new DlNeoForgeListEntry(name))
|
||||
.OrderByDescending(a => a).ToList(); // 这个版本虽然在版本列表中,但不能下载
|
||||
if (!versions.Any())
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NotFound"));
|
||||
return versions;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlCleanroomList | Cleanroom 版本列表
|
||||
|
||||
public struct DlCleanroomListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 所有版本的列表。已经按从新到老排序。
|
||||
/// </summary>
|
||||
public List<DlCleanroomListEntry> Value;
|
||||
}
|
||||
|
||||
public class DlCleanroomListEntry : DlForgelikeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// API 使用的原始版本字符串,如 “0.2.4-alpha”。
|
||||
/// </summary>
|
||||
public string ApiName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否是 Beta 版。
|
||||
/// </summary>
|
||||
public bool IsBeta;
|
||||
|
||||
public DlCleanroomListEntry(string apiName)
|
||||
{
|
||||
forgeType = ForgelikeType.Cleanroom;
|
||||
this.ApiName = apiName;
|
||||
IsBeta = apiName.Contains("alpha");
|
||||
VersionName = apiName;
|
||||
version = new Version(apiName.BeforeFirst("-"));
|
||||
Inherit = "1.12.2";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件在官网的基础地址,不包含后缀。
|
||||
/// </summary>
|
||||
public string UrlBase =>
|
||||
$"https://github.com/CleanroomMC/Cleanroom/releases/download/{ApiName}/cleanroom-{ApiName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanroom 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlCleanroomListResult> dlCleanroomListLoader =
|
||||
new("DlCleanroomList Main", DlCleanroomListMain);
|
||||
|
||||
private static void DlCleanroomListMain(ModLoader.LoaderTask<int, DlCleanroomListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlCleanroomListResult>, int>>
|
||||
{ new(dlCleanroomListOfficialLoader, 30) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlCleanroomListResult>, int>>
|
||||
{ new(dlCleanroomListOfficialLoader, 5) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlCleanroomListResult>, int>>
|
||||
{ new(dlCleanroomListOfficialLoader, 60) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanroom 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlCleanroomListResult> dlCleanroomListOfficialLoader =
|
||||
new("DlCleanroomList Official", DlCleanroomListOfficialMain);
|
||||
|
||||
private static void DlCleanroomListOfficialMain(ModLoader.LoaderTask<int, DlCleanroomListResult> loader)
|
||||
{
|
||||
// 获取版本列表 JSON
|
||||
var resultLatest = Requester.FetchJson(
|
||||
"https://api.github.com/repos/CleanroomMC/Cleanroom/releases", new RequestParam
|
||||
{
|
||||
UseBrowserUserAgent = true
|
||||
}).ToString();
|
||||
if (resultLatest.Length < 100)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Cleanroom",
|
||||
resultLatest));
|
||||
// 解析
|
||||
try
|
||||
{
|
||||
loader.output = new DlCleanroomListResult
|
||||
{
|
||||
isOfficial = true,
|
||||
sourceName = Lang.Text("Download.Source.CleanroomOfficial"),
|
||||
Value = GetCleanroomEntries(resultLatest)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Cleanroom", resultLatest), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<DlCleanroomListEntry> GetCleanroomEntries(string latestJson)
|
||||
{
|
||||
var versions = new List<DlCleanroomListEntry>();
|
||||
var json = JsonArray.Parse(latestJson);
|
||||
foreach (JsonObject Token in json.AsArray())
|
||||
versions.Add(new DlCleanroomListEntry(Token["tag_name"].ToString())
|
||||
{ forgeType = (DlForgelikeEntry.ForgelikeType)2 });
|
||||
if (!versions.Any())
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NoAvailableVersion"));
|
||||
versions = versions.OrderByDescending(a => a.version).ToList();
|
||||
return versions;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlLiteLoaderList | LiteLoader 版本列表
|
||||
|
||||
public struct DlLiteLoaderListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public List<DlLiteLoaderListEntry> Value;
|
||||
|
||||
/// <summary>
|
||||
/// 官方源的失败原因。若没有则为 Nothing。
|
||||
/// </summary>
|
||||
public Exception officialError;
|
||||
}
|
||||
|
||||
public class DlLiteLoaderListEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 实际的文件名,如“liteloader-installer-1.12-00-SNAPSHOT.jar”。
|
||||
/// </summary>
|
||||
public string FileName;
|
||||
|
||||
/// <summary>
|
||||
/// 对应的 Minecraft 版本,如“1.12.2”。
|
||||
/// </summary>
|
||||
public string Inherit;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 1.7 及更早的远古版。
|
||||
/// </summary>
|
||||
public bool IsLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为测试版。
|
||||
/// </summary>
|
||||
public bool IsPreview;
|
||||
|
||||
/// <summary>
|
||||
/// 对应的 Json 项。
|
||||
/// </summary>
|
||||
public JsonNode jsonToken;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 MD5。
|
||||
/// </summary>
|
||||
public string MD5;
|
||||
|
||||
/// <summary>
|
||||
/// 发布时间,格式为“yyyy/mm/dd HH:mm”。
|
||||
/// </summary>
|
||||
public string ReleaseTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LiteLoader 版本列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLiteLoaderListResult> dlLiteLoaderListLoader =
|
||||
new("DlLiteLoaderList Main", DlLiteLoaderListMain);
|
||||
|
||||
private static void DlLiteLoaderListMain(ModLoader.LoaderTask<int, DlLiteLoaderListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLiteLoaderListResult>, int>>
|
||||
{
|
||||
new(dlLiteLoaderListBmclapiLoader, 30), new(dlLiteLoaderListOfficialLoader, 30 + 60)
|
||||
}, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLiteLoaderListResult>, int>>
|
||||
{
|
||||
new(dlLiteLoaderListOfficialLoader, 5), new(dlLiteLoaderListBmclapiLoader, 5 + 30)
|
||||
}, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLiteLoaderListResult>, int>>
|
||||
{
|
||||
new(dlLiteLoaderListOfficialLoader, 60), new(dlLiteLoaderListBmclapiLoader, 60 + 60)
|
||||
}, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LiteLoader 版本列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLiteLoaderListResult> dlLiteLoaderListOfficialLoader =
|
||||
new("DlLiteLoaderList Official", DlLiteLoaderListOfficialMain);
|
||||
|
||||
private static void DlLiteLoaderListOfficialMain(ModLoader.LoaderTask<int, DlLiteLoaderListResult> loader)
|
||||
{
|
||||
var result =
|
||||
(JsonObject)Requester.FetchJson("https://dl.liteloader.com/versions/versions.json");
|
||||
try
|
||||
{
|
||||
var json = (JsonObject)result["versions"];
|
||||
var versions = new List<DlLiteLoaderListEntry>();
|
||||
foreach (var Pair in json)
|
||||
{
|
||||
if (Pair.Key.StartsWithF("1.6") || Pair.Key.StartsWithF("1.5"))
|
||||
continue;
|
||||
var realEntry =
|
||||
(Pair.Value["artefacts"] ?? Pair.Value["snapshots"])["com.mumfrey:liteloader"]["latest"];
|
||||
versions.Add(new DlLiteLoaderListEntry
|
||||
{
|
||||
Inherit = Pair.Key,
|
||||
IsLegacy = double.Parse(Pair.Key.Split(".")[1]) < 8d,
|
||||
IsPreview = realEntry["stream"].ToString().ToLower() == "snapshot",
|
||||
FileName = "liteloader-installer-" + Pair.Key +
|
||||
(Pair.Key == "1.8" || Pair.Key == "1.9" ? ".0" : "") + "-00-SNAPSHOT.jar",
|
||||
MD5 = (string)realEntry["md5"],
|
||||
ReleaseTime = TimeUtils.FormatUnixTimestamp(long.Parse(realEntry["timestamp"].ToString())),
|
||||
jsonToken = realEntry
|
||||
});
|
||||
}
|
||||
|
||||
loader.output = new DlLiteLoaderListResult
|
||||
{ isOfficial = true, sourceName = Lang.Text("Download.Source.LiteLoaderOfficial"), Value = versions };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LiteLoader", result),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LiteLoader 版本列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLiteLoaderListResult> dlLiteLoaderListBmclapiLoader =
|
||||
new("DlLiteLoaderList Bmclapi", DlLiteLoaderListBmclapiMain);
|
||||
|
||||
private static void DlLiteLoaderListBmclapiMain(ModLoader.LoaderTask<int, DlLiteLoaderListResult> loader)
|
||||
{
|
||||
var result =
|
||||
(JsonObject)Requester.FetchJson(
|
||||
"https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json");
|
||||
try
|
||||
{
|
||||
var json = (JsonObject)result["versions"];
|
||||
var versions = new List<DlLiteLoaderListEntry>();
|
||||
foreach (var Pair in json)
|
||||
{
|
||||
if (Pair.Key.StartsWithF("1.6") || Pair.Key.StartsWithF("1.5"))
|
||||
continue;
|
||||
var realEntry =
|
||||
(Pair.Value["artefacts"] ?? Pair.Value["snapshots"])["com.mumfrey:liteloader"]["latest"];
|
||||
versions.Add(new DlLiteLoaderListEntry
|
||||
{
|
||||
Inherit = Pair.Key,
|
||||
IsLegacy = double.Parse(Pair.Key.Split(".")[1]) < 8d,
|
||||
IsPreview = realEntry["stream"].ToString().ToLower() == "snapshot",
|
||||
FileName = "liteloader-installer-" + Pair.Key +
|
||||
(Pair.Key == "1.8" || Pair.Key == "1.9" ? ".0" : "") + "-00-SNAPSHOT.jar",
|
||||
MD5 = (string)realEntry["md5"],
|
||||
ReleaseTime = TimeUtils.FormatUnixTimestamp(long.Parse((string)realEntry["timestamp"])),
|
||||
jsonToken = realEntry
|
||||
});
|
||||
}
|
||||
|
||||
loader.output = new DlLiteLoaderListResult { isOfficial = false, sourceName = "BMCLAPI", Value = versions };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LiteLoader BMCLAPI", result), ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlFabricList | Fabric 列表
|
||||
|
||||
public struct DlFabricListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public JsonObject Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fabric 列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlFabricListResult> dlFabricListLoader =
|
||||
new("DlFabricList Main", DlFabricListMain);
|
||||
|
||||
private static void DlFabricListMain(ModLoader.LoaderTask<int, DlFabricListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlFabricListResult>, int>>
|
||||
{ new(dlFabricListBmclapiLoader, 30), new(dlFabricListOfficialLoader, 30 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlFabricListResult>, int>>
|
||||
{ new(dlFabricListOfficialLoader, 5), new(dlFabricListBmclapiLoader, 5 + 30) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlFabricListResult>, int>>
|
||||
{ new(dlFabricListOfficialLoader, 60), new(dlFabricListBmclapiLoader, 60 + 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fabric 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlFabricListResult> dlFabricListOfficialLoader =
|
||||
new("DlFabricList Official", DlFabricListOfficialMain);
|
||||
|
||||
private static void DlFabricListOfficialMain(ModLoader.LoaderTask<int, DlFabricListResult> loader)
|
||||
{
|
||||
var result = (JsonObject)Requester.FetchJson("https://meta.fabricmc.net/v2/versions");
|
||||
try
|
||||
{
|
||||
var output = new DlFabricListResult
|
||||
{ isOfficial = true, sourceName = Lang.Text("Download.Source.FabricOfficial"), Value = result };
|
||||
if (output.Value["game"] is null || output.Value["loader"] is null || output.Value["installer"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Fabric", result));
|
||||
loader.output = output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Fabric", result), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fabric 列表,BMCLAPI。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlFabricListResult> dlFabricListBmclapiLoader =
|
||||
new("DlFabricList Bmclapi", DlFabricListBmclapiMain);
|
||||
|
||||
private static void DlFabricListBmclapiMain(ModLoader.LoaderTask<int, DlFabricListResult> loader)
|
||||
{
|
||||
var result = (JsonObject)Requester.FetchJson("https://bmclapi2.bangbang93.com/fabric-meta/v2/versions");
|
||||
try
|
||||
{
|
||||
var output = new DlFabricListResult { isOfficial = false, sourceName = "BMCLAPI", Value = result };
|
||||
if (output.Value["game"] is null || output.Value["loader"] is null || output.Value["installer"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Fabric BMCLAPI",
|
||||
result));
|
||||
loader.output = output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "Fabric BMCLAPI", result), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fabric API 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, List<ModComp.CompFile>> dlFabricApiLoader = new("Fabric API List Loader",
|
||||
task => task.output = ModComp.CompFilesGet("fabric-api", false));
|
||||
|
||||
/// <summary>
|
||||
/// OptiFabric 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, List<ModComp.CompFile>> dlOptiFabricLoader =
|
||||
new("OptiFabric List Loader", task => task.output = ModComp.CompFilesGet("322385", true));
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlLabyModList | LabyMod 列表
|
||||
|
||||
public struct DlLabyModListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public JsonObject Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LabyMod 列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLabyModListResult> dlLabyModListLoader =
|
||||
new("DlLabyModList Main", DlLabyModListMain);
|
||||
|
||||
private static void DlLabyModListMain(ModLoader.LoaderTask<int, DlLabyModListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLabyModListResult>, int>>
|
||||
{ new(dlLabyModListOfficialLoader, 30), new(dlLabyModListOfficialLoader, 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLabyModListResult>, int>>
|
||||
{ new(dlLabyModListOfficialLoader, 5), new(dlLabyModListOfficialLoader, 35) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLabyModListResult>, int>>
|
||||
{ new(dlLabyModListOfficialLoader, 60), new(dlLabyModListOfficialLoader, 60) },
|
||||
loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LabyMod 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLabyModListResult> dlLabyModListOfficialLoader =
|
||||
new("DlLabyModList Official", DlLabyModListOfficialMain);
|
||||
|
||||
private static void DlLabyModListOfficialMain(ModLoader.LoaderTask<int, DlLabyModListResult> loader)
|
||||
{
|
||||
JsonObject resultProduction;
|
||||
using (var productionResponse = HttpRequest
|
||||
.Create("https://releases.r2.labymod.net/api/v1/manifest/production/latest.json")
|
||||
.WithHttpVersionOption(HttpVersion.Version20)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
resultProduction = (JsonObject)ModBase.GetJson(productionResponse.AsString());
|
||||
}
|
||||
|
||||
JsonObject resultSnapshot;
|
||||
using (var snapshotResponse = HttpRequest
|
||||
.Create("https://releases.r2.labymod.net/api/v1/manifest/snapshot/latest.json")
|
||||
.WithHttpVersionOption(HttpVersion.Version20)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
snapshotResponse.EnsureSuccessStatusCode();
|
||||
resultSnapshot = (JsonObject)ModBase.GetJson(snapshotResponse.AsString());
|
||||
}
|
||||
|
||||
var result = new JsonObject();
|
||||
result.Add("production", resultProduction);
|
||||
result.Add("snapshot", resultSnapshot);
|
||||
try
|
||||
{
|
||||
var output = new DlLabyModListResult { Value = result };
|
||||
if (output.Value["production"]["labyModVersion"] is null ||
|
||||
output.Value["snapshot"]["labyModVersion"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LabyMod",
|
||||
result));
|
||||
loader.output = output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LabyMod", result),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlMod | Mod 镜像源请求
|
||||
|
||||
/// <summary>
|
||||
/// 对可能涉及 Mod 镜像源的请求进行处理,返回字符串。
|
||||
/// 调用 NetGetCodeByRequest,会进行重试。
|
||||
/// </summary>
|
||||
public static string DlModRequest(string url) => DlModRequest<string>(url);
|
||||
|
||||
/// <summary>
|
||||
/// 对可能涉及 Mod 镜像源的请求进行处理,返回字符串或 JSON 对象。
|
||||
/// 调用 NetGetCodeByRequest,会进行重试。
|
||||
/// </summary>
|
||||
public static T DlModRequest<T>(string url)
|
||||
{
|
||||
var urls = new List<KeyValuePair<string, int>>();
|
||||
var mcimUrl = DlSourceModGet(url);
|
||||
if ((mcimUrl ?? "") != (url ?? ""))
|
||||
switch (Config.Download.Comp.CompSourceSolution)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(url, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(url, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var exs = "";
|
||||
foreach (var Source in urls)
|
||||
try
|
||||
{
|
||||
var json = Requester.FetchString(Source.Key, new RequestParam
|
||||
{
|
||||
Timeout = Source.Value * 1000,
|
||||
UseBrowserUserAgent = true
|
||||
});
|
||||
if (typeof(T) == typeof(string)) return (T)(object)json;
|
||||
return (T)(object)ModBase.GetJson(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 镜像源可能随机爆炸,忽略就好
|
||||
if (!ex.Message.ContainsF("mcimirror")) exs += ex.Message + "\r\n";
|
||||
}
|
||||
|
||||
throw new Exception(exs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非泛型版本的 DlModRequest,返回 string
|
||||
/// 对可能涉及 Mod 镜像源的请求进行处理。
|
||||
/// 调用 NetRequest,会进行重试。
|
||||
/// </summary>
|
||||
public static string DlModRequest(string url, string method, string data, string contentType,
|
||||
bool allowMirror = false) => DlModRequest<string>(url, method, data, contentType, allowMirror);
|
||||
|
||||
/// <summary>
|
||||
/// 对可能涉及 Mod 镜像源的请求进行处理。
|
||||
/// 调用 NetRequest,会进行重试。
|
||||
/// </summary>
|
||||
public static T DlModRequest<T>(string url, string method, string data, string contentType,
|
||||
bool allowMirror = false)
|
||||
{
|
||||
var urls = new List<KeyValuePair<string, int>>();
|
||||
var mcimUrl = DlSourceModGet(url);
|
||||
if ((mcimUrl ?? "") != (url ?? ""))
|
||||
switch (allowMirror ? Config.Download.Comp.CompSourceSolution : 2)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(url, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
urls.Add(new KeyValuePair<string, int>(url, 5));
|
||||
urls.Add(new KeyValuePair<string, int>(url, 15));
|
||||
urls.Add(new KeyValuePair<string, int>(mcimUrl, 10));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var exs = "";
|
||||
foreach (var Source in urls)
|
||||
try
|
||||
{
|
||||
string json = Requester.Fetch(Source.Key, new FetchParam
|
||||
{
|
||||
Method = method,
|
||||
Content = data,
|
||||
ContentType = contentType,
|
||||
Timeout = Source.Value * 1000
|
||||
});
|
||||
if (typeof(T) == typeof(string)) return (T)(object)json; // 沟槽的,为什么不能写 T is string
|
||||
return (T)(object)ModBase.GetJson(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!ex.Message.ContainsF("mcimirror")) exs += ex.Message + "\r\n";
|
||||
}
|
||||
|
||||
throw new Exception(exs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlSource | 镜像下载源
|
||||
|
||||
private static bool dlPreferMojang;
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件(而非获取版本列表)的时候,是否优先使用官方源。
|
||||
/// </summary>
|
||||
public static bool DlSourcePreferMojang =>
|
||||
Config.Download.FileSource == 2 ||
|
||||
(Config.Download.FileSource == 1 && dlPreferMojang);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件(而非获取版本列表)的时候,根据是否优先使用官方源决定使用 Url 的顺序。
|
||||
/// </summary>
|
||||
public static IEnumerable<string> DlSourceOrder(IEnumerable<string> officialUrls, IEnumerable<string> mirrorUrls)
|
||||
{
|
||||
return DlSourcePreferMojang ? officialUrls.Union(mirrorUrls) : mirrorUrls.Union(officialUrls);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取版本列表(而非下载文件)的时候,是否优先使用官方源。
|
||||
/// </summary>
|
||||
public static bool DlVersionListPreferMojang =>
|
||||
Config.Download.VersionListSource == 2 ||
|
||||
(Config.Download.VersionListSource == 1 && dlPreferMojang);
|
||||
|
||||
/// <summary>
|
||||
/// 获取版本列表(而非下载文件)的时候,根据是否优先使用官方源决定使用 Url 的顺序。
|
||||
/// </summary>
|
||||
public static IEnumerable<string> DlVersionListOrder(IEnumerable<string> officialUrls,
|
||||
IEnumerable<string> mirrorUrls)
|
||||
{
|
||||
return DlVersionListPreferMojang ? officialUrls.Union(mirrorUrls) : mirrorUrls.Union(officialUrls);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 下载 Assets 文件。
|
||||
/// </summary>
|
||||
public static IEnumerable<string> DlSourceAssetsGet(string original)
|
||||
{
|
||||
original = original.Replace("http://resources.download.minecraft.net",
|
||||
"https://resources.download.minecraft.net");
|
||||
return DlSourceOrder(new[] { original },
|
||||
new[]
|
||||
{
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/assets")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/assets")
|
||||
.Replace("https://resources.download.minecraft.net", "https://bmclapi2.bangbang93.com/assets")
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载 Libraries 文件。
|
||||
/// </summary>
|
||||
public static IEnumerable<string> DlSourceLibraryGet(string original)
|
||||
{
|
||||
if (new[] { "minecraftforge", "fabricmc", "neoforged" }.Any(k => original.Contains(k))) // 不添加原版源
|
||||
return new[]
|
||||
{
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft",
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft",
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
|
||||
};
|
||||
|
||||
return DlSourceOrder(new[] { original },
|
||||
new[]
|
||||
{
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/maven")
|
||||
.Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft",
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft",
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
original
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载 Launcher 或 Meta 文件。
|
||||
/// 不应使用它来获取版本列表(因为它只使用文件下载源设置来决定源顺序)。
|
||||
/// </summary>
|
||||
public static IEnumerable<string> DlSourceLauncherOrMetaGet(string original)
|
||||
{
|
||||
if (original is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.NoJsonDownloadAddress"));
|
||||
return DlSourceOrder(new[] { original },
|
||||
new[]
|
||||
{
|
||||
original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.Replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.Replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft",
|
||||
"https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
original
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod Api 镜像源
|
||||
/// </summary>
|
||||
/// <param name="original"></param>
|
||||
/// <returns></returns>
|
||||
public static string DlSourceModGet(string original)
|
||||
{
|
||||
return original.Replace("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth")
|
||||
.Replace("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod 下载镜像源
|
||||
/// </summary>
|
||||
/// <param name="original"></param>
|
||||
/// <returns></returns>
|
||||
public static List<string> DlSourceModDownloadGet(string original)
|
||||
{
|
||||
var res = new List<string>();
|
||||
var mirrorDl = original.Replace("https://cdn.modrinth.com", "https://mod.mcimirror.top")
|
||||
.Replace("https://edge.forgecdn.net",
|
||||
"https://mod.mcimirror.top"); // like https://cdn.modrinth.com/data/P7dR8mSH/versions/X2hTodix/fabric-api-0.129.0%2B1.21.8.jar
|
||||
// like https://edge.forgecdn.net/files/6767/951/jei-1.21.5-neoforge-21.4.0.27.jar
|
||||
switch (Config.Download.Comp.CompSourceSolution)
|
||||
{
|
||||
case 0: // 镜像源
|
||||
{
|
||||
res.Add(mirrorDl);
|
||||
res.Add(mirrorDl);
|
||||
break;
|
||||
}
|
||||
case 1: // 平衡
|
||||
{
|
||||
res.Add(original);
|
||||
res.Add(mirrorDl);
|
||||
break;
|
||||
}
|
||||
case 2: // 官方源
|
||||
{
|
||||
res.Add(original);
|
||||
res.Add(original); // 错误
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
Config.Download.Comp.CompSourceSolution = 1;
|
||||
res.Add(original);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.Add(original);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Loader 自动切换
|
||||
private static void DlSourceLoader<InputType, OutputType>(ModLoader.LoaderTask<InputType, OutputType> mainLoader,
|
||||
List<KeyValuePair<ModLoader.LoaderTask<InputType, OutputType>, int>> loaderList, bool isForceRestart = false)
|
||||
{
|
||||
var waitCycle = 0;
|
||||
while (true)
|
||||
{
|
||||
// 检查状态
|
||||
var beforeLoadersAllFailed = true;
|
||||
foreach (var SubLoader in loaderList)
|
||||
{
|
||||
if (waitCycle == 0) // 判断是否可以不加载,直接使用已经加载好的结果
|
||||
{
|
||||
if (isForceRestart)
|
||||
continue; // 强制刷新,不行
|
||||
if (SubLoader.Key.input is null ^ mainLoader.input is null || (SubLoader.Key.input is not null &&
|
||||
!SubLoader.Key.input.Equals(mainLoader.input)))
|
||||
continue; // 父子加载器的输入不一样,也不行
|
||||
}
|
||||
|
||||
if (SubLoader.Key.State != ModBase.LoadState.Failed)
|
||||
beforeLoadersAllFailed = false;
|
||||
if (SubLoader.Key.State == ModBase.LoadState.Finished)
|
||||
{
|
||||
// 检查加载器成功
|
||||
mainLoader.output = SubLoader.Key.output;
|
||||
DlSourceLoaderAbort(loaderList);
|
||||
return;
|
||||
}
|
||||
|
||||
if (beforeLoadersAllFailed)
|
||||
// 此前的加载器全部失败,直接启动后续加载器
|
||||
if (waitCycle < SubLoader.Value * 100)
|
||||
waitCycle = SubLoader.Value * 100;
|
||||
}
|
||||
|
||||
// 第一轮时:既然不直接使用已经加载好的结果,那就启动第一个加载器
|
||||
if (waitCycle == 0)
|
||||
{
|
||||
loaderList.First().Key.Start(mainLoader.input, isForceRestart);
|
||||
foreach (var Loader in loaderList.Skip(1))
|
||||
Loader.Key.State = ModBase.LoadState.Waiting; // 将其他源标记为未启动,以确保可以切换下载源(#184)
|
||||
}
|
||||
|
||||
// 检查加载器失败或超时
|
||||
for (int i = 0, loopTo = loaderList.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
if (waitCycle != loaderList[i].Value * 100)
|
||||
continue;
|
||||
if (i < loaderList.Count - 1 && !loaderList.All(l => l.Key.State == ModBase.LoadState.Failed))
|
||||
{
|
||||
// 若还有下一个源,则启动下一个源
|
||||
loaderList[i + 1].Key.Start(mainLoader.input, isForceRestart);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 若没有,则失败
|
||||
Exception errorInfo = null;
|
||||
for (int ii = 0, loopTo1 = loaderList.Count - 1; ii <= loopTo1; ii++)
|
||||
{
|
||||
loaderList[ii].Key.input = default; // 重置输入,以免以同样的输入“重试加载”时直接失败
|
||||
if (loaderList[ii].Key.Error is null) continue;
|
||||
if (errorInfo is null || loaderList[ii].Key.Error.Message
|
||||
.Contains(Lang.Text("Minecraft.Download.Error.NotFound")))
|
||||
errorInfo = loaderList[ii].Key.Error;
|
||||
}
|
||||
|
||||
errorInfo ??= new TimeoutException(Lang.Text("Minecraft.Download.Error.Timeout"));
|
||||
DlSourceLoaderAbort(loaderList);
|
||||
throw errorInfo;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// 计时
|
||||
Thread.Sleep(10);
|
||||
waitCycle += 1;
|
||||
// 检查父加载器中断
|
||||
if (mainLoader.IsAborted)
|
||||
{
|
||||
DlSourceLoaderAbort(loaderList);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DlSourceLoaderAbort<InputType, OutputType>(
|
||||
List<KeyValuePair<ModLoader.LoaderTask<InputType, OutputType>, int>> loaderList)
|
||||
{
|
||||
foreach (var Loader in loaderList)
|
||||
if (Loader.Key.State == ModBase.LoadState.Loading)
|
||||
Loader.Key.Abort();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DlLegacyFabricList | LegacyFabric 列表
|
||||
|
||||
public struct DlLegacyFabricListResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据来源名称,如“Official”,“BMCLAPI”。
|
||||
/// </summary>
|
||||
public string sourceName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为官方的实时数据。
|
||||
/// </summary>
|
||||
public bool isOfficial;
|
||||
|
||||
/// <summary>
|
||||
/// 获取到的数据。
|
||||
/// </summary>
|
||||
public JsonObject Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LegacyFabric 列表,主加载器。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLegacyFabricListResult> dlLegacyFabricListLoader =
|
||||
new("DlLegacyFabricList Main", DlLegacyFabricListMain);
|
||||
|
||||
private static void DlLegacyFabricListMain(ModLoader.LoaderTask<int, DlLegacyFabricListResult> loader)
|
||||
{
|
||||
switch (Config.Download.VersionListSource)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLegacyFabricListResult>, int>>
|
||||
{ new(dlLegacyFabricListOfficialLoader, 30) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLegacyFabricListResult>, int>>
|
||||
{ new(dlLegacyFabricListOfficialLoader, 5) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DlSourceLoader(loader,
|
||||
new List<KeyValuePair<ModLoader.LoaderTask<int, DlLegacyFabricListResult>, int>>
|
||||
{ new(dlLegacyFabricListOfficialLoader, 60) }, loader.isForceRestarting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LegacyFabric 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, DlLegacyFabricListResult> dlLegacyFabricListOfficialLoader =
|
||||
new("DlLegacyFabricList Official", DlLegacyFabricListOfficialMain);
|
||||
|
||||
private static void DlLegacyFabricListOfficialMain(ModLoader.LoaderTask<int, DlLegacyFabricListResult> loader)
|
||||
{
|
||||
var result =
|
||||
(JsonObject)Requester.FetchJson("https://meta.legacyfabric.net/v2/versions");
|
||||
try
|
||||
{
|
||||
var output = new DlLegacyFabricListResult
|
||||
{ isOfficial = true, sourceName = Lang.Text("Download.Source.LegacyFabricOfficial"), Value = result };
|
||||
if (output.Value["game"] is null || output.Value["loader"] is null || output.Value["installer"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LegacyFabric",
|
||||
result));
|
||||
loader.output = output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Download.Error.VersionListOperationFailed", "LegacyFabric", result), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy Fabric API 列表,官方源。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, List<ModComp.CompFile>> dlLegacyFabricApiLoader =
|
||||
new("Legacy Fabric API List Loader", task => task.output = ModComp.CompFilesGet("legacy-fabric-api", false));
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 发送 Minecraft 更新提示。
|
||||
/// </summary>
|
||||
public static void McDownloadClientUpdateHint(string versionName, JsonObject json)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取对应版本
|
||||
JsonNode version = null;
|
||||
foreach (var Token in json["versions"].AsArray())
|
||||
if (Token["id"] is not null && (Token["id"].ToString() ?? "") == (versionName ?? ""))
|
||||
{
|
||||
version = Token;
|
||||
break;
|
||||
}
|
||||
|
||||
// 进行提示
|
||||
if (version is null)
|
||||
return;
|
||||
var time = version["releaseTime"].ToObject<DateTime>();
|
||||
var msgBoxText = Lang.Text("Minecraft.Update.NewVersion", versionName) + "\r\n" +
|
||||
((DateTime.Now - time).TotalDays > 1d
|
||||
? Lang.Text("Minecraft.Update.UpdateTime") + Lang.Date(time)
|
||||
: Lang.Text("Minecraft.Update.UpdatedAt") + Lang.TimeSpan(time - DateTime.Now));
|
||||
var msgResult = ModMain.MyMsgBox(msgBoxText, Lang.Text("Minecraft.Update.Title"),
|
||||
Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Download"),
|
||||
(DateTime.Now - time).TotalHours > 3d ? Lang.Text("Common.Action.UpdateLog") : "",
|
||||
button3Action: () => ModDownloadLib.McUpdateLogShow(version));
|
||||
// 弹窗结果
|
||||
if (msgResult == 2)
|
||||
// 下载
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
PageDownloadInstall.mcVersionWaitingForSelect = versionName;
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall);
|
||||
});
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Error.UpdateNotify", versionName ?? "Nothing"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Error.UpdateNotify", versionName ?? "Nothing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModFolder
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前的 Minecraft 文件夹路径,以"\"结尾。
|
||||
/// </summary>
|
||||
public static string mcFolderSelected;
|
||||
|
||||
/// <summary>
|
||||
/// 当前的 Minecraft 文件夹列表。
|
||||
/// </summary>
|
||||
public static List<McFolder> mcFolderList = new();
|
||||
|
||||
public class McFolder // 必须是 Class,否则不是引用类型,在 ForEach 中不会得到刷新
|
||||
{
|
||||
public enum Types
|
||||
{
|
||||
Original,
|
||||
RenamedOriginal,
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件夹路径。
|
||||
/// 以 \ 结尾,例如 "D:\Game\MC\.minecraft\"。
|
||||
/// </summary>
|
||||
public string Location;
|
||||
|
||||
public string Name;
|
||||
public Types type;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not McFolder)
|
||||
return false;
|
||||
var folder = (McFolder)obj;
|
||||
return (Name ?? "") == (folder.Name ?? "") && (Location ?? "") == (folder.Location ?? "") &&
|
||||
type == folder.type;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Location;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Minecraft 文件夹列表。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<int, int> mcFolderListLoader = new("Minecraft Folder List",
|
||||
_ => McFolderListLoadSub(), priority: ThreadPriority.AboveNormal);
|
||||
|
||||
private static void McFolderListLoadSub()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 初始化
|
||||
var cacheMcFolderList = new List<McFolder>();
|
||||
|
||||
#region 读取自定义(Custom)文件夹,可能没有结果
|
||||
|
||||
// 格式:TMZ 12>C://xxx/xx/|Test>D://xxx/xx/|名称>路径
|
||||
foreach (string folder in (IEnumerable)((dynamic)States.Game.Folders).Split("|"))
|
||||
{
|
||||
if (string.IsNullOrEmpty(folder))
|
||||
continue;
|
||||
if (!folder.Contains(">") || !folder.EndsWithF(@"\"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Select.Folder.Invalid", folder), HintType.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = folder.Split(">")[0];
|
||||
var path = folder.Split(">")[1];
|
||||
try
|
||||
{
|
||||
ModBase.CheckPermissionWithException(path);
|
||||
cacheMcFolderList.Add(new McFolder { Name = name, Location = path, type = McFolder.Types.Custom });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Select.Folder.Invalid.WithDetail", path, ex.ToString()),
|
||||
Lang.Text("Select.Folder.InvalidTitle"), isWarn: true);
|
||||
ModBase.Log(ex, $"无法访问 Minecraft 文件夹 {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 读取默认(Original)文件夹,即当前、官启文件夹,可能没有结果
|
||||
|
||||
var currentMcFolderList = new List<McFolder>();
|
||||
var originalMcFolderList = new List<McFolder>();
|
||||
// 扫描当前文件夹
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(ModBase.exePath + @"versions\"))
|
||||
originalMcFolderList.Add(new McFolder
|
||||
{ Name = Lang.Text("Select.Folder.CurrentFolder"), Location = ModBase.exePath, type = McFolder.Types.Original });
|
||||
foreach (var folder in new DirectoryInfo(ModBase.exePath).GetDirectories())
|
||||
if (Directory.Exists(Path.Combine(folder.FullName, "versions")) || folder.Name == ".minecraft")
|
||||
{
|
||||
var newCurrentFolder = new McFolder
|
||||
{ Name = folder.Name, Location = folder.FullName + @"\", type = McFolder.Types.Original };
|
||||
originalMcFolderList.Add(newCurrentFolder);
|
||||
currentMcFolderList.Add(newCurrentFolder);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "扫描 PCL 所在文件夹中是否有 MC 文件夹失败");
|
||||
}
|
||||
|
||||
// 扫描官启文件夹
|
||||
var mojangPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".minecraft") + @"\";
|
||||
if ((!currentMcFolderList.Any() || (mojangPath ?? "") != (currentMcFolderList[0].Location ?? "")) &&
|
||||
Directory.Exists(Path.Combine(mojangPath, "versions"))) // 当前文件夹不是官启文件夹
|
||||
// 具有权限且存在 versions 文件夹
|
||||
originalMcFolderList.Add(new McFolder
|
||||
{ Name = Lang.Text("Select.Folder.OfficialLauncherFolder"), Location = mojangPath, type = McFolder.Types.Original });
|
||||
|
||||
ModBase.Log(cacheMcFolderList.Count + " 个自定义文件夹," + originalMcFolderList.Count + " 个原始文件夹");
|
||||
|
||||
foreach (var newOriginalFolder in originalMcFolderList)
|
||||
{
|
||||
var unAdded = true;
|
||||
foreach (var cacheFolder in cacheMcFolderList)
|
||||
if ((cacheFolder.Location ?? "") == (newOriginalFolder.Location ?? ""))
|
||||
{
|
||||
if ((cacheFolder.Name ?? "") != (newOriginalFolder.Name ?? ""))
|
||||
cacheFolder.type = McFolder.Types.RenamedOriginal;
|
||||
else
|
||||
cacheFolder.type = McFolder.Types.Original;
|
||||
unAdded = false;
|
||||
}
|
||||
|
||||
if (unAdded)
|
||||
cacheMcFolderList.Add(newOriginalFolder); // 如果没有重命名,则添加当前文件夹
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 读取自定义文件夹情况并写入设置
|
||||
|
||||
// 将自定义文件夹情况同步到设置
|
||||
var config = new List<string>();
|
||||
foreach (var Folder in cacheMcFolderList)
|
||||
config.Add(Folder.Name + ">" + Folder.Location);
|
||||
if (!config.Any())
|
||||
config.Add(""); // 防止 0 元素 Join 返回 Nothing
|
||||
States.Game.Folders = config.Join("|");
|
||||
|
||||
#endregion
|
||||
|
||||
// 若没有可用文件夹,则创建 .minecraft
|
||||
if (!cacheMcFolderList.Any())
|
||||
{
|
||||
Directory.CreateDirectory(ModBase.exePath + @".minecraft\versions\");
|
||||
cacheMcFolderList.Add(new McFolder
|
||||
{ Name = Lang.Text("Select.Folder.CurrentFolder"), Location = ModBase.exePath + @".minecraft\", type = McFolder.Types.Original });
|
||||
}
|
||||
|
||||
foreach (var Folder in cacheMcFolderList) McFolderLauncherProfilesJsonCreate(Folder.Location);
|
||||
if (Config.Debug.AddRandomDelay)
|
||||
Thread.Sleep(RandomUtils.NextInt(200, 2000));
|
||||
|
||||
// 回设
|
||||
mcFolderList = cacheMcFolderList;
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Folder.Error.Load"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Select.Folder.Error.Load"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 Minecraft 文件夹创建 launcher_profiles.json 文件。
|
||||
/// </summary>
|
||||
public static void McFolderLauncherProfilesJsonCreate(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(Path.Combine(folder, "launcher_profiles.json")))
|
||||
return;
|
||||
var now = DateTime.Now;
|
||||
var resultJson = @"{
|
||||
""profiles"": {
|
||||
""PCL"": {
|
||||
""icon"": ""Grass"",
|
||||
""name"": ""PCL"",
|
||||
""lastVersionId"": ""latest-release"",
|
||||
""type"": ""latest-release"",
|
||||
""lastUsed"": """ + now.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture) + "T" +
|
||||
now.ToString("HH':'mm':'ss", CultureInfo.InvariantCulture) + @".0000Z""
|
||||
}
|
||||
},
|
||||
""selectedProfile"": ""PCL"",
|
||||
""clientToken"": ""23323323323323323323323323323333""
|
||||
}";
|
||||
ModBase.WriteFile(Path.Combine(folder, "launcher_profiles.json"), resultJson, encoding: Encoding.GetEncoding("GB18030"));
|
||||
ModBase.Log("[Minecraft] 已创建 launcher_profiles.json:" + folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"创建 launcher_profiles.json 失败(" + folder + ")",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Folder.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModInstanceList
|
||||
{
|
||||
#region 实例处理
|
||||
|
||||
public const int mcInstanceCacheVersion = 30;
|
||||
|
||||
private static object _McInstanceSelected_mcInstanceSelectedLast = 0; // 为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
|
||||
|
||||
/// <summary>
|
||||
/// 当前的 Minecraft 版本。
|
||||
/// </summary>
|
||||
public static McInstance McMcInstanceSelected
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_McInstanceSelected_mcInstanceSelectedLast, value))
|
||||
return;
|
||||
field = value; // 由于有可能是 Nothing,导致无法初始化,才得这样弄一圈
|
||||
_McInstanceSelected_mcInstanceSelectedLast = value;
|
||||
if (value is null)
|
||||
return;
|
||||
// 重置缓存的 Mod 文件夹
|
||||
PageDownloadCompDetail.cachedFolder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前按卡片分类的所有版本列表。
|
||||
/// </summary>
|
||||
public static Dictionary<McInstanceCardType, List<PCL.McInstance>> mcInstanceList = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 实例列表加载
|
||||
|
||||
/// <summary>
|
||||
/// 是否要求本次加载强制刷新实例列表。
|
||||
/// </summary>
|
||||
public static bool mcInstanceListForceRefresh;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为本次打开 PCL 后第一次加载实例列表。
|
||||
/// 这会清理所有 .pclignore 文件,而非跳过这些对应实例。
|
||||
/// </summary>
|
||||
private static bool _isFirstMcInstanceListLoad = true;
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Minecraft 文件夹的实例列表。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderTask<string, int> mcInstanceListLoader =
|
||||
new("Minecraft Instance List", InitMcInstanceList) { reloadTimeout = 1 };
|
||||
|
||||
private static void InitMcInstanceList(ModLoader.LoaderTask<string, int> loader)
|
||||
{
|
||||
var path = loader.input;
|
||||
try
|
||||
{
|
||||
// 初始化
|
||||
mcInstanceList = new Dictionary<McInstanceCardType, List<PCL.McInstance>>();
|
||||
var versionsPath = Path.Combine(path, "versions");
|
||||
var folderList = new List<string>();
|
||||
|
||||
// 读取版本文件夹
|
||||
if (Directory.Exists(versionsPath))
|
||||
try
|
||||
{
|
||||
foreach (var folder in new DirectoryInfo(versionsPath).GetDirectories())
|
||||
folderList.Add(folder.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Error.CannotReadInstanceFolder", versionsPath), ex);
|
||||
}
|
||||
|
||||
// 如果没有可用实例,清空缓存并跳过后续处理
|
||||
if (!folderList.Any())
|
||||
{
|
||||
ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", "");
|
||||
McMcInstanceSelected = null;
|
||||
States.Game.SelectedInstance = "";
|
||||
ModBase.Log("[Minecraft] 未找到可用 Minecraft 实例");
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据文件夹名列表生成辨识码
|
||||
var folderListHash = ModBase.GetHash(mcInstanceCacheVersion + "#" + string.Join("#", folderList));
|
||||
var folderListCheck = (int)(folderListHash % (int.MaxValue - 1));
|
||||
|
||||
// 尝试使用缓存
|
||||
var useCache = !mcInstanceListForceRefresh &&
|
||||
ModBase.Val(ModBase.ReadIni(Path.Combine(path, "PCL.ini"), "InstanceCache")) ==
|
||||
folderListCheck;
|
||||
|
||||
if (useCache)
|
||||
{
|
||||
var cachedResult = InitMcInstanceListWithCache(path);
|
||||
if (cachedResult is not null)
|
||||
mcInstanceList = cachedResult;
|
||||
else
|
||||
useCache = false; // 缓存无效,需要重载
|
||||
}
|
||||
|
||||
// 如果不能使用缓存,重新加载
|
||||
if (!useCache)
|
||||
{
|
||||
mcInstanceListForceRefresh = false;
|
||||
ModBase.Log("[Minecraft] 文件夹列表变更或缓存无效,重载所有实例");
|
||||
ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", folderListCheck.ToString());
|
||||
mcInstanceList = InitMcInstanceListWithoutCache(path);
|
||||
}
|
||||
|
||||
_isFirstMcInstanceListLoad = false;
|
||||
|
||||
if (loader.IsAborted)
|
||||
return;
|
||||
|
||||
// 尝试读取已储存的选择
|
||||
var savedSelection = ModBase.ReadIni(Path.Combine(path, "PCL.ini"), "Version");
|
||||
if (!string.IsNullOrEmpty(savedSelection))
|
||||
foreach (var card in mcInstanceList)
|
||||
foreach (var instance in card.Value)
|
||||
if ((instance.Name ?? "") == savedSelection && instance.state != McInstanceState.Error)
|
||||
{
|
||||
McMcInstanceSelected = instance;
|
||||
States.Game.SelectedInstance = McMcInstanceSelected.Name;
|
||||
ModBase.Log("[Minecraft] 选择该文件夹储存的 Minecraft 实例:" + McMcInstanceSelected.PathInstance);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动选择第一项
|
||||
var firstInstance = mcInstanceList
|
||||
.SelectMany(kv => kv.Value)
|
||||
.FirstOrDefault(i => i.state != McInstanceState.Error);
|
||||
|
||||
if (firstInstance is not null)
|
||||
{
|
||||
McMcInstanceSelected = firstInstance;
|
||||
States.Game.SelectedInstance = McMcInstanceSelected.Name;
|
||||
ModBase.Log("[Launch] 自动选择 Minecraft 实例:" + McMcInstanceSelected.PathInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
McMcInstanceSelected = null;
|
||||
States.Game.SelectedInstance = "";
|
||||
ModBase.Log("[Minecraft] 未找到可用 Minecraft 实例");
|
||||
}
|
||||
|
||||
// 调试延迟
|
||||
if (Config.Debug.AddRandomDelay is bool debugDelay && debugDelay)
|
||||
Thread.Sleep(RandomUtils.NextInt(200, 3000));
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
// 中断线程时什么也不做
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", ""); // 要求下次重新加载
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Instance.Error.ListLoad"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Select.Instance.Error.ListLoad"));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取实例列表
|
||||
private static Dictionary<McInstanceCardType, List<PCL.McInstance>> InitMcInstanceListWithCache(string path)
|
||||
{
|
||||
var results = new Dictionary<McInstanceCardType, List<PCL.McInstance>>();
|
||||
try
|
||||
{
|
||||
var cardCount = int.Parse(ModBase.ReadIni(path + "PCL.ini", "CardCount", (-1).ToString()));
|
||||
if (cardCount == -1)
|
||||
return null;
|
||||
for (int i = 0, loopTo = cardCount - 1; i <= loopTo; i++)
|
||||
{
|
||||
var cardType =
|
||||
(McInstanceCardType)int.Parse(ModBase.ReadIni(path + "PCL.ini", "CardKey" + (i + 1),
|
||||
"0"));
|
||||
var instanceList = new List<PCL.McInstance>();
|
||||
|
||||
// 循环读取实例
|
||||
foreach (var folder in ModBase.ReadIni(path + "PCL.ini", "CardValue" + (i + 1), ":").Split(":"))
|
||||
{
|
||||
if (string.IsNullOrEmpty(folder))
|
||||
continue;
|
||||
var versionFolder = $@"{path}versions\{folder}\";
|
||||
if (File.Exists(versionFolder + ".pclignore"))
|
||||
{
|
||||
if (_isFirstMcInstanceListLoad)
|
||||
{
|
||||
ModBase.Log("[Minecraft] 清理残留的忽略项目:" + versionFolder); // #2781
|
||||
File.Delete(versionFolder + ".pclignore");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[Minecraft] 跳过要求忽略的项目:" + versionFolder);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 读取单个实例
|
||||
var instance = new PCL.McInstance(versionFolder);
|
||||
instanceList.Add(instance);
|
||||
var instanceCfg = States.Instance;
|
||||
instance.Desc = instanceCfg.CustomInfo[instance.PathInstance];
|
||||
|
||||
if (string.IsNullOrEmpty(instance.Desc))
|
||||
instance.Desc = instanceCfg.Info[instance.PathInstance];
|
||||
if (!instanceCfg.LogoPathConfig.IsDefault(instance.PathInstance))
|
||||
instance.Logo = instanceCfg.LogoPath[instance.PathInstance];
|
||||
if (!instanceCfg.ReleaseTimeConfig.IsDefault(instance.PathInstance))
|
||||
instance.releaseTime = DateTime.Parse(instanceCfg.ReleaseTime[instance.PathInstance]);
|
||||
if (!instanceCfg.StateConfig.IsDefault(instance.PathInstance))
|
||||
instance.state =
|
||||
(McInstanceState)(int)instanceCfg.State[instance.PathInstance];
|
||||
instance.IsStar = instanceCfg.Starred[instance.PathInstance];
|
||||
instance.displayType =
|
||||
(McInstanceCardType)(int)instanceCfg.CardType[instance.PathInstance];
|
||||
if (instance.state != McInstanceState.Error &&
|
||||
!instanceCfg.VanillaVersionNameConfig.IsDefault(instance.PathInstance) &&
|
||||
!instanceCfg.VanillaVersionConfig
|
||||
.IsDefault(instance.PathInstance)) // 旧版本可能没有这一项,导致 Instance 不加载(#643)
|
||||
{
|
||||
var instanceInfo = new McInstanceInfo
|
||||
{
|
||||
Fabric = instanceCfg.FabricVersion[instance.PathInstance],
|
||||
LegacyFabric = instanceCfg.LegacyFabricVersion[instance.PathInstance],
|
||||
Quilt = instanceCfg.QuiltVersion[instance.PathInstance],
|
||||
Forge = instanceCfg.ForgeVersion[instance.PathInstance],
|
||||
LabyMod = instanceCfg.LabyModVersion[instance.PathInstance],
|
||||
NeoForge = instanceCfg.NeoForgeVersion[instance.PathInstance],
|
||||
Cleanroom = instanceCfg.CleanroomVersion[instance.PathInstance],
|
||||
OptiFine = instanceCfg.OptiFineVersion[instance.PathInstance],
|
||||
HasLiteLoader = instanceCfg.HasLiteLoader[instance.PathInstance],
|
||||
VanillaName = instanceCfg.VanillaVersionName[instance.PathInstance],
|
||||
vanilla = new Version(instanceCfg.VanillaVersion[instance.PathInstance])
|
||||
};
|
||||
instanceInfo.HasFabric = instanceInfo.Fabric.Any();
|
||||
instanceInfo.HasLegacyFabric = instanceInfo.LegacyFabric.Any();
|
||||
instanceInfo.HasQuilt = instanceInfo.Quilt.Any();
|
||||
instanceInfo.HasForge = instanceInfo.Forge.Any();
|
||||
instanceInfo.HasNeoForge = instanceInfo.NeoForge.Any();
|
||||
instanceInfo.HasCleanroom = instanceInfo.Cleanroom.Any();
|
||||
instanceInfo.HasOptiFine = instanceInfo.OptiFine.Any();
|
||||
instance.Info = instanceInfo;
|
||||
}
|
||||
|
||||
// 重新检查错误实例
|
||||
if (instance.state == McInstanceState.Error)
|
||||
{
|
||||
// 重新获取实例错误信息
|
||||
var oldDesc = instance.Desc;
|
||||
instance.state = McInstanceState.Original;
|
||||
instance.Check();
|
||||
// 校验错误原因是否改变
|
||||
var customInfo = States.Instance.CustomInfo[instance.PathInstance];
|
||||
if (instance.state == McInstanceState.Original || (string.IsNullOrEmpty(customInfo) &&
|
||||
!((oldDesc ?? "") ==
|
||||
(instance.Desc ?? ""))))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 实例 " + instance.Name + " 的错误状态已变更,新的状态为:" + instance.Desc);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 校验未加载的实例
|
||||
if (string.IsNullOrEmpty(instance.Logo))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 实例 " + instance.Name + " 未被加载");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取实例加载缓存失败(" + folder + ")");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceList.Any())
|
||||
results.Add(cardType, instanceList);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取实例缓存失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<McInstanceCardType, List<PCL.McInstance>> InitMcInstanceListWithoutCache(string path)
|
||||
{
|
||||
var instanceList = new List<PCL.McInstance>();
|
||||
|
||||
#region 循环加载每个实例的信息
|
||||
|
||||
foreach (var folder in new DirectoryInfo(path + "versions").GetDirectories())
|
||||
{
|
||||
if (!folder.Exists || !folder.EnumerateFiles().Any())
|
||||
{
|
||||
ModBase.Log("[Minecraft] 跳过空文件夹:" + folder.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((folder.Name == "cache" || folder.Name == "BLClient" || folder.Name == "PCL") &&
|
||||
!File.Exists(Path.Combine(folder.FullName, folder.Name + ".json")))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 跳过可能不是实例文件夹的项目:" + folder.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
var instanceFolder = folder.FullName + @"\";
|
||||
if (File.Exists(instanceFolder + ".pclignore"))
|
||||
{
|
||||
if (_isFirstMcInstanceListLoad)
|
||||
{
|
||||
ModBase.Log("[Minecraft] 清理残留的忽略项目:" + instanceFolder); // #2781
|
||||
try
|
||||
{
|
||||
File.Delete(instanceFolder + ".pclignore");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Folder.Error.Cleanup", instanceFolder),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Select.Folder.Error.Cleanup", instanceFolder));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[Minecraft] 跳过要求忽略的项目:" + instanceFolder);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var instance = new PCL.McInstance(instanceFolder);
|
||||
instanceList.Add(instance);
|
||||
instance.Load();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
var results = new Dictionary<McInstanceCardType, List<PCL.McInstance>>();
|
||||
|
||||
#region 将实例分类到各个卡片
|
||||
|
||||
try
|
||||
{
|
||||
// 未经过自定义的实例列表
|
||||
var instanceListOriginal = new Dictionary<McInstanceCardType, List<PCL.McInstance>>();
|
||||
|
||||
// 单独列出收藏的实例
|
||||
var staredInstances = new List<PCL.McInstance>();
|
||||
foreach (var instance in instanceList.ToList())
|
||||
{
|
||||
if (!instance.IsStar)
|
||||
continue;
|
||||
if (instance.displayType == McInstanceCardType.Hidden)
|
||||
continue;
|
||||
staredInstances.Add(instance);
|
||||
instanceList.Remove(instance);
|
||||
}
|
||||
|
||||
if (staredInstances.Any())
|
||||
instanceListOriginal.Add(McInstanceCardType.Star, staredInstances);
|
||||
|
||||
// 预先筛选出愚人节和错误的实例
|
||||
McInstanceFilter(ref instanceList, ref instanceListOriginal, new[] { McInstanceState.Error },
|
||||
McInstanceCardType.Error);
|
||||
McInstanceFilter(ref instanceList, ref instanceListOriginal, new[] { McInstanceState.Fool },
|
||||
McInstanceCardType.Fool);
|
||||
|
||||
// 筛选 API 实例
|
||||
McInstanceFilter(ref instanceList, ref instanceListOriginal,
|
||||
new[]
|
||||
{
|
||||
McInstanceState.Forge, McInstanceState.NeoForge, McInstanceState.LiteLoader, McInstanceState.Fabric,
|
||||
McInstanceState.LegacyFabric, McInstanceState.Quilt, McInstanceState.Cleanroom,
|
||||
McInstanceState.LabyMod
|
||||
}, McInstanceCardType.API);
|
||||
|
||||
// 将老实例预先分类入不常用,只剩余原版、快照、OptiFine
|
||||
var instanceUseful = new List<PCL.McInstance>();
|
||||
var instanceRubbish = new List<PCL.McInstance>();
|
||||
McInstanceFilter(ref instanceList, new[] { McInstanceState.Old }, ref instanceRubbish);
|
||||
|
||||
// 确认最新实例,若为快照则加入常用列表
|
||||
var latestInstance = instanceList
|
||||
.Where(v => v.state == McInstanceState.Original || v.state == McInstanceState.Snapshot)
|
||||
.MaxOrDefault(v => v.releaseTime);
|
||||
if (latestInstance is not null && latestInstance.state == McInstanceState.Snapshot)
|
||||
{
|
||||
instanceUseful.Add(latestInstance);
|
||||
instanceList.Remove(latestInstance);
|
||||
}
|
||||
|
||||
// 将剩余的快照全部拖进不常用列表
|
||||
McInstanceFilter(ref instanceList, new[] { McInstanceState.Snapshot }, ref instanceRubbish);
|
||||
|
||||
// 获取每个 Drop 下最新的原版与 OptiFine
|
||||
var newerInstance = new Dictionary<string, PCL.McInstance>();
|
||||
var existDrops = new List<int>();
|
||||
foreach (var instance in instanceList)
|
||||
{
|
||||
if (!instance.Info.Valid)
|
||||
continue;
|
||||
if (!existDrops.Contains(instance.Info.Drop))
|
||||
existDrops.Add(instance.Info.Drop);
|
||||
var key = instance.Info.Drop + "-" + (int)instance.state;
|
||||
if (!newerInstance.ContainsKey(key))
|
||||
{
|
||||
newerInstance.Add(key, instance);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instance.Info.HasOptiFine)
|
||||
{
|
||||
if (instance.Info.OptiFineCode > newerInstance[key].Info.OptiFineCode)
|
||||
newerInstance[key] = instance; // OptiFine 根据版本号判断
|
||||
}
|
||||
else if (instance.releaseTime > newerInstance[key].releaseTime)
|
||||
{
|
||||
newerInstance[key] = instance; // 原版根据发布时间判断
|
||||
}
|
||||
}
|
||||
|
||||
// 将每个 Drop 下的最常规版本加入
|
||||
foreach (var drop in existDrops)
|
||||
if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.OptiFine) &&
|
||||
newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.Original))
|
||||
{
|
||||
// 同时存在 OptiFine 与原版
|
||||
var vanillaInstance = newerInstance[drop + "-" + (int)McInstanceState.Original];
|
||||
var optiFineInstance = newerInstance[drop + "-" + (int)McInstanceState.OptiFine];
|
||||
if (vanillaInstance.Info.Drop > optiFineInstance.Info.Drop)
|
||||
{
|
||||
// 仅在原版比 OptiFine 更新时才加入原版
|
||||
instanceUseful.Add(vanillaInstance);
|
||||
instanceList.Remove(vanillaInstance);
|
||||
}
|
||||
|
||||
instanceUseful.Add(optiFineInstance);
|
||||
instanceList.Remove(optiFineInstance);
|
||||
}
|
||||
else if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.OptiFine))
|
||||
{
|
||||
// 没有原版,直接加入 OptiFine
|
||||
instanceUseful.Add(newerInstance[drop + "-" + (int)McInstanceState.OptiFine]);
|
||||
instanceList.Remove(newerInstance[drop + "-" + (int)McInstanceState.OptiFine]);
|
||||
}
|
||||
else if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.Original))
|
||||
{
|
||||
// 没有 OptiFine,直接加入原版
|
||||
instanceUseful.Add(newerInstance[drop + "-" + (int)McInstanceState.Original]);
|
||||
instanceList.Remove(newerInstance[drop + "-" + (int)McInstanceState.Original]);
|
||||
}
|
||||
|
||||
// 将剩余的东西添加进去
|
||||
instanceRubbish.AddRange(instanceList);
|
||||
if (instanceUseful.Any())
|
||||
instanceListOriginal.Add(McInstanceCardType.OriginalLike, instanceUseful);
|
||||
if (instanceRubbish.Any())
|
||||
instanceListOriginal.Add(McInstanceCardType.Rubbish, instanceRubbish);
|
||||
|
||||
// 按照自定义实例分类重新添加
|
||||
foreach (var instancePair in instanceListOriginal)
|
||||
foreach (var instance in instancePair.Value)
|
||||
{
|
||||
var realType = instance.displayType == 0 || instancePair.Key == McInstanceCardType.Star
|
||||
? instancePair.Key
|
||||
: instance.displayType;
|
||||
if (!results.ContainsKey(realType))
|
||||
results.Add(realType, new List<PCL.McInstance>());
|
||||
results[realType].Add(instance);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Clear();
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Instance.Error.Classify"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Select.Instance.Error.Classify"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 对卡片与实例进行排序
|
||||
|
||||
// 卡片排序
|
||||
var sortedInstanceList = new Dictionary<McInstanceCardType, List<PCL.McInstance>>();
|
||||
foreach (var sortRule in new[]
|
||||
{
|
||||
McInstanceCardType.Star, McInstanceCardType.API, McInstanceCardType.OriginalLike,
|
||||
McInstanceCardType.Rubbish, McInstanceCardType.Fool, McInstanceCardType.Error,
|
||||
McInstanceCardType.Hidden
|
||||
})
|
||||
if (results.ContainsKey(sortRule))
|
||||
sortedInstanceList.Add(sortRule,
|
||||
results[sortRule]);
|
||||
results = sortedInstanceList;
|
||||
|
||||
// 版本排序
|
||||
foreach (var cardType in new[]
|
||||
{
|
||||
McInstanceCardType.Star, McInstanceCardType.API, McInstanceCardType.OriginalLike,
|
||||
McInstanceCardType.Rubbish, McInstanceCardType.Fool
|
||||
})
|
||||
{
|
||||
if (!results.ContainsKey(cardType))
|
||||
continue;
|
||||
|
||||
int getComponentCode(PCL.McInstance instance)
|
||||
{
|
||||
if (instance.Info.ForgelikeCode > 0)
|
||||
return instance.Info.ForgelikeCode;
|
||||
if (instance.Info.HasOptiFine)
|
||||
return instance.Info.OptiFineCode;
|
||||
return 0;
|
||||
}
|
||||
|
||||
;
|
||||
results[cardType] = SortUtils.Sort(results[cardType], (left, right) =>
|
||||
{
|
||||
// 发布时间
|
||||
if ((left.releaseTime.Year >= 2000 || right.releaseTime.Year >= 2000) &&
|
||||
left.releaseTime != right.releaseTime)
|
||||
return left.releaseTime > right.releaseTime;
|
||||
// 附加组件种类
|
||||
if (left.Info.HasFabric != right.Info.HasFabric)
|
||||
return left.Info.HasFabric;
|
||||
if (left.Info.HasQuilt != right.Info.HasQuilt)
|
||||
return left.Info.HasQuilt;
|
||||
if (left.Info.HasLegacyFabric != right.Info.HasLegacyFabric)
|
||||
return left.Info.HasLegacyFabric;
|
||||
if (left.Info.HasNeoForge != right.Info.HasNeoForge)
|
||||
return left.Info.HasNeoForge;
|
||||
if (left.Info.HasForge != right.Info.HasForge)
|
||||
return left.Info.HasForge;
|
||||
if (left.Info.HasCleanroom != right.Info.HasCleanroom)
|
||||
return left.Info.HasCleanroom;
|
||||
if (left.Info.HasLabyMod != right.Info.HasLabyMod)
|
||||
return left.Info.HasLabyMod;
|
||||
if (left.Info.HasOptiFine != right.Info.HasOptiFine)
|
||||
return left.Info.HasOptiFine;
|
||||
if (left.Info.HasLiteLoader != right.Info.HasLiteLoader)
|
||||
return left.Info.HasLiteLoader;
|
||||
// 附加组件版本
|
||||
if (getComponentCode(left) != getComponentCode(right))
|
||||
return getComponentCode(left) > getComponentCode(right);
|
||||
// 名称
|
||||
return string.CompareOrdinal(left.Name, right.Name) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 保存卡片缓存
|
||||
|
||||
ModBase.WriteIni(path + "PCL.ini", "CardCount", results.Count.ToString());
|
||||
for (int i = 0, loopTo = results.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
ModBase.WriteIni(path + "PCL.ini", "CardKey" + (i + 1),
|
||||
((int)results.Keys.ElementAtOrDefault(i)).ToString());
|
||||
var value = "";
|
||||
foreach (var Instance in results.Values.ElementAtOrDefault(i))
|
||||
value += Instance.Name + ":";
|
||||
ModBase.WriteIni(path + "PCL.ini", "CardValue" + (i + 1), value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 筛选特定种类的实例,并直接添加为卡片。
|
||||
/// </summary>
|
||||
/// <param name="instanceList">用于筛选的列表。</param>
|
||||
/// <param name="formula">需要筛选出的实例类型。-2 代表隐藏的实例。</param>
|
||||
/// <param name="cardType">卡片的名称。</param>
|
||||
private static void McInstanceFilter(ref List<PCL.McInstance> instanceList,
|
||||
ref Dictionary<McInstanceCardType, List<PCL.McInstance>> target, McInstanceState[] formula,
|
||||
McInstanceCardType cardType)
|
||||
{
|
||||
var keepList = instanceList.Where(v => formula.Contains(v.state)).ToList();
|
||||
// 加入实例列表,并从剩余中删除
|
||||
if (keepList.Any())
|
||||
{
|
||||
target.Add(cardType, keepList);
|
||||
instanceList = instanceList.Except(keepList).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 筛选特定种类的实例,并增加入一个已有列表中。
|
||||
/// </summary>
|
||||
/// <param name="instanceList">用于筛选的列表。</param>
|
||||
/// <param name="formula">需要筛选出的实例类型。-2 代表隐藏的实例。</param>
|
||||
/// <param name="keepList">传入需要增加入的列表。</param>
|
||||
private static void McInstanceFilter(ref List<PCL.McInstance> instanceList, McInstanceState[] formula,
|
||||
ref List<McInstance> keepList)
|
||||
{
|
||||
keepList.AddRange(instanceList.Where(v => formula.Contains(v.state)));
|
||||
// 加入实例列表,并从剩余中删除
|
||||
if (keepList.Any()) instanceList = instanceList.Except(keepList).ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
/// <summary>
|
||||
/// Jar-in-Jar(内嵌模组)解析。
|
||||
/// </summary>
|
||||
public static class ModJarInJar
|
||||
{
|
||||
private const int MaxDepth = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 解析 <paramref name="jar" /> 内嵌套的其它 Mod jar,返回内嵌 Mod 列表。
|
||||
/// </summary>
|
||||
public static List<ModLocalComp.LocalCompFile> Resolve(string parentPath, ZipArchive jar, int depth = 0)
|
||||
{
|
||||
var result = new List<ModLocalComp.LocalCompFile>();
|
||||
if (depth >= MaxDepth) return result;
|
||||
|
||||
var nestedPaths = new List<string>();
|
||||
_CollectFabricNestedJars(jar, nestedPaths);
|
||||
_CollectForgeNestedJars(jar, nestedPaths);
|
||||
|
||||
foreach (var nestedPath in nestedPaths.Distinct())
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = jar.GetEntry(nestedPath);
|
||||
if (entry is null) continue;
|
||||
using var ms = new MemoryStream();
|
||||
using (var es = entry.Open()) es.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
using var nestedJar = new ZipArchive(ms, ZipArchiveMode.Read);
|
||||
|
||||
var childPath = parentPath + "!/" + nestedPath;
|
||||
var child = new ModLocalComp.LocalCompFile(childPath);
|
||||
child.LookupMetadata(nestedJar);
|
||||
child.MarkLoaded();
|
||||
child.EmbeddedMods = Resolve(childPath, nestedJar, depth + 1);
|
||||
result.Add(child);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "解析内嵌 Mod 失败(" + parentPath + " -> " + nestedPath + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void _CollectFabricNestedJars(ZipArchive jar, List<string> paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = jar.GetEntry("fabric.mod.json");
|
||||
if (entry is null) return;
|
||||
var obj = (JsonObject)ModBase.GetJson(ModBase.ReadFile(entry.Open()));
|
||||
if (obj.TryGetPropertyValue("jars", out var jars) && jars is JsonArray arr)
|
||||
foreach (var j in arr)
|
||||
if (j is JsonObject jo && jo.TryGetPropertyValue("file", out var file) && file is not null)
|
||||
paths.Add(file.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "解析 fabric.mod.json 内嵌清单失败", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _CollectForgeNestedJars(ZipArchive jar, List<string> paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = jar.GetEntry("META-INF/jarjar/metadata.json");
|
||||
if (entry is null) return;
|
||||
var obj = (JsonObject)ModBase.GetJson(ModBase.ReadFile(entry.Open()));
|
||||
if (obj.TryGetPropertyValue("jars", out var jars) && jars is JsonArray arr)
|
||||
foreach (var j in arr)
|
||||
if (j is JsonObject jo && jo.TryGetPropertyValue("path", out var p) && p is not null)
|
||||
paths.Add(p.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "解析 META-INF/jarjar/metadata.json 内嵌清单失败", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.IO;
|
||||
using PCL.Core.Minecraft;
|
||||
using PCL.Core.Minecraft.Java.UserPreference;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModJava
|
||||
{
|
||||
public static int javaListCacheVersion = 7;
|
||||
|
||||
/// <summary>
|
||||
/// 防止多个需要 Java 的部分同时要求下载 Java(#3797)。
|
||||
/// </summary>
|
||||
public static object javaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 目前所有可用的 Java。
|
||||
/// </summary>
|
||||
public static JavaManager Javas => JavaService.JavaManager;
|
||||
|
||||
/// <summary>
|
||||
/// 根据要求返回最适合的 Java,若找不到则返回 Nothing。
|
||||
/// 最小与最大版本在与输入相同时也会通过。
|
||||
/// 必须在工作线程调用,且必须包括 SyncLock JavaLock。
|
||||
/// </summary>
|
||||
public static JavaEntry JavaSelect(string cancelException, Version minVersion = null, Version maxVersion = null,
|
||||
McInstance relatedInstance = null, bool enforceVersionRange = false)
|
||||
{
|
||||
ModBase.Log(
|
||||
$"[Java] 要求选择合适 Java,要求最低版本 {(minVersion is not null ? minVersion.ToString() : "未指定")},要求选择的最高版本 {(maxVersion is not null ? maxVersion.ToString() : "未指定")},关联实例 {(relatedInstance is not null ? relatedInstance.Name : "未指定")}");
|
||||
|
||||
// 版本范围验证函数(安全处理 null 边界)
|
||||
bool IsVersionSuitable(Version ver)
|
||||
{
|
||||
return (minVersion is null || ver >= minVersion) && (maxVersion is null || ver <= maxVersion);
|
||||
}
|
||||
|
||||
// ===== 优先级 1:实例专属 Java 偏好 =====
|
||||
if (relatedInstance is not null && relatedInstance.PathInstance is not null)
|
||||
{
|
||||
var rawPreference = Config.Instance.SelectedJava[relatedInstance.PathInstance];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rawPreference))
|
||||
{
|
||||
var preference = GetInstanceJavaPreference(relatedInstance);
|
||||
|
||||
// 处理解析成功的偏好
|
||||
if (preference is not null)
|
||||
switch (true)
|
||||
{
|
||||
case object _ when preference is ExistingJava: // "exist"
|
||||
{
|
||||
var existPref = (ExistingJava)preference;
|
||||
var candidate = Javas.AddOrGet(existPref.JavaExePath);
|
||||
|
||||
if (candidate is not null && candidate.IsEnabled)
|
||||
{
|
||||
if (!IsVersionSuitable(candidate.Installation.Version))
|
||||
HintService.Hint(_GetJavaRangeWarning(
|
||||
"Minecraft.Launch.Java.Compatibility.InstanceSelectedOutOfRange",
|
||||
candidate.Installation.Version,
|
||||
minVersion,
|
||||
maxVersion));
|
||||
ModBase.Log($"[Java] 返回实例 '{relatedInstance.Name}' 指定的 Java: {candidate}");
|
||||
return candidate;
|
||||
}
|
||||
|
||||
ModBase.Log($"[Java] 警告:实例指定的 Java 路径无效或不可用: {existPref.JavaExePath}");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case object _ when preference is UseRelativePath: // "relative"
|
||||
{
|
||||
var relPref = (UseRelativePath)preference;
|
||||
var absPath =
|
||||
Path.GetFullPath(Path.Combine(Basics.ExecutableDirectory, relPref.RelativePath));
|
||||
|
||||
if (Files.IsPathWithinDirectory(absPath, Basics.ExecutableDirectory))
|
||||
{
|
||||
var candidate = Javas.Get(absPath);
|
||||
if (candidate is not null && candidate.IsEnabled)
|
||||
{
|
||||
if (!IsVersionSuitable(candidate.Installation.Version))
|
||||
HintService.Hint(_GetJavaRangeWarning(
|
||||
"Minecraft.Launch.Java.Compatibility.RelativePathSelectedOutOfRange",
|
||||
candidate.Installation.Version,
|
||||
minVersion,
|
||||
maxVersion),
|
||||
HintType.Error);
|
||||
ModBase.Log(
|
||||
$"[Java] 返回实例 '{relatedInstance.Name}' 相对路径指定的 Java ({relPref.RelativePath}): {candidate}");
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[Java] 警告:实例相对路径指定的 Java 无效: {absPath}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case object _ when preference is UseGlobalPreference: // "global"
|
||||
{
|
||||
// 不返回,继续到全局设置检查
|
||||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 配置为使用全局 Java 设置,继续检查全局配置");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ModBase.Log($"[Java] 警告:未知的 Java 偏好类型 '{preference}',跳过处理");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 未指定 Java 偏好(空值),使用自动选择策略");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 无 Java 偏好配置,使用自动选择策略");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 优先级 2:全局指定的 Java =====
|
||||
var globalJavaPath = Config.Launch.SelectedJava;
|
||||
if (!string.IsNullOrWhiteSpace(globalJavaPath))
|
||||
{
|
||||
globalJavaPath = globalJavaPath.Trim();
|
||||
var candidate = Javas.AddOrGet(globalJavaPath);
|
||||
|
||||
if (candidate is not null && candidate.IsEnabled)
|
||||
{
|
||||
var versionSuitable = IsVersionSuitable(candidate.Installation.Version);
|
||||
if (enforceVersionRange && !versionSuitable)
|
||||
{
|
||||
ModBase.Log($"[Java] 全局指定的 Java 版本不满足强制范围要求,忽略并继续自动搜索: {candidate}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!versionSuitable)
|
||||
HintService.Hint(_GetJavaRangeWarning(
|
||||
"Minecraft.Launch.Java.Compatibility.GlobalSelectedOutOfRange",
|
||||
candidate.Installation.Version,
|
||||
minVersion,
|
||||
maxVersion));
|
||||
ModBase.Log($"[Java] 返回全局指定的 Java: {candidate}");
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[Java] 警告:全局指定的 Java 路径无效或不可用: {globalJavaPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[Java] 无全局 Java 配置,使用自动选择策略");
|
||||
}
|
||||
|
||||
// ===== 优先级 3:自动搜索合适版本 =====
|
||||
ModBase.Log("[Java] 开始自动搜索符合版本要求的 Java 运行时");
|
||||
Javas.CheckAllAvailability();
|
||||
|
||||
var reqMin = minVersion ?? new Version(1, 0, 0);
|
||||
var reqMax = maxVersion ?? new Version(999, 999, 999);
|
||||
|
||||
var candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult();
|
||||
var ret = candidates.FirstOrDefault();
|
||||
|
||||
if (ret is null && candidates.Length == 0)
|
||||
{
|
||||
ModBase.Log("[Java] 未找到符合版本要求的 Java,触发全盘重新扫描");
|
||||
Javas.ScanJavaAsync().GetAwaiter().GetResult();
|
||||
candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult();
|
||||
ret = candidates.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (ret is not null)
|
||||
ModBase.Log($"[Java] 返回自动选择的 Java: {ret}");
|
||||
else
|
||||
ModBase.Log("[Java] 最终未能确定可用的 Java 运行时");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static string _GetJavaRangeWarning(
|
||||
string key,
|
||||
Version selectedVersion,
|
||||
Version? minVersion,
|
||||
Version? maxVersion)
|
||||
{
|
||||
return Lang.Text(
|
||||
key,
|
||||
selectedVersion,
|
||||
minVersion?.ToString() ?? Lang.Text("Minecraft.Launch.Java.Compatibility.NoMinimum"),
|
||||
maxVersion?.ToString() ?? Lang.Text("Minecraft.Launch.Java.Compatibility.NoMaximum"));
|
||||
}
|
||||
|
||||
public static JavaPreference GetInstanceJavaPreference(McInstance instance)
|
||||
{
|
||||
var rawPreference = Config.Instance.SelectedJava[instance.PathInstance];
|
||||
|
||||
JavaPreference preference = default;
|
||||
|
||||
// 尝试读取 JSON 配置
|
||||
if (!string.IsNullOrEmpty(rawPreference))
|
||||
{
|
||||
try
|
||||
{
|
||||
preference = JsonSerializer.Deserialize<JavaPreference>(rawPreference, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
// 以旧方式读取配置
|
||||
if (preference is null)
|
||||
{
|
||||
var trimmed = rawPreference?.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
preference = new AutoSelect();
|
||||
}
|
||||
else if (trimmed == "使用全局设置")
|
||||
{
|
||||
preference = new UseGlobalPreference();
|
||||
}
|
||||
else
|
||||
{
|
||||
preference = new ExistingJava(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case object _ when preference is ExistingJava:
|
||||
{
|
||||
var m = (ExistingJava)preference;
|
||||
if (!Path.IsPathRooted(m.JavaExePath)) preference = new UseGlobalPreference();
|
||||
|
||||
break;
|
||||
}
|
||||
case object _ when preference is UseRelativePath:
|
||||
{
|
||||
var m = (UseRelativePath)preference;
|
||||
if (!Files.IsPathWithinDirectory(m.RelativePath, Basics.ExecutableDirectory))
|
||||
preference = new UseGlobalPreference();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return preference;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否强制指定了 64 位 Java。如果没有强制指定,返回是否安装了 64 位 Java。
|
||||
/// </summary>
|
||||
public static bool IsGameSet64BitJava(McInstance relatedVersion = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查强制指定
|
||||
var userSetup = Config.Launch.SelectedJava;
|
||||
if (userSetup.StartsWith("{")) // 旧版本 Json 格式
|
||||
{
|
||||
var js = ModBase.GetJson(userSetup);
|
||||
userSetup = $"{js["Path"]}java.exe";
|
||||
Config.Launch.SelectedJava = userSetup;
|
||||
}
|
||||
|
||||
if (relatedVersion is not null)
|
||||
{
|
||||
var instancePreference = GetInstanceJavaPreference(relatedVersion);
|
||||
switch (true)
|
||||
{
|
||||
case object _ when instancePreference is AutoSelect:
|
||||
{
|
||||
return Javas.Existing64BitJava();
|
||||
}
|
||||
case object _ when instancePreference is ExistingJava:
|
||||
{
|
||||
var m = (ExistingJava)instancePreference;
|
||||
var java = Javas.AddOrGet(m.JavaExePath);
|
||||
return java is not null && java.Installation.Is64Bit;
|
||||
}
|
||||
case object _ when instancePreference is UseRelativePath:
|
||||
{
|
||||
var m = (UseRelativePath)instancePreference;
|
||||
var javaExePath = Path.GetFullPath(m.RelativePath);
|
||||
if (Files.IsPathWithinDirectory(javaExePath, Basics.ExecutableDirectory))
|
||||
{
|
||||
var java = Javas.Get(javaExePath);
|
||||
return java is not null && java.Installation.Is64Bit;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userSetup) && !File.Exists(userSetup))
|
||||
{
|
||||
Config.Launch.SelectedJava = "";
|
||||
userSetup = string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(userSetup)) return Javas.Existing64BitJava();
|
||||
var j = Javas.AddOrGet(userSetup);
|
||||
return j is not null && j.Installation.Is64Bit;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"检查 Java 类别时出错",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Java.Compatibility.CheckFailed"));
|
||||
if (relatedVersion is not null)
|
||||
Config.Instance.SelectedJava[relatedVersion.PathInstance] = "使用全局设置";
|
||||
Config.Launch.SelectedJava = "";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region 下载
|
||||
|
||||
/// <summary>
|
||||
/// 提示 Java 缺失,并弹窗确认是否自动下载。返回玩家选择是否下载。
|
||||
/// </summary>
|
||||
public static bool JavaDownloadConfirm(string versionDescription, bool forcedManualDownload = false)
|
||||
{
|
||||
if (!forcedManualDownload)
|
||||
return ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Message", versionDescription),
|
||||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Title"),
|
||||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Action"),
|
||||
Lang.Text("Common.Action.Cancel")) == 1;
|
||||
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Manual.Message", versionDescription),
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取下载 Java 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
|
||||
/// </summary>
|
||||
public static ModLoader.LoaderCombo<string> GetJavaDownloadLoader()
|
||||
{
|
||||
var javaDownloadLoader = new LoaderDownload(
|
||||
Lang.Text("Minecraft.Launch.Java.Task.DownloadFiles"),
|
||||
[])
|
||||
{
|
||||
ProgressWeight = 10d
|
||||
};
|
||||
|
||||
var loader = new ModLoader.LoaderCombo<string>(
|
||||
Lang.Text("Minecraft.Launch.Java.Task.Download"),
|
||||
[
|
||||
new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Launch.Java.Task.GetDownloadInfo"),
|
||||
JavaFileList)
|
||||
{
|
||||
ProgressWeight = 2d
|
||||
},
|
||||
javaDownloadLoader
|
||||
]);
|
||||
|
||||
javaDownloadLoader.OnStateChangedThread += (_, newState, _) =>
|
||||
{
|
||||
switch (newState)
|
||||
{
|
||||
case ModBase.LoadState.Failed or ModBase.LoadState.Aborted
|
||||
when lastJavaBaseDir is not null:
|
||||
ModBase.Log(
|
||||
$"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{lastJavaBaseDir}",
|
||||
ModBase.LogLevel.Debug);
|
||||
|
||||
ModBase.DeleteDirectory(lastJavaBaseDir);
|
||||
break;
|
||||
case ModBase.LoadState.Finished:
|
||||
Javas.ScanJavaAsync().GetAwaiter().GetResult();
|
||||
lastJavaBaseDir = null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
javaDownloadLoader.hasOnStateChangedThread = true;
|
||||
return loader;
|
||||
}
|
||||
|
||||
private static string lastJavaBaseDir; // 用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java
|
||||
|
||||
private static readonly HashSet<string> ignoreHash = new[]
|
||||
{
|
||||
"12976a6c2b227cbac58969c1455444596c894656", "c80e4bab46e34d02826eab226a4441d0970f2aba",
|
||||
"84d2102ad171863db04e7ee22a259d1f6c5de4a5"
|
||||
}.ToHashSet();
|
||||
|
||||
private static void JavaFileList(ModLoader.LoaderTask<string, List<DownloadFile>> loader)
|
||||
{
|
||||
ModBase.Log("[Java] 开始获取 Java 下载信息");
|
||||
var indexFileStr = ModNet.NetGetCodeByLoader(
|
||||
ModDownload.DlVersionListOrder(
|
||||
new[]
|
||||
{
|
||||
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"
|
||||
},
|
||||
new[]
|
||||
{
|
||||
"https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"
|
||||
}), isJson: true);
|
||||
// 查找要下载的目标 Java
|
||||
string? targetName = null;
|
||||
JsonNode? targetValue = null;
|
||||
var components =
|
||||
(JsonObject)((JsonObject)ModBase.GetJson(indexFileStr))[$"windows-x{(SystemInfo.Is32BitSystem ? "86" : "64")}"];
|
||||
if (components.ContainsKey(loader.input)) // 精确匹配
|
||||
{
|
||||
targetName = loader.input;
|
||||
targetValue = components[loader.input];
|
||||
}
|
||||
else // 模糊匹配
|
||||
{
|
||||
var match = components.FirstOrDefault(c =>
|
||||
c.Value?.AsArray().FirstOrDefault()?["version"]?["name"]?.ToString().StartsWithF(loader.input) ?? false);
|
||||
targetName = match.Key;
|
||||
targetValue = match.Value;
|
||||
if (targetName is null)
|
||||
throw new Exception($"未能找到所需的 Java {loader.input}");
|
||||
}
|
||||
|
||||
var targetComponent = targetValue?.AsArray().FirstOrDefault();
|
||||
if (targetComponent is null)
|
||||
throw new Exception($"Mojang 未提供所需的 Java {loader.input}");
|
||||
// 获取文件列表
|
||||
var address = (string)targetComponent["manifest"]["url"];
|
||||
ModLaunch.McLaunchLog($"准备下载 Java {targetComponent["version"]["name"]}({targetName}):{address}");
|
||||
var listFileStr = (JsonObject)Requester.FetchJson(
|
||||
ModDownload.DlSourceOrder(new[] { address },
|
||||
new[] { address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com") }).First(), RequestParam.WithRetry);
|
||||
lastJavaBaseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
".minecraft", "runtime", targetName);
|
||||
var results = new List<DownloadFile>(listFileStr["files"].AsObject().Count);
|
||||
foreach (var File in listFileStr["files"].AsObject())
|
||||
{
|
||||
if (File.Value?.AsObject()?["downloads"]?["raw"] is null)
|
||||
continue;
|
||||
|
||||
var info = File.Value["downloads"]["raw"].AsObject();
|
||||
var checkHash = info["sha1"];
|
||||
if (ignoreHash.Contains((string)checkHash))
|
||||
continue; // 跳过 3 个无意义大量重复文件(#3827)
|
||||
|
||||
var checker = new ModBase.FileChecker(actualSize: (long)info["size"], hash: (string)info["sha1"]);
|
||||
var filePath = Path.GetFullPath(Path.Combine(lastJavaBaseDir, File.Key));
|
||||
if (!Files.IsPathWithinDirectory(filePath, lastJavaBaseDir))
|
||||
throw new Exception($"{filePath} 不在 {lastJavaBaseDir} 中");
|
||||
|
||||
if (checker.Check(filePath) is null)
|
||||
continue; // 跳过已存在的文件
|
||||
var url = (string)info["url"];
|
||||
results.Add(new DownloadFile(
|
||||
ModDownload.DlSourceOrder(new[] { url },
|
||||
new[] { url.Replace("piston-data.mojang.com", "bmclapi2.bangbang93.com") }), filePath, checker));
|
||||
}
|
||||
|
||||
loader.output = results;
|
||||
ModBase.Log($"[Java] 需要下载 {results.Count} 个文件,目标文件夹:{lastJavaBaseDir}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,3781 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Windows;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Minecraft;
|
||||
using PCL.Core.Minecraft.Launch.Utils;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Core.Utils.Secret;
|
||||
using PCL.Network;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Minecraft.IdentityModel.Yggdrasil;
|
||||
using System.Globalization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModLaunch
|
||||
{
|
||||
public const string mesaLoaderWindowsVersion = "26.0.4";
|
||||
|
||||
#region 预检测
|
||||
|
||||
private static void McLaunchPrecheck()
|
||||
{
|
||||
if (Config.Debug.AddRandomDelay)
|
||||
Thread.Sleep(RandomUtils.NextInt(100, 2000));
|
||||
// 检查路径
|
||||
if (ModInstanceList.McMcInstanceSelected.PathIndie.Contains("!") ||
|
||||
ModInstanceList.McMcInstanceSelected.PathIndie.Contains(";"))
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InvalidPathChars", ModInstanceList.McMcInstanceSelected.PathIndie));
|
||||
if (ModInstanceList.McMcInstanceSelected.PathInstance.Contains("!") ||
|
||||
ModInstanceList.McMcInstanceSelected.PathInstance.Contains(";"))
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InvalidPathChars", ModInstanceList.McMcInstanceSelected.PathInstance));
|
||||
if (ModBase.IsUtf8CodePage() && !States.Hint.NonAsciiGamePath &&
|
||||
!ModInstanceList.McMcInstanceSelected.PathInstance.IsASCII())
|
||||
{
|
||||
var userChoice = ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Message", ModInstanceList.McMcInstanceSelected.Name),
|
||||
Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Title"), Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Continue"), Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Back"), Lang.Text("Common.Hint.DoNotShowAgain"));
|
||||
if (userChoice == 2) throw new Exception("$$");
|
||||
if (userChoice == 3) States.Hint.NonAsciiGamePath = true;
|
||||
}
|
||||
|
||||
// 检查实例
|
||||
if (ModInstanceList.McMcInstanceSelected is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.NoInstance"));
|
||||
ModInstanceList.McMcInstanceSelected.Load();
|
||||
if (ModInstanceList.McMcInstanceSelected.state == McInstanceState.Error)
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InstanceError", ModInstanceList.McMcInstanceSelected.Desc));
|
||||
// 检查输入信息
|
||||
var checkResult = "";
|
||||
ModBase.RunInUiWait(() => checkResult = ModProfile.IsProfileValid());
|
||||
if (ModProfile.selectedProfile is null) // 没选档案
|
||||
{
|
||||
checkResult = Lang.Text("Minecraft.Launch.Precheck.NoProfile");
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.HasLabyMod ||
|
||||
Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 1) // 要求正版验证
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type != McLoginType.Ms) checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireMicrosoft");
|
||||
}
|
||||
else if (Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 2) // 要求第三方验证
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type != McLoginType.Auth)
|
||||
checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireThirdParty");
|
||||
else if (ModProfile.selectedProfile.Server.BeforeLast("/authserver") !=
|
||||
Config.InstanceAuth.AuthServerAddress[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
checkResult = Lang.Text("Minecraft.Launch.Precheck.AuthServerMismatch");
|
||||
}
|
||||
else if (Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 3) // 要求正版验证或第三方验证
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type == McLoginType.Legacy)
|
||||
checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireMicrosoftOrThirdParty");
|
||||
else if (ModProfile.selectedProfile.Type == McLoginType.Auth &&
|
||||
ModProfile.selectedProfile.Server.BeforeLast("/authserver") !=
|
||||
Config.InstanceAuth.AuthServerAddress[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
checkResult = Lang.Text("Minecraft.Launch.Precheck.AuthServerMismatch");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(checkResult))
|
||||
throw new ArgumentException(checkResult);
|
||||
|
||||
#if BETA
|
||||
if (currentLaunchOptions?.SaveBatch is null) // 保存脚本时不提示
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
switch (States.System.LaunchCount)
|
||||
{
|
||||
case 10:
|
||||
case 20:
|
||||
case 40:
|
||||
case 60:
|
||||
case 80:
|
||||
case 100:
|
||||
case 120:
|
||||
case 150:
|
||||
case 200:
|
||||
case 250:
|
||||
case 300:
|
||||
case 350:
|
||||
case 400:
|
||||
case 500:
|
||||
case 600:
|
||||
case 700:
|
||||
case 800:
|
||||
case 900:
|
||||
case 1000:
|
||||
case 1200:
|
||||
case 1400:
|
||||
case 1600:
|
||||
case 1800:
|
||||
case 2000:
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Donate.Message", States.System.LaunchCount),
|
||||
Lang.Text("Minecraft.Launch.Donate.Title", States.System.LaunchCount),
|
||||
Lang.Text("Minecraft.Launch.Donate.Support"),
|
||||
Lang.Text("Minecraft.Launch.Donate.Decline")) == 1)
|
||||
{
|
||||
ModBase.OpenWebsite("https://afdian.com/a/LTCat");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, "Donate");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if DEBUG || DEBUGCI
|
||||
return;
|
||||
#endif
|
||||
|
||||
// 正版购买提示
|
||||
if (!ModProfile.profileList.Any(x => x.Type == McLoginType.Ms))
|
||||
{
|
||||
if (Lang.IsFeaturesUnrestricted)
|
||||
{
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.PurchaseHint.Message"),
|
||||
Lang.Text("Minecraft.Launch.PurchaseHint.Title"), Lang.Text("Minecraft.Launch.PurchaseHint.Purchase"), Lang.Text("Minecraft.Launch.PurchaseHint.Later")) ==
|
||||
1)
|
||||
ModBase.OpenWebsite(
|
||||
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj");
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.AccountVerification.Message"),
|
||||
Lang.Text("Minecraft.Launch.AccountVerification.Title"),
|
||||
Lang.Text("Minecraft.Launch.AccountVerification.Purchase"),
|
||||
Lang.Text("Minecraft.Launch.AccountVerification.Demo"),
|
||||
Lang.Text("Minecraft.Launch.AccountVerification.Back"),
|
||||
button1Action: () =>
|
||||
ModBase.OpenWebsite(
|
||||
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj")))
|
||||
{
|
||||
case 2:
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.DemoMode"), HintType.Error);
|
||||
currentLaunchOptions.ExtraArgs.Add("--demo");
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
throw new Exception("$$");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 开始
|
||||
|
||||
public static bool isLaunching;
|
||||
public static McLaunchOptions currentLaunchOptions;
|
||||
|
||||
public class McLaunchOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 额外的启动参数。
|
||||
/// </summary>
|
||||
public List<string> ExtraArgs = new();
|
||||
|
||||
/// <summary>
|
||||
/// 强行指定启动的 MC 实例。
|
||||
/// 默认值:Nothing。使用 McInstanceCurrent。
|
||||
/// </summary>
|
||||
public McInstance instance = null;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 “测试游戏” 按钮启动的游戏。
|
||||
/// 如果是,则显示游戏实时日志。
|
||||
/// </summary>
|
||||
public bool IsTest = false;
|
||||
|
||||
/// <summary>
|
||||
/// 将启动脚本保存到该地址,然后取消启动。这同时会改变启动时的提示等。
|
||||
/// 默认值:Nothing。不保存。
|
||||
/// </summary>
|
||||
public string SaveBatch = null;
|
||||
|
||||
/// <summary>
|
||||
/// 强制指定在启动后进入的服务器 IP。
|
||||
/// 默认值:Nothing。使用实例设置的值。
|
||||
/// </summary>
|
||||
public string ServerIp = null;
|
||||
|
||||
/// <summary>
|
||||
/// 指定在启动之后进入的存档名称。
|
||||
/// 默认值:Nothing。使用实例设置的值。
|
||||
/// </summary>
|
||||
public string WorldName = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试启动 Minecraft。必须在 UI 线程调用。
|
||||
/// 返回是否实际开始了启动(如果没有,则一定弹出了错误提示)。
|
||||
/// </summary>
|
||||
public static bool McLaunchStart(McLaunchOptions options = null)
|
||||
{
|
||||
isLaunching = true;
|
||||
currentLaunchOptions = options ?? new McLaunchOptions();
|
||||
// 预检查
|
||||
if (!ModBase.RunInUi())
|
||||
throw new Exception("McLaunchStart 必须在 UI 线程调用!");
|
||||
if (mcLaunchLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Error.AlreadyLaunching"), HintType.Error);
|
||||
isLaunching = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 强制切换需要启动的实例
|
||||
if (currentLaunchOptions.instance is not null &&
|
||||
ModInstanceList.McMcInstanceSelected != currentLaunchOptions.instance)
|
||||
{
|
||||
McLaunchLog("在启动前切换到实例 " + currentLaunchOptions.instance.Name);
|
||||
// 检查实例
|
||||
currentLaunchOptions.instance.Load();
|
||||
if (currentLaunchOptions.instance.state == McInstanceState.Error)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Error.CannotLaunch", currentLaunchOptions.instance.Desc), HintType.Error);
|
||||
isLaunching = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 切换实例
|
||||
ModInstanceList.McMcInstanceSelected = currentLaunchOptions.instance;
|
||||
States.Game.SelectedInstance = ModInstanceList.McMcInstanceSelected.Name;
|
||||
ModMain.frmLaunchLeft.RefreshButtonsUI();
|
||||
ModMain.frmLaunchLeft.RefreshPage(false);
|
||||
}
|
||||
|
||||
ModMain.frmMain.AprilGiveup();
|
||||
// 禁止进入实例选择页面(否则就可以在启动中切换 McInstanceCurrent 了)
|
||||
ModMain.frmMain.pageStack =
|
||||
ModMain.frmMain.pageStack.Where(p => p.page != FormMain.PageType.InstanceSelect).ToList();
|
||||
// 实际启动加载器
|
||||
mcLaunchLoader.Start(options, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录启动日志。
|
||||
/// </summary>
|
||||
public static void McLaunchLog(string text)
|
||||
{
|
||||
text = McLogFilter.FilterUserName(McLogFilter.FilterAccessToken(text, '*'), '*');
|
||||
ModBase.RunInUi(() =>
|
||||
ModMain.frmLaunchRight.LabLog.Text += "\r\n" + "[" + TimeUtils.GetTimeNow() + "] " + text);
|
||||
ModBase.Log("[Launch] " + text);
|
||||
}
|
||||
|
||||
// 启动状态切换
|
||||
public static ModLoader.LoaderTask<McLaunchOptions, object> mcLaunchLoader = new("Loader Launch", McLaunchStart)
|
||||
{ OnStateChanged = a => McLaunchState((dynamic)a) };
|
||||
|
||||
public static ModLoader.LoaderCombo<object> mcLaunchLoaderReal;
|
||||
public static Process mcLaunchProcess;
|
||||
public static ModWatcher.Watcher mcLaunchWatcher;
|
||||
|
||||
private static void McLaunchState(ModLoader.LoaderTask<McLaunchOptions, object> loader)
|
||||
{
|
||||
switch (mcLaunchLoader.State)
|
||||
{
|
||||
case ModBase.LoadState.Finished:
|
||||
case ModBase.LoadState.Failed:
|
||||
case ModBase.LoadState.Waiting:
|
||||
case ModBase.LoadState.Aborted:
|
||||
{
|
||||
ModMain.frmLaunchLeft.PageChangeToLogin();
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
// 在预检测结束后再触发动画
|
||||
ModMain.frmLaunchRight.LabLog.Text = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指定启动中断时的提示文本。若不为 Nothing 则会显示为绿色。
|
||||
/// </summary>
|
||||
private static string abortHint;
|
||||
|
||||
// 实际的启动方法
|
||||
private static void McLaunchStart(ModLoader.LoaderTask<McLaunchOptions, object> loader)
|
||||
{
|
||||
// 开始动画
|
||||
ModBase.RunInUiWait(ModMain.frmLaunchLeft.PageChangeToLaunching);
|
||||
// 预检测(预检测的错误将直接抛出)
|
||||
try
|
||||
{
|
||||
McLaunchPrecheck();
|
||||
McLaunchLog("预检测已通过");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!ex.Message.StartsWithF("$$"))
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Precheck.Failed.WithDetail", ex.Message), HintType.Error);
|
||||
throw;
|
||||
}
|
||||
|
||||
// 正式加载
|
||||
try
|
||||
{
|
||||
// 构造主加载器
|
||||
var loaders = new List<ModLoader.LoaderBase>
|
||||
{
|
||||
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.GetJava"), McLaunchJava) { ProgressWeight = 4d, block = false },
|
||||
mcLoginLoader,
|
||||
new ModLoader.LoaderCombo<string>(Lang.Text("Minecraft.Launch.Stage.CompleteFiles"),
|
||||
ModDownload.DlClientFix(ModInstanceList.McMcInstanceSelected, false,
|
||||
ModDownload.AssetsIndexExistsBehaviour.DownloadInBackground))
|
||||
{ ProgressWeight = 15d, show = false },
|
||||
new ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>>(Lang.Text("Minecraft.Launch.Stage.GetArguments"), McLaunchArgumentMain)
|
||||
{ ProgressWeight = 2d },
|
||||
new ModLoader.LoaderTask<List<ModLibrary.McLibToken>, int>(Lang.Text("Minecraft.Launch.Stage.ExtractNatives"), McLaunchNatives)
|
||||
{ ProgressWeight = 2d },
|
||||
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.PreLaunch"), _ => McLaunchPrerun()) { ProgressWeight = 1d },
|
||||
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.CustomCommand"), McLaunchCustom) { ProgressWeight = 1d },
|
||||
new ModLoader.LoaderTask<int, Process>(Lang.Text("Minecraft.Launch.Stage.StartProcess"), McLaunchRun) { ProgressWeight = 2d },
|
||||
new ModLoader.LoaderTask<Process, int>(Lang.Text("Minecraft.Launch.Stage.WaitWindow"), McLaunchWait) { ProgressWeight = 1d },
|
||||
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.End"), _ => McLaunchEnd()) { ProgressWeight = 1d }
|
||||
}; // .ProgressWeight = 15, .Block = False
|
||||
|
||||
var launchLoader = new ModLoader.LoaderCombo<object>(Lang.Text("Minecraft.Launch.Stage.Root"), loaders) { show = false };
|
||||
if (mcLoginLoader.State == ModBase.LoadState.Finished)
|
||||
mcLoginLoader.State = ModBase.LoadState.Waiting; // 要求重启登录主加载器,它会自行决定是否启动副加载器
|
||||
// 等待加载器执行并更新 UI
|
||||
mcLaunchLoaderReal = launchLoader;
|
||||
abortHint = null;
|
||||
launchLoader.Start();
|
||||
// 任务栏进度条
|
||||
ModLoader.LoaderTaskbarAdd(launchLoader);
|
||||
while (launchLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
ModMain.frmLaunchLeft.Dispatcher.Invoke(ModMain.frmLaunchLeft.LaunchingRefresh);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
ModMain.frmLaunchLeft.Dispatcher.Invoke(ModMain.frmLaunchLeft.LaunchingRefresh);
|
||||
// 成功与失败处理
|
||||
switch (launchLoader.State)
|
||||
{
|
||||
case ModBase.LoadState.Finished:
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Success", ModInstanceList.McMcInstanceSelected.Name), HintType.Success);
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Aborted:
|
||||
{
|
||||
if (abortHint is null)
|
||||
HintService.Hint(currentLaunchOptions?.SaveBatch is null ? Lang.Text("Minecraft.Launch.Cancelled") : Lang.Text("Minecraft.Launch.ExportScript.Cancelled"));
|
||||
else
|
||||
HintService.Hint(abortHint, HintType.Success);
|
||||
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Failed:
|
||||
{
|
||||
throw launchLoader.Error;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Error.InvalidState", ModBase.GetStringFromEnum(launchLoader.State)));
|
||||
}
|
||||
}
|
||||
|
||||
isLaunching = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var currentEx = ex;
|
||||
while (currentEx is not null)
|
||||
{
|
||||
if (currentEx.Message.StartsWithF("$"))
|
||||
{
|
||||
// 若有以 $ 开头的错误信息,则以此为准显示提示
|
||||
// 若错误信息为 $$,则不提示
|
||||
if (currentEx.Message != "$$")
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Error.SpecialMessage.WithDetail",
|
||||
currentEx.Message.TrimStart('$')),
|
||||
currentLaunchOptions?.SaveBatch is null
|
||||
? Lang.Text("Launch.Error.Title")
|
||||
: Lang.Text("Launch.Error.ExportScriptTitle"));
|
||||
throw;
|
||||
}
|
||||
|
||||
if (currentEx.InnerException is null)
|
||||
break;
|
||||
|
||||
// 检查下一级错误
|
||||
currentEx = currentEx.InnerException;
|
||||
}
|
||||
|
||||
// 没有特殊处理过的错误信息
|
||||
McLaunchLog("错误:" + ex);
|
||||
ModBase.Log(
|
||||
ex,
|
||||
currentLaunchOptions?.SaveBatch is null
|
||||
? "Minecraft launch failed"
|
||||
: "Export script failed",
|
||||
ModBase.LogLevel.Msgbox,
|
||||
currentLaunchOptions?.SaveBatch is null
|
||||
? Lang.Text("Launch.Error.Title")
|
||||
: Lang.Text("Launch.Error.ExportScriptTitle"),
|
||||
userSummary: currentLaunchOptions?.SaveBatch is null
|
||||
? Lang.Text("Minecraft.Launch.Error.LaunchFailed")
|
||||
: Lang.Text("Minecraft.Launch.Error.ExportScriptFailed"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 档案验证
|
||||
|
||||
#region 主模块
|
||||
|
||||
// 登录方式
|
||||
public enum McLoginType
|
||||
{
|
||||
Legacy = 1,
|
||||
Auth = 2,
|
||||
Ms = 3
|
||||
}
|
||||
|
||||
// 各个登录方式的对应数据
|
||||
public abstract class McLoginData
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录方式。
|
||||
/// </summary>
|
||||
public McLoginType LoginType;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is not null && obj.GetHashCode() == GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
#region 第三方验证类型
|
||||
|
||||
public class McLoginServer : McLoginData
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录服务器基础地址。
|
||||
/// </summary>
|
||||
public string BaseUrl;
|
||||
|
||||
/// <summary>
|
||||
/// 登录方式的描述字符串,如 “正版”、“统一通行证”。
|
||||
/// </summary>
|
||||
public string Description;
|
||||
|
||||
/// <summary>
|
||||
/// 是否在本次登录中强制要求玩家重新选择角色,目前仅对 Authlib-Injector 生效。
|
||||
/// </summary>
|
||||
public bool ForceReselectProfile = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已经存在该验证信息,用于判断是否为新增档案。
|
||||
/// </summary>
|
||||
public bool IsExist = false;
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码。
|
||||
/// </summary>
|
||||
public string Password;
|
||||
|
||||
/// <summary>
|
||||
/// 登录用户名。
|
||||
/// </summary>
|
||||
public string UserName;
|
||||
|
||||
public McLoginServer(McLoginType type)
|
||||
{
|
||||
this.LoginType = type;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (int)Math.Round(ModBase.GetHash(UserName + Password + BaseUrl + (int)LoginType) %
|
||||
(decimal)int.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 正版验证类型
|
||||
|
||||
public class McLoginMs : McLoginData
|
||||
{
|
||||
public string AccessToken = "";
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的 OAuth RefreshToken。若没有则为空字符串。
|
||||
/// </summary>
|
||||
public string OAuthRefreshToken = "";
|
||||
|
||||
public string ProfileJson = "";
|
||||
public string UserName = "";
|
||||
public string Uuid = "";
|
||||
|
||||
public McLoginMs()
|
||||
{
|
||||
LoginType = McLoginType.Ms;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (int)Math.Round(ModBase.GetHash(OAuthRefreshToken + AccessToken + Uuid + UserName + ProfileJson) %
|
||||
(decimal)int.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 离线验证类型
|
||||
|
||||
public class McLoginLegacy : McLoginData
|
||||
{
|
||||
/// <summary>
|
||||
/// 若采用正版皮肤,则为该皮肤名。
|
||||
/// </summary>
|
||||
public string SkinName;
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤种类。
|
||||
/// </summary>
|
||||
public int SkinType;
|
||||
|
||||
/// <summary>
|
||||
/// 登录用户名。
|
||||
/// </summary>
|
||||
public string UserName;
|
||||
|
||||
/// <summary>
|
||||
/// UUID。
|
||||
/// </summary>
|
||||
public string Uuid;
|
||||
|
||||
public McLoginLegacy()
|
||||
{
|
||||
LoginType = McLoginType.Legacy;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (int)Math.Round(
|
||||
ModBase.GetHash(UserName + SkinType + SkinName + (int)LoginType) % (decimal)int.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// 登录返回结果
|
||||
public struct McLoginResult
|
||||
{
|
||||
public string Name;
|
||||
public string Uuid;
|
||||
public string AccessToken;
|
||||
public string Type;
|
||||
public string ClientToken;
|
||||
|
||||
/// <summary>
|
||||
/// 进行微软登录时返回的 profile 信息。
|
||||
/// </summary>
|
||||
public string ProfileJson;
|
||||
}
|
||||
|
||||
// 登录主模块加载器
|
||||
public static ModLoader.LoaderTask<McLoginData, McLoginResult> mcLoginLoader =
|
||||
new(Lang.Text("Minecraft.Launch.Stage.Login"), McLoginStart, McLoginInput, ThreadPriority.BelowNormal)
|
||||
{ reloadTimeout = 1, ProgressWeight = 15d, block = false };
|
||||
|
||||
public static McLoginData McLoginInput()
|
||||
{
|
||||
McLoginData loginData = null;
|
||||
try
|
||||
{
|
||||
loginData = ModProfile.GetLoginData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Login.Error.Input"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Login.Error.Input"));
|
||||
}
|
||||
|
||||
return loginData;
|
||||
}
|
||||
|
||||
private static void McLoginStart(ModLoader.LoaderTask<McLoginData, McLoginResult> data)
|
||||
{
|
||||
ModBase.Log("[Profile] 开始加载选定档案");
|
||||
// 校验登录信息
|
||||
var checkResult = ModProfile.IsProfileValid();
|
||||
if (!string.IsNullOrEmpty(checkResult))
|
||||
throw new ArgumentException(checkResult);
|
||||
// 获取对应加载器
|
||||
ModLoader.LoaderBase loader = null;
|
||||
switch (data.input.LoginType)
|
||||
{
|
||||
case McLoginType.Ms:
|
||||
{
|
||||
loader = mcLoginMsLoader;
|
||||
break;
|
||||
}
|
||||
case McLoginType.Legacy:
|
||||
{
|
||||
loader = mcLoginLegacyLoader;
|
||||
break;
|
||||
}
|
||||
case McLoginType.Auth:
|
||||
{
|
||||
loader = mcLoginAuthLoader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试加载
|
||||
loader.WaitForExit(data.input, mcLoginLoader, data.isForceRestarting);
|
||||
data.output = (McLoginResult)((dynamic)loader).output;
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(false)); // 刷新自动填充列表
|
||||
ModBase.Log("[Profile] 选定档案加载完成");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// 各个登录方式的主对象与输入构造
|
||||
public static ModLoader.LoaderTask<McLoginMs, McLoginResult> mcLoginMsLoader =
|
||||
new("Loader Login Ms", McLoginMsStart) { reloadTimeout = 1 };
|
||||
|
||||
public static ModLoader.LoaderTask<McLoginLegacy, McLoginResult> mcLoginLegacyLoader =
|
||||
new("Loader Login Legacy", McLoginLegacyStart);
|
||||
|
||||
public static ModLoader.LoaderTask<McLoginServer, McLoginResult> mcLoginAuthLoader =
|
||||
new("Loader Login Auth", McLoginServerStart) { reloadTimeout = 1000 * 60 * 10 };
|
||||
|
||||
// 主加载函数,返回所有需要的登录信息
|
||||
private static long mcLoginMsRefreshTime; // 上次刷新登录的时间
|
||||
|
||||
#region 正版验证
|
||||
|
||||
private static void McLoginMsStart(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
|
||||
{
|
||||
var input = data.input;
|
||||
var logUsername = input.UserName;
|
||||
var isNewProfile = true;
|
||||
|
||||
ModProfile.ProfileLog($"验证方式:正版({(string.IsNullOrEmpty(logUsername) ? "尚未登录" : logUsername)})");
|
||||
data.Progress = 0.05d;
|
||||
|
||||
// 已登录且不需要强制重启且登录未过期
|
||||
if (!data.isForceRestarting && !string.IsNullOrEmpty(input.AccessToken) &&
|
||||
mcLoginMsRefreshTime > 0L &&
|
||||
TimeUtils.GetTimeTick() - mcLoginMsRefreshTime < 1000 * 60 * 10)
|
||||
{
|
||||
data.output = new McLoginResult
|
||||
{
|
||||
AccessToken = input.AccessToken,
|
||||
Name = input.UserName,
|
||||
Uuid = input.Uuid,
|
||||
Type = "Microsoft",
|
||||
ClientToken = input.Uuid,
|
||||
ProfileJson = input.ProfileJson
|
||||
};
|
||||
|
||||
mcLoginMsRefreshTime = TimeUtils.GetTimeTick();
|
||||
ModProfile.ProfileLog("正版验证完成");
|
||||
return;
|
||||
}
|
||||
|
||||
data.Progress = 0.1d;
|
||||
|
||||
// 尝试获取 OAuthToken
|
||||
var oauthTokens = GetOAuthTokens(data, input, out var skipAuth);
|
||||
if (skipAuth)
|
||||
{
|
||||
data.Progress = 0.99d;
|
||||
var profile = ModProfile.selectedProfile;
|
||||
data.output = new McLoginResult
|
||||
{
|
||||
AccessToken = profile.AccessToken,
|
||||
Name = profile.Username,
|
||||
Uuid = profile.Uuid,
|
||||
Type = "Microsoft"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
var oauthAccessToken = oauthTokens[0];
|
||||
var oauthRefreshToken = oauthTokens[1];
|
||||
ThrowIfAborted(data);
|
||||
|
||||
data.Progress = 0.25d;
|
||||
|
||||
// Step 2: XBL Token
|
||||
var xblToken = MsLoginStep2(oauthAccessToken);
|
||||
if (string.IsNullOrEmpty(xblToken) || xblToken == "Ignore")
|
||||
goto SkipLogin;
|
||||
|
||||
data.Progress = 0.4d;
|
||||
ThrowIfAborted(data);
|
||||
|
||||
// Step 3: XSTS / Minecraft login
|
||||
var tokens = MsLoginStep3(xblToken);
|
||||
if (tokens.Length < 2 || tokens[1] == "Ignore")
|
||||
goto SkipLogin;
|
||||
|
||||
data.Progress = 0.55d;
|
||||
ThrowIfAborted(data);
|
||||
|
||||
// Step 4: Final access token
|
||||
var accessToken = MsLoginStep4(tokens);
|
||||
if (string.IsNullOrEmpty(accessToken) || accessToken == "Ignore")
|
||||
goto SkipLogin;
|
||||
|
||||
data.Progress = 0.7d;
|
||||
ThrowIfAborted(data);
|
||||
|
||||
// Step 5: Additional setup
|
||||
MsLoginStep5(accessToken);
|
||||
data.Progress = 0.85d;
|
||||
ThrowIfAborted(data);
|
||||
|
||||
// Step 6: Profile info
|
||||
var result = MsLoginStep6(accessToken);
|
||||
if (result.Length < 3 || result[2] == "Ignore")
|
||||
goto SkipLogin;
|
||||
|
||||
data.Progress = 0.98d;
|
||||
|
||||
// 检查是否已有相同档案
|
||||
foreach (var profile in ModProfile.profileList)
|
||||
if (profile.Type == McLoginType.Ms &&
|
||||
string.Equals(profile.Username, result[1], StringComparison.Ordinal) &&
|
||||
string.Equals(profile.Uuid, result[0], StringComparison.Ordinal))
|
||||
{
|
||||
isNewProfile = false;
|
||||
if (ModProfile.isCreatingProfile)
|
||||
{
|
||||
var index = ModProfile.profileList.IndexOf(profile);
|
||||
ModProfile.profileList[index].Username = result[1];
|
||||
ModProfile.profileList[index].AccessToken = accessToken;
|
||||
ModProfile.profileList[index].RefreshToken = oauthRefreshToken;
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Microsoft.ProfileAlreadyAdded"));
|
||||
goto SkipLogin;
|
||||
}
|
||||
}
|
||||
|
||||
// 输出登录结果
|
||||
if (isNewProfile)
|
||||
{
|
||||
var newProfile = new ModProfile.McProfile
|
||||
{
|
||||
Type = McLoginType.Ms,
|
||||
Uuid = result[0],
|
||||
Username = result[1],
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = oauthRefreshToken,
|
||||
Expires = 1743779140286L,
|
||||
Desc = "",
|
||||
RawJson = result[2]
|
||||
};
|
||||
ModProfile.profileList.Add(newProfile);
|
||||
ModProfile.selectedProfile = newProfile;
|
||||
ModProfile.isCreatingProfile = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var index = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
|
||||
ModProfile.profileList[index].Username = result[1];
|
||||
ModProfile.profileList[index].AccessToken = accessToken;
|
||||
ModProfile.profileList[index].RefreshToken = oauthRefreshToken;
|
||||
}
|
||||
|
||||
ModProfile.SaveProfile();
|
||||
|
||||
data.output = new McLoginResult
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
Name = result[1],
|
||||
Uuid = result[0],
|
||||
Type = "Microsoft",
|
||||
ClientToken = result[0],
|
||||
ProfileJson = result[2]
|
||||
};
|
||||
|
||||
SkipLogin:
|
||||
mcLoginMsRefreshTime = TimeUtils.GetTimeTick();
|
||||
ModProfile.ProfileLog("正版验证完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 OAuth Tokens,处理刷新和重新登录逻辑
|
||||
/// </summary>
|
||||
private static string[] GetOAuthTokens(ModLoader.LoaderTask<McLoginMs, McLoginResult> data, McLoginMs input,
|
||||
out bool skipAuth)
|
||||
{
|
||||
skipAuth = false;
|
||||
string[] tokens;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input.OAuthRefreshToken))
|
||||
{
|
||||
tokens = MsLoginStep1New(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens = MsLoginStep1Refresh(input.OAuthRefreshToken);
|
||||
if (tokens.Length > 0 && tokens[0] == "Relogin")
|
||||
{
|
||||
// 刷新令牌已失效,清除后回退到设备代码流重新登录,避免无限循环
|
||||
input.OAuthRefreshToken = "";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens.Length > 0 && tokens[0] == "Ignore")
|
||||
{
|
||||
skipAuth = true;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否被中断
|
||||
/// </summary>
|
||||
private static void ThrowIfAborted(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
|
||||
{
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 1:通过设备代码流获取账号信息
|
||||
/// </summary>
|
||||
/// <returns>OAuth 验证完成的返回结果</returns>
|
||||
private static string[] MsLoginStep1New(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
|
||||
{
|
||||
// 参考:https://learn.microsoft.com/zh-cn/entra/identity-platform/v2-oauth2-device-code
|
||||
|
||||
// 初始请求
|
||||
Retry: ;
|
||||
|
||||
McLaunchLog("开始正版验证 Step 1/6(原始登录)");
|
||||
JsonObject prepareJson;
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "client_id", Secrets.MSOAuthClientId },
|
||||
{ "tenant", "/consumers" },
|
||||
{ "scope", "XboxLive.signin offline_access" }
|
||||
};
|
||||
|
||||
using (var response = HttpRequest
|
||||
.CreatePost("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
|
||||
.WithFormContent(parameters)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
prepareJson = (JsonObject)ModBase.GetJson(response.AsString());
|
||||
}
|
||||
|
||||
McLaunchLog("网页登录地址:" + prepareJson["verification_uri"]);
|
||||
|
||||
// 弹窗
|
||||
var converter = new ModMain.MyMsgBoxConverter
|
||||
{ Content = prepareJson, ForceWait = true, Type = ModMain.MyMsgBoxType.Login };
|
||||
ModMain.WaitingMyMsgBox.Add(converter);
|
||||
while (converter.Result is null)
|
||||
Thread.Sleep(100);
|
||||
if (converter.Result is ModBase.RestartException)
|
||||
{
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.PasswordRequired.Message", ModBase.vbLQ, ModBase.vbRQ),
|
||||
Lang.Text("Minecraft.Launch.Login.PasswordRequired.Title"), Lang.Text("Minecraft.Launch.Login.PasswordRequired.Relogin"), Lang.Text("Minecraft.Launch.Login.PasswordRequired.SetPassword"), Lang.Text("Common.Action.Cancel"),
|
||||
button2Action: () => ModBase.OpenWebsite("https://account.live.com/password/Change")) ==
|
||||
1) goto Retry;
|
||||
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
if (converter.Result is Exception) throw (Exception)converter.Result;
|
||||
|
||||
return (string[])converter.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 1,刷新登录:从 OAuth Code 或 OAuth RefreshToken 获取 {OAuth accessToken, OAuth RefreshToken}
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
private static string[] MsLoginStep1Refresh(string code)
|
||||
{
|
||||
McLaunchLog("开始正版验证 Step 1/6(刷新登录)");
|
||||
if (string.IsNullOrEmpty(code))
|
||||
throw new ArgumentException("传入的 Code 为空", nameof(code));
|
||||
string result = null;
|
||||
try
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "client_id", Secrets.MSOAuthClientId },
|
||||
{ "refresh_token", code },
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "scope", "XboxLive.signin offline_access" }
|
||||
};
|
||||
|
||||
using (var response = HttpRequest
|
||||
.CreatePost("https://login.live.com/oauth20_token.srf")
|
||||
.WithFormContent(parameters)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
result = response.AsString();
|
||||
if (!response.IsSuccess)
|
||||
throw new HttpRequestException(
|
||||
$"刷新登录请求失败,状态码 {(int)response.StatusCode}:{result}");
|
||||
}
|
||||
}
|
||||
catch (ThreadInterruptedException ex)
|
||||
{
|
||||
ModBase.Log(ex, "加载线程已终止");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message.ContainsF("invalid_grant", true) || ex.Message.ContainsF("must sign in again", true) ||
|
||||
ex.Message.ContainsF("must first sign in", true) || ex.Message.ContainsF("password expired", true) ||
|
||||
(ex.Message.Contains("refresh_token") && ex.Message.Contains("is not valid"))) // #269
|
||||
return new[] { "Relogin", "" };
|
||||
|
||||
ModProfile.ProfileLog("正版验证 Step 1/6 获取 OAuth Token 失败:" + ex);
|
||||
var isIgnore = false;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (!isLaunching)
|
||||
return;
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
isIgnore = true;
|
||||
});
|
||||
if (isIgnore) return new[] { "Ignore", "" };
|
||||
// 用户取消或登录线程已结束,静默中止启动,避免落入下方的 JSON 解析空引用
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
var accessToken = resultJson["access_token"].ToString();
|
||||
var refreshToken = resultJson["refresh_token"].ToString();
|
||||
return new[] { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
|
||||
private class XBLTokenRequestData
|
||||
{
|
||||
public PropertiesData Properties { get; set; }
|
||||
public string RelyingParty { get; set; }
|
||||
public string TokenType { get; set; }
|
||||
|
||||
public class PropertiesData
|
||||
{
|
||||
public string AuthMethod { get; set; }
|
||||
public string SiteName { get; set; }
|
||||
public string RpsTicket { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 2:从 OAuth accessToken 获取 XBLToken
|
||||
/// </summary>
|
||||
/// <param name="accessToken">OAuth accessToken</param>
|
||||
/// <returns>XBLToken</returns>
|
||||
private static string MsLoginStep2(string accessToken)
|
||||
{
|
||||
ModProfile.ProfileLog("开始正版验证 Step 2/6: 获取 XBLToken");
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
|
||||
var requestData = new XBLTokenRequestData
|
||||
{
|
||||
Properties = new XBLTokenRequestData.PropertiesData
|
||||
{
|
||||
AuthMethod = "RPS",
|
||||
SiteName = "user.auth.xboxlive.com",
|
||||
RpsTicket = $"d={accessToken}"
|
||||
},
|
||||
RelyingParty = "http://auth.xboxlive.com",
|
||||
TokenType = "JWT"
|
||||
};
|
||||
string result = null;
|
||||
try
|
||||
{
|
||||
using (var response = HttpRequest
|
||||
.CreatePost("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.WithJsonContent(requestData)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
result = response.AsString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModProfile.ProfileLog("正版验证 Step 2/6 获取 XBLToken 失败:" + ex);
|
||||
var isIgnore = false;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (!isLaunching)
|
||||
return;
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
isIgnore = true;
|
||||
});
|
||||
if (isIgnore) return "Ignore";
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
var xBLToken = resultJson["Token"].ToString();
|
||||
return xBLToken;
|
||||
}
|
||||
|
||||
|
||||
private class XSTSTokenRequestData
|
||||
{
|
||||
public PropertiesData Properties { get; set; }
|
||||
public string RelyingParty { get; set; }
|
||||
public string TokenType { get; set; }
|
||||
|
||||
public class PropertiesData
|
||||
{
|
||||
public string SandboxId { get; set; }
|
||||
public List<string> UserTokens { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 3:从 XBLToken 获取 {XSTSToken, UHS}
|
||||
/// </summary>
|
||||
/// <returns>包含 XSTSToken 与 UHS 的字符串组</returns>
|
||||
private static string[] MsLoginStep3(string xBLToken)
|
||||
{
|
||||
ModProfile.ProfileLog("开始正版验证 Step 3/6: 获取 XSTSToken");
|
||||
if (string.IsNullOrEmpty(xBLToken))
|
||||
throw new ArgumentException("XBLToken 为空,无法获取数据", nameof(xBLToken));
|
||||
var requestData = new XSTSTokenRequestData
|
||||
{
|
||||
Properties = new XSTSTokenRequestData.PropertiesData
|
||||
{
|
||||
SandboxId = "RETAIL",
|
||||
UserTokens = new[] { xBLToken }.ToList()
|
||||
},
|
||||
RelyingParty = "rp://api.minecraftservices.com/",
|
||||
TokenType = "JWT"
|
||||
};
|
||||
string result;
|
||||
using (var response = HttpRequest
|
||||
.CreatePost("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.WithJsonContent(requestData)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
result = response.AsString();
|
||||
|
||||
if (!response.IsSuccess)
|
||||
{
|
||||
// 参考 https://github.com/PrismarineJS/prismarine-auth/blob/master/src/common/Constants.js
|
||||
if (result.Contains("2148916227"))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.Banned"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.IKnow"), isWarn: true);
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
if (result.Contains("2148916233"))
|
||||
{
|
||||
if (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.XboxNotRegistered"), Lang.Text("Minecraft.Launch.Login.Hint"), Lang.Text("Minecraft.Launch.Login.Register"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
ModBase.OpenWebsite("https://signup.live.com/signup");
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
if (result.Contains("2148916235"))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.RegionBlocked"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.IKnow"));
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
if (result.Contains("2148916238"))
|
||||
{
|
||||
if (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.Hint"), Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.AgeOver13"), Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.AgeUnder13"), Lang.Text("Common.Option.IDontKnow")) == 1)
|
||||
{
|
||||
ModBase.OpenWebsite("https://account.live.com/editprof.aspx");
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.Microsoft.ChangeBirthDate.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.Hint"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.OpenWebsite(
|
||||
"https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a");
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.Microsoft.ChangeBirthDate.SupportMessage"),
|
||||
Lang.Text("Minecraft.Launch.Login.Hint"));
|
||||
}
|
||||
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
ModProfile.ProfileLog("正版验证 Step 3/6 获取 XSTSToken 失败:" + response.StatusCode);
|
||||
var isIgnore = false;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (!isLaunching)
|
||||
return;
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
isIgnore = true;
|
||||
});
|
||||
if (isIgnore)
|
||||
{
|
||||
return new[] { ModProfile.selectedProfile.AccessToken, "Ignore" };
|
||||
return default;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
var xSTSToken = resultJson["Token"].ToString();
|
||||
var uhs = resultJson["DisplayClaims"]["xui"][0]["uhs"].ToString();
|
||||
return new[] { xSTSToken, uhs };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 4:从 {XSTSToken, UHS} 获取 Minecraft accessToken
|
||||
/// </summary>
|
||||
/// <param name="tokens">包含 XSTSToken 与 UHS 的字符串组</param>
|
||||
/// <returns>Minecraft accessToken</returns>
|
||||
private static string MsLoginStep4(string[] tokens)
|
||||
{
|
||||
ModProfile.ProfileLog("开始正版验证 Step 4/6: 获取 Minecraft AccessToken");
|
||||
if (tokens.Length < 2 || string.IsNullOrEmpty(tokens.ElementAt(0)) || string.IsNullOrEmpty(tokens.ElementAt(1)))
|
||||
throw new ArgumentException("传入的 XSTSToken 或者 UHS 错误", nameof(tokens));
|
||||
var requestData = new Dictionary<string, string> { { "identityToken", $"XBL3.0 x={tokens[1]};{tokens[0]}" } };
|
||||
string result;
|
||||
try
|
||||
{
|
||||
using (var response = HttpRequest
|
||||
.CreatePost("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.WithJsonContent(requestData)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
result = response.AsString();
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests))
|
||||
{
|
||||
ModBase.Log(ex, "正版验证 Step 4 汇报 429");
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.TooManyRequests"));
|
||||
}
|
||||
|
||||
if (ex.StatusCode is { } arg1 && arg1 == HttpStatusCode.Forbidden)
|
||||
{
|
||||
ModBase.Log(ex, "正版验证 Step 4 汇报 403");
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.AbnormalIp"));
|
||||
}
|
||||
|
||||
ModProfile.ProfileLog("正版验证 Step 4/6 获取 MC AccessToken 失败:" + ex);
|
||||
var isIgnore = false;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (!isLaunching)
|
||||
return;
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
isIgnore = true;
|
||||
});
|
||||
if (isIgnore)
|
||||
{
|
||||
return "Ignore";
|
||||
return default;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
var accessToken = resultJson["access_token"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
throw new Exception("获取到的 Minecraft AccessToken 为空,登录流程异常!");
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 5:验证微软账号是否持有 MC,这也会刷新 XGP
|
||||
/// </summary>
|
||||
/// <param name="accessToken">Minecraft accessToken</param>
|
||||
private static void MsLoginStep5(string accessToken)
|
||||
{
|
||||
ModProfile.ProfileLog("开始正版验证 Step 5/6: 验证账户是否持有 MC");
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
|
||||
var result = "";
|
||||
try
|
||||
{
|
||||
using (var response = HttpRequest
|
||||
.Create("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
.WithBearerToken(accessToken)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
result = response.AsString();
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
if (!(resultJson.ContainsKey("items") && resultJson["items"].AsArray().Any(x =>
|
||||
x["name"]?.ToString() == "product_minecraft" || x["name"]?.ToString() == "game_minecraft")))
|
||||
{
|
||||
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.NotPurchased"),
|
||||
Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.Microsoft.PurchaseMinecraft"), Lang.Text("Common.Action.Cancel")))
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
ModBase.OpenWebsite(
|
||||
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("$$");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "正版验证 Step 5 异常:" + result);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正版验证步骤 6:从 Minecraft accessToken 获取 {UUID, UserName, ProfileJson}
|
||||
/// </summary>
|
||||
/// <param name="accessToken">Minecraft accessToken</param>
|
||||
/// <returns>包含 UUID, UserName 和 ProfileJson 的字符串组</returns>
|
||||
private static string[] MsLoginStep6(string accessToken)
|
||||
{
|
||||
ModProfile.ProfileLog("开始正版验证 Step 6/6: 获取玩家 ID 与 UUID 等相关信息");
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
|
||||
string result;
|
||||
try
|
||||
{
|
||||
using (var response = HttpRequest
|
||||
.Create("https://api.minecraftservices.com/minecraft/profile")
|
||||
.WithBearerToken(accessToken)
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
result = response.AsString();
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
var message = ex.Message;
|
||||
if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests))
|
||||
{
|
||||
ModBase.Log(ex, "正版验证 Step 6 汇报 429");
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.TooManyRequests"));
|
||||
}
|
||||
|
||||
if (ex.StatusCode is { } arg2 && arg2 == HttpStatusCode.NotFound)
|
||||
{
|
||||
ModBase.Log(ex, "正版验证 Step 6 汇报 404");
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.CreateProfile.Message"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.Microsoft.CreateProfile.Button"), Lang.Text("Common.Action.Cancel")))
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
ModBase.OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, "Login Failed: Create Profile");
|
||||
throw new Exception("$$");
|
||||
}
|
||||
|
||||
ModProfile.ProfileLog("正版验证 Step 6/6 获取玩家档案信息失败:" + ex);
|
||||
var isIgnore = false;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (!isLaunching)
|
||||
return;
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
|
||||
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
|
||||
isIgnore = true;
|
||||
});
|
||||
if (isIgnore)
|
||||
{
|
||||
return new[] { ModProfile.selectedProfile.Uuid, ModProfile.selectedProfile.Username, "Ignore" };
|
||||
return default;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
var uuid = resultJson["id"].ToString();
|
||||
var userName = resultJson["name"].ToString();
|
||||
return new[] { uuid, userName, result };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 第三方验证
|
||||
|
||||
private static void McLoginServerStart(ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
|
||||
{
|
||||
var input = data.input;
|
||||
var needRefresh = false;
|
||||
var wasRefreshed = false;
|
||||
|
||||
ModProfile.ProfileLog("验证方式:" + input.Description);
|
||||
data.Progress = 0.05d;
|
||||
|
||||
// 尝试验证登录(如果不需要重新选择档案且不是创建档案)
|
||||
if (!input.ForceReselectProfile && !ModProfile.isCreatingProfile)
|
||||
{
|
||||
try
|
||||
{
|
||||
ThrowIfAborted(data);
|
||||
McLoginRequestValidate(ref data);
|
||||
data.Progress = 0.95d;
|
||||
return; // 登录成功,直接返回
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_HandleHttpWebException(ex, "验证登录失败");
|
||||
}
|
||||
catch (HttpResponseException ex){
|
||||
ModProfile.ProfileLog($"验证登录失败: {ex}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_HandleException(ex, "验证登录失败", "Minecraft.Launch.Login.Auth.ValidationFailed.WithDetail");
|
||||
}
|
||||
|
||||
data.Progress = 0.25d;
|
||||
|
||||
// 尝试刷新登录
|
||||
try
|
||||
{
|
||||
ThrowIfAborted(data);
|
||||
McLoginRequestRefresh(ref data, needRefresh);
|
||||
data.Progress = needRefresh ? 0.85d : 0.45d;
|
||||
data.Progress = 0.95d;
|
||||
return; // 刷新成功,直接返回
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModProfile.ProfileLog(Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed") + ": " + ex);
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed.WithDetail", ex.ToString()),
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
|
||||
isWarn: true);
|
||||
if (wasRefreshed)
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.SecondRefreshFailed"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试普通登录
|
||||
try
|
||||
{
|
||||
ThrowIfAborted(data);
|
||||
needRefresh = McLoginRequestLogin(ref data);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_HandleLoginHttpException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_HandleException(ex, "第三方登录失败", "Minecraft.Launch.Login.Auth.LoginFailed.WithDetail");
|
||||
}
|
||||
|
||||
// 如果需要刷新,循环刷新一次
|
||||
if (needRefresh)
|
||||
{
|
||||
ModProfile.ProfileLog("重新进行刷新登录");
|
||||
wasRefreshed = true;
|
||||
data.Progress = 0.65d;
|
||||
|
||||
try
|
||||
{
|
||||
ThrowIfAborted(data);
|
||||
McLoginRequestRefresh(ref data, needRefresh);
|
||||
data.Progress = 0.95d;
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModProfile.ProfileLog(Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed") + ": " + ex);
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed.WithDetail", ex.ToString()),
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
|
||||
isWarn: true);
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.SecondRefreshFailed"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 最终完成
|
||||
// 兜底校验:若走到这里仍未取得有效 AccessToken(例如回退登录的 HTTP 失败被 McLoginRequestLogin
|
||||
// 吞掉并返回 false),说明登录实际失败,必须中止,避免带着空凭据继续启动(见 #3307 review)。
|
||||
if (string.IsNullOrEmpty(data.output.AccessToken))
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Failed"));
|
||||
data.Progress = 0.95d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查任务是否被中断
|
||||
/// </summary>
|
||||
private static void ThrowIfAborted(ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
|
||||
{
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一处理 HttpWebException
|
||||
/// </summary>
|
||||
private static void _HandleHttpWebException(WebException ex, string logPrefix)
|
||||
{
|
||||
var allMessage = ex.ToString();
|
||||
ModProfile.ProfileLog(logPrefix + ":" + allMessage);
|
||||
|
||||
if ((!allMessage.Contains("超时") && !allMessage.Contains("imeout"))
|
||||
|| allMessage.Contains("403"))
|
||||
return;
|
||||
ModProfile.ProfileLog("已触发超时登录失败");
|
||||
var message = Lang.Text("Minecraft.Launch.Login.Auth.Timeout.WithDetail", ex.ToString());
|
||||
ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
|
||||
isWarn: true);
|
||||
|
||||
throw new Exception("$" + message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一处理普通异常
|
||||
/// </summary>
|
||||
private static void _HandleException(
|
||||
Exception ex,
|
||||
string logPrefix,
|
||||
string userMessageKey)
|
||||
{
|
||||
ModProfile.ProfileLog(logPrefix + ":" + ex);
|
||||
var message = Lang.Text(userMessageKey, ex.ToString());
|
||||
ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
|
||||
isWarn: true);
|
||||
throw new Exception("$" + message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理普通登录 HttpWebException
|
||||
/// </summary>
|
||||
private static void _HandleLoginHttpException(WebException ex)
|
||||
{
|
||||
ModProfile.ProfileLog("验证失败:" + ex);
|
||||
var message = Lang.Text("Minecraft.Launch.Login.Auth.NetworkFailed.WithDetail", ex.ToString());
|
||||
ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
|
||||
isWarn: true);
|
||||
throw new Exception("$" + message);
|
||||
}
|
||||
|
||||
// Server 登录:三种验证方式的请求
|
||||
private static void McLoginRequestValidate(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
|
||||
{
|
||||
ModProfile.ProfileLog("验证登录开始(Validate, Authlib");
|
||||
// 提前缓存信息,否则如果在登录请求过程中退出登录,设置项目会被清空,导致输出存在空值
|
||||
var accessToken = "";
|
||||
var clientToken = "";
|
||||
var uuid = "";
|
||||
var name = "";
|
||||
if (ModProfile.selectedProfile is not null)
|
||||
{
|
||||
accessToken = ModProfile.selectedProfile.AccessToken;
|
||||
clientToken = ModProfile.selectedProfile.ClientToken;
|
||||
uuid = ModProfile.selectedProfile.Uuid;
|
||||
name = ModProfile.selectedProfile.Username;
|
||||
}
|
||||
|
||||
// 发送登录请求
|
||||
var requestData = new JsonObject { ["accessToken"] = accessToken, ["clientToken"] = clientToken };
|
||||
Requester.Fetch(data.input.BaseUrl + "/validate",
|
||||
new FetchParam
|
||||
{
|
||||
Method = "POST",
|
||||
Content = requestData.ToJsonString(),
|
||||
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
|
||||
ContentType = "application/json"
|
||||
}); // 没有返回值的
|
||||
// 将登录结果输出
|
||||
data.output.AccessToken = accessToken;
|
||||
data.output.ClientToken = clientToken;
|
||||
data.output.Uuid = uuid;
|
||||
data.output.Name = name;
|
||||
data.output.Type = "Auth";
|
||||
// 不更改缓存,直接结束
|
||||
ModProfile.ProfileLog("验证登录成功(Validate, Authlib");
|
||||
}
|
||||
|
||||
private static void McLoginRequestRefresh(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data,
|
||||
bool requestUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var refreshInfo = new JsonObject();
|
||||
var selectProfile = new JsonObject
|
||||
{ { "name", ModProfile.selectedProfile.Username }, { "id", ModProfile.selectedProfile.Uuid } };
|
||||
refreshInfo.Add("selectedProfile", selectProfile);
|
||||
refreshInfo.Add("accessToken", ModProfile.selectedProfile.AccessToken);
|
||||
refreshInfo.Add("requestUser", true);
|
||||
ModProfile.ProfileLog("刷新登录开始(Refresh, Authlib");
|
||||
var loginJson = (JsonObject)ModBase.GetJson(Requester.Fetch(data.input.BaseUrl + "/refresh",
|
||||
new FetchParam
|
||||
{
|
||||
Method = "POST",
|
||||
Content = refreshInfo.ToJsonString(),
|
||||
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
|
||||
ContentType = "application/json",
|
||||
RequireContent = true
|
||||
}
|
||||
));
|
||||
// 将登录结果输出
|
||||
if (loginJson["selectedProfile"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.InvalidProfile", ModProfile.selectedProfile.Username));
|
||||
data.output.AccessToken = loginJson["accessToken"].ToString();
|
||||
data.output.ClientToken = loginJson["clientToken"].ToString();
|
||||
data.output.Uuid = loginJson["selectedProfile"]["id"].ToString();
|
||||
data.output.Name = loginJson["selectedProfile"]["name"].ToString();
|
||||
data.output.Type = "Auth";
|
||||
// 保存缓存
|
||||
var profileIndex = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
|
||||
ModProfile.profileList[profileIndex].Username = data.output.Name;
|
||||
ModProfile.profileList[profileIndex].AccessToken = data.output.AccessToken;
|
||||
ModProfile.profileList[profileIndex].ClientToken = data.output.ClientToken;
|
||||
ModProfile.profileList[profileIndex].Uuid = data.output.Uuid;
|
||||
ModProfile.profileList[profileIndex].Name = data.input.UserName;
|
||||
ModProfile.profileList[profileIndex].Password = data.input.Password;
|
||||
ModProfile.ProfileLog("刷新登录成功(Refresh, Authlib)");
|
||||
}
|
||||
catch (HttpResponseException ex)
|
||||
{
|
||||
// 刷新失败必须向上抛出:否则 McLoginServerStart 会把本次登录判为“刷新成功”、带着空令牌继续
|
||||
// 启动,并丧失“回退到普通登录”的自动恢复机会。保留服务端错误详情作为消息,并把原始
|
||||
// HttpResponseException(含状态码/堆栈)作为 InnerException 以便诊断;同时显式 Dispose 及时
|
||||
// 释放底层 Response,不依赖终结器兜底(其回收时机不确定,可能令底层资源驻留)。
|
||||
var message = _TryGetLastError(ex, out var detail) ? detail : ex.Message;
|
||||
ex.Dispose();
|
||||
throw new Exception(message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool McLoginRequestLogin(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var needRefresh = false;
|
||||
ModProfile.ProfileLog("登录开始(Login, Authlib)");
|
||||
var requestData = new JsonObject
|
||||
{
|
||||
["agent"] = new JsonObject { ["name"] = "Minecraft", ["version"] = 1 },
|
||||
["username"] = data.input.UserName,
|
||||
["password"] = data.input.Password,
|
||||
["requestUser"] = true
|
||||
};
|
||||
var loginJson = (JsonObject)ModBase.GetJson(Requester.Fetch(data.input.BaseUrl + "/authenticate",
|
||||
new FetchParam
|
||||
{
|
||||
Method = "POST",
|
||||
Content = requestData.ToJsonString(),
|
||||
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
|
||||
ContentType = "application/json",
|
||||
RequireContent = true
|
||||
}));
|
||||
// 检查登录结果
|
||||
if (loginJson["availableProfiles"].AsArray().Count == 0)
|
||||
{
|
||||
if (data.input.ForceReselectProfile)
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Auth.NoProfileCannotSwitch"), HintType.Error);
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.NoProfile"));
|
||||
}
|
||||
|
||||
if (data.input.ForceReselectProfile && loginJson["availableProfiles"].AsArray().Count == 1)
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Auth.OnlyOneProfile"), HintType.Error);
|
||||
string selectedName = null;
|
||||
string selectedId = null;
|
||||
if ((loginJson["selectedProfile"] is null || data.input.ForceReselectProfile) &&
|
||||
loginJson["availableProfiles"].AsArray().Count > 1)
|
||||
{
|
||||
// 要求选择档案;优先从缓存读取
|
||||
needRefresh = true;
|
||||
var cacheId = ModProfile.selectedProfile is not null ? ModProfile.selectedProfile.Uuid : "";
|
||||
foreach (var profile in loginJson["availableProfiles"].AsArray())
|
||||
if ((profile["id"].ToString() ?? "") == (cacheId ?? ""))
|
||||
{
|
||||
selectedName = profile["name"].ToString();
|
||||
selectedId = profile["id"].ToString();
|
||||
ModProfile.ProfileLog("根据缓存选择的角色:" + selectedName);
|
||||
}
|
||||
|
||||
// 缓存无效,要求玩家选择
|
||||
if (selectedName is null)
|
||||
{
|
||||
ModProfile.ProfileLog("要求玩家选择角色");
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
var selectionControl = new List<IMyRadio>();
|
||||
var selectionJson = new List<JsonNode>();
|
||||
foreach (var profile in loginJson["availableProfiles"].AsArray())
|
||||
{
|
||||
selectionControl.Add(new MyRadioBox { Text = profile["name"].ToString() });
|
||||
selectionJson.Add(profile);
|
||||
}
|
||||
|
||||
var selectedIndex = (int)ModMain.MyMsgBoxSelect(selectionControl, Lang.Text("Minecraft.Launch.Login.Auth.SelectProfile"));
|
||||
selectedName = selectionJson[selectedIndex]["name"].ToString();
|
||||
selectedId = selectionJson[selectedIndex]["id"].ToString();
|
||||
});
|
||||
|
||||
ModProfile.ProfileLog("玩家选择的角色:" + selectedName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedName = loginJson["selectedProfile"]["name"].ToString();
|
||||
selectedId = loginJson["selectedProfile"]["id"].ToString();
|
||||
}
|
||||
|
||||
// 将登录结果输出
|
||||
data.output.AccessToken = loginJson["accessToken"].ToString();
|
||||
data.output.ClientToken = loginJson["clientToken"].ToString();
|
||||
data.output.Name = selectedName;
|
||||
data.output.Uuid = selectedId;
|
||||
data.output.Type = "Auth";
|
||||
// 获取服务器信息
|
||||
var response =
|
||||
Requester.FetchString(data.input.BaseUrl.Replace("/authserver", ""));
|
||||
var serverName = ModBase.GetJson(response)["meta"]?["serverName"]?.ToString() ?? data.input.BaseUrl.Replace("/authserver", "");
|
||||
// 保存缓存
|
||||
if (data.input.IsExist)
|
||||
{
|
||||
var profileIndex = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
|
||||
ModProfile.profileList[profileIndex].Username = data.output.Name;
|
||||
ModProfile.profileList[profileIndex].Uuid = data.output.Uuid;
|
||||
ModProfile.profileList[profileIndex].ServerName = serverName;
|
||||
ModProfile.profileList[profileIndex].AccessToken = data.output.AccessToken;
|
||||
ModProfile.profileList[profileIndex].ClientToken = data.output.ClientToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newProfile = new ModProfile.McProfile
|
||||
{
|
||||
Type = McLoginType.Auth,
|
||||
Uuid = data.output.Uuid,
|
||||
Username = data.output.Name,
|
||||
Server = data.input.BaseUrl,
|
||||
ServerName = serverName,
|
||||
Name = data.input.UserName,
|
||||
Password = data.input.Password,
|
||||
AccessToken = data.output.AccessToken,
|
||||
ClientToken = data.output.ClientToken,
|
||||
Expires = 1743779140286L,
|
||||
Desc = ""
|
||||
};
|
||||
ModProfile.profileList.Add(newProfile);
|
||||
ModProfile.selectedProfile = newProfile;
|
||||
ModProfile.isCreatingProfile = false;
|
||||
}
|
||||
|
||||
ModProfile.SaveProfile();
|
||||
ModProfile.ProfileLog("登录成功(Login, Authlib)");
|
||||
return needRefresh;
|
||||
}
|
||||
catch (HttpResponseException ex)
|
||||
{
|
||||
|
||||
if (_TryGetLastError(ex, out var message)) ModMain.MyMsgBox(message, Lang.Text("Minecraft.Launch.Login.Failed"));
|
||||
ex.Dispose();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
ModProfile.ProfileLog($"第三方验证失败: {ex}");
|
||||
if (ex.Message.StartsWithF("$")) throw;
|
||||
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.LoginFailed", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _TryGetLastError(HttpResponseException ex,[NotNullWhen(true)] out string? message)
|
||||
{
|
||||
message = null;
|
||||
try
|
||||
{
|
||||
using var responseStream = ex.Response?.Content.ReadAsStream();
|
||||
if (responseStream is null) return false;
|
||||
var result = JsonSerializer.Deserialize<YggdrasilAuthenticateResult>(responseStream, JsonCompat.SerializerOptions);
|
||||
if (result?.ErrorMessage is null) return false;
|
||||
message = result.ErrorMessage;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Suppress Exception
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 离线验证
|
||||
|
||||
private static void McLoginLegacyStart(ModLoader.LoaderTask<McLoginLegacy, McLoginResult> data)
|
||||
{
|
||||
var input = data.input;
|
||||
ModProfile.ProfileLog($"验证方式:离线({input.UserName}, {input.Uuid})");
|
||||
data.Progress = 0.1d;
|
||||
{
|
||||
ref var withBlock = ref data.output;
|
||||
withBlock.Name = input.UserName;
|
||||
withBlock.Uuid = ModProfile.selectedProfile.Uuid;
|
||||
withBlock.Type = "Legacy";
|
||||
}
|
||||
// 将结果扩展到所有项目中
|
||||
data.output.AccessToken = data.output.Uuid;
|
||||
data.output.ClientToken = data.output.Uuid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Java 处理
|
||||
|
||||
public static JavaEntry mcLaunchJavaSelected;
|
||||
|
||||
private static void McLaunchJava(ModLoader.LoaderTask<int, int> task)
|
||||
{
|
||||
var minVer = new Version(0, 0, 0, 0);
|
||||
var maxVer = new Version(999, 999, 999, 999);
|
||||
|
||||
// MC 大版本检测
|
||||
if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2024, 4, 2)) ||
|
||||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla >= new Version(20, 0, 5)))
|
||||
{
|
||||
// 1.20.5+ (24w14a+):至少 Java 21
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] MC 1.20.5+ (24w14a+) 要求至少 Java 21");
|
||||
minVer = new Version(21, 0, 0, 0);
|
||||
}
|
||||
else if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2021, 11, 16)) ||
|
||||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18))
|
||||
{
|
||||
// 1.18 pre2+:至少 Java 17
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] MC 1.18 pre2+ 要求至少 Java 17");
|
||||
minVer = new Version(17, 0, 0, 0);
|
||||
}
|
||||
else if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2021, 5, 11)) ||
|
||||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 17))
|
||||
{
|
||||
// 1.17+ (21w19a+):至少 Java 16
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] MC 1.17+ (21w19a+) 要求至少 Java 16");
|
||||
minVer = new Version(16, 0, 0, 0);
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017) // Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了
|
||||
{
|
||||
// 1.12+:至少 Java 8
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] MC 1.12+ 要求至少 Java 8");
|
||||
minVer = new Version(1, 8, 0, 0);
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.releaseTime <= new DateTime(2013, 5, 1) &&
|
||||
ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2001) // 避免某些版本写个 1960 年
|
||||
{
|
||||
// 1.5.2-:最高 Java 8
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] MC 1.5.2- 要求最高 Java 12");
|
||||
maxVer = new Version(1, 8, 999, 999);
|
||||
}
|
||||
|
||||
// 原版 26+:获取 Mojang 要求的 Java 版本
|
||||
string recommendedComponent = null;
|
||||
var recommendedCode =
|
||||
ModInstanceList.McMcInstanceSelected.JsonObject?["javaVersion"]?["majorVersion"]?.ToObject<int>() ??
|
||||
ModInstanceList.McMcInstanceSelected.JsonVersion?["java_version"]?.ToObject<int>() ?? 0;
|
||||
if (recommendedCode >= 22)
|
||||
{
|
||||
McLaunchLog("Mojang 要求至少使用 Java " + recommendedCode);
|
||||
minVer = new Version(recommendedCode, 0, 0, 0);
|
||||
recommendedComponent =
|
||||
ModInstanceList.McMcInstanceSelected.JsonObject?["javaVersion"]?["component"]?.ToString() ??
|
||||
ModInstanceList.McMcInstanceSelected.JsonVersion?["java_component"]?.ToString();
|
||||
if (string.IsNullOrEmpty(recommendedComponent))
|
||||
recommendedComponent = null;
|
||||
}
|
||||
|
||||
// OptiFine 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasOptiFine && ModInstanceList.McMcInstanceSelected.Info.Valid) // 不管非标准版本
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 7)
|
||||
{
|
||||
// <1.7:至多 Java 8
|
||||
maxVer = new Version(1, 8, 999, 999);
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 8 &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 12)
|
||||
{
|
||||
// 1.8 - 1.11:必须恰好 Java 8
|
||||
minVer = new Version(1, 8, 0, 0);
|
||||
maxVer = new Version(1, 8, 999, 999);
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major == 12)
|
||||
{
|
||||
// 1.12:最高 Java 8
|
||||
maxVer = new Version(1, 8, 999, 999);
|
||||
}
|
||||
}
|
||||
|
||||
// Forge 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasForge)
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.vanilla >= new Version(6, 0, 1) &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla <= new Version(7, 0, 2))
|
||||
{
|
||||
// 1.6.1 - 1.7.2:必须 Java 7
|
||||
minVer = new Version(1, 7, 0, 0) > minVer ? new Version(1, 7, 0, 0) : minVer;
|
||||
maxVer = new Version(1, 7, 999, 999) < maxVer ? new Version(1, 7, 999, 999) : maxVer;
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 12 ||
|
||||
!ModInstanceList.McMcInstanceSelected.Info.Valid) // 非标准版本
|
||||
{
|
||||
// <=1.12:Java 8
|
||||
maxVer = new Version(1, 8, 999, 999);
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 14)
|
||||
{
|
||||
// 1.13 - 1.14:Java 8 - 10
|
||||
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
|
||||
maxVer = new Version(1, 10, 999, 999) < maxVer ? new Version(1, 10, 999, 999) : maxVer;
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major == 15)
|
||||
{
|
||||
// 1.15:Java 8 - 15
|
||||
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
|
||||
maxVer = new Version(1, 15, 999, 999) < maxVer ? new Version(1, 15, 999, 999) : maxVer;
|
||||
}
|
||||
else if (McVersionComparer.CompareVersionGe(ModInstanceList.McMcInstanceSelected.Info.Forge, "34.0.0") &&
|
||||
McVersionComparer.CompareVersionGe("36.2.25", ModInstanceList.McMcInstanceSelected.Info.Forge))
|
||||
{
|
||||
// 1.16,Forge 34.X ~ 36.2.25:最高 Java 8u321
|
||||
maxVer = new Version(1, 8, 0, 320) < maxVer ? new Version(1, 8, 0, 321) : maxVer;
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18 &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 19 &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.HasOptiFine) // #305
|
||||
{
|
||||
// 1.18:若安装了 OptiFine,最高 Java 18
|
||||
maxVer = new Version(1, 18, 999, 999) < maxVer ? new Version(1, 18, 999, 999) : maxVer;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanroom 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasCleanroom)
|
||||
{
|
||||
if (!Version.TryParse(ModInstanceList.McMcInstanceSelected.Info.Cleanroom.Split('-')[0], out Version cleanroomVersion))
|
||||
throw new FormatException("无法解析 Cleanroom 版本号:" + ModInstanceList.McMcInstanceSelected.Info.Cleanroom);
|
||||
if (cleanroomVersion < new Version(0, 5, 0, 0))
|
||||
{
|
||||
if (ModBase.modeDebug) ModBase.Log("[Launch] [Debug] Cleanroom 版本低于 0.5,要求至少 Java 21");
|
||||
minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ModBase.modeDebug) ModBase.Log("[Launch] [Debug] Cleanroom 版本高于 0.5,要求至少 Java 25");
|
||||
minVer = new Version(25, 0, 0, 0) > minVer ? new Version(25, 0, 0, 0) : minVer;
|
||||
}
|
||||
}
|
||||
|
||||
// Fabric 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasFabric && ModInstanceList.McMcInstanceSelected.Info.Valid) // 不管非标准版本
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 15 &&
|
||||
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 16)
|
||||
// 1.15 - 1.16:Java 8+
|
||||
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
|
||||
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18)
|
||||
// 1.18+:Java 17+
|
||||
minVer = new Version(1, 17, 0, 0) > minVer ? new Version(1, 17, 0, 0) : minVer;
|
||||
}
|
||||
|
||||
// LiteLoader 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasLiteLoader && ModInstanceList.McMcInstanceSelected.Info.Valid)
|
||||
{
|
||||
// 最高 Java 8
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] LiteLoader 要求最高 Java 8");
|
||||
maxVer = new Version(8, 999, 999, 999) < maxVer ? new Version(8, 999, 999, 999) : maxVer;
|
||||
}
|
||||
|
||||
// LabyMod 检测
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasLabyMod)
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] LabyMod 要求至少 Java 21");
|
||||
minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer;
|
||||
maxVer = new Version(999, 999, 999, 999);
|
||||
}
|
||||
|
||||
// JSON 中要求的版本
|
||||
if (ModInstanceList.McMcInstanceSelected.JsonObject["javaVersion"] is not null)
|
||||
{
|
||||
var majorVersion = ModBase.Val(ModInstanceList.McMcInstanceSelected.JsonObject["javaVersion"]["majorVersion"]);
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log("[Launch] [Debug] JSON 中参数要求至少 Java " + majorVersion);
|
||||
if (majorVersion <= 8d)
|
||||
minVer = new Version(1, (int)Math.Round(majorVersion), 0, 0) > minVer
|
||||
? new Version(1, (int)Math.Round(majorVersion), 0, 0)
|
||||
: minVer;
|
||||
else
|
||||
minVer = new Version((int)Math.Round(majorVersion), 0, 0, 0) > minVer
|
||||
? new Version((int)Math.Round(majorVersion), 0, 0, 0)
|
||||
: minVer;
|
||||
|
||||
if (maxVer < minVer)
|
||||
maxVer = new Version(999, 999, 999, 999);
|
||||
}
|
||||
|
||||
lock (ModJava.javaLock)
|
||||
{
|
||||
// 选择 Java
|
||||
McLaunchLog("Java 版本需求:最低 " + minVer + ",最高 " + maxVer);
|
||||
mcLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModInstanceList.McMcInstanceSelected);
|
||||
if (task.IsAborted)
|
||||
return;
|
||||
if (mcLaunchJavaSelected is not null)
|
||||
{
|
||||
McLaunchLog("选择的 Java:" + mcLaunchJavaSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
// 无合适的 Java
|
||||
if (task.IsAborted)
|
||||
return; // 中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java
|
||||
McLaunchLog("无合适的 Java,需要确认是否自动下载");
|
||||
string javaCode;
|
||||
if (minVer >= new Version(1, 9))
|
||||
{
|
||||
javaCode = minVer.Major.ToString();
|
||||
}
|
||||
else if (maxVer < new Version(1, 8))
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasForge)
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.NeedLegacyJavaFixerOrJava7"),
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||||
else
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.NeedJava7"),
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||||
throw new Exception("$$");
|
||||
}
|
||||
else if (minVer > new Version(1, 8, 0, 140) && maxVer < new Version(1, 8, 0, 321))
|
||||
{
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.NeedJava8U141ToU320"),
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||||
throw new Exception("$$");
|
||||
}
|
||||
else if (minVer > new Version(1, 8, 0, 140))
|
||||
{
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Launch.Java.NeedJava8U141OrLater"),
|
||||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||||
throw new Exception("$$");
|
||||
}
|
||||
else
|
||||
{
|
||||
javaCode = 8.ToString();
|
||||
}
|
||||
|
||||
if (!ModJava.JavaDownloadConfirm($"Java {javaCode}"))
|
||||
throw new Exception("$$");
|
||||
// 开始自动下载
|
||||
var javaLoader = ModJava.GetJavaDownloadLoader();
|
||||
try
|
||||
{
|
||||
javaLoader.Start(recommendedComponent ?? javaCode, true); // 在 Java 22+ 时优先使用 Mojang 提供的 Component 字段
|
||||
while (javaLoader.State == ModBase.LoadState.Loading && !task.IsAborted)
|
||||
{
|
||||
task.Progress = javaLoader.Progress;
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
javaLoader.Abort(); // 确保取消时中止 Java 下载
|
||||
}
|
||||
|
||||
// 检查下载结果
|
||||
mcLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModInstanceList.McMcInstanceSelected);
|
||||
if (task.IsAborted)
|
||||
return;
|
||||
if (mcLaunchJavaSelected is not null)
|
||||
{
|
||||
McLaunchLog("选择的 Java:" + mcLaunchJavaSelected);
|
||||
}
|
||||
else
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Error.NoJava"), HintType.Error);
|
||||
throw new Exception("$$");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动参数
|
||||
|
||||
internal static void SecretLaunchJvmArgs(ref List<string> dataList)
|
||||
{
|
||||
var dataJvmCustom = Config.Instance.JvmArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
dataList.Insert(0,
|
||||
string.IsNullOrEmpty(dataJvmCustom)
|
||||
? Config.Launch.JvmArgs
|
||||
: dataJvmCustom); // 可变 JVM 参数
|
||||
switch (Config.Launch.PreferredIpStack)
|
||||
{
|
||||
case JvmPreferredIpStack.PreferV4:
|
||||
{
|
||||
dataList.Add("-Djava.net.preferIPv4Stack=true");
|
||||
dataList.Add("-Djava.net.preferIPv4Addresses=true");
|
||||
break;
|
||||
}
|
||||
case JvmPreferredIpStack.PreferV6:
|
||||
{
|
||||
dataList.Add("-Djava.net.preferIPv6Stack=true");
|
||||
dataList.Add("-Djava.net.preferIPv6Addresses=true");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double availableGb = KernelInterop.GetAvailablePhysicalMemoryBytes() / 1073741824.0;
|
||||
ModLaunch.McLaunchLog($"当前剩余内存:{availableGb.ToString("N1", CultureInfo.InvariantCulture)}G");
|
||||
double totalRamMb = PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected) * 1024d;
|
||||
var maxHeapArg = Math.Floor(totalRamMb).ToString(CultureInfo.InvariantCulture);
|
||||
dataList.Add("-Xmn" + Math.Floor(totalRamMb * 0.15).ToString(CultureInfo.InvariantCulture) + "m");
|
||||
dataList.Add("-Xmx" + maxHeapArg + "m");
|
||||
// #3282: 固定堆大小时追加 -Xms 使其等于 -Xmx(复用同一数值以保持一致),隐式禁用内存归还降低延迟抖动、利于 ZGC。
|
||||
// 若 dataList 中已存在 -Xms(例如用户自定义参数已设)则跳过,避免重复/冲突。
|
||||
if (Config.Launch.LockMemory && !dataList.Any(d => d.Contains("-Xms", StringComparison.OrdinalIgnoreCase)))
|
||||
dataList.Add("-Xms" + maxHeapArg + "m");
|
||||
if (!dataList.Any(d => d.Contains("-Dlog4j2.formatMsgNoLookups=true")))
|
||||
dataList.Add("-Dlog4j2.formatMsgNoLookups=true");
|
||||
}
|
||||
|
||||
public class LaunchArgument
|
||||
{
|
||||
private readonly List<string> _features = new();
|
||||
|
||||
public LaunchArgument(McInstance minecraft)
|
||||
{
|
||||
var curArgu = string.Empty;
|
||||
if (minecraft.IsOldJson)
|
||||
_features = minecraft.JsonObject["minecraftArguments"].ToString().Split(' ').ToList();
|
||||
else
|
||||
foreach (var item in minecraft.JsonObject["arguments"]["game"].AsArray())
|
||||
if (item.GetValueKind() == JsonValueKind.String)
|
||||
_features.Add(item.ToString());
|
||||
else if (item.GetValueKind() == JsonValueKind.Object)
|
||||
{
|
||||
var valueNode = item["value"];
|
||||
if (valueNode.GetValueKind() == JsonValueKind.Array)
|
||||
_features.AddRange(valueNode.AsArray().Select(x => x.ToString()));
|
||||
else if (valueNode.GetValueKind() == JsonValueKind.String)
|
||||
_features.Add(valueNode.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public object HasArguments(string key)
|
||||
{
|
||||
return _features.Contains(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static string mcLaunchArgument;
|
||||
|
||||
/// <summary>
|
||||
/// 释放 Java Wrapper 并返回完整文件路径。
|
||||
/// </summary>
|
||||
public static string ExtractJavaWrapper()
|
||||
{
|
||||
var wrapperPath = Path.Combine(ModBase.pathPure, "JavaWrapper.jar");
|
||||
ModBase.Log("[Java] 选定的 Java Wrapper 路径:" + wrapperPath);
|
||||
lock (extractJavaWrapperLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteJavaWrapper(wrapperPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (File.Exists(wrapperPath))
|
||||
{
|
||||
// 因为未知原因 Java Wrapper 可能变为只读文件(#4243)
|
||||
ModBase.Log(ex, "Java Wrapper 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer);
|
||||
try
|
||||
{
|
||||
File.Delete(wrapperPath);
|
||||
WriteJavaWrapper(wrapperPath);
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
ModBase.Log(ex2, "Java Wrapper 文件重新释放失败,将尝试更换文件名重新生成", ModBase.LogLevel.Developer);
|
||||
wrapperPath = Path.Combine(ModBase.pathPure, "JavaWrapper2.jar");
|
||||
try
|
||||
{
|
||||
WriteJavaWrapper(wrapperPath);
|
||||
}
|
||||
catch (Exception ex3)
|
||||
{
|
||||
throw new FileNotFoundException("释放 Java Wrapper 最终尝试失败", ex3);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException("释放 Java Wrapper 失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wrapperPath;
|
||||
}
|
||||
|
||||
private static readonly object extractJavaWrapperLock = new();
|
||||
|
||||
private static void WriteJavaWrapper(string path)
|
||||
{
|
||||
ModBase.WriteFile(path, ModBase.GetResourceStream("Resources/java-wrapper.jar"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 linkd 并返回完整文件路径。
|
||||
/// </summary>
|
||||
public static string ExtractLinkD()
|
||||
{
|
||||
var linkDPath = Path.Combine(ModBase.pathPure, "linkd.exe");
|
||||
lock (extractLinkDLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteLinkD(linkDPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (File.Exists(linkDPath))
|
||||
{
|
||||
ModBase.Log(ex, "linkd 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer);
|
||||
try
|
||||
{
|
||||
File.Delete(linkDPath);
|
||||
WriteLinkD(linkDPath);
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
throw new FileNotFoundException("释放 linkd 失败", ex2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException("释放 linkd 失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linkDPath;
|
||||
}
|
||||
|
||||
private static readonly object extractLinkDLock = new();
|
||||
|
||||
private static void WriteLinkD(string path)
|
||||
{
|
||||
ModBase.WriteFile(path, ModBase.GetResourceStream("Resources/linkd.exe"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否使用 LegacyFix。
|
||||
/// </summary>
|
||||
private static bool McLaunchNeedsLegacyFix(McInstance mc)
|
||||
{
|
||||
if (Config.Launch.DisableLF || Config.Instance.DisableLF[mc.PathInstance])
|
||||
{
|
||||
ModBase.Log("[Launch] LegacyFix 已被禁用");
|
||||
return false;
|
||||
}
|
||||
if (mc.releaseTime < new DateTime(2013, 6, 25) && mc.releaseTime.Year > 2000)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实例所依赖的 LWJGL 版本
|
||||
/// </summary>
|
||||
private static string McLaunchGetLwjglVersion(McInstance mc)
|
||||
{
|
||||
foreach (ModLibrary.McLibToken library in ModLibrary.McLibListGet(mc, false))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(library.OriginalName))
|
||||
continue;
|
||||
|
||||
string[] parts = library.OriginalName.Split(':');
|
||||
if (parts.Length >= 3 &&
|
||||
parts[0].Equals("org.lwjgl", StringComparison.OrdinalIgnoreCase) &&
|
||||
parts[1].Equals("lwjgl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否启用了针对 Minecraft 26.1 的性能问题补丁
|
||||
/// </summary>
|
||||
private static bool McLaunchUsesLwjglUnsafeAgent(McInstance mc)
|
||||
{
|
||||
if (McLaunchGetLwjglVersion(mc) == "3.4.1")
|
||||
{
|
||||
bool globalDisabled = Config.Launch.DisableLwjglUnsafeAgent;
|
||||
bool instanceDisabled = Config.Instance.DisableLwjglUnsafeAgent[mc.PathInstance];
|
||||
|
||||
return !globalDisabled && !instanceDisabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 主方法,合并 Jvm、Game、Replace 三部分的参数数据
|
||||
private static void McLaunchArgumentMain(ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>> loader)
|
||||
{
|
||||
McLaunchLog("开始获取 Minecraft 启动参数");
|
||||
// 获取基准字符串与参数信息
|
||||
string arguments;
|
||||
if (ModInstanceList.McMcInstanceSelected.JsonObject["arguments"] is not null &&
|
||||
ModInstanceList.McMcInstanceSelected.JsonObject["arguments"]["jvm"] is not null)
|
||||
{
|
||||
McLaunchLog("获取新版 JVM 参数");
|
||||
arguments = McLaunchArgumentsJvmNew(ModInstanceList.McMcInstanceSelected);
|
||||
McLaunchLog("新版 JVM 参数获取成功:");
|
||||
McLaunchLog(arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
McLaunchLog("获取旧版 JVM 参数");
|
||||
arguments = McLaunchArgumentsJvmOld(ModInstanceList.McMcInstanceSelected);
|
||||
McLaunchLog("旧版 JVM 参数获取成功:");
|
||||
McLaunchLog(arguments);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(
|
||||
(string)ModInstanceList.McMcInstanceSelected.JsonObject["minecraftArguments"])) // 有的实例 JSON 中是空字符串
|
||||
{
|
||||
McLaunchLog("获取旧版 Game 参数");
|
||||
arguments += " " + McLaunchArgumentsGameOld(ModInstanceList.McMcInstanceSelected);
|
||||
McLaunchLog("旧版 Game 参数获取成功");
|
||||
}
|
||||
|
||||
if (ModInstanceList.McMcInstanceSelected.JsonObject["arguments"] is not null &&
|
||||
ModInstanceList.McMcInstanceSelected.JsonObject["arguments"]["game"] is not null)
|
||||
{
|
||||
McLaunchLog("获取新版 Game 参数");
|
||||
arguments += " " + McLaunchArgumentsGameNew(ModInstanceList.McMcInstanceSelected);
|
||||
McLaunchLog("新版 Game 参数获取成功");
|
||||
}
|
||||
|
||||
// 编码参数(#4700、#5892、#5909)
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion > 8)
|
||||
{
|
||||
if (!arguments.Contains("-Dstdout.encoding="))
|
||||
arguments = "-Dstdout.encoding=UTF-8 " + arguments;
|
||||
if (!arguments.Contains("-Dstderr.encoding="))
|
||||
arguments = "-Dstderr.encoding=UTF-8 " + arguments;
|
||||
}
|
||||
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion >= 18)
|
||||
if (!arguments.Contains("-Dfile.encoding="))
|
||||
arguments = "-Dfile.encoding=COMPAT " + arguments;
|
||||
// MJSB
|
||||
arguments = arguments.Replace(" -Dos.name=Windows 10", " -Dos.name=\"Windows 10\"");
|
||||
// 全屏
|
||||
if (Config.Launch.GameWindowMode == 0)
|
||||
arguments += " --fullscreen";
|
||||
// 由 Option 传入的额外参数
|
||||
foreach (var arg in currentLaunchOptions.ExtraArgs)
|
||||
arguments += " " + arg.Trim();
|
||||
// 自定义参数
|
||||
var argumentGame = Config.Instance.GameArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
arguments = arguments + " " + (string.IsNullOrEmpty(argumentGame) ? Config.Launch.GameArgs : argumentGame);
|
||||
// 替换参数
|
||||
var replaceArguments = McLaunchArgumentsReplace(ModInstanceList.McMcInstanceSelected, ref loader);
|
||||
if (string.IsNullOrWhiteSpace(replaceArguments["${version_type}"]))
|
||||
{
|
||||
// 若自定义信息为空,则去掉该部分
|
||||
arguments = arguments.Replace(" --versionType ${version_type}", "");
|
||||
replaceArguments["${version_type}"] = "\"\"";
|
||||
}
|
||||
|
||||
var finalArguments = "";
|
||||
foreach (var argumentRaw in arguments.Split(" "))
|
||||
{
|
||||
var argument = argumentRaw;
|
||||
foreach (var entry in replaceArguments)
|
||||
argument = argument.Replace(entry.Key, entry.Value);
|
||||
if ((argument.Contains(" ") || argument.Contains(@":\")) && !argument.EndsWithF("\""))
|
||||
argument = $"\"{argument}\"";
|
||||
finalArguments += argument + " ";
|
||||
}
|
||||
|
||||
finalArguments = finalArguments.TrimEnd();
|
||||
// 进存档
|
||||
var worldName = currentLaunchOptions.WorldName;
|
||||
if (worldName is not null) finalArguments += $" --quickPlaySingleplayer \"{worldName}\"";
|
||||
// 进服
|
||||
var server = string.IsNullOrEmpty(currentLaunchOptions.ServerIp)
|
||||
? Config.Instance.ServerToEnter[ModInstanceList.McMcInstanceSelected?.PathInstance]
|
||||
: currentLaunchOptions.ServerIp;
|
||||
if (string.IsNullOrWhiteSpace(worldName) && !string.IsNullOrWhiteSpace(server))
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.releaseTime > new DateTime(2023, 4, 4))
|
||||
{
|
||||
// QuickPlay
|
||||
finalArguments += $" --quickPlayMultiplayer \"{server}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 老版本
|
||||
if (server.Contains(":"))
|
||||
// 包含端口号
|
||||
finalArguments += " --server " + server.Split(":")[0] + " --port " + server.Split(":")[1];
|
||||
else
|
||||
// 不包含端口号
|
||||
finalArguments += " --server " + server + " --port 25565";
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasOptiFine)
|
||||
HintService.Hint(Lang.Text("Minecraft.Launch.Error.OptiFineAutoJoinWarning"), HintType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 输出
|
||||
McLaunchLog("Minecraft 启动参数:");
|
||||
McLaunchLog(finalArguments);
|
||||
mcLaunchArgument = finalArguments;
|
||||
}
|
||||
|
||||
// Jvm 部分(第一段)
|
||||
private static string McLaunchArgumentsJvmOld(McInstance instance)
|
||||
{
|
||||
// 存储以空格为间隔的启动参数列表
|
||||
var dataList = new List<string>();
|
||||
|
||||
// 输出固定参数
|
||||
dataList.Add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump");
|
||||
var argumentJvm = Config.Instance.JvmArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
if (string.IsNullOrEmpty(argumentJvm))
|
||||
argumentJvm = Config.Launch.JvmArgs;
|
||||
if (!argumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true"))
|
||||
argumentJvm += " -Dlog4j2.formatMsgNoLookups=true";
|
||||
argumentJvm = argumentJvm.Replace(" -XX:MaxDirectMemorySize=256M", ""); // #3511 的清理
|
||||
dataList.Insert(0, argumentJvm); // 可变 JVM 参数
|
||||
dataList.Add("-Xmn" +
|
||||
Math.Floor(PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
|
||||
!mcLaunchJavaSelected.Installation.Is64Bit) * 1024d * 0.15d) + "m");
|
||||
var maxHeapArg = Math.Floor(PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
|
||||
!mcLaunchJavaSelected.Installation.Is64Bit) * 1024d);
|
||||
dataList.Add("-Xmx" + maxHeapArg + "m");
|
||||
// #3282: 固定堆大小时追加 -Xms 使其等于 -Xmx(复用同一数值以保持一致),隐式禁用内存归还降低延迟抖动、利于 ZGC。
|
||||
// 若 dataList 中已存在 -Xms(例如用户自定义参数已设)则跳过,避免重复/冲突。
|
||||
if (Config.Launch.LockMemory && !dataList.Any(d => d.Contains("-Xms", StringComparison.OrdinalIgnoreCase)))
|
||||
dataList.Add("-Xms" + maxHeapArg + "m");
|
||||
dataList.Add("\"-Djava.library.path=" + GetNativesFolder() + "\"");
|
||||
dataList.Add("-cp ${classpath}"); // 把支持库添加进启动参数表
|
||||
|
||||
// Authlib-Injector
|
||||
if (mcLoginLoader.output.Type == "Auth")
|
||||
{
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion >= 6)
|
||||
dataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252)
|
||||
var server = mcLoginAuthLoader.input.BaseUrl.Replace("/authserver", "");
|
||||
try
|
||||
{
|
||||
var response = Requester.FetchString(server);
|
||||
dataList.Insert(0,
|
||||
"-javaagent:\"" + Path.Combine(ModBase.pathPure, "authlib-injector.jar") + "\"=" + server +
|
||||
" -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" +
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes(response)));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new Exception(
|
||||
Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServerWithDetail", server ?? null) + ex.InnerException, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServer", server ?? null), ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie])
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017)
|
||||
dataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\"");
|
||||
else
|
||||
dataList.Insert(0,
|
||||
"-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\"");
|
||||
}
|
||||
|
||||
// 渲染器
|
||||
var renderer = 0;
|
||||
var instanceRenderer = Config.Instance.Renderer[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
if (instanceRenderer != 0)
|
||||
renderer = instanceRenderer - 1;
|
||||
else
|
||||
renderer = Config.Launch.Renderer;
|
||||
var mesaLoaderWindowsTargetFile =
|
||||
Path.Combine(ModBase.pathPure, "mesa-loader-windows", mesaLoaderWindowsVersion, "Loader.jar");
|
||||
|
||||
if (renderer != 0)
|
||||
dataList.Insert(0,
|
||||
"-javaagent:\"" + mesaLoaderWindowsTargetFile + "\"=" +
|
||||
(renderer == 1 ? "llvmpipe" : renderer == 2 ? "d3d12" : "zink"));
|
||||
|
||||
// 设置代理
|
||||
if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) &&
|
||||
!string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress))
|
||||
try
|
||||
{
|
||||
var proxyAddress = new Uri(Config.Network.HttpProxy.CustomAddress);
|
||||
dataList.Add(
|
||||
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={proxyAddress.AbsoluteUri}");
|
||||
dataList.Add(
|
||||
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={proxyAddress.Port}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.Proxy"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.Proxy"));
|
||||
}
|
||||
|
||||
// 添加 LegacyFix 相关参数
|
||||
if (McLaunchNeedsLegacyFix(instance))
|
||||
{
|
||||
var legacyFixPath = Path.Combine(ModBase.pathPure, "legacyfix.jar");
|
||||
dataList.Add("-javaagent:\"" + legacyFixPath + "\"");
|
||||
|
||||
// Beta 1.6 以前版本需要添加的参数
|
||||
if (instance.releaseTime < new DateTime(2011, 5, 25))
|
||||
{
|
||||
dataList.Add("-Djava.util.Arrays.useLegacyMergeSort=true");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 Java Wrapper 作为主 Jar
|
||||
if (ModBase.IsUtf8CodePage() && !Config.Launch.DisableJlw &&
|
||||
!Config.Instance.DisableJlw[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
{
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion >= 9)
|
||||
dataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED");
|
||||
dataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.pathPure.TrimEnd('\\') + "\"");
|
||||
dataList.Add("-jar \"" + ExtractJavaWrapper() + "\"");
|
||||
}
|
||||
|
||||
// 添加 MainClass
|
||||
if (instance.JsonObject["mainClass"] is null) throw new Exception(Lang.Text("Minecraft.Launch.Error.MissingMainClass"));
|
||||
|
||||
dataList.Add((string)instance.JsonObject["mainClass"]);
|
||||
|
||||
return dataList.Join(" ");
|
||||
}
|
||||
|
||||
private static string McLaunchArgumentsJvmNew(McInstance instance)
|
||||
{
|
||||
var dataList = new List<string>();
|
||||
|
||||
// 获取 Json 中的 DataList
|
||||
var currentInstance = instance;
|
||||
while (true)
|
||||
{
|
||||
if (currentInstance.JsonObject["arguments"] is not null &&
|
||||
currentInstance.JsonObject["arguments"]["jvm"] is not null)
|
||||
foreach (var subJson in currentInstance.JsonObject["arguments"]["jvm"].AsArray())
|
||||
if (subJson.GetValueKind() == JsonValueKind.String)
|
||||
{
|
||||
// 字符串类型
|
||||
dataList.Add(subJson.ToString());
|
||||
}
|
||||
// 非字符串类型
|
||||
else if (ModLibrary.McJsonRuleCheck(subJson["rules"]))
|
||||
{
|
||||
// 满足准则
|
||||
if (subJson["value"].GetValueKind() == JsonValueKind.String)
|
||||
dataList.Add(subJson["value"].ToString());
|
||||
else
|
||||
foreach (var value in subJson["value"].AsArray())
|
||||
dataList.Add(value.ToString());
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(currentInstance.InheritInstanceName))
|
||||
break;
|
||||
|
||||
currentInstance = new McInstance(currentInstance.InheritInstanceName);
|
||||
}
|
||||
|
||||
// 内存、Log4j 防御参数等
|
||||
SecretLaunchJvmArgs(ref dataList);
|
||||
|
||||
// Authlib-Injector
|
||||
if (mcLoginLoader.output.Type == "Auth")
|
||||
{
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion >= 6)
|
||||
dataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252)
|
||||
var server = mcLoginAuthLoader.input.BaseUrl.Replace("/authserver", "");
|
||||
try
|
||||
{
|
||||
var response = ModNet.NetGetCodeByRequestRetry(server, Encoding.UTF8)?.ToString();
|
||||
dataList.Insert(0,
|
||||
"-javaagent:\"" + Path.Combine(ModBase.pathPure, "authlib-injector.jar") + "\"=" + server +
|
||||
" -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" +
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes(response)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServer", server ?? null), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// LWJGL Unsafe Agent
|
||||
if (McLaunchUsesLwjglUnsafeAgent(ModInstanceList.McMcInstanceSelected))
|
||||
{
|
||||
ModBase.Log($"获取到的 LWJGL 版本:{McLaunchGetLwjglVersion(ModInstanceList.McMcInstanceSelected)}");
|
||||
dataList.Insert(0, $"-javaagent:\"{ModBase.pathPure}lwjgl-unsafe-agent.jar\"");
|
||||
}
|
||||
|
||||
if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie])
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017)
|
||||
dataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\"");
|
||||
else
|
||||
dataList.Insert(0,
|
||||
"-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\"");
|
||||
}
|
||||
|
||||
// 渲染器
|
||||
var renderer = 0;
|
||||
var instanceRenderer = Config.Instance.Renderer[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
if (instanceRenderer != 0)
|
||||
renderer = instanceRenderer - 1;
|
||||
else
|
||||
renderer = Config.Launch.Renderer;
|
||||
var mesaLoaderWindowsTargetFile =
|
||||
Path.Combine(ModBase.pathPure, "mesa-loader-windows", mesaLoaderWindowsVersion, "Loader.jar");
|
||||
|
||||
if (renderer != 0)
|
||||
dataList.Insert(0,
|
||||
"-javaagent:\"" + mesaLoaderWindowsTargetFile + "\"=" +
|
||||
(renderer == 1 ? "llvmpipe" : renderer == 2 ? "d3d12" : "zink"));
|
||||
|
||||
// 设置代理
|
||||
if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) &&
|
||||
!string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress))
|
||||
try
|
||||
{
|
||||
var proxyAddress = new Uri(Config.Network.HttpProxy.CustomAddress);
|
||||
dataList.Add(
|
||||
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={proxyAddress.AbsoluteUri}");
|
||||
dataList.Add(
|
||||
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={proxyAddress.Port}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.Proxy"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.Proxy"));
|
||||
}
|
||||
|
||||
// 添加 Java Wrapper 作为主 Jar
|
||||
if (ModBase.IsUtf8CodePage() && !Config.Launch.DisableJlw &&
|
||||
!Config.Instance.DisableJlw[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
{
|
||||
if (mcLaunchJavaSelected.Installation.MajorVersion >= 9)
|
||||
dataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED");
|
||||
dataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.pathPure.TrimEnd('\\') + "\"");
|
||||
dataList.Add("-jar \"" + ExtractJavaWrapper() + "\"");
|
||||
}
|
||||
|
||||
|
||||
// 将 "-XXX" 与后面 "XXX" 合并到一起
|
||||
// 如果不合并,会导致 Forge 1.17 启动无效,它有两个 --add-exports,进一步导致其中一个在后面被去重
|
||||
var deDuplicateDataList = new List<string>();
|
||||
for (int i = 0, loopTo = dataList.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
var currentEntry = dataList[i];
|
||||
if (dataList[i].StartsWithF("-"))
|
||||
while (i < dataList.Count - 1)
|
||||
{
|
||||
if (dataList[i + 1].StartsWithF("-")) break;
|
||||
|
||||
i += 1;
|
||||
currentEntry += " " + dataList[i];
|
||||
}
|
||||
|
||||
deDuplicateDataList.Add(currentEntry.Trim().Replace("McEmu= ", "McEmu="));
|
||||
}
|
||||
|
||||
// #3511 的清理
|
||||
deDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M");
|
||||
|
||||
// 去重
|
||||
var result = deDuplicateDataList.Distinct().ToList().Join(" ");
|
||||
|
||||
// 添加 MainClass
|
||||
if (instance.JsonObject["mainClass"] is null) throw new Exception(Lang.Text("Minecraft.Launch.Error.MissingMainClass"));
|
||||
|
||||
result += " " + instance.JsonObject["mainClass"];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Game 部分(第二段)
|
||||
private static string McLaunchArgumentsGameOld(McInstance version)
|
||||
{
|
||||
var dataList = new List<string>();
|
||||
|
||||
// 本地化 Minecraft 启动信息
|
||||
var basicString = version.JsonObject["minecraftArguments"].ToString();
|
||||
if (!basicString.Contains("--height"))
|
||||
basicString += " --height ${resolution_height} --width ${resolution_width}";
|
||||
dataList.Add(basicString);
|
||||
|
||||
var result = dataList.Join(" ");
|
||||
|
||||
// 特别改变 OptiFineTweaker
|
||||
if ((version.Info.HasForge || version.Info.HasLiteLoader) && version.Info.HasOptiFine)
|
||||
{
|
||||
// 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
|
||||
if (result.Contains("--tweakClass optifine.OptiFineForgeTweaker"))
|
||||
{
|
||||
ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + result);
|
||||
result = result.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "")
|
||||
.Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") +
|
||||
" --tweakClass optifine.OptiFineForgeTweaker";
|
||||
}
|
||||
|
||||
if (result.Contains("--tweakClass optifine.OptiFineTweaker"))
|
||||
{
|
||||
ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + result);
|
||||
result = result.Replace(" --tweakClass optifine.OptiFineTweaker", "")
|
||||
.Replace("--tweakClass optifine.OptiFineTweaker ", "") +
|
||||
" --tweakClass optifine.OptiFineForgeTweaker";
|
||||
try
|
||||
{
|
||||
ModBase.WriteFile(Path.Combine(version.PathInstance, version.Name + ".json"),
|
||||
ModBase.ReadFile(Path.Combine(version.PathInstance, version.Name + ".json"))
|
||||
.Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string McLaunchArgumentsGameNew(McInstance instance)
|
||||
{
|
||||
string mcLaunchArgumentsGameNewRet = default;
|
||||
var dataList = new List<string>();
|
||||
|
||||
// 获取 Json 中的 DataList
|
||||
var currentInstance = instance;
|
||||
while (true)
|
||||
{
|
||||
if (currentInstance.JsonObject["arguments"] is not null &&
|
||||
currentInstance.JsonObject["arguments"]["game"] is not null)
|
||||
foreach (var subJson in currentInstance.JsonObject["arguments"]["game"].AsArray())
|
||||
if (subJson.GetValueKind() == JsonValueKind.String)
|
||||
{
|
||||
// 字符串类型
|
||||
dataList.Add(subJson.ToString());
|
||||
}
|
||||
// 非字符串类型
|
||||
else if (ModLibrary.McJsonRuleCheck(subJson["rules"]))
|
||||
{
|
||||
// 满足准则
|
||||
if (subJson["value"].GetValueKind() == JsonValueKind.String)
|
||||
dataList.Add(subJson["value"].ToString());
|
||||
else
|
||||
foreach (var value in subJson["value"].AsArray())
|
||||
dataList.Add(value.ToString());
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(currentInstance.InheritInstanceName))
|
||||
break;
|
||||
|
||||
currentInstance = new McInstance(currentInstance.InheritInstanceName);
|
||||
}
|
||||
|
||||
// 将 "-XXX" 与后面 "XXX" 合并到一起
|
||||
// 如果不进行合并 Impact 会启动无效,它有两个 --tweakclass
|
||||
var deDuplicateDataList = new List<string>();
|
||||
for (int i = 0, loopTo = dataList.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
var currentEntry = dataList[i];
|
||||
if (dataList[i].StartsWithF("-"))
|
||||
while (i < dataList.Count - 1)
|
||||
{
|
||||
if (dataList[i + 1].StartsWithF("-")) break;
|
||||
|
||||
i += 1;
|
||||
currentEntry += " " + dataList[i];
|
||||
}
|
||||
|
||||
deDuplicateDataList.Add(currentEntry);
|
||||
}
|
||||
|
||||
// 去重
|
||||
mcLaunchArgumentsGameNewRet = deDuplicateDataList.Distinct().ToList().Join(" ");
|
||||
|
||||
// 特别改变 OptiFineTweaker
|
||||
if ((instance.Info.HasForge || instance.Info.HasLiteLoader) && instance.Info.HasOptiFine)
|
||||
{
|
||||
// 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
|
||||
if (mcLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineForgeTweaker"))
|
||||
{
|
||||
ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + mcLaunchArgumentsGameNewRet);
|
||||
mcLaunchArgumentsGameNewRet =
|
||||
mcLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "")
|
||||
.Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") +
|
||||
" --tweakClass optifine.OptiFineForgeTweaker";
|
||||
}
|
||||
|
||||
if (mcLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineTweaker"))
|
||||
{
|
||||
ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + mcLaunchArgumentsGameNewRet);
|
||||
mcLaunchArgumentsGameNewRet =
|
||||
mcLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineTweaker", "")
|
||||
.Replace("--tweakClass optifine.OptiFineTweaker ", "") +
|
||||
" --tweakClass optifine.OptiFineForgeTweaker";
|
||||
try
|
||||
{
|
||||
ModBase.WriteFile(Path.Combine(instance.PathInstance, instance.Name + ".json"),
|
||||
ModBase.ReadFile(Path.Combine(instance.PathInstance, instance.Name + ".json"))
|
||||
.Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mcLaunchArgumentsGameNewRet;
|
||||
}
|
||||
|
||||
// 替换 Arguments
|
||||
private static Dictionary<string, string> McLaunchArgumentsReplace(McInstance instance,
|
||||
ref ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>> loader)
|
||||
{
|
||||
var gameArguments = new Dictionary<string, string>();
|
||||
|
||||
// 基础参数
|
||||
gameArguments.Add("${classpath_separator}", ";");
|
||||
gameArguments.Add("${natives_directory}", ModBase.ShortenPath(GetNativesFolder()));
|
||||
gameArguments.Add("${library_directory}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "libraries"));
|
||||
gameArguments.Add("${libraries_directory}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "libraries"));
|
||||
gameArguments.Add("${launcher_name}", "PCLCE");
|
||||
gameArguments.Add("${launcher_version}", ModBase.versionCode.ToString());
|
||||
gameArguments.Add("${version_name}", instance.Name);
|
||||
var argumentInfo = Config.Instance.TypeInfo[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
gameArguments.Add("${version_type}",
|
||||
string.IsNullOrEmpty(argumentInfo)
|
||||
? Config.Launch.TypeInfo
|
||||
: argumentInfo);
|
||||
gameArguments.Add("${game_directory}",
|
||||
ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie[..^1]));
|
||||
gameArguments.Add("${assets_root}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "assets"));
|
||||
gameArguments.Add("${user_properties}", "{}");
|
||||
gameArguments.Add("${auth_player_name}", mcLoginLoader.output.Name);
|
||||
gameArguments.Add("${auth_uuid}", mcLoginLoader.output.Uuid);
|
||||
gameArguments.Add("${auth_access_token}", mcLoginLoader.output.AccessToken);
|
||||
gameArguments.Add("${access_token}", mcLoginLoader.output.AccessToken);
|
||||
gameArguments.Add("${auth_session}", mcLoginLoader.output.AccessToken);
|
||||
gameArguments.Add("${user_type}", "msa"); // #1221
|
||||
|
||||
// 窗口尺寸参数
|
||||
Size gameSize;
|
||||
switch (Config.Launch.GameWindowMode)
|
||||
{
|
||||
case GameWindowSizeMode.Launcher: // 与启动器尺寸一致
|
||||
{
|
||||
Size result;
|
||||
ModBase.RunInUiWait(() => result = new Size(ModBase.GetPixelSize(ModMain.frmMain.PanForm.ActualWidth),
|
||||
ModBase.GetPixelSize(ModMain.frmMain.PanForm.ActualHeight)));
|
||||
gameSize = result;
|
||||
gameSize.Height -= 29.5d * ModBase.dpi / 96d; // 标题栏高度
|
||||
break;
|
||||
}
|
||||
case GameWindowSizeMode.Custom: // 自定义
|
||||
{
|
||||
gameSize = new Size(Math.Max(100, (double)Config.Launch.GameWindowWidth),
|
||||
Math.Max(100, (double)Config.Launch.GameWindowHeight));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
gameSize = new Size(854d, 480d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.Drop <= 120 && mcLaunchJavaSelected.Installation.MajorVersion <= 8 &&
|
||||
mcLaunchJavaSelected.Installation.Version.Revision >= 200 &&
|
||||
mcLaunchJavaSelected.Installation.Version.Revision <= 321 &&
|
||||
!ModInstanceList.McMcInstanceSelected.Info.HasOptiFine && !ModInstanceList.McMcInstanceSelected.Info.HasForge)
|
||||
{
|
||||
// 修复 #3463:1.12.2-,JRE 8u200~321 下窗口大小为设置大小的 DPI% 倍
|
||||
McLaunchLog($"已应用窗口大小过大修复({mcLaunchJavaSelected.Installation.Version.Revision})");
|
||||
gameSize.Width /= ModBase.dpi / 96d;
|
||||
gameSize.Height /= ModBase.dpi / 96d;
|
||||
}
|
||||
|
||||
gameArguments.Add("${resolution_width}", Math.Round(gameSize.Width).ToString(CultureInfo.InvariantCulture));
|
||||
gameArguments.Add("${resolution_height}", Math.Round(gameSize.Height).ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
// Assets 相关参数
|
||||
gameArguments.Add("${game_assets}",
|
||||
ModBase.ShortenPath(ModFolder.mcFolderSelected +
|
||||
@"assets\virtual\legacy")); // 1.5.2 的 pre-1.6 资源索引应与 legacy 合并
|
||||
gameArguments.Add("${assets_index_name}", ModAssets.McAssetsGetIndexName(instance));
|
||||
|
||||
// 支持库参数
|
||||
var libList = ModLibrary.McLibListGet(instance, true);
|
||||
loader.output = libList;
|
||||
var cpStrings = new List<string>();
|
||||
string optiFineCp = null;
|
||||
|
||||
// LegacyFix 释放
|
||||
if (McLaunchNeedsLegacyFix(instance))
|
||||
{
|
||||
var legacyFixPath = Path.Combine(ModBase.pathPure, "legacyfix.jar");
|
||||
try
|
||||
{
|
||||
ModBase.WriteFile(legacyFixPath, ModBase.GetResourceStream("Resources/legacyfix.jar"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "LegacyFix 释放失败");
|
||||
}
|
||||
}
|
||||
|
||||
// LWJGL Unsafe Agent 释放
|
||||
if (McLaunchUsesLwjglUnsafeAgent(instance))
|
||||
{
|
||||
string agentPath = Path.Combine(ModBase.pathPure, "lwjgl-unsafe-agent.jar");
|
||||
try
|
||||
{
|
||||
ModBase.WriteFile(agentPath, ModBase.GetResourceStream("Resources/lwjgl-unsafe-agent.jar"));
|
||||
cpStrings.Add(agentPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "LWJGL Unsafe Agent 释放失败");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var library in libList)
|
||||
{
|
||||
if (library.IsNatives)
|
||||
continue;
|
||||
if (ModInstanceList.McMcInstanceSelected.Info.HasCleanroom
|
||||
&& library.OriginalName is not null
|
||||
&& (library.OriginalName.Contains("org.lwjgl.lwjgl:lwjgl:2.9.4")
|
||||
|| library.OriginalName.Contains("net.java.dev.jna:platform:3.4.0")
|
||||
|| library.OriginalName.Contains("com.ibm.icu:icu4j-core-mojang:51.2")))
|
||||
continue;
|
||||
if (library.Name is not null && library.Name == "optifine:OptiFine")
|
||||
optiFineCp = library.LocalPath;
|
||||
else
|
||||
cpStrings.Add(library.LocalPath);
|
||||
}
|
||||
|
||||
foreach (var library in Config.Instance.ClasspathHead[instance.PathInstance].Split(";")) // 自定义 Classpath 头部
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(library))
|
||||
continue;
|
||||
cpStrings.Insert(0, library);
|
||||
}
|
||||
|
||||
if (optiFineCp is not null)
|
||||
cpStrings.Insert(cpStrings.Count - 2, optiFineCp); // OptiFine 的总是需要放到倒数第二位
|
||||
gameArguments.Add("${classpath}", cpStrings.Select(c => ModBase.ShortenPath(c)).Join(";"));
|
||||
|
||||
return gameArguments;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 解压 Natives
|
||||
|
||||
private static void McLaunchNatives(ModLoader.LoaderTask<List<ModLibrary.McLibToken>, int> loader)
|
||||
{
|
||||
// 创建文件夹
|
||||
var target = GetNativesFolder() + @"\";
|
||||
Directory.CreateDirectory(target);
|
||||
|
||||
// 解压文件
|
||||
McLaunchLog("正在解压 Natives 文件");
|
||||
var existFiles = new List<string>();
|
||||
foreach (var native in loader.input)
|
||||
{
|
||||
if (!native.IsNatives)
|
||||
continue;
|
||||
ZipArchive zip;
|
||||
try
|
||||
{
|
||||
zip = new ZipArchive(new FileStream(native.LocalPath, FileMode.Open));
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
ModBase.Log(ex, "打开 Natives 文件失败(" + native.LocalPath + ")");
|
||||
File.Delete(native.LocalPath);
|
||||
throw new Exception(Lang.Text("Minecraft.Launch.Error.NativesCorrupted", native.LocalPath));
|
||||
}
|
||||
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
var fileName = entry.FullName;
|
||||
if (fileName.EndsWithF(".dll", true))
|
||||
{
|
||||
// 实际解压文件的步骤
|
||||
var filePath = target + fileName;
|
||||
existFiles.Add(filePath);
|
||||
var originalFile = new FileInfo(filePath);
|
||||
if (originalFile.Exists)
|
||||
{
|
||||
if (originalFile.Length == entry.Length)
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
McLaunchLog("无需解压:" + filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 删除原文件
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
McLaunchLog("删除原 dll 访问被拒绝,这通常代表有一个 MC 正在运行,跳过解压:" + filePath);
|
||||
McLaunchLog("实际的错误信息:" + ex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 解压新文件
|
||||
ModBase.WriteFile(filePath, entry.Open());
|
||||
McLaunchLog("已解压:" + filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (zip is not null)
|
||||
zip.Dispose();
|
||||
}
|
||||
|
||||
// 删除多余文件
|
||||
foreach (var fileName in Directory.GetFiles(target))
|
||||
{
|
||||
if (existFiles.Contains(fileName))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
McLaunchLog("删除:" + fileName);
|
||||
File.Delete(fileName);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
McLaunchLog("删除多余文件访问被拒绝,跳过删除步骤");
|
||||
McLaunchLog("实际的错误信息:" + ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Natives 文件夹路径,不以 \ 结尾。
|
||||
/// </summary>
|
||||
private static string GetNativesFolder()
|
||||
{
|
||||
var result = Path.Combine(ModInstanceList.McMcInstanceSelected.PathInstance, ModInstanceList.McMcInstanceSelected.Name + "-natives");
|
||||
if (SystemInfo.IsGBKEncoding || result.IsASCII())
|
||||
return result;
|
||||
result = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".minecraft", "bin", "natives");
|
||||
if (result.IsASCII())
|
||||
return result;
|
||||
return Path.Combine(SystemPaths.DriveLetter, "ProgramData", "PCL", "natives");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动与前后处理
|
||||
|
||||
private static void McLaunchPrerun()
|
||||
{
|
||||
// 要求 Java 使用高性能显卡
|
||||
var javaExePath = mcLaunchJavaSelected.Installation.JavawExePath ??
|
||||
mcLaunchJavaSelected.Installation.JavaExePath;
|
||||
try
|
||||
{
|
||||
ModMain.SetGPUPreference(javaExePath, Config.Launch.SetGpuPreference);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ProcessInterop.IsAdmin() || !Config.Launch.SetGpuPreference)
|
||||
{
|
||||
ModBase.Log(ex, "直接调整显卡设置失败");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log(ex, "直接调整显卡设置失败,将以管理员权限重启 PCL 再次尝试");
|
||||
try
|
||||
{
|
||||
if (ProcessInterop.StartAsAdmin($"--gpu \"{javaExePath}\"").ExitCode ==
|
||||
(int)ModBase.ProcessReturnValues.TaskDone)
|
||||
McLaunchLog("以管理员权限重启 PCL 并调整显卡设置成功");
|
||||
else
|
||||
throw new Exception("调整过程中出现异常");
|
||||
}
|
||||
catch (Exception exx)
|
||||
{
|
||||
ModBase.Log(
|
||||
exx,
|
||||
Lang.Text("Minecraft.Launch.Error.GpuSet"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.GpuSet"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 launcher_profiles.json
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保可用
|
||||
if (mcLoginLoader.output.Type != "Microsoft")
|
||||
break;
|
||||
ModFolder.McFolderLauncherProfilesJsonCreate(ModFolder.mcFolderSelected);
|
||||
// 构建需要替换的 Json 对象
|
||||
var replaceJsonString = @"
|
||||
{
|
||||
""authenticationDatabase"": {
|
||||
""00000111112222233333444445555566"": {
|
||||
""username"": """ + mcLoginLoader.output.Name.Replace("\"", "-") + @""",
|
||||
""profiles"": {
|
||||
""66666555554444433333222221111100"": {
|
||||
""displayName"": """ + mcLoginLoader.output.Name + @"""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
""clientToken"": """ + mcLoginLoader.output.ClientToken + @""",
|
||||
""selectedUser"": {
|
||||
""account"": ""00000111112222233333444445555566"",
|
||||
""profile"": ""66666555554444433333222221111100""
|
||||
}
|
||||
}";
|
||||
var replaceJson = (JsonObject)ModBase.GetJson(replaceJsonString);
|
||||
// 更新文件
|
||||
var profiles =
|
||||
(JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(ModFolder.mcFolderSelected + "launcher_profiles.json"));
|
||||
profiles.Merge(replaceJson);
|
||||
ModBase.WriteFile(ModFolder.mcFolderSelected + "launcher_profiles.json", profiles.ToString(),
|
||||
encoding: Encoding.GetEncoding("GB18030"));
|
||||
McLaunchLog("已更新 launcher_profiles.json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试");
|
||||
try
|
||||
{
|
||||
File.Delete(ModFolder.mcFolderSelected + "launcher_profiles.json");
|
||||
ModFolder.McFolderLauncherProfilesJsonCreate(ModFolder.mcFolderSelected);
|
||||
// 构建需要替换的 Json 对象
|
||||
var replaceJsonString = @"
|
||||
{
|
||||
""authenticationDatabase"": {
|
||||
""00000111112222233333444445555566"": {
|
||||
""username"": """ + mcLoginLoader.output.Name.Replace("\"", "-") + @""",
|
||||
""profiles"": {
|
||||
""66666555554444433333222221111100"": {
|
||||
""displayName"": """ + mcLoginLoader.output.Name + @"""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
""clientToken"": """ + mcLoginLoader.output.ClientToken + @""",
|
||||
""selectedUser"": {
|
||||
""account"": ""00000111112222233333444445555566"",
|
||||
""profile"": ""66666555554444433333222221111100""
|
||||
}
|
||||
}";
|
||||
var replaceJson = (JsonObject)ModBase.GetJson(replaceJsonString);
|
||||
// 更新文件
|
||||
var profiles =
|
||||
(JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(ModFolder.mcFolderSelected + "launcher_profiles.json"));
|
||||
profiles.Merge(replaceJson);
|
||||
ModBase.WriteFile(ModFolder.mcFolderSelected + "launcher_profiles.json", profiles.ToString(),
|
||||
encoding: Encoding.GetEncoding("GB18030"));
|
||||
McLaunchLog("已在删除后更新 launcher_profiles.json");
|
||||
}
|
||||
catch (Exception exx)
|
||||
{
|
||||
ModBase.Log(
|
||||
exx,
|
||||
"更新 launcher_profiles.json 失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.UpdateProfilesFailed"));
|
||||
}
|
||||
}
|
||||
} while (false);
|
||||
|
||||
// 更新 options.txt
|
||||
var setupFileAddress = Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "options.txt");
|
||||
|
||||
// 辅助切换游戏语言
|
||||
if (Config.Tool.AutoChangeLanguage)
|
||||
{
|
||||
if (!File.Exists(setupFileAddress))
|
||||
{
|
||||
// Yosbr Mod 兼容(#2385):https://www.curseforge.com/minecraft/mc-mods/yosbr
|
||||
var yosbrFileAddress = Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "config", "yosbr", "options.txt");
|
||||
if (File.Exists(yosbrFileAddress))
|
||||
{
|
||||
McLaunchLog("将修改 Yosbr Mod 中的 options.txt");
|
||||
setupFileAddress = yosbrFileAddress;
|
||||
ModBase.WriteIni(setupFileAddress, "lang", "none"); // 忽略默认语言
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 语言
|
||||
// 1.0- :没有语言选项
|
||||
// 1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
|
||||
// 1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
|
||||
// 1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
|
||||
// 1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
|
||||
var currentLang = ModBase.ReadIni(setupFileAddress, "lang", "none");
|
||||
var isLanguageUnconfigured = string.Equals(currentLang, "none", StringComparison.OrdinalIgnoreCase);
|
||||
var hasExistingSaves = Directory.Exists(Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "saves"));
|
||||
var shouldUseDefault = isLanguageUnconfigured || !hasExistingSaves;
|
||||
var requiredLang = _ResolveMinecraftLanguage(currentLang, shouldUseDefault,
|
||||
ModInstanceList.McMcInstanceSelected.releaseTime);
|
||||
|
||||
if (currentLang == requiredLang)
|
||||
{
|
||||
McLaunchLog($"需要的语言为 {requiredLang},当前语言为 {currentLang},无需修改");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.WriteIni(setupFileAddress, "lang", "-"); // 触发缓存更改,避免删除后重新下载残留缓存
|
||||
ModBase.WriteIni(setupFileAddress, "lang", requiredLang);
|
||||
McLaunchLog($"已将语言从 {currentLang} 修改为 {requiredLang}");
|
||||
}
|
||||
|
||||
// 如果是初次设置,一并按启动器语言需要修改 forceUnicodeFont,确保 CJK 字符正常显示
|
||||
if ((isLanguageUnconfigured || !hasExistingSaves) && _ShouldEnableForceUnicodeFont())
|
||||
{
|
||||
ModBase.WriteIni(setupFileAddress, "forceUnicodeFont", "true");
|
||||
McLaunchLog("已开启 forceUnicodeFont,确保当前启动器语言字体正常显示");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"更新 options.txt 失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.UpdateOptionsFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口
|
||||
switch (Config.Launch.GameWindowMode)
|
||||
{
|
||||
case GameWindowSizeMode.Fullscreen: // 全屏
|
||||
{
|
||||
ModBase.WriteIni(setupFileAddress, "fullscreen", "true");
|
||||
break;
|
||||
}
|
||||
case GameWindowSizeMode.Default: // 默认
|
||||
// 其他
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ModBase.WriteIni(setupFileAddress, "fullscreen", "false");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string _ResolveMinecraftLanguage(string? currentLanguage, bool shouldUseLauncherLanguage,
|
||||
DateTime? mcReleaseTime)
|
||||
{
|
||||
if (_IsMinecraftVersionUnder1Dot1(mcReleaseTime)) return "none";
|
||||
|
||||
var useLegacyRegionCase = _ShouldUseLegacyMinecraftLanguageCode(mcReleaseTime);
|
||||
var languageCode = shouldUseLauncherLanguage
|
||||
? LocalizationService.CurrentLanguage.Code
|
||||
: currentLanguage;
|
||||
return _NormalizeMinecraftLanguageCode(languageCode, useLegacyRegionCase);
|
||||
}
|
||||
|
||||
private static string _NormalizeMinecraftLanguageCode(string? languageCode, bool useLegacyRegionCase)
|
||||
{
|
||||
var normalizedCode = string.IsNullOrWhiteSpace(languageCode)
|
||||
? "none"
|
||||
: languageCode.Replace('-', '_').Trim();
|
||||
if (string.Equals(normalizedCode, "none", StringComparison.OrdinalIgnoreCase)) return "none";
|
||||
|
||||
var segments = normalizedCode.Split('_', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length < 2) return normalizedCode.ToLowerInvariant();
|
||||
|
||||
var language = segments[0].ToLowerInvariant();
|
||||
var region = useLegacyRegionCase ? segments[1].ToUpperInvariant() : segments[1].ToLowerInvariant();
|
||||
return $"{language}_{region}";
|
||||
}
|
||||
|
||||
private static bool _IsMinecraftVersionUnder1Dot1(DateTime? releaseTime)
|
||||
{
|
||||
return releaseTime.HasValue &&
|
||||
releaseTime.Value > new DateTime(2000, 1, 1) &&
|
||||
releaseTime.Value <= new DateTime(2011, 11, 18);
|
||||
}
|
||||
|
||||
private static bool _ShouldUseLegacyMinecraftLanguageCode(DateTime? releaseTime)
|
||||
{
|
||||
return releaseTime.HasValue &&
|
||||
releaseTime.Value >= new DateTime(2012, 1, 12) &&
|
||||
releaseTime.Value <= new DateTime(2016, 6, 8);
|
||||
}
|
||||
|
||||
private static bool _ShouldEnableForceUnicodeFont()
|
||||
{
|
||||
return LocalizationService.CurrentLanguage.FontProfile is LocalizationFontProfile.SimplifiedChinese
|
||||
or LocalizationFontProfile.TraditionalChinese
|
||||
or LocalizationFontProfile.Japanese
|
||||
or LocalizationFontProfile.Korean;
|
||||
}
|
||||
|
||||
private static void McLaunchCustom(ModLoader.LoaderTask<int, int> loader)
|
||||
{
|
||||
// 获取自定义命令
|
||||
var customCommandGlobal = Config.Launch.PreLaunchCommand;
|
||||
if (!string.IsNullOrEmpty(customCommandGlobal))
|
||||
customCommandGlobal = ArgumentReplace(customCommandGlobal, true);
|
||||
var customCommandVersion = Config.Instance.PreLaunchCommand[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
if (!string.IsNullOrEmpty(customCommandVersion))
|
||||
customCommandVersion = ArgumentReplace(customCommandVersion, true);
|
||||
|
||||
// 输出 bat
|
||||
try
|
||||
{
|
||||
var cmdString =
|
||||
$"{(mcLaunchJavaSelected.Installation.MajorVersion > 8 ? "chcp 65001>nul" + "\r\n" : "")}" +
|
||||
"@echo off" + "\r\n" + $"title 启动 - {ModInstanceList.McMcInstanceSelected.Name}" +
|
||||
"\r\n" + "echo 游戏正在启动,请稍候。" + "\r\n" +
|
||||
$"cd /D \"{ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie)}\"" + "\r\n" +
|
||||
customCommandGlobal + "\r\n" + customCommandVersion + "\r\n" +
|
||||
$"\"{mcLaunchJavaSelected.Installation.JavaExePath}\" {mcLaunchArgument}" + "\r\n" +
|
||||
"echo 游戏已退出。" + "\r\n" + "pause";
|
||||
ModBase.WriteFile(currentLaunchOptions.SaveBatch ?? ModBase.exePath + @"PCL\LatestLaunch.bat",
|
||||
McLogFilter.FilterAccessToken(cmdString, 'F'),
|
||||
encoding: mcLaunchJavaSelected.Installation.MajorVersion > 8 ? Encoding.UTF8 : Encoding.Default);
|
||||
if (currentLaunchOptions.SaveBatch is not null)
|
||||
{
|
||||
McLaunchLog("导出启动脚本完成,强制结束启动过程");
|
||||
abortHint = Lang.Text("Minecraft.Launch.ExportScript.Success");
|
||||
ModBase.OpenExplorer(currentLaunchOptions.SaveBatch);
|
||||
loader.parent.Abort();
|
||||
return; // 导出脚本完成
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "输出启动脚本失败");
|
||||
if (currentLaunchOptions.SaveBatch is not null)
|
||||
throw; // 直接触发启动失败
|
||||
}
|
||||
|
||||
// 执行自定义命令
|
||||
if (!string.IsNullOrEmpty(customCommandGlobal))
|
||||
{
|
||||
McLaunchLog("正在执行全局自定义命令:" + customCommandGlobal);
|
||||
var customProcess = new Process();
|
||||
try
|
||||
{
|
||||
customProcess.StartInfo.FileName = "cmd.exe";
|
||||
customProcess.StartInfo.Arguments = "/c \"" + customCommandGlobal + "\"";
|
||||
customProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModFolder.mcFolderSelected);
|
||||
customProcess.StartInfo.UseShellExecute = false;
|
||||
customProcess.StartInfo.CreateNoWindow = true;
|
||||
customProcess.Start();
|
||||
if (Config.Launch.PreLaunchCommandWait)
|
||||
while (!customProcess.HasExited && !loader.IsAborted)
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.CustomCommand"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.CustomCommand"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!customProcess.HasExited && loader.IsAborted)
|
||||
{
|
||||
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183
|
||||
customProcess.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(customCommandVersion))
|
||||
{
|
||||
McLaunchLog("正在执行实例自定义命令:" + customCommandVersion);
|
||||
var customProcess = new Process();
|
||||
try
|
||||
{
|
||||
customProcess.StartInfo.FileName = "cmd.exe";
|
||||
customProcess.StartInfo.Arguments = "/c \"" + customCommandVersion + "\"";
|
||||
customProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModFolder.mcFolderSelected);
|
||||
customProcess.StartInfo.UseShellExecute = false;
|
||||
customProcess.StartInfo.CreateNoWindow = true;
|
||||
customProcess.Start();
|
||||
if (Config.Instance.PreLaunchCommandWait[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
while (!customProcess.HasExited && !loader.IsAborted)
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.CustomCommand"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.CustomCommand"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!customProcess.HasExited && loader.IsAborted)
|
||||
{
|
||||
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183
|
||||
customProcess.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void McLaunchRun(ModLoader.LoaderTask<int, Process> loader)
|
||||
{
|
||||
var noJavaw = Config.Launch.NoJavaw &&
|
||||
mcLaunchJavaSelected.Installation.JavawExePath is not null;
|
||||
|
||||
// 启动信息
|
||||
var gameProcess = new Process();
|
||||
var startInfo = new ProcessStartInfo(noJavaw
|
||||
? mcLaunchJavaSelected.Installation.JavaExePath
|
||||
: mcLaunchJavaSelected.Installation.JavawExePath);
|
||||
|
||||
// 设置环境变量
|
||||
var paths = new List<string>(startInfo.EnvironmentVariables["Path"].Split(";"));
|
||||
paths.Add(ModBase.ShortenPath(mcLaunchJavaSelected.Installation.JavaFolder));
|
||||
startInfo.EnvironmentVariables["Path"] = paths.Distinct().ToList().Join(";");
|
||||
startInfo.EnvironmentVariables["appdata"] = ModBase.ShortenPath(ModFolder.mcFolderSelected);
|
||||
|
||||
// 设置其他参数
|
||||
startInfo.WorkingDirectory = ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie);
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
startInfo.CreateNoWindow = noJavaw;
|
||||
startInfo.Arguments = mcLaunchArgument;
|
||||
gameProcess.StartInfo = startInfo;
|
||||
|
||||
// 开始进程
|
||||
gameProcess.Start();
|
||||
McLaunchLog("已启动游戏进程:" + startInfo.FileName);
|
||||
if (loader.IsAborted)
|
||||
{
|
||||
McLaunchLog("由于取消启动,已强制结束游戏进程"); // #1631
|
||||
gameProcess.Kill();
|
||||
return;
|
||||
}
|
||||
|
||||
loader.output = gameProcess;
|
||||
mcLaunchProcess = gameProcess;
|
||||
// 进程优先级处理
|
||||
try
|
||||
{
|
||||
gameProcess.PriorityBoostEnabled = true;
|
||||
switch (Config.Launch.ProcessPriority)
|
||||
{
|
||||
case GameProcessPriority.RealTime: // 实时
|
||||
{
|
||||
gameProcess.PriorityClass = ProcessPriorityClass.RealTime;
|
||||
break;
|
||||
}
|
||||
case GameProcessPriority.High: // 极高
|
||||
{
|
||||
gameProcess.PriorityClass = ProcessPriorityClass.High;
|
||||
break;
|
||||
}
|
||||
case GameProcessPriority.AboveNormal: // 高
|
||||
{
|
||||
gameProcess.PriorityClass = ProcessPriorityClass.AboveNormal;
|
||||
break;
|
||||
}
|
||||
case GameProcessPriority.BelowNormal: // 低
|
||||
{
|
||||
gameProcess.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.PrioritySet"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.PrioritySet"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void McLaunchWait(ModLoader.LoaderTask<Process, int> loader)
|
||||
{
|
||||
// 输出信息
|
||||
McLaunchLog("");
|
||||
McLaunchLog("~ 基础参数 ~");
|
||||
McLaunchLog("PCL 版本:" + ModBase.versionBaseName + " (" + ModBase.versionCode + ")");
|
||||
McLaunchLog(
|
||||
$"游戏版本:{ModInstanceList.McMcInstanceSelected.Info.VanillaName}({ModInstanceList.McMcInstanceSelected.Info.vanilla},Drop {ModInstanceList.McMcInstanceSelected.Info.Drop}{(ModInstanceList.McMcInstanceSelected.Info.Reliable ? "" : ",无法完全确定")})");
|
||||
McLaunchLog("资源版本:" + ModAssets.McAssetsGetIndexName(ModInstanceList.McMcInstanceSelected));
|
||||
McLaunchLog("实例继承:" + (string.IsNullOrEmpty(ModInstanceList.McMcInstanceSelected.InheritInstanceName)
|
||||
? "无"
|
||||
: ModInstanceList.McMcInstanceSelected.InheritInstanceName));
|
||||
var launchRamGb = PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
|
||||
!mcLaunchJavaSelected.Installation.Is64Bit);
|
||||
McLaunchLog("分配的内存:" +
|
||||
launchRamGb.ToString("N1", CultureInfo.InvariantCulture) + " GiB(" +
|
||||
Math.Round(launchRamGb * 1024d).ToString("N0", CultureInfo.InvariantCulture) + " MiB)");
|
||||
McLaunchLog("MC 文件夹:" + ModFolder.mcFolderSelected);
|
||||
McLaunchLog("实例文件夹:" + ModInstanceList.McMcInstanceSelected.PathInstance);
|
||||
McLaunchLog("版本隔离:" + ((ModInstanceList.McMcInstanceSelected.PathIndie ?? "") ==
|
||||
(ModInstanceList.McMcInstanceSelected.PathInstance ?? "")));
|
||||
McLaunchLog("HMCL 格式:" + ModInstanceList.McMcInstanceSelected.IsHmclFormatJson);
|
||||
McLaunchLog("Java 信息:" + mcLaunchJavaSelected.Installation);
|
||||
// McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
|
||||
McLaunchLog("Natives 文件夹:" + GetNativesFolder());
|
||||
McLaunchLog("");
|
||||
McLaunchLog("~ 档案参数 ~");
|
||||
McLaunchLog("玩家用户名:" + mcLoginLoader.output.Name);
|
||||
McLaunchLog("AccessToken:" + mcLoginLoader.output.AccessToken);
|
||||
McLaunchLog("ClientToken:" + mcLoginLoader.output.ClientToken);
|
||||
McLaunchLog("UUID:" + mcLoginLoader.output.Uuid);
|
||||
McLaunchLog("验证方式:" + mcLoginLoader.output.Type);
|
||||
McLaunchLog("");
|
||||
|
||||
// 获取窗口标题
|
||||
var windowTitle = Config.Instance.Title[ModInstanceList.McMcInstanceSelected?.PathInstance];
|
||||
if (string.IsNullOrEmpty(windowTitle) &&
|
||||
!Config.Instance.UseGlobalTitle[ModInstanceList.McMcInstanceSelected?.PathInstance])
|
||||
windowTitle = Config.Launch.Title;
|
||||
windowTitle = ArgumentReplace(windowTitle, false);
|
||||
|
||||
// JStack 路径
|
||||
var jStackPath = Path.Combine(mcLaunchJavaSelected.Installation.JavaFolder, "jstack.exe");
|
||||
|
||||
// 初始化等待
|
||||
var watcher = new ModWatcher.Watcher(loader, ModInstanceList.McMcInstanceSelected, windowTitle,
|
||||
File.Exists(jStackPath) ? jStackPath : "", currentLaunchOptions.IsTest);
|
||||
mcLaunchWatcher = watcher;
|
||||
|
||||
// 显示实时日志
|
||||
if (currentLaunchOptions.IsTest)
|
||||
{
|
||||
if (ModMain.frmLogLeft is null)
|
||||
ModBase.RunInUiWait(() => ModMain.frmLogLeft = new PageLogLeft());
|
||||
if (ModMain.frmLogRight is null)
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
ModMain.frmLogRight = new PageLogRight();
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
});
|
||||
ModMain.frmLogLeft.Add(watcher);
|
||||
McLaunchLog("已显示游戏实时日志");
|
||||
}
|
||||
|
||||
// 等待
|
||||
while (watcher.State == ModWatcher.Watcher.MinecraftState.Loading)
|
||||
Thread.Sleep(100);
|
||||
if (watcher.State == ModWatcher.Watcher.MinecraftState.Crashed) throw new Exception("$$");
|
||||
}
|
||||
|
||||
private static void McLaunchEnd()
|
||||
{
|
||||
McLaunchLog("开始启动结束处理");
|
||||
|
||||
// 暂停或开始音乐播放
|
||||
if (Config.Preference.Music.StopInGame)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在启动后暂停音乐播放");
|
||||
});
|
||||
else if (Config.Preference.Music.StartInGame)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在启动后开始音乐播放");
|
||||
});
|
||||
// 暂停视频背景播放
|
||||
ModVideoBack.IsGaming = true;
|
||||
ModVideoBack.VideoPause();
|
||||
// 启动器可见性
|
||||
McLaunchLog(
|
||||
"启动器可见性:" + Config.Launch.LauncherVisibility);
|
||||
switch (Config.Launch.LauncherVisibility)
|
||||
{
|
||||
case LauncherVisibility.ExitImmediately:
|
||||
{
|
||||
// 直接关闭
|
||||
McLaunchLog("已根据设置,在启动后关闭启动器");
|
||||
ModBase.RunInUi(() => ModMain.frmMain.EndProgram(false));
|
||||
break;
|
||||
}
|
||||
case LauncherVisibility.HideAndExit:
|
||||
case LauncherVisibility.HideAndReopen:
|
||||
{
|
||||
// 隐藏
|
||||
McLaunchLog("已根据设置,在启动后隐藏启动器");
|
||||
ModBase.RunInUi(() => ModMain.frmMain.Hidden = true);
|
||||
break;
|
||||
}
|
||||
case LauncherVisibility.MinimizeAndReopen:
|
||||
{
|
||||
// 最小化
|
||||
McLaunchLog("已根据设置,在启动后最小化启动器");
|
||||
ModBase.RunInUi(() => ModMain.frmMain.WindowState = WindowState.Minimized);
|
||||
break;
|
||||
}
|
||||
case LauncherVisibility.DoNothing:
|
||||
{
|
||||
break;
|
||||
}
|
||||
// 啥都不干
|
||||
}
|
||||
|
||||
// 启动计数
|
||||
States.System.LaunchCount += 1;
|
||||
|
||||
States.Instance.LaunchCount[ModInstanceList.McMcInstanceSelected.PathInstance] =
|
||||
States.Instance.LaunchCount[ModInstanceList.McMcInstanceSelected.PathInstance] + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
|
||||
/// </summary>
|
||||
private static string ArgumentReplace(string text, bool replaceTime, Func<string, string> escapeHandler = null)
|
||||
{
|
||||
// 预处理
|
||||
if (text is null)
|
||||
return null;
|
||||
|
||||
string replacer(string s)
|
||||
{
|
||||
if (s is null)
|
||||
return "";
|
||||
if (escapeHandler is null)
|
||||
return s;
|
||||
if (s.Contains(@":\"))
|
||||
s = ModBase.ShortenPath(s);
|
||||
return escapeHandler(s);
|
||||
}
|
||||
|
||||
;
|
||||
// 基础
|
||||
text = text.Replace("{pcl_version}", replacer(ModBase.versionBaseName));
|
||||
text = text.Replace("{pcl_version_code}", replacer(ModBase.versionCode.ToString()));
|
||||
text = text.Replace("{pcl_version_branch}", replacer(ModBase.versionBranchName));
|
||||
text = text.Replace("{identify}", replacer(Identify.LauncherId));
|
||||
text = text.Replace("{path}", replacer(Basics.CurrentDirectory));
|
||||
text = text.Replace("{path_with_name}", replacer(Basics.ExecutablePath));
|
||||
text = text.Replace("{path_temp}", replacer(ModBase.pathTemp));
|
||||
// 时间
|
||||
if (replaceTime) // 在窗口标题中,时间会被后续动态替换,所以此时不应该替换
|
||||
{
|
||||
text = text.Replace("{date}", replacer(Lang.Date(DateTime.Now, "d")));
|
||||
text = text.Replace("{time}", replacer(Lang.Date(DateTime.Now, "T")));
|
||||
}
|
||||
|
||||
// Minecraft
|
||||
text = text.Replace("{java}", replacer(mcLaunchJavaSelected?.Installation.JavaFolder));
|
||||
text = text.Replace("{minecraft}", replacer(ModFolder.mcFolderSelected));
|
||||
if (ModInstanceList.McMcInstanceSelected?.IsLoaded == true)
|
||||
{
|
||||
text = text.Replace("{version_path}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance));
|
||||
text = text.Replace("{verpath}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance));
|
||||
text = text.Replace("{version_indie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie));
|
||||
text = text.Replace("{verindie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie));
|
||||
text = text.Replace("{name}", replacer(ModInstanceList.McMcInstanceSelected.Name));
|
||||
if (new[] { "unknown", "old", "pending" }.Contains(
|
||||
ModInstanceList.McMcInstanceSelected.Info.VanillaName.ToLower()))
|
||||
text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Name));
|
||||
else
|
||||
text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Info.VanillaName));
|
||||
}
|
||||
else
|
||||
{
|
||||
text = text.Replace("{version_path}", replacer(null));
|
||||
text = text.Replace("{verpath}", replacer(null));
|
||||
text = text.Replace("{version_indie}", replacer(null));
|
||||
text = text.Replace("{verindie}", replacer(null));
|
||||
text = text.Replace("{name}", replacer(null));
|
||||
text = text.Replace("{version}", replacer(null));
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
if (mcLoginLoader.State == ModBase.LoadState.Finished)
|
||||
{
|
||||
text = text.Replace("{user}", replacer(mcLoginLoader.output.Name));
|
||||
text = text.Replace("{uuid}", replacer(mcLoginLoader.output.Uuid?.ToLower()));
|
||||
switch (mcLoginLoader.input.LoginType)
|
||||
{
|
||||
case McLoginType.Legacy:
|
||||
{
|
||||
text = text.Replace("{login}", replacer("离线"));
|
||||
break;
|
||||
}
|
||||
case McLoginType.Ms:
|
||||
{
|
||||
text = text.Replace("{login}", replacer("正版"));
|
||||
break;
|
||||
}
|
||||
case McLoginType.Auth:
|
||||
{
|
||||
text = text.Replace("{login}", replacer("Authlib-Injector"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text = text.Replace("{user}", replacer(null));
|
||||
text = text.Replace("{uuid}", replacer(null));
|
||||
text = text.Replace("{login}", replacer(null));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModLibrary
|
||||
{
|
||||
public class McLibToken
|
||||
{
|
||||
private string _Url;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为纯本地文件,若是则不尝试联网下载。
|
||||
/// </summary>
|
||||
public bool IsLocal;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 Natives 文件。
|
||||
/// </summary>
|
||||
public bool IsNatives;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的完整本地路径。
|
||||
/// </summary>
|
||||
public string LocalPath;
|
||||
|
||||
/// <summary>
|
||||
/// 原 JSON 中的 Name 项。
|
||||
/// </summary>
|
||||
public string OriginalName;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 SHA1。
|
||||
/// </summary>
|
||||
public string Sha1;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小。若无有效数据即为 0。
|
||||
/// </summary>
|
||||
public long size;
|
||||
|
||||
/// <summary>
|
||||
/// 由 JSON 提供的 URL,若没有则为 Nothing。
|
||||
/// </summary>
|
||||
public string Url
|
||||
{
|
||||
get => _Url;
|
||||
set =>
|
||||
// 孤儿 Forge 作者喜欢把没有 URL 的写个空字符串
|
||||
_Url = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 原 JSON 中 Name 项除去版本号部分的较前部分。可能为 Nothing。
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OriginalName is null)
|
||||
return null;
|
||||
var splited = new List<string>(OriginalName.Split(":"));
|
||||
splited.RemoveAt(2); // Java 的此格式下版本号固定为第三段,第四段可能包含架构、分包等其他信息
|
||||
return splited.Join(":");
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (IsNatives ? "[Native] " : "") + ModBase.GetString(size) + " | " + LocalPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否符合 JSON 中的 Rules。
|
||||
/// </summary>
|
||||
/// <param name="ruleToken">JSON 中的 "rules" 项目。</param>
|
||||
public static bool McJsonRuleCheck(JsonNode ruleToken)
|
||||
{
|
||||
if (ruleToken is null)
|
||||
return true;
|
||||
|
||||
// 初始化
|
||||
var required = false;
|
||||
foreach (var Rule in ruleToken.AsArray())
|
||||
{
|
||||
// 单条条件验证
|
||||
var isRightRule = true; // 是否为正确的规则
|
||||
if (Rule["os"] is not null) // 操作系统
|
||||
{
|
||||
if (Rule["os"]["name"] is not null) // 操作系统名称
|
||||
{
|
||||
var osName = Rule["os"]["name"].ToString();
|
||||
if (osName == "unknown")
|
||||
{
|
||||
}
|
||||
else if (osName == "windows")
|
||||
{
|
||||
if (Rule["os"]["version"] is not null) // 操作系统版本
|
||||
{
|
||||
var cr = Rule["os"]["version"].ToString();
|
||||
isRightRule = isRightRule && osVersion.RegexCheck(cr);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isRightRule = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Rule["os"]["arch"] is not null) // 操作系统架构
|
||||
isRightRule = isRightRule && Rule["os"]["arch"].ToString() == "x86" == SystemInfo.Is32BitSystem;
|
||||
}
|
||||
|
||||
if (Rule["features"] is not null) // 标签
|
||||
{
|
||||
isRightRule = isRightRule && Rule["features"]["is_demo_user"] is null; // 反选是否为 Demo 用户
|
||||
if (Rule["features"].AsObject().Any(prop => prop.Key.Contains("quick_play")))
|
||||
isRightRule = false; // 不开 Quick Play,让玩家自己加去
|
||||
}
|
||||
|
||||
// 反选确认
|
||||
if (Rule["action"].ToString() == "allow")
|
||||
{
|
||||
if (isRightRule)
|
||||
required = true; // allow
|
||||
}
|
||||
else if (isRightRule)
|
||||
{
|
||||
required = false; // disallow
|
||||
}
|
||||
}
|
||||
|
||||
return required;
|
||||
}
|
||||
|
||||
private static readonly string osVersion = Environment.OSVersion.Version.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 递归获取 Minecraft 某一实例的完整支持库列表。
|
||||
/// </summary>
|
||||
public static List<McLibToken> McLibListGet(McInstance mcInstance, bool includeInstanceJar)
|
||||
{
|
||||
// 获取当前支持库列表
|
||||
ModBase.Log("[Minecraft] 获取支持库列表:" + mcInstance.Name);
|
||||
var result = McLibListGetWithJson(mcInstance.JsonObject, targetMcInstance: mcInstance);
|
||||
|
||||
// 需要添加原版 Jar
|
||||
if (includeInstanceJar)
|
||||
{
|
||||
McInstance realMcInstance;
|
||||
var requiredJar = mcInstance.JsonObject["jar"]?.ToString();
|
||||
if (mcInstance.IsHmclFormatJson || requiredJar is null)
|
||||
{
|
||||
// HMCL 项直接使用自身的 Jar
|
||||
// 根据 Inherit 获取最深层实例
|
||||
var originalInstance = mcInstance;
|
||||
// 1.17+ 的 Forge 不寻找 Inherit
|
||||
if (!((mcInstance.Info.HasForge || mcInstance.Info.HasNeoForge) && mcInstance.Info.Drop >= 170))
|
||||
while (!string.IsNullOrEmpty(originalInstance.InheritInstanceName))
|
||||
{
|
||||
if ((originalInstance.InheritInstanceName ?? "") == (originalInstance.Name ?? ""))
|
||||
break;
|
||||
originalInstance = new McInstance(Path.Combine(ModFolder.mcFolderSelected, "versions", originalInstance.InheritInstanceName));
|
||||
}
|
||||
|
||||
// 需要新建对象,否则后面的 Check 会导致 McInstanceCurrent 的 State 变回 Original
|
||||
// 复现:启动一个 Snapshot 实例
|
||||
realMcInstance = new McInstance(originalInstance.PathInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Json 已提供 Jar 字段,使用该字段的信息
|
||||
realMcInstance = new McInstance(requiredJar);
|
||||
}
|
||||
|
||||
string clientUrl;
|
||||
string clientSHA1;
|
||||
// 判断需求的实例是否存在
|
||||
// 不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在
|
||||
if (!File.Exists(realMcInstance.PathInstance + realMcInstance.Name + ".json"))
|
||||
{
|
||||
realMcInstance = mcInstance;
|
||||
ModBase.Log("[Minecraft] 可能缺少前置实例 " + realMcInstance.Name + ",找不到对应的 JSON 文件", ModBase.LogLevel.Debug);
|
||||
}
|
||||
|
||||
// 获取详细下载信息
|
||||
if (realMcInstance.JsonObject["downloads"] is not null &&
|
||||
realMcInstance.JsonObject["downloads"]["client"] is not null)
|
||||
{
|
||||
clientUrl = (string)realMcInstance.JsonObject["downloads"]["client"]["url"];
|
||||
clientSHA1 = (string)realMcInstance.JsonObject["downloads"]["client"]["sha1"];
|
||||
}
|
||||
else
|
||||
{
|
||||
clientUrl = null;
|
||||
clientSHA1 = null;
|
||||
}
|
||||
|
||||
// 把所需的原版 Jar 添加进去
|
||||
result.Add(new McLibToken
|
||||
{
|
||||
LocalPath = realMcInstance.PathInstance + realMcInstance.Name + ".jar", size = 0L, IsNatives = false,
|
||||
Url = clientUrl, Sha1 = clientSHA1
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Minecraft 某一实例忽视继承的支持库列表,即结果中没有继承项。
|
||||
/// </summary>
|
||||
public static List<McLibToken> McLibListGetWithJson(JsonObject jsonObject,
|
||||
bool keepSameNameDifferentVersionResult = false, string customMcFolder = null, McInstance targetMcInstance = null)
|
||||
{
|
||||
customMcFolder = customMcFolder ?? ModFolder.mcFolderSelected;
|
||||
var basicArray = new List<McLibToken>();
|
||||
|
||||
// 添加基础 Json 项
|
||||
var allLibs = (JsonArray)jsonObject["libraries"];
|
||||
|
||||
// 转换为 LibToken
|
||||
foreach (var LibraryNode in allLibs)
|
||||
{
|
||||
var library = LibraryNode.AsObject();
|
||||
// 清理 null 项(BakaXL 会把没有的项序列化为 null;这导致了 #409)
|
||||
var keysToRemove = library.Where(p => p.Value?.GetValueKind() == JsonValueKind.Null).Select(p => p.Key).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
library.Remove(key);
|
||||
|
||||
// 检查是否需要(Rules)
|
||||
if (!McJsonRuleCheck(library["rules"]))
|
||||
continue;
|
||||
|
||||
// 获取根节点下的 url
|
||||
var rootUrl = (string)library["url"];
|
||||
if (rootUrl is not null)
|
||||
rootUrl += McLibGet((string)library["name"], false, true, customMcFolder).Replace(@"\", "/");
|
||||
|
||||
// 是否为纯本地项
|
||||
var hint = (string)library["hint"];
|
||||
var isLocal = hint is not null ? hint == "local" : false;
|
||||
|
||||
// 根据是否本地化处理(Natives)
|
||||
if (library["natives"] is null) // 没有 Natives
|
||||
{
|
||||
string localPath;
|
||||
if (isLocal && targetMcInstance is not null) // 纯本地项
|
||||
localPath = targetMcInstance.PathInstance + @"libraries\" +
|
||||
library["name"].ToString().AfterFirst(":").Replace(":", "-") + ".jar";
|
||||
else
|
||||
localPath = McLibGet((string)library["name"], customMcFolder: customMcFolder);
|
||||
var artifactPath = library["downloads"] is not null && library["downloads"]["artifact"] is not null
|
||||
? library["downloads"]["artifact"]["path"]
|
||||
: null;
|
||||
try
|
||||
{
|
||||
if (library["downloads"] is not null && library["downloads"]["artifact"] is not null)
|
||||
{
|
||||
var init = new McLibToken();
|
||||
basicArray.Add((init.OriginalName = (string)library["name"],
|
||||
init.Url = (string)(rootUrl ?? library["downloads"]["artifact"]["url"]),
|
||||
init.LocalPath = artifactPath is null
|
||||
? McLibGet((string)library["name"], customMcFolder: customMcFolder)
|
||||
: McLibGetByArtifactPath(artifactPath.ToString(), customMcFolder),
|
||||
init.size = (long)Math.Round(
|
||||
ModBase.Val(library["downloads"]["artifact"]["size"].ToString())),
|
||||
init.IsNatives = false, init.Sha1 = library["downloads"]["artifact"]["sha1"]?.ToString(),
|
||||
init.IsLocal = isLocal, init).init);
|
||||
}
|
||||
else
|
||||
{
|
||||
basicArray.Add(new McLibToken
|
||||
{
|
||||
OriginalName = (string)library["name"], Url = rootUrl, LocalPath = localPath, size = 0L,
|
||||
IsNatives = false, Sha1 = null, IsLocal = isLocal
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (artifactPath is not null && (ex is ArgumentException || ex is IOException))
|
||||
{
|
||||
ModBase.Log(ex, "支持库下载路径非法,已跳过(无 Natives," + (library["name"] ?? "Nothing") + ")");
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "处理实际支持库列表失败(无 Natives," + (library["name"] ?? "Nothing") + ")");
|
||||
basicArray.Add(new McLibToken
|
||||
{
|
||||
OriginalName = (string)library["name"], Url = rootUrl, LocalPath = localPath, size = 0L,
|
||||
IsNatives = false, Sha1 = null
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (library["natives"]["windows"] is not null) // 有 Windows Natives
|
||||
{
|
||||
var nativePath = library["downloads"] is not null && library["downloads"]["classifiers"] is not null &&
|
||||
library["downloads"]["classifiers"]["natives-windows"] is not null
|
||||
? library["downloads"]["classifiers"]["natives-windows"]["path"]
|
||||
: null;
|
||||
try
|
||||
{
|
||||
if (library["downloads"] is not null && library["downloads"]["classifiers"] is not null &&
|
||||
library["downloads"]["classifiers"]["natives-windows"] is not null)
|
||||
basicArray.Add(new McLibToken
|
||||
{
|
||||
OriginalName = (string)library["name"],
|
||||
Url = (string)(rootUrl ?? library["downloads"]["classifiers"]["natives-windows"]["url"]),
|
||||
LocalPath = nativePath is null
|
||||
? McLibGet((string)library["name"], customMcFolder: customMcFolder)
|
||||
.Replace(".jar", "-" + library["natives"]["windows"] + ".jar")
|
||||
.Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32")
|
||||
: McLibGetByArtifactPath(nativePath.ToString(), customMcFolder),
|
||||
size = (long)Math.Round(
|
||||
ModBase.Val(library["downloads"]["classifiers"]["natives-windows"]["size"].ToString())),
|
||||
IsNatives = true,
|
||||
Sha1 = library["downloads"]["classifiers"]["natives-windows"]["sha1"].ToString(),
|
||||
IsLocal = isLocal
|
||||
});
|
||||
else
|
||||
basicArray.Add(new McLibToken
|
||||
{
|
||||
OriginalName = (string)library["name"], Url = rootUrl,
|
||||
LocalPath = McLibGet((string)library["name"], customMcFolder: customMcFolder)
|
||||
.Replace(".jar", "-" + library["natives"]["windows"] + ".jar")
|
||||
.Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32"),
|
||||
size = 0L, IsNatives = true, Sha1 = null, IsLocal = isLocal
|
||||
});
|
||||
}
|
||||
catch (Exception ex) when (nativePath is not null && (ex is ArgumentException || ex is IOException))
|
||||
{
|
||||
ModBase.Log(ex, "支持库下载路径非法,已跳过(有 Natives," + (library["name"] ?? "Nothing") + ")");
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "处理实际支持库列表失败(有 Natives," + (library["name"] ?? "Nothing") + ")");
|
||||
basicArray.Add(new McLibToken
|
||||
{
|
||||
OriginalName = (string)library["name"], Url = rootUrl,
|
||||
LocalPath = McLibGet((string)library["name"], customMcFolder: customMcFolder)
|
||||
.Replace(".jar", "-" + library["natives"]["windows"] + ".jar")
|
||||
.Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32"),
|
||||
size = 0L, IsNatives = true, Sha1 = null, IsLocal = false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
var resultArray = new Dictionary<string, McLibToken>();
|
||||
|
||||
// 测试例:
|
||||
// D:\Minecraft\test\libraries\net\neoforged\mergetool\2.0.0\mergetool-2.0.0-api.jar
|
||||
// D:\Minecraft\test\libraries\org\apache\commons\commons-collections4\4.2\commons-collections4-4.2.jar
|
||||
// D:\Minecraft\test\libraries\com\google\guava\guava\31.1-jre\guava-31.1-jre.jar
|
||||
string GetVersion(McLibToken token)
|
||||
{
|
||||
return ModBase.GetFolderNameFromPath(ModBase.GetPathFromFullPath(token.LocalPath));
|
||||
}
|
||||
|
||||
for (int i = 0, loopTo = basicArray.Count - 1; i <= loopTo; i++)
|
||||
{
|
||||
var key = basicArray[i].Name + basicArray[i].IsNatives;
|
||||
if (resultArray.ContainsKey(key))
|
||||
{
|
||||
var basicArrayVersion = GetVersion(basicArray[i]);
|
||||
var resultArrayVersion = GetVersion(resultArray[key]);
|
||||
if ((basicArrayVersion ?? "") != (resultArrayVersion ?? "") && keepSameNameDifferentVersionResult)
|
||||
{
|
||||
ModBase.Log(
|
||||
$"[Minecraft] 发现疑似重复的支持库:{basicArray[i]} ({basicArrayVersion}) 与 {resultArray[key]} ({resultArrayVersion})");
|
||||
resultArray.Add(key + ModBase.GetUuid(), basicArray[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log(
|
||||
$"[Minecraft] 发现重复的支持库:{basicArray[i]} ({basicArrayVersion}) 与 {resultArray[key]} ({resultArrayVersion}),已忽略其中之一");
|
||||
if (McVersionComparer.CompareVersionGe(basicArrayVersion, resultArrayVersion)) resultArray[key] = basicArray[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultArray.Add(key, basicArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return resultArray.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实例所需支持库文件的 NetFile。
|
||||
/// </summary>
|
||||
public static List<DownloadFile> McLibNetFilesFromInstance(McInstance mcInstance)
|
||||
{
|
||||
if (!mcInstance.IsLoaded)
|
||||
mcInstance.Load();
|
||||
var result = new List<DownloadFile>();
|
||||
|
||||
// 更新此方法时需要同步更新 Forge 新版自动安装方法!
|
||||
|
||||
// 主 Jar 文件
|
||||
try
|
||||
{
|
||||
var mainJar = ModDownload.DlClientJarGet(mcInstance, true);
|
||||
if (mainJar is not null)
|
||||
result.Add(mainJar);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "实例缺失主 Jar 文件所必须的信息", ModBase.LogLevel.Developer);
|
||||
}
|
||||
|
||||
// Library 文件
|
||||
result.AddRange(McLibNetFilesFromTokens(McLibListGet(mcInstance, false)));
|
||||
|
||||
// Authlib-Injector 文件
|
||||
var authlibTargetFile = Path.Combine(ModBase.pathPure, "authlib-injector.jar");
|
||||
JsonObject authlibDownloadInfo = null;
|
||||
try
|
||||
{
|
||||
ModBase.Log("[Minecraft] 开始获取 Authlib-Injector 下载信息");
|
||||
authlibDownloadInfo = (JsonObject)ModBase.GetJson(ModNet.NetGetCodeByLoader(
|
||||
new[]
|
||||
{
|
||||
"https://authlib-injector.yushi.moe/artifact/latest.json",
|
||||
"https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json"
|
||||
}, isJson: true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取 Authlib-Injector 下载信息失败");
|
||||
}
|
||||
|
||||
// 校验文件
|
||||
if (authlibDownloadInfo is not null)
|
||||
{
|
||||
var checker = new ModBase.FileChecker(hash: authlibDownloadInfo["checksums"]["sha256"].ToString());
|
||||
if (checker.Check(authlibTargetFile) is not null)
|
||||
{
|
||||
// 开始下载
|
||||
var downloadAddress = authlibDownloadInfo["download_url"].ToString()
|
||||
.Replace("bmclapi2.bangbang93.com/mirrors/authlib-injector", "authlib-injector.yushi.moe");
|
||||
ModBase.Log("[Minecraft] Authlib-Injector 需要更新:" + downloadAddress, ModBase.LogLevel.Developer);
|
||||
result.Add(new DownloadFile(
|
||||
new[]
|
||||
{
|
||||
downloadAddress,
|
||||
downloadAddress.Replace("authlib-injector.yushi.moe",
|
||||
"bmclapi2.bangbang93.com/mirrors/authlib-injector")
|
||||
}, authlibTargetFile,
|
||||
new ModBase.FileChecker(hash: authlibDownloadInfo["checksums"]["sha256"].ToString())));
|
||||
}
|
||||
}
|
||||
|
||||
// 修改渲染器
|
||||
var mesaLoaderWindowsTargetFile =
|
||||
Path.Combine(ModBase.pathPure, "mesa-loader-windows", ModLaunch.mesaLoaderWindowsVersion, "Loader.jar");
|
||||
var renderer = -1;
|
||||
if (ModInstanceList.McMcInstanceSelected is not null)
|
||||
renderer = Config.Instance.Renderer[ModInstanceList.McMcInstanceSelected?.PathInstance] - 1;
|
||||
if (renderer == -1) renderer = Config.Launch.Renderer;
|
||||
|
||||
if (renderer != 0 && !File.Exists(mesaLoaderWindowsTargetFile))
|
||||
{
|
||||
var downloadAddress =
|
||||
"https://mirrors.cloud.tencent.com/nexus/repository/maven-public/org/glavo/mesa-loader-windows/" +
|
||||
ModLaunch.mesaLoaderWindowsVersion + "/mesa-loader-windows-" + ModLaunch.mesaLoaderWindowsVersion + "-" +
|
||||
(SystemInfo.Is32BitSystem ? "x86" : SystemInfo.IsArm64System ? "arm64" : "x64") + ".jar";
|
||||
result.Add(new DownloadFile(new[] { downloadAddress }, mesaLoaderWindowsTargetFile));
|
||||
}
|
||||
|
||||
// LabyMod Assets 文件
|
||||
if (mcInstance.Info.HasLabyMod)
|
||||
{
|
||||
if ((mcInstance.PathIndie ?? "") == (mcInstance.PathInstance ?? ""))
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(mcInstance.PathInstance, "labymod-neo")))
|
||||
Directory.Delete(Path.Combine(mcInstance.PathInstance, "labymod-neo"), true);
|
||||
ModBase.CreateSymbolicLink(Path.Combine(mcInstance.PathInstance, "labymod-neo"), Path.Combine(ModFolder.mcFolderSelected, "labymod-neo"),
|
||||
0x2);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var channelType = mcInstance.JsonObject["labymod_data"]["channelType"].ToString();
|
||||
Directory.CreateDirectory($@"{ModFolder.mcFolderSelected}labymod-neo\libraries");
|
||||
ModBase.Log("[Minecraft] 开始获取 LabyMod 信息");
|
||||
var labyManifest = (JsonObject)ModNet.NetGetCodeByRequestRetry(
|
||||
$"https://releases.r2.labymod.net/api/v1/manifest/{channelType}/latest.json", isJson: true);
|
||||
var labyAssets = (JsonObject)labyManifest["assets"];
|
||||
var labyModCommitRef = labyManifest["commitReference"].ToString();
|
||||
foreach (var Asset in labyAssets)
|
||||
{
|
||||
var assetName = Asset.Key;
|
||||
var assetSHA1 = Asset.Value.ToString();
|
||||
var assetPath = $@"{ModFolder.mcFolderSelected}labymod-neo\assets\{assetName}.jar";
|
||||
var assetUrl =
|
||||
$"https://releases.r2.labymod.net/api/v1/download/assets/labymod4/{channelType}/{labyModCommitRef}/{assetName}/{assetSHA1}.jar";
|
||||
var checker = new ModBase.FileChecker(hash: assetSHA1);
|
||||
if (checker.Check(assetPath) is null)
|
||||
continue;
|
||||
result.Add(new DownloadFile(new[] { assetUrl }, assetPath, checker));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取 LabyMod 信息失败,跳过检查");
|
||||
}
|
||||
}
|
||||
|
||||
// 跳过校验
|
||||
if (ShouldIgnoreFileCheck(mcInstance))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 用户要求尽量忽略文件检查,这可能会保留有误的文件");
|
||||
result = result.Where(f =>
|
||||
{
|
||||
if (File.Exists(f.LocalPath))
|
||||
{
|
||||
ModBase.Log("[Minecraft] 跳过下载的支持库文件:" + f.LocalPath, ModBase.LogLevel.Debug);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 McLibToken 列表转换为 NetFile。
|
||||
/// </summary>
|
||||
public static List<DownloadFile> McLibNetFilesFromTokens(List<McLibToken> libs, string customMcFolder = null)
|
||||
{
|
||||
customMcFolder = customMcFolder ?? ModFolder.mcFolderSelected;
|
||||
var result = new List<DownloadFile>();
|
||||
// 获取
|
||||
foreach (var token in libs)
|
||||
{
|
||||
// 检查文件
|
||||
var checker = new ModBase.FileChecker(actualSize: token.size == 0L ? -1 : token.size, hash: token.Sha1);
|
||||
if (checker.Check(token.LocalPath) is null)
|
||||
continue;
|
||||
if (token.IsLocal)
|
||||
{
|
||||
ModBase.Log("[Download] 已跳过被标记为本地文件的支持库: " + token.OriginalName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// URL
|
||||
var urls = new List<string>();
|
||||
if (token.Url is null && token.Name == "net.minecraftforge:forge:universal")
|
||||
// 特判修复 Forge 部分 universal 文件缺失 URL(#5455)
|
||||
token.Url = "https://maven.minecraftforge.net" +
|
||||
token.LocalPath.Replace(customMcFolder + "libraries", "").Replace(@"\", "/");
|
||||
if (token.Url is not null)
|
||||
{
|
||||
// 获取 URL 的真实地址
|
||||
urls.Add(token.Url);
|
||||
if (token.Url.Contains("launcher.mojang.com/v1/objects") || token.Url.Contains("client.txt") ||
|
||||
token.Url.Contains(".tsrg"))
|
||||
urls.AddRange(ModDownload.DlSourceLauncherOrMetaGet(token.Url)); // Mappings(#4425)
|
||||
if (token.Url.Contains("maven"))
|
||||
{
|
||||
var bmclapiUrl = token.Url
|
||||
.Replace(token.Url.Substring(0, token.Url.IndexOfF("maven")),
|
||||
"https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven")
|
||||
.Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven");
|
||||
if (ModDownload.DlSourcePreferMojang)
|
||||
urls.Add(bmclapiUrl); // 官方源优先
|
||||
else
|
||||
urls.Insert(0, bmclapiUrl); // 镜像源优先
|
||||
}
|
||||
}
|
||||
|
||||
if (token.LocalPath.Contains("transformer-discovery-service"))
|
||||
{
|
||||
// Transformer 文件释放
|
||||
if (!File.Exists(token.LocalPath))
|
||||
ModBase.WriteFile(token.LocalPath, ModBase.GetResourceStream("Resources/transformer.jar"));
|
||||
ModBase.Log("[Download] 已自动释放 Transformer Discovery Service", ModBase.LogLevel.Developer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.LocalPath.Contains(@"optifine\OptiFine"))
|
||||
{
|
||||
// OptiFine 主 Jar
|
||||
var optiFineBase =
|
||||
token.LocalPath.Replace(Path.Combine(customMcFolder, "libraries", "optifine", "OptiFine") + @"\", "").Split("_")[0] + "/" +
|
||||
ModBase.GetFileNameFromPath(token.LocalPath).Replace("-", "_");
|
||||
optiFineBase = "/maven/com/optifine/" + optiFineBase;
|
||||
if (optiFineBase.Contains("_pre"))
|
||||
optiFineBase = optiFineBase.Replace("com/optifine/", "com/optifine/preview_");
|
||||
urls.Add("https://bmclapi2.bangbang93.com" + optiFineBase);
|
||||
}
|
||||
else if (token.Name.Contains("LabyMod"))
|
||||
{
|
||||
// LabyMod 只有一个下载源
|
||||
urls.Add(token.Url);
|
||||
ModBase.Log(
|
||||
$"[Download] 获取到 LabyMod 主要库文件的 Size = {token.size},SHA1 = {token.Sha1},由于 LabyMod 乱写 Size,已忽略 Size");
|
||||
checker = new ModBase.FileChecker(hash: token.Sha1); // 只校验 SHA1
|
||||
}
|
||||
else if (urls.Count <= 2)
|
||||
{
|
||||
// 普通文件
|
||||
urls.AddRange(ModDownload.DlSourceLibraryGet("https://libraries.minecraft.net" +
|
||||
token.LocalPath.Replace(customMcFolder + "libraries", "")
|
||||
.Replace(@"\", "/")));
|
||||
}
|
||||
|
||||
result.Add(new DownloadFile(urls.Distinct(), token.LocalPath, checker));
|
||||
}
|
||||
|
||||
// 去重并返回
|
||||
return result.Distinct((a, b) => (a.LocalPath ?? "") == (b.LocalPath ?? ""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对应的支持库文件地址。
|
||||
/// </summary>
|
||||
/// <param name="original">原始地址,如 com.mumfrey:liteloader:1.12.2-SNAPSHOT。</param>
|
||||
/// <param name="withHead">是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。</param>
|
||||
public static string McLibGet(string original, bool withHead = true, bool ignoreLiteLoader = false,
|
||||
string customMcFolder = null)
|
||||
{
|
||||
string mcLibGetRet = default;
|
||||
customMcFolder = customMcFolder ?? ModFolder.mcFolderSelected;
|
||||
var splited = original.Split(":");
|
||||
mcLibGetRet = withHead
|
||||
? Path.Combine(customMcFolder, "libraries", splited[0].Replace(".", @"\"), splited[1], splited[2], splited[1] + "-" + splited[2] + ".jar")
|
||||
: Path.Combine(splited[0].Replace(".", @"\"), splited[1], splited[2], splited[1] + "-" + splited[2] + ".jar");
|
||||
// 判断 OptiFine 是否应该使用 installer
|
||||
if (mcLibGetRet.Contains(@"optifine\OptiFine\1.") && splited[2].Split(".").Count() > 1)
|
||||
{
|
||||
var majorVersion = (int)Math.Round(ModBase.Val(splited[2].Split(".")[1].BeforeFirst("_")));
|
||||
var minorVersion = (int)Math.Round(splited[2].Split(".").Count() > 2
|
||||
? ModBase.Val(splited[2].Split(".")[2].BeforeFirst("_"))
|
||||
: 0d);
|
||||
if ((majorVersion == 12 || (majorVersion == 20 && minorVersion >= 4) || majorVersion >= 21) && File.Exists(
|
||||
$@"{customMcFolder}libraries\{splited[0].Replace(".", @"\")}\{splited[1]}\{splited[2]}\{splited[1]}-{splited[2]}-installer.jar")) // 仅在 1.12 (无法追溯) 和 1.20.4+ (#5376) 遇到此问题
|
||||
{
|
||||
ModLaunch.McLaunchLog("已将 " + original + " 替换为对应的 Installer 文件");
|
||||
mcLibGetRet = mcLibGetRet.Replace(".jar", "-installer.jar");
|
||||
}
|
||||
}
|
||||
|
||||
return mcLibGetRet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 JSON 中的 downloads.artifact.path 获取支持库文件地址。
|
||||
/// </summary>
|
||||
private static string McLibGetByArtifactPath(string artifactPath, string customMcFolder = null)
|
||||
{
|
||||
customMcFolder = customMcFolder ?? ModFolder.mcFolderSelected;
|
||||
if (string.IsNullOrWhiteSpace(artifactPath))
|
||||
throw new ArgumentException("支持库下载路径无效", nameof(artifactPath));
|
||||
|
||||
var librariesRoot = Path.GetFullPath(Path.Combine(customMcFolder, "libraries"));
|
||||
var normalizedArtifactPath = artifactPath.Replace('/', Path.DirectorySeparatorChar);
|
||||
if (Path.IsPathRooted(normalizedArtifactPath))
|
||||
throw new IOException("支持库下载路径不能为绝对路径: " + artifactPath);
|
||||
|
||||
var localPath = Path.GetFullPath(Path.Combine(librariesRoot, normalizedArtifactPath));
|
||||
var librariesRootWithSeparator = librariesRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) +
|
||||
Path.DirectorySeparatorChar;
|
||||
if (!localPath.StartsWith(librariesRootWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
throw new IOException("支持库下载路径不能位于 libraries 文件夹外: " + artifactPath);
|
||||
|
||||
return localPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查设置,是否应当忽略文件检查?
|
||||
/// </summary>
|
||||
public static bool ShouldIgnoreFileCheck(McInstance version)
|
||||
{
|
||||
return Config.Instance.DisableAssetVerifyV2[version.PathInstance] ||
|
||||
Config.Instance.AssetVerifySolutionV1[version.PathInstance] == 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2493 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using fNbt;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Core.Utils.Hash;
|
||||
using static PCL.ModComp;
|
||||
using static PCL.ModLoader;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModLocalComp
|
||||
{
|
||||
private const int localModCacheVersion = 7;
|
||||
|
||||
private static readonly Lazy<HashCache> _hashCache = new(() =>
|
||||
new HashCache(ModBase.pathTemp + @"Cache\HashCache.db"));
|
||||
|
||||
public class LocalCompFile
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否可能为前置 Mod。
|
||||
/// </summary>
|
||||
public bool IsPresetMod()
|
||||
{
|
||||
return !Dependencies.Any() && Name is not null &&
|
||||
(Name.ToLower().Contains("core") || Name.ToLower().Contains("lib"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
|
||||
/// </summary>
|
||||
public static bool IsModFile(string path)
|
||||
{
|
||||
if (path is null || !path.Contains("."))
|
||||
return false;
|
||||
path = path.ToLower();
|
||||
if (path.EndsWithF(".jar", true) || path.EndsWithF(".zip", true) || path.EndsWithF(".litemod", true) ||
|
||||
path.EndsWithF(".jar.disabled", true) || path.EndsWithF(".zip.disabled", true) ||
|
||||
path.EndsWithF(".litemod.disabled", true) || path.EndsWithF(".jar.old", true) ||
|
||||
path.EndsWithF(".zip.old", true) || path.EndsWithF(".litemod.old", true))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否为指定类型的组件文件。
|
||||
/// </summary>
|
||||
public static bool IsCompFile(string path, CompType compType)
|
||||
{
|
||||
if (path is null || !path.Contains("."))
|
||||
return false;
|
||||
path = path.ToLower();
|
||||
switch (compType)
|
||||
{
|
||||
case CompType.Mod:
|
||||
{
|
||||
return IsModFile(path);
|
||||
}
|
||||
case CompType.ResourcePack:
|
||||
case CompType.Shader:
|
||||
{
|
||||
return path.EndsWithF(".zip", true);
|
||||
}
|
||||
case CompType.DataPack:
|
||||
{
|
||||
return path.EndsWithF(".zip", true) || path.EndsWithF(".zip.disabled", true);
|
||||
}
|
||||
case CompType.Schematic:
|
||||
{
|
||||
return path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) ||
|
||||
path.EndsWithF(".schematic", true) || path.EndsWithF(".schem", true);
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图标路径。
|
||||
/// </summary>
|
||||
public string GetLogo()
|
||||
{
|
||||
if (Comp is not null && Comp.LogoUrl is not null)
|
||||
return Comp.LogoUrl;
|
||||
if (Logo is not null)
|
||||
return Logo;
|
||||
|
||||
// 为文件夹设置特定图标
|
||||
if (IsFolder) return "pack://application:,,,/images/Icons/Folder.png";
|
||||
|
||||
return ModBase.pathImage + "Icons/NoIcon.png";
|
||||
}
|
||||
|
||||
#region Litematic 文件处理
|
||||
|
||||
/// <summary>
|
||||
/// 读取 Litematic 文件的 NBT 数据。
|
||||
/// </summary>
|
||||
private void LoadLitematicNbtData()
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.Log($"开始读取 Litematic NBT 数据:{path}", ModBase.LogLevel.Debug);
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
var scheNbt = new NbtFile();
|
||||
scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect);
|
||||
// 读取版本信息
|
||||
var versionTag = (NbtInt)scheNbt.RootTag.Get("Version");
|
||||
if (versionTag is not null) _litematicVersion = versionTag.Value;
|
||||
|
||||
// 读取 Metadata 节点
|
||||
var metadataTag = scheNbt.RootTag.Get<NbtCompound>("Metadata");
|
||||
if (metadataTag is not null)
|
||||
{
|
||||
ModBase.Log("找到 Litematic Metadata 节点", ModBase.LogLevel.Debug);
|
||||
|
||||
// 读取名称
|
||||
var nameTag = metadataTag.Get<NbtString>("Name");
|
||||
if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value) &&
|
||||
nameTag.Value != "Unnamed") _litematicOriginalName = nameTag.Value;
|
||||
|
||||
// 读取描述信息
|
||||
var descriptionTag = metadataTag.Get<NbtString>("Description");
|
||||
if (descriptionTag is not null && !string.IsNullOrWhiteSpace(descriptionTag.Value))
|
||||
_Description = descriptionTag.Value;
|
||||
|
||||
// 读取作者信息
|
||||
var authorTag = metadataTag.Get<NbtString>("Author");
|
||||
if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value))
|
||||
_Authors = authorTag.Value;
|
||||
|
||||
// 读取时间信息
|
||||
var timeCreatedTag = metadataTag.Get<NbtLong>("TimeCreated");
|
||||
if (timeCreatedTag is not null) _litematicTimeCreated = timeCreatedTag.Value;
|
||||
|
||||
var timeModifiedTag = metadataTag.Get<NbtLong>("TimeModified");
|
||||
if (timeModifiedTag is not null) _litematicTimeModified = timeModifiedTag.Value;
|
||||
|
||||
// 读取包围盒大小
|
||||
var enclosingSizeTag = metadataTag.Get<NbtCompound>("EnclosingSize");
|
||||
if (enclosingSizeTag is not null)
|
||||
{
|
||||
var xTag = enclosingSizeTag.Get<NbtInt>("x");
|
||||
var yTag = enclosingSizeTag.Get<NbtInt>("y");
|
||||
var zTag = enclosingSizeTag.Get<NbtInt>("z");
|
||||
if (xTag is not null && yTag is not null && zTag is not null)
|
||||
_litematicEnclosingSize = $"{xTag.Value} × {yTag.Value} × {zTag.Value}";
|
||||
}
|
||||
|
||||
// 读取区域数量
|
||||
var regionCountTag = metadataTag.Get<NbtInt>("RegionCount");
|
||||
if (regionCountTag is not null) _litematicRegionCount = regionCountTag.Value;
|
||||
|
||||
// 读取总方块数
|
||||
var totalBlocksTag = metadataTag.Get<NbtInt>("TotalBlocks");
|
||||
if (totalBlocksTag is not null) _litematicTotalBlocks = totalBlocksTag.Value;
|
||||
|
||||
// 读取总体积
|
||||
var totalVolumeTag = metadataTag.Get<NbtInt>("TotalVolume");
|
||||
if (totalVolumeTag is not null) _litematicTotalVolume = totalVolumeTag.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("未找到 Litematic Metadata 节点", ModBase.LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.Log("Litematic NBT 数据读取完成", ModBase.LogLevel.Debug);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 Litematic NBT 数据时出错(" + path + ")");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Schem 文件处理
|
||||
|
||||
/// <summary>
|
||||
/// 读取 .schem 文件的 NBT 数据(Sponge Schematic 格式)。
|
||||
/// </summary>
|
||||
private void LoadSchemNbtData()
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.Log($"开始读取 Schem NBT 数据:{path}", ModBase.LogLevel.Debug);
|
||||
|
||||
// 使用自动检测压缩格式
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
var scheNbt = new NbtFile();
|
||||
scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect);
|
||||
|
||||
// 读取Sponge版本信息
|
||||
var versionTag = scheNbt.RootTag.Get<NbtInt>("Version");
|
||||
if (versionTag is not null) _spongeVersion = versionTag.Value;
|
||||
|
||||
// 读取数据版本信息
|
||||
var dataVersionTag = scheNbt.RootTag.Get<NbtInt>("DataVersion");
|
||||
if (dataVersionTag is not null) _structureDataVersion = dataVersionTag.Value;
|
||||
|
||||
// 读取尺寸信息
|
||||
var widthTag = scheNbt.RootTag.Get<NbtShort>("Width");
|
||||
var heightTag = scheNbt.RootTag.Get<NbtShort>("Height");
|
||||
var lengthTag = scheNbt.RootTag.Get<NbtShort>("Length");
|
||||
|
||||
if (widthTag is not null && heightTag is not null && lengthTag is not null)
|
||||
{
|
||||
_litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}";
|
||||
_litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value;
|
||||
|
||||
// 对于Sponge格式,方块数量等于总体积(因为包含空气方块)
|
||||
_litematicTotalBlocks = _litematicTotalVolume;
|
||||
}
|
||||
|
||||
// 读取调色板信息来计算区域数量
|
||||
var paletteTag = scheNbt.RootTag.Get<NbtCompound>("Palette");
|
||||
if (paletteTag is not null) _litematicRegionCount = 1; // Sponge Schematic 通常只有一个区域
|
||||
|
||||
// 读取元数据
|
||||
var metadataTag = scheNbt.RootTag.Get<NbtCompound>("Metadata");
|
||||
if (metadataTag is not null)
|
||||
{
|
||||
// 读取名称
|
||||
var nameTag = metadataTag.Get<NbtString>("Name");
|
||||
if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value))
|
||||
_schemOriginalName = nameTag.Value;
|
||||
|
||||
// 读取作者信息
|
||||
var authorTag = metadataTag.Get<NbtString>("Author");
|
||||
if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value))
|
||||
{
|
||||
_structureAuthor = authorTag.Value;
|
||||
if (_Authors is null)
|
||||
_Authors = _structureAuthor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.Log("Schem NBT 数据读取完成", ModBase.LogLevel.Debug);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 Schem NBT 数据时出错(" + path + ")");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Schematic 文件处理
|
||||
|
||||
/// <summary>
|
||||
/// 读取 .schematic 文件的 NBT 数据(MCEdit/WorldEdit 格式)。
|
||||
/// </summary>
|
||||
private void LoadSchematicNbtData()
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.Log($"开始读取 Schematic NBT 数据:{path}", ModBase.LogLevel.Debug);
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
var scheNbt = new NbtFile();
|
||||
scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect);
|
||||
// 读取尺寸信息
|
||||
var widthTag = scheNbt.RootTag.Get<NbtShort>("Width");
|
||||
var heightTag = scheNbt.RootTag.Get<NbtShort>("Height");
|
||||
var lengthTag = scheNbt.RootTag.Get<NbtShort>("Length");
|
||||
if (widthTag is not null && heightTag is not null && lengthTag is not null)
|
||||
{
|
||||
_litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}";
|
||||
_litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value;
|
||||
}
|
||||
|
||||
// 读取材料列表
|
||||
var materialsTag = scheNbt.RootTag.Get<NbtString>("Materials");
|
||||
if (materialsTag is not null)
|
||||
ModBase.Log($"Schematic 材料类型:{materialsTag.Value}", ModBase.LogLevel.Debug);
|
||||
|
||||
ModBase.Log("Schematic NBT 数据读取完成", ModBase.LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 Schematic NBT 数据时出错(" + path + ")");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NBT 结构文件处理
|
||||
|
||||
/// <summary>
|
||||
/// 读取 .nbt 文件的 NBT 数据(Minecraft 结构文件格式)。
|
||||
/// </summary>
|
||||
private void LoadStructureNbtData()
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.Log($"开始读取 NBT 结构文件数据:{path}", ModBase.LogLevel.Debug);
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
var scheNbt = new NbtFile();
|
||||
scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect);
|
||||
// 读取作者信息
|
||||
var authorTag = scheNbt.RootTag.Get<NbtString>("author");
|
||||
if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value))
|
||||
{
|
||||
_structureAuthor = authorTag.Value;
|
||||
if (_Authors is null)
|
||||
_Authors = _structureAuthor;
|
||||
}
|
||||
|
||||
// 读取尺寸信息
|
||||
var sizeTag = scheNbt.RootTag.Get<NbtList>("size");
|
||||
if (sizeTag is not null)
|
||||
{
|
||||
var sizeElements = sizeTag.ToArray();
|
||||
if (sizeElements.Length >= 3)
|
||||
{
|
||||
var sizeArray = sizeElements.Take(3).Select(e => e.IntValue).ToArray();
|
||||
_litematicEnclosingSize = $"{sizeArray[0]} × {sizeArray[1]} × {sizeArray[2]}";
|
||||
_litematicTotalVolume = sizeArray[0] * sizeArray[1] * sizeArray[2];
|
||||
}
|
||||
}
|
||||
|
||||
// 读取方块数量信息
|
||||
var blocksTag = scheNbt.RootTag.Get<NbtList>("blocks");
|
||||
if (blocksTag is not null)
|
||||
_litematicTotalBlocks = blocksTag.Where(x => x.TagType == NbtTagType.Compound).Count();
|
||||
|
||||
// 读取调色板信息来计算区域数量
|
||||
var paletteTag = scheNbt.RootTag.Get<NbtList>("palette");
|
||||
if (paletteTag is not null) _litematicRegionCount = 1; // 原版结构文件通常只有一个区域
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 NBT 结构文件数据时出错(" + path + ")");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 基础
|
||||
|
||||
/// <summary>
|
||||
/// 资源的文件的地址。
|
||||
/// </summary>
|
||||
public readonly string path;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为文件夹项。
|
||||
/// </summary>
|
||||
public bool IsFolder => path.EndsWithF(@"\__FOLDER__", true);
|
||||
|
||||
/// <summary>
|
||||
/// 获取实际的文件夹路径(去除 __FOLDER__ 标记)。
|
||||
/// </summary>
|
||||
public string ActualPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsFolder) return path.Replace(@"\__FOLDER__", "");
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public LocalCompFile(string path)
|
||||
{
|
||||
this.path = path ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NBT数据是否已加载(用于延迟加载优化)。
|
||||
/// </summary>
|
||||
private bool _nbtDataLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Mod 资源的完整路径,去除最后的 .disabled 和 .old。
|
||||
/// </summary>
|
||||
public string RawPath => ModBase.GetPathFromFullPath(path) + RawFileName;
|
||||
|
||||
/// <summary>
|
||||
/// 资源的完整文件名。
|
||||
/// </summary>
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsFolder && !string.IsNullOrEmpty(Name)) return Name;
|
||||
|
||||
return ModBase.GetFileNameFromPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod 资源的完整文件名,去除最后的 .disabled 和 .old。
|
||||
/// </summary>
|
||||
public string RawFileName => FileName.Replace(".disabled", "").Replace(".old", "");
|
||||
|
||||
/// <summary>
|
||||
/// 资源的状态。对于 Mod 有 Disabled
|
||||
/// </summary>
|
||||
public LocalFileStatus State
|
||||
{
|
||||
get
|
||||
{
|
||||
Load();
|
||||
if (!IsFileAvailable) return LocalFileStatus.Unavailable;
|
||||
|
||||
if (path.EndsWithF(".disabled", true) || path.EndsWithF(".old", true)) return LocalFileStatus.Disabled;
|
||||
|
||||
return LocalFileStatus.Fine;
|
||||
}
|
||||
}
|
||||
|
||||
public enum LocalFileStatus
|
||||
{
|
||||
Fine = 0,
|
||||
Disabled = 1,
|
||||
Unavailable = 2
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 信息项
|
||||
|
||||
/// <summary>
|
||||
/// Mod 的名称。若不可用则为 ModID 或无扩展的文件名。
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Name is null)
|
||||
Load();
|
||||
if (_Name is null)
|
||||
_Name = _ModId;
|
||||
if (_Name is null)
|
||||
{
|
||||
if (IsFolder)
|
||||
_Name = ModBase.GetFolderNameFromPath(ActualPath);
|
||||
else
|
||||
_Name = ModBase.GetFileNameWithoutExtentionFromPath(path);
|
||||
}
|
||||
|
||||
return _Name;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Name is null && value is not null && !value.Contains("modname") && value.ToLower() != "name" &&
|
||||
value.Length > 1 && (ModBase.Val(value).ToString() ?? "") != (value ?? "")) _Name = value;
|
||||
}
|
||||
}
|
||||
|
||||
private string _Name;
|
||||
|
||||
/// <summary>
|
||||
/// Mod 的描述信息。
|
||||
/// </summary>
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Description is null)
|
||||
Load();
|
||||
if (_Description is null && FileUnavailableReason is not null)
|
||||
_Description = FileUnavailableReason.Message;
|
||||
// If _Description Is Nothing Then _Description = Path
|
||||
return _Description;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Description is null && value is not null && value.Length > 2)
|
||||
{
|
||||
_Description = value.Trim('\n');
|
||||
// 优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
|
||||
if (_Description.ToLower().LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789".ToCharArray()) ==
|
||||
_Description.Length - 1)
|
||||
_Description += ".";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _Description;
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型标签。
|
||||
/// </summary>
|
||||
public List<string> Tags
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
{
|
||||
field = new List<string>();
|
||||
if (IsFolder)
|
||||
{
|
||||
field.Add("文件夹");
|
||||
}
|
||||
else
|
||||
{
|
||||
var extension = System.IO.Path.GetExtension(RawPath).ToLower();
|
||||
switch (extension ?? "")
|
||||
{
|
||||
case ".litematic":
|
||||
{
|
||||
field.Add("原理图");
|
||||
break;
|
||||
}
|
||||
case ".schem":
|
||||
case ".schematic":
|
||||
{
|
||||
field.Add("Schematic结构");
|
||||
break;
|
||||
}
|
||||
case ".nbt":
|
||||
{
|
||||
field.Add("原版结构");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod 的版本,不保证符合版本格式规范。
|
||||
/// </summary>
|
||||
public string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Version is null)
|
||||
Load();
|
||||
return _Version;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Version is not null && _Version.RegexCheck(@"[0-9.\-]+"))
|
||||
return;
|
||||
if (value?.ContainsF("version", true) == true)
|
||||
value = "version"; // 需要修改的标识
|
||||
_Version = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string _Version;
|
||||
|
||||
/// <summary>
|
||||
/// 用于依赖检查的 ModID。
|
||||
/// </summary>
|
||||
public string ModId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ModId is null)
|
||||
Load();
|
||||
return _ModId;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
value = value.RegexSeek(RegexPatterns.ModIdMatch);
|
||||
if (value is null || value.Length <= 1 || (ModBase.Val(value).ToString() ?? "") == (value ?? ""))
|
||||
return;
|
||||
if (value.ContainsF("name", true) || value.ContainsF("modid", true))
|
||||
return;
|
||||
if (!possibleModId.Contains(value))
|
||||
possibleModId.Add(value);
|
||||
if (_ModId is null)
|
||||
_ModId = value;
|
||||
}
|
||||
}
|
||||
|
||||
private string _ModId;
|
||||
|
||||
/// <summary>
|
||||
/// 其他可能的 ModID。
|
||||
/// </summary>
|
||||
public List<string> possibleModId = new();
|
||||
|
||||
/// <summary>
|
||||
/// Mod 的主页。
|
||||
/// </summary>
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
Load();
|
||||
return field;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (field is null && value is not null && value.StartsWithF("http")) field = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod 的作者列表。
|
||||
/// </summary>
|
||||
public string Authors
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Authors is null)
|
||||
Load();
|
||||
return _Authors;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Authors is null && !string.IsNullOrWhiteSpace(value)) _Authors = value;
|
||||
}
|
||||
}
|
||||
|
||||
private string _Authors;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的创建时间戳。
|
||||
/// </summary>
|
||||
public long? LitematicTimeCreated
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicTimeCreated;
|
||||
}
|
||||
}
|
||||
|
||||
private long? _litematicTimeCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的修改时间戳。
|
||||
/// </summary>
|
||||
public long? LitematicTimeModified
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicTimeModified;
|
||||
}
|
||||
}
|
||||
|
||||
private long? _litematicTimeModified;
|
||||
|
||||
/// <summary>
|
||||
/// Schem 读取到的原始名称。
|
||||
/// </summary>
|
||||
public string SchemOriginalName
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _schemOriginalName;
|
||||
}
|
||||
}
|
||||
|
||||
private string _schemOriginalName;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 读取到的原始名称。
|
||||
/// </summary>
|
||||
public string LitematicOriginalName
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicOriginalName;
|
||||
}
|
||||
}
|
||||
|
||||
private string _litematicOriginalName;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的版本。
|
||||
/// </summary>
|
||||
public int? LitematicVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _litematicVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的包围盒大小。
|
||||
/// </summary>
|
||||
public string LitematicEnclosingSize
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicEnclosingSize;
|
||||
}
|
||||
}
|
||||
|
||||
private string _litematicEnclosingSize;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的区域数量。
|
||||
/// </summary>
|
||||
public int? LitematicRegionCount
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicRegionCount;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _litematicRegionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的总方块数。
|
||||
/// </summary>
|
||||
public int? LitematicTotalBlocks
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicTotalBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _litematicTotalBlocks;
|
||||
|
||||
/// <summary>
|
||||
/// Litematic 文件的总体积。
|
||||
/// </summary>
|
||||
public int? LitematicTotalVolume
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _litematicTotalVolume;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _litematicTotalVolume;
|
||||
|
||||
/// <summary>
|
||||
/// 原版结构文件的游戏版本。
|
||||
/// </summary>
|
||||
public string StructureGameVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _structureGameVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private string _structureGameVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 原版结构文件的数据版本。
|
||||
/// </summary>
|
||||
public int? StructureDataVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _structureDataVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _structureDataVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 原版结构文件的作者。
|
||||
/// </summary>
|
||||
public string StructureAuthor
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _structureAuthor;
|
||||
}
|
||||
}
|
||||
|
||||
private string _structureAuthor;
|
||||
|
||||
/// <summary>
|
||||
/// Sponge Schematic 文件的版本。
|
||||
/// </summary>
|
||||
public int? SpongeVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
LoadNbtDataIfNeeded();
|
||||
return _spongeVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private int? _spongeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Mod 图标路径。
|
||||
/// </summary>
|
||||
public string Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Dependencies
|
||||
{
|
||||
get
|
||||
{
|
||||
Load();
|
||||
return _Dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> _Dependencies = new();
|
||||
|
||||
private void AddDependency(string modID, string versionRequirement = null)
|
||||
{
|
||||
// 确保信息正确
|
||||
if (modID is null || modID.Length < 2)
|
||||
return;
|
||||
modID = modID.ToLower();
|
||||
if (modID == "name" || (ModBase.Val(modID).ToString() ?? "") == (modID ?? ""))
|
||||
return; // 跳过 name 与纯数字 id
|
||||
if (versionRequirement is null ||
|
||||
(!versionRequirement.Contains(".") && !versionRequirement.Contains("-")) ||
|
||||
versionRequirement.Contains("$"))
|
||||
versionRequirement = null;
|
||||
else if (!versionRequirement.StartsWithF("[") && !versionRequirement.StartsWithF("(") &&
|
||||
!versionRequirement.EndsWithF("]") && !versionRequirement.EndsWithF(")"))
|
||||
versionRequirement = "[" + versionRequirement + ",)";
|
||||
// 向依赖项中添加
|
||||
if (_Dependencies.ContainsKey(modID))
|
||||
{
|
||||
if (_Dependencies[modID] is null)
|
||||
_Dependencies[modID] = versionRequirement;
|
||||
}
|
||||
else
|
||||
{
|
||||
_Dependencies.Add(modID, versionRequirement);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 加载步骤标记
|
||||
|
||||
// 1. 进行文件可用性检查
|
||||
// 成功:继续第二步。
|
||||
// 失败:标记 FileUnavailableReason, 并停止后续加载。
|
||||
/// <summary>
|
||||
/// 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步)
|
||||
/// </summary>
|
||||
private bool isLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// 标记为已加载。用于内嵌(Jar-in-Jar)子项——其元数据已由 <see cref="ModJarInJar" /> 通过
|
||||
/// LookupMetadata 从已打开的嵌套流读入,虚拟路径不是真实文件,须避免属性 getter 再触发 Load() 清空元数据。
|
||||
/// </summary>
|
||||
internal void MarkLoaded() => isLoaded = true;
|
||||
|
||||
/// <summary>
|
||||
/// Mod 文件是否可被正常读取。
|
||||
/// </summary>
|
||||
public bool IsFileAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
Load();
|
||||
return FileUnavailableReason is null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mod 文件出错的原因。若无错误,则为 Nothing。
|
||||
/// </summary>
|
||||
public Exception FileUnavailableReason
|
||||
{
|
||||
get
|
||||
{
|
||||
Load();
|
||||
return _FileUnavailableReason;
|
||||
}
|
||||
}
|
||||
|
||||
private Exception _FileUnavailableReason;
|
||||
|
||||
// 2. 进行 .class 以外的信息获取
|
||||
// 成功:标记 IsInfoWithoutClassAvailable。
|
||||
// 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。
|
||||
/// <summary>
|
||||
/// 是否已在不获取 .class 文件的前提下完成了所需信息的加载。
|
||||
/// </summary>
|
||||
private bool isInfoWithoutClassAvailable = false;
|
||||
|
||||
// 3. 尝试从 .class 文件中获取信息
|
||||
// 成功:标记 IsInfoWithClassAvailable。
|
||||
// 失败:什么也不干。
|
||||
/// <summary>
|
||||
/// 是否已进行 .class 文件的信息获取。
|
||||
/// </summary>
|
||||
private bool isInfoWithClassLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已在 .class 文件中完成了所需信息的加载。
|
||||
/// </summary>
|
||||
private bool isInfoWithClassAvailable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 加载
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有数据。
|
||||
/// </summary>
|
||||
private void Init()
|
||||
{
|
||||
_Name = null;
|
||||
_Description = null;
|
||||
_Version = null;
|
||||
_ModId = null;
|
||||
possibleModId = new List<string>();
|
||||
_Dependencies = new Dictionary<string, string>();
|
||||
isLoaded = false;
|
||||
_FileUnavailableReason = null;
|
||||
isInfoWithClassLoaded = false;
|
||||
isInfoWithClassAvailable = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载基本信息(不解析NBT数据)。
|
||||
/// </summary>
|
||||
public void LoadBasicInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 可用性检查
|
||||
if (IsFolder)
|
||||
{
|
||||
// 文件夹项不需要进一步处理
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + path + ")");
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于原理图文件,只设置基本状态,不解析NBT数据
|
||||
if (path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) ||
|
||||
path.EndsWithF(".schem", true) || path.EndsWithF(".schematic", true))
|
||||
{
|
||||
_Name = ModBase.GetFileNameWithoutExtentionFromPath(path);
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于其他文件类型,正常加载
|
||||
Load();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"加载基本信息失败:{path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 延迟加载NBT数据。
|
||||
/// </summary>
|
||||
public void LoadNbtDataIfNeeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果已经加载过NBT数据,则跳过
|
||||
if (_nbtDataLoaded)
|
||||
return;
|
||||
|
||||
// 根据文件类型加载NBT数据
|
||||
if (path.EndsWithF(".litematic", true))
|
||||
LoadLitematicNbtData();
|
||||
else if (path.EndsWithF(".nbt", true))
|
||||
LoadStructureNbtData();
|
||||
else if (path.EndsWithF(".schem", true))
|
||||
LoadSchemNbtData();
|
||||
else if (path.EndsWithF(".schematic", true)) LoadSchematicNbtData();
|
||||
|
||||
_nbtDataLoaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"延迟加载NBT数据失败:{path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进行文件可用性检查与 .class 以外的信息获取。
|
||||
/// </summary>
|
||||
public void Load(bool forceReload = false)
|
||||
{
|
||||
if (isLoaded && !forceReload)
|
||||
return;
|
||||
// 初始化
|
||||
Init();
|
||||
|
||||
// 基础可用性检查
|
||||
if (path.Length < 2)
|
||||
{
|
||||
_FileUnavailableReason = new FileNotFoundException("错误的资源文件路径(" + (path ?? "null") + ")");
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于文件夹项,检查实际文件夹路径是否存在
|
||||
if (IsFolder)
|
||||
{
|
||||
if (!Directory.Exists(ActualPath))
|
||||
{
|
||||
_FileUnavailableReason = new DirectoryNotFoundException("未找到文件夹(" + ActualPath + ")");
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件夹项不需要进一步处理
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + path + ")");
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于投影文件,跳过 zip 解析
|
||||
if (path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) || path.EndsWithF(".schem", true) ||
|
||||
path.EndsWithF(".schematic", true))
|
||||
{
|
||||
try
|
||||
{
|
||||
_Name = ModBase.GetFileNameWithoutExtentionFromPath(path);
|
||||
// 根据文件类型加载数据
|
||||
if (path.EndsWithF(".litematic", true))
|
||||
{
|
||||
LoadLitematicNbtData();
|
||||
}
|
||||
else if (path.EndsWithF(".schem", true) || path.EndsWithF(".schematic", true))
|
||||
{
|
||||
if (path.EndsWithF(".schem", true))
|
||||
LoadSchemNbtData();
|
||||
else
|
||||
LoadSchematicNbtData();
|
||||
}
|
||||
else if (path.EndsWithF(".nbt", true))
|
||||
{
|
||||
LoadStructureNbtData();
|
||||
}
|
||||
|
||||
_nbtDataLoaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "投影文件信息获取失败(" + path + ")", ModBase.LogLevel.Developer);
|
||||
_FileUnavailableReason = ex;
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于其他文件,尝试作为 Jar 文件打开
|
||||
ZipArchive jar = null;
|
||||
try
|
||||
{
|
||||
jar = new ZipArchive(new FileStream(path, FileMode.Open));
|
||||
// 信息获取
|
||||
LookupMetadata(jar);
|
||||
EmbeddedMods = ModJarInJar.Resolve(path, jar);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
ModBase.Log(ex, "资源文件由于无权限无法打开(" + path + ")", ModBase.LogLevel.Developer);
|
||||
_FileUnavailableReason = new UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "资源文件无法打开(" + path + ")", ModBase.LogLevel.Developer);
|
||||
_FileUnavailableReason = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (jar is not null)
|
||||
jar.Dispose();
|
||||
}
|
||||
|
||||
// 完成标记
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内嵌模组列表,由 <see cref="ModJarInJar" /> 解析填充。
|
||||
/// </summary>
|
||||
public List<LocalCompFile> EmbeddedMods { get; internal set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 从 Jar 文件中获取 Mod 信息。
|
||||
/// </summary>
|
||||
internal void LookupMetadata(ZipArchive jar)
|
||||
{
|
||||
#region 尝试使用 mcmod.info
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取信息文件
|
||||
var infoEntry = jar.GetEntry("mcmod.info");
|
||||
string infoString = null;
|
||||
if (infoEntry is not null)
|
||||
{
|
||||
infoString = ModBase.ReadFile(infoEntry.Open());
|
||||
if (infoString.Length < 15)
|
||||
infoString = null;
|
||||
}
|
||||
|
||||
if (infoString is null)
|
||||
break;
|
||||
// 获取可用 Json 项
|
||||
JsonObject infoObject;
|
||||
var jsonObject = (JsonNode)ModBase.GetJson(infoString);
|
||||
if (jsonObject.GetValueKind() == JsonValueKind.Array)
|
||||
infoObject = (JsonObject)jsonObject[0];
|
||||
else
|
||||
infoObject = (JsonObject)jsonObject["modList"][0];
|
||||
// 从文件中获取 Mod 信息项
|
||||
Name = (string)infoObject["name"];
|
||||
Description = (string)infoObject["description"];
|
||||
Version = (string)infoObject["version"];
|
||||
Url = (string)infoObject["url"];
|
||||
ModId = (string)infoObject["modid"];
|
||||
var authorJson = (JsonArray)infoObject["authorList"];
|
||||
if (authorJson is not null)
|
||||
{
|
||||
var author = new List<string>();
|
||||
foreach (var Token in authorJson)
|
||||
author.Add(Token.ToString());
|
||||
if (author.Any())
|
||||
Authors = author.Join(", ");
|
||||
}
|
||||
|
||||
var logoFile = (string)infoObject["logoFile"];
|
||||
if (logoFile is not null)
|
||||
{
|
||||
var logoItem = jar.GetEntry(logoFile);
|
||||
if (logoItem is not null)
|
||||
{
|
||||
var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path);
|
||||
Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png");
|
||||
using (var entryStream = logoItem.Open())
|
||||
{
|
||||
ModBase.WriteFile(Logo, entryStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reqs = (JsonArray)infoObject["requiredMods"];
|
||||
if (reqs is not null)
|
||||
foreach (string item in reqs) // 将迭代变量重命名为 item
|
||||
if (!string.IsNullOrEmpty(item))
|
||||
{
|
||||
// 使用一个局部变量 token 来处理逻辑
|
||||
var token = item;
|
||||
|
||||
token = token.Substring(token.IndexOfF(":") + 1);
|
||||
if (token.Contains("@"))
|
||||
{
|
||||
var parts = token.Split("@");
|
||||
AddDependency(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddDependency(token);
|
||||
}
|
||||
}
|
||||
|
||||
reqs = (JsonArray)infoObject["dependencies"];
|
||||
if (reqs is not null)
|
||||
foreach (string rawToken in reqs)
|
||||
if (!string.IsNullOrEmpty(rawToken))
|
||||
{
|
||||
var id = rawToken.Substring(rawToken.IndexOfF(":") + 1);
|
||||
|
||||
if (id.Contains("@"))
|
||||
{
|
||||
var parts = id.Split("@");
|
||||
AddDependency(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddDependency(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 mcmod.info 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
} while (false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 尝试使用 fabric.mod.json
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var fabricEntry = jar.GetEntry("fabric.mod.json");
|
||||
string fabricText = null;
|
||||
if (fabricEntry is not null)
|
||||
{
|
||||
fabricText = ModBase.ReadFile(fabricEntry.Open(), Encoding.UTF8);
|
||||
if (!fabricText.Contains("schemaVersion")) fabricText = null;
|
||||
}
|
||||
|
||||
if (fabricText is null) break;
|
||||
|
||||
var fabricObject = (JsonObject)ModBase.GetJson(fabricText);
|
||||
|
||||
if (fabricObject.ContainsKey("name")) Name = fabricObject["name"].ToString();
|
||||
if (fabricObject.ContainsKey("version")) Version = fabricObject["version"].ToString();
|
||||
if (fabricObject.ContainsKey("description")) Description = fabricObject["description"].ToString();
|
||||
if (fabricObject.ContainsKey("id")) ModId = fabricObject["id"].ToString();
|
||||
if (fabricObject.ContainsKey("contact") && fabricObject["contact"]["homepage"] is not null)
|
||||
Url = fabricObject["contact"]["homepage"].ToString();
|
||||
|
||||
var authorJson = (JsonArray)fabricObject["authors"];
|
||||
if (authorJson is not null)
|
||||
{
|
||||
var authorList = authorJson.Select(t => t.ToString()).ToList();
|
||||
if (authorList.Any()) Authors = string.Join(", ", authorList);
|
||||
}
|
||||
|
||||
if (fabricObject.ContainsKey("icon"))
|
||||
{
|
||||
var logoFile = fabricObject["icon"].ToString();
|
||||
var logoItem = jar.GetEntry(logoFile);
|
||||
if (logoItem is not null)
|
||||
{
|
||||
var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path);
|
||||
Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png");
|
||||
using (var entryStream = logoItem.Open())
|
||||
{
|
||||
ModBase.WriteFile(Logo, entryStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 依赖处理 (省略了 VB 中的注释部分,按逻辑实现)
|
||||
if (fabricObject.ContainsKey("depends"))
|
||||
foreach (var dep in (JsonObject)fabricObject["depends"])
|
||||
AddDependency(dep.Key, dep.Value.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 fabric.mod.json 时出错(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
} while (false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 尝试使用 quilt.mod.json
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取 quilt.mod.json 文件
|
||||
var quiltEntry = jar.GetEntry("quilt.mod.json");
|
||||
string quiltText = null;
|
||||
if (quiltEntry is not null)
|
||||
{
|
||||
quiltText = ModBase.ReadFile(quiltEntry.Open(), Encoding.UTF8);
|
||||
if (!quiltText.Contains("schema_version"))
|
||||
quiltText = null;
|
||||
}
|
||||
|
||||
if (quiltText is null)
|
||||
break;
|
||||
var quiltObject = (JsonObject)((JsonObject)ModBase.GetJson(quiltText))["quilt_loader"];
|
||||
// 从文件中获取 Mod 信息项
|
||||
if (quiltObject.ContainsKey("id"))
|
||||
ModId = (string)quiltObject["id"];
|
||||
if (quiltObject.ContainsKey("version"))
|
||||
Version = (string)quiltObject["version"];
|
||||
if (quiltObject.ContainsKey("metadata"))
|
||||
{
|
||||
var quiltMetadata = (JsonObject)quiltObject["metadata"];
|
||||
if (quiltMetadata.ContainsKey("name"))
|
||||
Name = (string)quiltMetadata["name"];
|
||||
if (quiltMetadata.ContainsKey("description"))
|
||||
Description = (string)quiltMetadata["description"];
|
||||
if (quiltMetadata.ContainsKey("contact"))
|
||||
Url = (string)(quiltMetadata["contact"]["homepage"] ?? "");
|
||||
}
|
||||
|
||||
if (quiltObject.ContainsKey("icon"))
|
||||
{
|
||||
var logoFile = (string)quiltObject["icon"];
|
||||
if (logoFile is not null)
|
||||
{
|
||||
var logoItem = jar.GetEntry(logoFile);
|
||||
if (logoItem is not null)
|
||||
{
|
||||
var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path);
|
||||
Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png");
|
||||
using (var entryStream = logoItem.Open())
|
||||
{
|
||||
ModBase.WriteFile(Logo, entryStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goto Finished;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 quilt.mod.json 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
} while (false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 尝试使用 mods.toml
|
||||
|
||||
try
|
||||
{
|
||||
// 获取 mods.toml 文件
|
||||
var tomlEntry = jar.GetEntry("META-INF/mods.toml")
|
||||
?? jar.GetEntry("META-INF/neoforge.mods.toml");
|
||||
string tomlText = null;
|
||||
if (tomlEntry is not null)
|
||||
{
|
||||
using (var reader = new StreamReader(tomlEntry.Open()))
|
||||
{
|
||||
tomlText = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
if (tomlText.Length < 15) tomlText = null;
|
||||
}
|
||||
|
||||
if (tomlText is not null)
|
||||
{
|
||||
// 文件标准化:统一换行符为 \n,去除注释、头尾的空格、空行
|
||||
var lines = new List<string>();
|
||||
var rawLines = tomlText.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
|
||||
|
||||
foreach (var rawLine in rawLines)
|
||||
{
|
||||
var line = rawLine;
|
||||
if (line.StartsWithF("#")) continue; // 去除注释
|
||||
if (line.Contains("#")) line = line.Substring(0, line.IndexOfF("#"));
|
||||
// 去除头尾的空格(包含全角空格)
|
||||
line = line.Trim(' ', '\t', ' ');
|
||||
if (!string.IsNullOrEmpty(line)) lines.Add(line);
|
||||
}
|
||||
|
||||
// 读取文件数据
|
||||
// TomlData 存储段落名及其对应的键值对
|
||||
var tomlData = new List<KeyValuePair<string, Dictionary<string, object>>>
|
||||
{
|
||||
new("", new Dictionary<string, object>())
|
||||
};
|
||||
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
if (line.StartsWithF("[") && line.EndsWithF("]"))
|
||||
{
|
||||
// 段落标记
|
||||
var header = line.Trim('[', ']');
|
||||
tomlData.Add(
|
||||
new KeyValuePair<string, Dictionary<string, object>>(header,
|
||||
new Dictionary<string, object>()));
|
||||
}
|
||||
else if (line.Contains("="))
|
||||
{
|
||||
// 字段标记
|
||||
var key = line.Substring(0, line.IndexOfF("=")).TrimEnd(' ', '\t', ' ');
|
||||
var rawValue = line.Substring(line.IndexOfF("=") + 1).TrimStart(' ', '\t', ' ');
|
||||
object value;
|
||||
|
||||
if (rawValue.StartsWithF("\"") && rawValue.EndsWithF("\""))
|
||||
{
|
||||
// 单行字符串
|
||||
value = rawValue.Trim('\"');
|
||||
}
|
||||
else if (rawValue.StartsWithF("'''"))
|
||||
{
|
||||
// 多行字符串
|
||||
var valueLines = new List<string> { rawValue.Replace("'''", "") };
|
||||
if (!rawValue.EndsWithF("'''") || rawValue.Length == 3)
|
||||
while (i < lines.Count - 1)
|
||||
{
|
||||
i++;
|
||||
var valueLine = lines[i];
|
||||
if (valueLine.EndsWithF("'''"))
|
||||
{
|
||||
valueLines.Add(valueLine.Replace("'''", ""));
|
||||
break;
|
||||
}
|
||||
|
||||
valueLines.Add(valueLine);
|
||||
}
|
||||
|
||||
value = string.Join("\n", valueLines).Trim('\n').Replace("\n", "\r\n");
|
||||
}
|
||||
else if (rawValue.ToLower() == "true" || rawValue.ToLower() == "false")
|
||||
{
|
||||
// 布尔型
|
||||
value = rawValue.ToLower() == "true";
|
||||
}
|
||||
else if (double.TryParse(rawValue, out var num))
|
||||
{
|
||||
// 数字型 (模拟 VB 的 Val)
|
||||
value = num;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认当做字符串存储
|
||||
value = rawValue;
|
||||
}
|
||||
|
||||
// 将值存入当前最后的段落中
|
||||
var lastPair = tomlData[tomlData.Count - 1];
|
||||
lastPair.Value[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 从解析出的数据中提取 Mod 信息
|
||||
Dictionary<string, object> modEntry = null;
|
||||
foreach (var subData in tomlData)
|
||||
if (subData.Key == "mods")
|
||||
{
|
||||
modEntry = subData.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (modEntry is not null && modEntry.ContainsKey("modId"))
|
||||
{
|
||||
ModId = modEntry["modId"].ToString();
|
||||
// 假设 _ModId 是内部属性,如果为 null 说明设置失败
|
||||
if (_ModId is not null)
|
||||
{
|
||||
if (modEntry.ContainsKey("displayName")) Name = modEntry["displayName"].ToString();
|
||||
if (modEntry.ContainsKey("description")) Description = modEntry["description"].ToString();
|
||||
if (modEntry.ContainsKey("version")) Version = modEntry["version"].ToString();
|
||||
|
||||
// [0] 是全局段落(无 Header)
|
||||
if (tomlData[0].Value.ContainsKey("displayURL"))
|
||||
Url = tomlData[0].Value["displayURL"].ToString();
|
||||
if (tomlData[0].Value.ContainsKey("authors"))
|
||||
Authors = tomlData[0].Value["authors"].ToString();
|
||||
|
||||
// 读取依赖
|
||||
foreach (var subData in tomlData)
|
||||
if (subData.Key.ToLower() == $"dependencies.{ModId.ToLower()}")
|
||||
{
|
||||
var depEntry = subData.Value;
|
||||
if (depEntry.ContainsKey("modId") &&
|
||||
depEntry.ContainsKey("mandatory") && (bool)depEntry["mandatory"] &&
|
||||
depEntry.ContainsKey("side") &&
|
||||
depEntry["side"].ToString().ToLower() != "server")
|
||||
AddDependency(
|
||||
depEntry["modId"].ToString(),
|
||||
depEntry.ContainsKey("versionRange")
|
||||
? depEntry["versionRange"].ToString()
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
// 加载成功,跳转到完成标签
|
||||
goto Finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 mods.toml 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 尝试使用 fml_cache_annotation.json
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取 fml_cache_annotation.json 文件
|
||||
var fmlEntry = jar.GetEntry("META-INF/fml_cache_annotation.json");
|
||||
string fmlText = null;
|
||||
if (fmlEntry is not null)
|
||||
{
|
||||
fmlText = ModBase.ReadFile(fmlEntry.Open(), Encoding.UTF8);
|
||||
if (!fmlText.Contains("Lnet/minecraftforge/fml/common/Mod;"))
|
||||
fmlText = null;
|
||||
}
|
||||
|
||||
if (fmlText is null)
|
||||
break;
|
||||
var fmlJson = (JsonObject)ModBase.GetJson(fmlText);
|
||||
// 获取可用 Json 项
|
||||
JsonObject fmlObject = null;
|
||||
foreach (var ModFilePair in fmlJson)
|
||||
{
|
||||
var modFileAnnos = (JsonArray)ModFilePair.Value["annotations"];
|
||||
if (modFileAnnos is not null)
|
||||
// 先获取 Mod
|
||||
foreach (var ModFileAnno in modFileAnnos)
|
||||
{
|
||||
var name = (string)(ModFileAnno["name"] ?? "");
|
||||
if (name == "Lnet/minecraftforge/fml/common/Mod;")
|
||||
{
|
||||
fmlObject = (JsonObject)ModFileAnno["values"];
|
||||
goto Got;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
Got: ;
|
||||
|
||||
// 从文件中获取 Mod 信息项
|
||||
if (fmlObject.ContainsKey("useMetadata") &&
|
||||
(fmlObject["useMetadata"]["value"] ?? "").ToString().ToLower() == "true")
|
||||
{
|
||||
// 要求使用 mcmod.info 中的信息
|
||||
var value = (string)fmlObject["modid"]["value"];
|
||||
if (value is null)
|
||||
break;
|
||||
value = value.ToLower().RegexSeek(RegexPatterns.ModIdMatch);
|
||||
if (value is not null && value.ToLower() != "name" && value.Length > 1 &&
|
||||
(ModBase.Val(value).ToString() ?? "") != (value ?? ""))
|
||||
if (!possibleModId.Contains(value))
|
||||
possibleModId.Add(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (fmlObject.ContainsKey("name"))
|
||||
Name = (string)fmlObject["name"]["value"];
|
||||
if (fmlObject.ContainsKey("version"))
|
||||
Version = (string)fmlObject["version"]["value"];
|
||||
if (fmlObject.ContainsKey("modid"))
|
||||
ModId = (string)fmlObject["modid"]["value"];
|
||||
if (!fmlObject.ContainsKey("serverSideOnly") ||
|
||||
!fmlObject["serverSideOnly"]["value"].ToObject<bool>())
|
||||
{
|
||||
// 添加 Minecraft 依赖
|
||||
var depMinecraft = (string)((fmlObject["acceptedMinecraftVersions"] is not null
|
||||
? fmlObject["acceptedMinecraftVersions"]["value"]
|
||||
: "") ?? "");
|
||||
if (!string.IsNullOrEmpty(depMinecraft))
|
||||
AddDependency("minecraft", depMinecraft);
|
||||
// 添加其他依赖
|
||||
var deps = (string)((fmlObject["dependencies"] is not null
|
||||
? fmlObject["dependencies"]["value"]
|
||||
: "") ?? "");
|
||||
if (!string.IsNullOrEmpty(deps))
|
||||
foreach (var item in deps.Split(";"))
|
||||
{
|
||||
if (string.IsNullOrEmpty(item) || !item.StartsWithF("required-"))
|
||||
continue;
|
||||
|
||||
// 使用局部变量处理逻辑,不要直接修改迭代变量 item
|
||||
var dep = item.Substring(item.IndexOfF(":") + 1);
|
||||
|
||||
if (dep.Contains("@"))
|
||||
{
|
||||
var parts = dep.Split("@");
|
||||
AddDependency(parts[0], parts[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddDependency(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" + path + ")");
|
||||
}
|
||||
} while (false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 尝试识别资源包图标
|
||||
|
||||
try
|
||||
{
|
||||
// 检查并提取资源包的 pack.png 图标
|
||||
var packPngEntry = jar.GetEntry("pack.png");
|
||||
if (packPngEntry is not null)
|
||||
try
|
||||
{
|
||||
var md5 = ModBase.GetStringMD5(packPngEntry.Length + packPngEntry.CompressedLength + path);
|
||||
Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png");
|
||||
using (var entryStream = packPngEntry.Open())
|
||||
{
|
||||
ModBase.WriteFile(Logo, entryStream);
|
||||
}
|
||||
|
||||
ModBase.Log("成功提取资源包图标:" + path, ModBase.LogLevel.Developer);
|
||||
}
|
||||
catch (Exception logoEx)
|
||||
{
|
||||
ModBase.Log(logoEx, "提取 pack.png 图标失败(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "识别资源包图标时出现未知错误(" + path + ")", ModBase.LogLevel.Developer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Finished: ;
|
||||
|
||||
#region 将 Version 代号转换为 META-INF 中的版本
|
||||
|
||||
if (_Version == "version")
|
||||
try
|
||||
{
|
||||
var metaEntry = jar.GetEntry("META-INF/MANIFEST.MF");
|
||||
if (metaEntry is not null)
|
||||
{
|
||||
var metaString = ModBase.ReadFile(metaEntry.Open()).Replace(" :", ":").Replace(": ", ":");
|
||||
if (metaString.Contains("Implementation-Version:"))
|
||||
{
|
||||
metaString = metaString.Substring(metaString.IndexOfF("Implementation-Version:") +
|
||||
"Implementation-Version:".Count());
|
||||
metaString = metaString.Substring(0, metaString.IndexOfAny("\r\n".ToCharArray()))
|
||||
.Trim();
|
||||
Version = metaString;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log("获取 META-INF 中的版本信息失败(" + path + ")", ModBase.LogLevel.Developer);
|
||||
Version = null;
|
||||
}
|
||||
|
||||
if (_Version is not null && !(_Version.Contains(".") || _Version.Contains("-")))
|
||||
Version = null;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 网络信息
|
||||
|
||||
/// <summary>
|
||||
/// 当任何网络信息更新时触发。
|
||||
/// </summary>
|
||||
public event OnCompUpdateEventHandler? OnCompUpdate;
|
||||
|
||||
public delegate void OnCompUpdateEventHandler(LocalCompFile sender);
|
||||
|
||||
/// <summary>
|
||||
/// 该 Mod 关联的网络项目。
|
||||
/// </summary>
|
||||
public CompProject Comp
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnCompUpdate?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地文件对应的联网文件信息。
|
||||
/// </summary>
|
||||
public CompFile compFile;
|
||||
|
||||
/// <summary>
|
||||
/// 该 Mod 对应的联网最新版本。
|
||||
/// </summary>
|
||||
public CompFile UpdateFile
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnCompUpdate?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该 Mod 的更新日志网址。
|
||||
/// </summary>
|
||||
public List<string> changelogUrls = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有网络信息是否已成功加载。
|
||||
/// </summary>
|
||||
public bool compLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// 将网络信息保存为 Json。
|
||||
/// </summary>
|
||||
public JsonObject ToJson()
|
||||
{
|
||||
var json = new JsonObject();
|
||||
if (Comp is not null)
|
||||
json.Add("Comp", Comp.ToJson());
|
||||
json.Add("ChangelogUrls", new JsonArray(changelogUrls.Select(s => (JsonNode)s).ToArray()));
|
||||
json.Add("CompLoaded", compLoaded);
|
||||
if (compFile is not null)
|
||||
json.Add("CompFile", compFile.ToJson());
|
||||
if (UpdateFile is not null)
|
||||
json.Add("UpdateFile", UpdateFile.ToJson());
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Json 中读取网络信息。
|
||||
/// </summary>
|
||||
public void FromJson(JsonObject json)
|
||||
{
|
||||
compLoaded = (bool)json["CompLoaded"];
|
||||
if (json.ContainsKey("Comp"))
|
||||
Comp = new CompProject((JsonObject)json["Comp"]);
|
||||
if (json.ContainsKey("ChangelogUrls"))
|
||||
changelogUrls = json["ChangelogUrls"].ToObject<List<string>>();
|
||||
if (json.ContainsKey("CompFile"))
|
||||
compFile = new CompFile((JsonObject)json["CompFile"], CompType.Mod);
|
||||
if (json.ContainsKey("UpdateFile"))
|
||||
UpdateFile = new CompFile((JsonObject)json["UpdateFile"], CompType.Mod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该文件是否可以更新。
|
||||
/// </summary>
|
||||
public bool CanUpdate => !Config.Preference.Hide.FunctionModUpdate && changelogUrls.Any();
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
|
||||
/// </summary>
|
||||
public uint CurseForgeHash
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_CurseForgeHash is null)
|
||||
{
|
||||
var buf = _hashCache.Value
|
||||
.GetMurmurHash2Async(path)
|
||||
.GetAwaiter().GetResult()
|
||||
.HexToBytes();
|
||||
_CurseForgeHash = BitConverter.ToUInt32(buf);
|
||||
}
|
||||
|
||||
return (uint)_CurseForgeHash;
|
||||
}
|
||||
}
|
||||
|
||||
private uint? _CurseForgeHash;
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
|
||||
/// </summary>
|
||||
public string ModrinthHash
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field is null)
|
||||
field = _hashCache.Value.GetSHA1Async(path).GetAwaiter().GetResult();
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{State} - {path}";
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var target = obj as LocalCompFile;
|
||||
return target is not null && (path ?? "") == (target.path ?? "");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件夹描述信息。
|
||||
/// </summary>
|
||||
private static string GetFolderDescription(string folderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(folderPath))
|
||||
return "空文件夹";
|
||||
return "文件夹";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"获取文件夹描述失败:{folderPath}");
|
||||
return "文件夹";
|
||||
}
|
||||
}
|
||||
|
||||
public class CompLocalLoaderData
|
||||
{
|
||||
public string compPath;
|
||||
public CompType compType;
|
||||
|
||||
public KeyValuePair<List<LocalCompFile>, JsonObject> detailInfo;
|
||||
public PageInstanceCompResource frm;
|
||||
public McInstance gameVersion;
|
||||
public List<CompLoaderType> loaders;
|
||||
}
|
||||
|
||||
// 加载资源列表
|
||||
public static LoaderTask<CompLocalLoaderData, List<LocalCompFile>> compResourceListLoader =
|
||||
new("Comp Resource List Loader", CompResourceListLoad);
|
||||
|
||||
private static void CompResourceListLoad(LoaderTask<CompLocalLoaderData, List<LocalCompFile>> loader)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (loader.input.frm is not null) loader.input.frm.Load.ShowProgress = false;
|
||||
});
|
||||
|
||||
// 等待 Mod 更新完成
|
||||
if (PageInstanceCompResource.updatingVersions.Contains(loader.input.compPath))
|
||||
{
|
||||
ModBase.Log("[Mod] 等待资源更新完成后才能继续加载资源列表:" + loader.input.compPath);
|
||||
try
|
||||
{
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (loader.input.frm is not null)
|
||||
loader.input.frm.Load.Text = Lang.Text("Instance.Resource.Update.WaitingForUpdate");
|
||||
});
|
||||
while (PageInstanceCompResource.updatingVersions.Contains(loader.input.compPath))
|
||||
{
|
||||
if (loader.IsAborted)
|
||||
return;
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
if (loader.input.frm is not null)
|
||||
loader.input.frm.Load.Text = Lang.Text("Instance.Resource.List.Loading");
|
||||
});
|
||||
}
|
||||
|
||||
loader.input.frm.LoaderRun(LoaderFolderRunType.UpdateOnly);
|
||||
}
|
||||
|
||||
// 获取 Mod 文件夹下的可用文件列表
|
||||
var modList = new List<LocalCompFile>();
|
||||
if (Directory.Exists(loader.input.compPath))
|
||||
{
|
||||
var rawName = loader.input.compPath.ToLower();
|
||||
|
||||
if (loader.input.compType == CompType.Schematic)
|
||||
{
|
||||
var currentFolderPath = "";
|
||||
if (loader.input.frm is not null) currentFolderPath = loader.input.frm.CurrentFolderPath;
|
||||
|
||||
var searchPath = string.IsNullOrEmpty(currentFolderPath)
|
||||
? loader.input.compPath
|
||||
: currentFolderPath;
|
||||
|
||||
try
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(searchPath);
|
||||
foreach (var Dir in dirInfo.EnumerateDirectories("*", SearchOption.AllDirectories))
|
||||
modList.Add(new LocalCompFile(Path.Combine(Dir.FullName, "__FOLDER__")));
|
||||
foreach (var File in dirInfo.EnumerateFiles("*", SearchOption.AllDirectories))
|
||||
try
|
||||
{
|
||||
if (LocalCompFile.IsCompFile(File.FullName, loader.input.compType))
|
||||
modList.Add(new LocalCompFile(File.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"处理文件失败:{File.FullName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"枚举文件失败:{searchPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var File in ModBase.EnumerateFiles(loader.input.compPath))
|
||||
try
|
||||
{
|
||||
if ((File.DirectoryName.ToLower() ?? "") != (rawName.TrimEnd('\\') ?? ""))
|
||||
if (!(PageInstanceLeft.McInstance is not null &&
|
||||
PageInstanceLeft.McInstance.Info.HasForge &&
|
||||
PageInstanceLeft.McInstance.Info.Drop < 130 && (File.Directory.Name ?? "") ==
|
||||
(PageInstanceLeft.McInstance.Info.VanillaName ?? "")))
|
||||
continue;
|
||||
|
||||
if (LocalCompFile.IsCompFile(File.FullName, loader.input.compType))
|
||||
modList.Add(new LocalCompFile(File.FullName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"处理文件失败:{File.FullName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"枚举文件夹失败:{loader.input.compPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确定是否显示进度
|
||||
loader.Progress = 0.05d;
|
||||
if (modList.Count > 50)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (loader.input.frm is not null) loader.input.frm.Load.ShowProgress = true;
|
||||
});
|
||||
|
||||
// 获取本地文件缓存
|
||||
var cachePath = ModBase.pathTemp + @"Cache\LocalComp.json";
|
||||
var cache = new JsonObject();
|
||||
try
|
||||
{
|
||||
var cacheContent = ModBase.ReadFile(cachePath);
|
||||
if (!string.IsNullOrWhiteSpace(cacheContent))
|
||||
{
|
||||
cache = (JsonObject)ModBase.GetJson(cacheContent);
|
||||
if (!cache.ContainsKey("version") || cache["version"].ToObject<int>() != localModCacheVersion)
|
||||
{
|
||||
ModBase.Log("[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", ModBase.LogLevel.Debug);
|
||||
cache = new JsonObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取本地 Mod 信息缓存失败,已重置");
|
||||
cache = new JsonObject();
|
||||
}
|
||||
|
||||
cache["version"] = localModCacheVersion;
|
||||
|
||||
// 加载 Mod 列表 - 优化:对于原理图文件,延迟加载NBT数据
|
||||
var modUpdateList = new List<LocalCompFile>();
|
||||
foreach (var ModEntry in modList)
|
||||
{
|
||||
loader.Progress += 0.94d / modList.Count;
|
||||
if (loader.IsAborted)
|
||||
return;
|
||||
if (ModEntry.IsFolder)
|
||||
continue;
|
||||
|
||||
// 优化:对于原理图文件,只进行基础加载,不解析NBT数据
|
||||
if (loader.input.compType == CompType.Schematic)
|
||||
ModEntry.LoadBasicInfo();
|
||||
else
|
||||
// 加载 McMod 对象
|
||||
ModEntry.Load();
|
||||
|
||||
// 读取 Comp 缓存
|
||||
if (ModEntry.State == LocalCompFile.LocalFileStatus.Unavailable)
|
||||
continue;
|
||||
var cacheKey = ModEntry.ModrinthHash + loader.input.gameVersion.Info.VanillaName +
|
||||
loader.input.loaders.Join("");
|
||||
if (cache.ContainsKey(cacheKey))
|
||||
{
|
||||
ModEntry.FromJson((JsonObject)cache[cacheKey]);
|
||||
// 如果缓存中的信息在 6 小时以内更新过,则无需重新获取
|
||||
if (ModEntry.compLoaded &&
|
||||
DateTime.Now - cache[cacheKey]["Comp"]["CacheTime"].ToObject<DateTime>() <
|
||||
new TimeSpan(6, 0, 0))
|
||||
continue;
|
||||
}
|
||||
|
||||
modUpdateList.Add(ModEntry);
|
||||
}
|
||||
|
||||
loader.Progress = 0.99d;
|
||||
ModBase.Log(
|
||||
$"[Mod] 共有 {modList.Count} 个 Mod,其中 {modUpdateList.Where(m => m.Comp is null).Count()} 个需要联网获取信息,{modUpdateList.Where(m => m.Comp is not null).Count()} 个需要更新信息");
|
||||
|
||||
// 排序
|
||||
modList.Sort((left, right) =>
|
||||
{
|
||||
if (left.State == LocalCompFile.LocalFileStatus.Unavailable !=
|
||||
(right.State == LocalCompFile.LocalFileStatus.Unavailable))
|
||||
return left.State == LocalCompFile.LocalFileStatus.Unavailable ? 1 : -1;
|
||||
|
||||
return right.FileName.CompareTo(left.FileName);
|
||||
});
|
||||
|
||||
// 回设
|
||||
if (loader.IsAborted)
|
||||
return;
|
||||
loader.output = modList;
|
||||
|
||||
// 开始联网加载
|
||||
if (modUpdateList.Any())
|
||||
{
|
||||
// TODO: 添加信息获取中提示
|
||||
loader.input.detailInfo = new KeyValuePair<List<LocalCompFile>, JsonObject>(modUpdateList, cache);
|
||||
compUpdateDetailLoader.Start(loader.input, true);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "Mod 列表加载失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 联网加载 Mod 详情
|
||||
public static LoaderTask<CompLocalLoaderData, int> compUpdateDetailLoader =
|
||||
new("Comp List Detail Loader", CompUpdateDetailLoad);
|
||||
|
||||
private static void CompUpdateDetailLoad(LoaderTask<CompLocalLoaderData, int> loader)
|
||||
{
|
||||
var mods = loader.input.detailInfo.Key;
|
||||
var cache = loader.input.detailInfo.Value;
|
||||
// 获取作为检查目标的加载器和版本
|
||||
var modLoaders = loader.input.loaders;
|
||||
var compType = loader.input.compType;
|
||||
var mcInstance = loader.input.gameVersion.Info.VanillaName;
|
||||
|
||||
// 开始网络获取
|
||||
ModBase.Log($"[Mod] 目标加载器:{string.Join("/", modLoaders)},版本:{mcInstance}");
|
||||
var endedThreadCount = 0;
|
||||
var isFailed = false;
|
||||
var currentTaskId = Task.CurrentId ?? -1;
|
||||
|
||||
// 从 Modrinth 获取信息
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 步骤 1:获取 Hash 与对应的工程 ID
|
||||
var modrinthHashes = mods.Select(m => m.ModrinthHash).ToList();
|
||||
var modrinthVersion = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.modrinth.com/v2/version_files", "POST",
|
||||
$"{{\"hashes\": [\"{string.Join("\",\"", modrinthHashes)}\"], \"algorithm\": \"sha1\"}}",
|
||||
"application/json"));
|
||||
ModBase.Log($"[Mod] 从 Modrinth 获取到 {modrinthVersion.Count} 个本地 Mod 的对应信息");
|
||||
|
||||
// 步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
|
||||
if (modrinthVersion.Count == 0) return;
|
||||
var modrinthMapping = new Dictionary<string, List<LocalCompFile>>();
|
||||
foreach (var Entry in mods)
|
||||
{
|
||||
if (modrinthVersion[Entry.ModrinthHash] is null) continue;
|
||||
if (modrinthVersion[Entry.ModrinthHash]["files"][0]["hashes"]["sha1"].ToString() !=
|
||||
Entry.ModrinthHash) continue;
|
||||
|
||||
var projectId = modrinthVersion[Entry.ModrinthHash]["project_id"].ToString();
|
||||
// 读取已加载的缓存,加快结果出现速度
|
||||
if (compProjectCache.ContainsKey(projectId) && Entry.Comp is null)
|
||||
Entry.Comp = compProjectCache[projectId];
|
||||
|
||||
if (!modrinthMapping.ContainsKey(projectId)) modrinthMapping[projectId] = new List<LocalCompFile>();
|
||||
modrinthMapping[projectId].Add(Entry);
|
||||
|
||||
// 记录对应的 CompFile
|
||||
var fileInfo = new CompFile((JsonObject)modrinthVersion[Entry.ModrinthHash], CompType.Mod);
|
||||
if (Entry.compFile is null || Entry.compFile.ReleaseDate < fileInfo.ReleaseDate)
|
||||
Entry.compFile = fileInfo;
|
||||
}
|
||||
|
||||
if (loader.IsAbortedWithThread(currentTaskId)) return;
|
||||
ModBase.Log($"[Mod] 需要从 Modrinth 获取 {modrinthMapping.Count} 个本地 Mod 的工程信息");
|
||||
|
||||
// 步骤 3:获取工程信息
|
||||
if (!modrinthMapping.Any()) return;
|
||||
var modrinthProject = (JsonArray)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
$"https://api.modrinth.com/v2/projects?ids=[\"{string.Join("\",\"", modrinthMapping.Keys)}\"]",
|
||||
"GET", "", "application/json"));
|
||||
|
||||
foreach (var ProjectJson in modrinthProject)
|
||||
{
|
||||
var project = new CompProject((JsonObject)ProjectJson);
|
||||
foreach (var Entry in modrinthMapping[project.Id]) Entry.Comp = project;
|
||||
}
|
||||
|
||||
ModBase.Log("[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息");
|
||||
|
||||
// 步骤 4:获取更新信息
|
||||
var targetLoaders = compType == CompType.DataPack
|
||||
? "datapack"
|
||||
: string.Join("\",\"", modLoaders).ToLower();
|
||||
var modrinthUpdate = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.modrinth.com/v2/version_files/update", "POST",
|
||||
$"{{\"hashes\": [\"{string.Join("\",\"", modrinthMapping.SelectMany(l => l.Value.Select(m => m.ModrinthHash)))}\"], \"algorithm\": \"sha1\", " +
|
||||
$"\"loaders\": [\"{targetLoaders}\"],\"game_versions\": [\"{mcInstance}\"]}}", "application/json"));
|
||||
|
||||
foreach (var Entry in mods)
|
||||
{
|
||||
if (modrinthUpdate[Entry.ModrinthHash] is null || Entry.compFile is null) continue;
|
||||
var updateFile = new CompFile((JsonObject)modrinthUpdate[Entry.ModrinthHash], CompType.Mod);
|
||||
if (!updateFile.Available) continue;
|
||||
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log($"[Mod] 本地文件 {Entry.compFile.FileName} 在 Modrinth 上的最新版为 {updateFile.FileName}");
|
||||
if (Entry.compFile.ReleaseDate >= updateFile.ReleaseDate ||
|
||||
Entry.compFile.Hash == updateFile.Hash) continue;
|
||||
|
||||
// 设置更新日志与更新文件
|
||||
if (Entry.UpdateFile is not null && updateFile.Hash == Entry.UpdateFile.Hash)
|
||||
{
|
||||
Entry.changelogUrls.Add(
|
||||
$"https://modrinth.com/mod/{modrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={mcInstance}");
|
||||
Entry.UpdateFile.DownloadUrls.AddRange(updateFile.DownloadUrls);
|
||||
Entry.UpdateFile = updateFile;
|
||||
}
|
||||
else if (Entry.UpdateFile is null || updateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate)
|
||||
{
|
||||
Entry.changelogUrls = new List<string>
|
||||
{
|
||||
$"https://modrinth.com/mod/{modrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={mcInstance}"
|
||||
};
|
||||
Entry.UpdateFile = updateFile;
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.Log("[Mod] 从 Modrinth 获取本地 Mod 信息结束");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败");
|
||||
isFailed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Increment(ref endedThreadCount);
|
||||
}
|
||||
}, "Mod List Detail Loader Modrinth");
|
||||
|
||||
// 从 CurseForge 获取信息(工程 ID 与文件 ID 均为数字)
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 步骤 1:获取 Hash 与对应的工程信息
|
||||
var curseForgeHashes = new List<uint>();
|
||||
foreach (var entry in mods)
|
||||
{
|
||||
curseForgeHashes.Add(entry.CurseForgeHash);
|
||||
if (loader.IsAbortedWithThread(currentTaskId)) return;
|
||||
}
|
||||
|
||||
var curseForgeResponse = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.curseforge.com/v1/fingerprints/432", "POST",
|
||||
$"{{\"fingerprints\": [{string.Join(",", curseForgeHashes)}]}}", "application/json"));
|
||||
var curseForgeRaw = (JsonArray)curseForgeResponse["data"]["exactMatches"];
|
||||
ModBase.Log($"[Mod] 从 CurseForge 获取到 {curseForgeRaw.Count} 个本地 Mod 的对应信息");
|
||||
|
||||
// 步骤 2:尝试读取工程信息缓存,构建本地文件与工程 ID 的对应关系
|
||||
if (!curseForgeRaw.Any()) return;
|
||||
var curseForgeMapping = new Dictionary<string, List<LocalCompFile>>();
|
||||
foreach (var project in curseForgeRaw)
|
||||
{
|
||||
var projectId = project["id"].ToString();
|
||||
var hash = project["file"]["fileFingerprint"].ToObject<uint>();
|
||||
foreach (var Entry in mods)
|
||||
{
|
||||
if (Entry.CurseForgeHash != hash) continue;
|
||||
// 读取已加载的缓存,加快结果出现速度
|
||||
if (compProjectCache.ContainsKey(projectId) && Entry.Comp is null)
|
||||
Entry.Comp = compProjectCache[projectId];
|
||||
|
||||
if (!curseForgeMapping.ContainsKey(projectId))
|
||||
curseForgeMapping[projectId] = new List<LocalCompFile>();
|
||||
curseForgeMapping[projectId].Add(Entry);
|
||||
|
||||
// 记录对应的 CompFile
|
||||
var fileInfo = new CompFile((JsonObject)project["file"], CompType.Mod);
|
||||
if (Entry.compFile is null || Entry.compFile.ReleaseDate < fileInfo.ReleaseDate)
|
||||
Entry.compFile = fileInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (loader.IsAbortedWithThread(currentTaskId)) return;
|
||||
ModBase.Log($"[Mod] 需要从 CurseForge 获取 {curseForgeMapping.Count} 个本地 Mod 的工程信息");
|
||||
|
||||
// 步骤 3:获取工程信息,同时归纳需要检查更新的文件 ID
|
||||
if (!curseForgeMapping.Any()) return;
|
||||
var curseForgeProject = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.curseforge.com/v1/mods", "POST",
|
||||
$"{{\"modIds\": [{string.Join(",", curseForgeMapping.Keys)}]}}", "application/json")))["data"];
|
||||
|
||||
var updateFileIds = new Dictionary<int, List<LocalCompFile>>(); // FileId -> 本地 Mod 文件列表
|
||||
var fileIdToProjectSlug = new Dictionary<int, string>();
|
||||
foreach (var projectJson in curseForgeProject)
|
||||
{
|
||||
if (projectJson["isAvailable"] is not null && !projectJson["isAvailable"].ToObject<bool>())
|
||||
continue;
|
||||
|
||||
// 设置 Entry 中的工程信息
|
||||
var project = new CompProject((JsonObject)projectJson);
|
||||
if (!curseForgeMapping.ContainsKey(project.Id)) continue;
|
||||
foreach (var Entry in curseForgeMapping[project.Id])
|
||||
{
|
||||
// 若已从 Modrinth 获取到工程信息,则保留它,仅再次触发修改事件以刷新 UI
|
||||
if (Entry.Comp is not null && !Entry.Comp.FromCurseForge)
|
||||
{
|
||||
var existing = Entry.Comp;
|
||||
Entry.Comp = existing;
|
||||
continue;
|
||||
}
|
||||
|
||||
Entry.Comp = project;
|
||||
}
|
||||
|
||||
// 仅在加载器唯一时查找可能有更新的文件
|
||||
if (modLoaders.Count != 1) continue;
|
||||
string newestVersion = null;
|
||||
var newestFileIds = new List<int>();
|
||||
foreach (var indexEntry in (JsonArray)projectJson["latestFilesIndexes"])
|
||||
{
|
||||
// 加载器唯一且匹配
|
||||
if (compType != CompType.DataPack &&
|
||||
(indexEntry["modLoader"] is null ||
|
||||
(int)modLoaders.Single() != indexEntry["modLoader"].ToObject<int>()))
|
||||
continue;
|
||||
|
||||
var indexVersion = indexEntry["gameVersion"].ToString();
|
||||
if (indexVersion != mcInstance) continue; // MC 版本匹配
|
||||
// latestFilesIndexes 按时间从新到老排序,只保留最新的 MC 版本
|
||||
if (newestVersion is not null &&
|
||||
McVersionComparer.CompareVersion(newestVersion, indexVersion) > -1)
|
||||
continue;
|
||||
|
||||
if (newestVersion != indexVersion)
|
||||
{
|
||||
newestVersion = indexVersion;
|
||||
newestFileIds.Clear();
|
||||
}
|
||||
|
||||
newestFileIds.Add(indexEntry["fileId"].ToObject<int>());
|
||||
}
|
||||
|
||||
foreach (var fileId in newestFileIds)
|
||||
{
|
||||
if (!updateFileIds.ContainsKey(fileId)) updateFileIds[fileId] = new List<LocalCompFile>();
|
||||
updateFileIds[fileId].AddRange(curseForgeMapping[project.Id]);
|
||||
fileIdToProjectSlug[fileId] = project.Slug;
|
||||
}
|
||||
}
|
||||
|
||||
if (loader.IsAbortedWithThread(currentTaskId)) return;
|
||||
ModBase.Log(
|
||||
$"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {updateFileIds.Count} 个用于检查更新的文件信息");
|
||||
|
||||
// 步骤 4:获取更新文件信息
|
||||
if (!updateFileIds.Any()) return;
|
||||
var curseForgeFiles = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.curseforge.com/v1/mods/files", "POST",
|
||||
$"{{\"fileIds\": [{string.Join(",", updateFileIds.Keys)}]}}", "application/json")))["data"];
|
||||
|
||||
var updateFiles = new Dictionary<LocalCompFile, CompFile>();
|
||||
foreach (var fileJson in curseForgeFiles)
|
||||
{
|
||||
var updateFile = new CompFile((JsonObject)fileJson, CompType.Mod);
|
||||
if (!updateFile.Available) continue;
|
||||
if (!int.TryParse(updateFile.Id, out var fileId) || !updateFileIds.ContainsKey(fileId))
|
||||
continue;
|
||||
foreach (var Entry in updateFileIds[fileId])
|
||||
{
|
||||
if (updateFiles.ContainsKey(Entry) && updateFiles[Entry].ReleaseDate >= updateFile.ReleaseDate)
|
||||
continue;
|
||||
updateFiles[Entry] = updateFile;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in updateFiles)
|
||||
{
|
||||
var Entry = pair.Key;
|
||||
var updateFile = pair.Value;
|
||||
if (Entry.compFile is null) continue;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log(
|
||||
$"[Mod] 本地文件 {Entry.compFile.FileName} 在 CurseForge 上的最新版为 {updateFile.FileName}");
|
||||
if (Entry.compFile.ReleaseDate >= updateFile.ReleaseDate ||
|
||||
Entry.compFile.Hash == updateFile.Hash) continue;
|
||||
|
||||
var changelogUrl =
|
||||
$"https://www.curseforge.com/minecraft/mc-mods/{fileIdToProjectSlug[int.Parse(updateFile.Id)]}/files/{updateFile.Id}";
|
||||
|
||||
// 设置更新日志与更新文件
|
||||
if (Entry.UpdateFile is not null && updateFile.Hash == Entry.UpdateFile.Hash)
|
||||
{
|
||||
// 合并下载源
|
||||
Entry.changelogUrls.Add(changelogUrl);
|
||||
Entry.UpdateFile.DownloadUrls.AddRange(updateFile.DownloadUrls);
|
||||
}
|
||||
else if (Entry.UpdateFile is null || updateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate)
|
||||
{
|
||||
// 替换
|
||||
Entry.changelogUrls = new List<string> { changelogUrl };
|
||||
Entry.UpdateFile = updateFile;
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.Log("[Mod] 从 CurseForge 获取 Mod 更新信息结束");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败");
|
||||
isFailed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Increment(ref endedThreadCount);
|
||||
}
|
||||
}, "Mod List Detail Loader CurseForge");
|
||||
|
||||
// 等待线程结束
|
||||
while (endedThreadCount < 2)
|
||||
{
|
||||
if (loader.IsAborted) return;
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
// 保存缓存
|
||||
var cachedMods = mods.Where(m => m.Comp is not null).ToList();
|
||||
ModBase.Log($"[Mod] 联网获取本地 Mod 信息完成,为 {cachedMods.Count} 个 Mod 更新缓存");
|
||||
if (!cachedMods.Any()) return;
|
||||
|
||||
foreach (var Entry in cachedMods)
|
||||
{
|
||||
Entry.compLoaded = !isFailed;
|
||||
cache[Entry.ModrinthHash + mcInstance + string.Join("", modLoaders)] = Entry.ToJson();
|
||||
}
|
||||
|
||||
ModBase.WriteFile(Path.Combine(ModBase.pathTemp, "Cache", "LocalComp.json"),
|
||||
cache.ToJsonString(ModBase.modeDebug ? new JsonSerializerOptions(JsonCompat.SerializerOptions) { WriteIndented = true } : null));
|
||||
|
||||
// 刷新 UI
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.frmInstanceMod?.Filter == PageInstanceCompResource.FilterType.CanUpdate)
|
||||
ModMain.frmInstanceMod?.RefreshUI();
|
||||
else
|
||||
ModMain.frmInstanceMod?.RefreshBars();
|
||||
});
|
||||
}
|
||||
|
||||
public static List<CompLoaderType> GetCurrentVersionModLoader()
|
||||
{
|
||||
var modLoaders = new List<CompLoaderType>();
|
||||
if (PageInstanceLeft.McInstance.Info.HasForge)
|
||||
modLoaders.Add(CompLoaderType.Forge);
|
||||
if (PageInstanceLeft.McInstance.Info.HasNeoForge)
|
||||
modLoaders.Add(CompLoaderType.NeoForge);
|
||||
if (PageInstanceLeft.McInstance.Info.HasFabric)
|
||||
modLoaders.Add(CompLoaderType.Fabric);
|
||||
if (PageInstanceLeft.McInstance.Info.HasQuilt)
|
||||
modLoaders.AddRange(new[] { CompLoaderType.Fabric, CompLoaderType.Quilt });
|
||||
if (PageInstanceLeft.McInstance.Info.HasLiteLoader)
|
||||
modLoaders.Add(CompLoaderType.LiteLoader);
|
||||
if (!modLoaders.Any())
|
||||
modLoaders.AddRange(new[]
|
||||
{
|
||||
CompLoaderType.Forge, CompLoaderType.NeoForge, CompLoaderType.Fabric, CompLoaderType.LiteLoader,
|
||||
CompLoaderType.Quilt
|
||||
});
|
||||
return modLoaders;
|
||||
}
|
||||
|
||||
public static string GetPathNameByCompType(CompType theType)
|
||||
{
|
||||
switch (theType)
|
||||
{
|
||||
case CompType.Mod:
|
||||
{
|
||||
return "mods";
|
||||
}
|
||||
case CompType.ResourcePack:
|
||||
{
|
||||
return "resourcepacks";
|
||||
}
|
||||
case CompType.Shader:
|
||||
{
|
||||
return "shaderpacks";
|
||||
}
|
||||
case CompType.Schematic:
|
||||
{
|
||||
return "schematics";
|
||||
}
|
||||
case CompType.World:
|
||||
{
|
||||
return "saves";
|
||||
}
|
||||
}
|
||||
|
||||
return "Nothing";
|
||||
}
|
||||
|
||||
private static readonly Regex regexIsJarFile = new(@"\.jar(\.disabled)?$");
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件名关键字和 Mod ID 比如 <c>fabric</c> <c>api</c> 和 <c>fabric-api</c> 来获取给定实例 mods 目录中某个 Mod 的
|
||||
/// <see cref="LocalCompFile" /> 对象
|
||||
/// <br />
|
||||
/// <b>为了不浪费性能,关键字统一用小写</b>
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// 如果文件名包含主关键字,以及其他关键字中的任意一个,同时 Mod ID 一致,即认为匹配,返回对应的对象,若没有匹配的文件则返回空值。
|
||||
/// </returns>
|
||||
public static LocalCompFile GetModLocalCompByKeywords(McInstance instance, string modId,
|
||||
string mainKeyword, params string[] keywords)
|
||||
{
|
||||
if (modId is null)
|
||||
return null;
|
||||
return GetModLocalCompByKeywords(instance, new[] { modId }, mainKeyword, keywords);
|
||||
}
|
||||
|
||||
public static LocalCompFile GetModLocalCompByKeywords(McInstance instance, string[] modIds,
|
||||
string mainKeyword, params string[] keywords)
|
||||
{
|
||||
if (!instance.Modable)
|
||||
return null; // 跳过不可安装 Mod 实例
|
||||
var modFolder = $"{instance.PathInstance}mods";
|
||||
if (!Directory.Exists(modFolder))
|
||||
return null; // 确保 mods 目录存在
|
||||
foreach (var file in Directory.EnumerateFiles(modFolder, $"*{mainKeyword}*"))
|
||||
{
|
||||
var lowerFilePath = file.ToLower(); // 统一转为小写
|
||||
if (!regexIsJarFile.IsMatch(lowerFilePath))
|
||||
continue; // 检查是否是 jar 文件
|
||||
if ((keywords.Length > 0) && !keywords.Any(keyword => lowerFilePath.Contains(keyword)))
|
||||
continue; // 检查是否包含关键字
|
||||
var localComp = new LocalCompFile(file);
|
||||
localComp.Load();
|
||||
if (modIds.Any(modId => (localComp.ModId ?? "") == (modId ?? "")))
|
||||
return localComp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1724 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Core.Utils.Validate;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
using static PCL.ModLoader;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModModpack
|
||||
{
|
||||
// 触发整合包安装的外部接口
|
||||
/// <summary>
|
||||
/// 弹窗要求选择一个整合包文件并进行安装。
|
||||
/// </summary>
|
||||
public static void ModpackInstall()
|
||||
{
|
||||
var file = SystemDialogs.SelectFile(Lang.Text("Minecraft.Download.Modpack.FileDialog.Filter"),
|
||||
Lang.Text("Minecraft.Download.Modpack.FileDialog.Title")); // 选择整合包文件
|
||||
if (string.IsNullOrEmpty(file))
|
||||
return;
|
||||
ModBase.RunInThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ModpackInstall(file);
|
||||
}
|
||||
catch (ModBase.CancelledException ex)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"手动安装整合包失败",
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Minecraft.Download.Modpack.Error.OperationFailed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建并启动安装给定的整合包文件的加载器,并返回该加载器。若失败则抛出异常。
|
||||
/// 必须在工作线程执行。
|
||||
/// </summary>
|
||||
/// <exception cref="ModBase.CancelledException" />
|
||||
public static LoaderCombo<string> ModpackInstall(string file, string instanceName = null, string logo = null,
|
||||
string resourceId = null, bool isOnlineInstall = false)
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包安装请求:" + (file ?? "null"));
|
||||
ZipArchive archive = null;
|
||||
var archiveBaseFolder = "";
|
||||
try
|
||||
{
|
||||
// 字符校验
|
||||
var targetFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\";
|
||||
if (targetFolder.Contains("!") || targetFolder.Contains(";"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder),
|
||||
HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 获取整合包种类与关键 Json
|
||||
var packType = -1;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
archive = new ZipArchive(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read));
|
||||
if (archive.Entries.Any(e => e.IsEncrypted))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.EncryptedArchiveUnsupported"));
|
||||
// 从根目录判断整合包类型
|
||||
if (archive.GetEntry("mcbbs.packmeta") is not null)
|
||||
{
|
||||
packType = 3;
|
||||
break;
|
||||
} // MCBBS 整合包(优先于 manifest.json 判断)
|
||||
|
||||
if (archive.GetEntry("mmc-pack.json") is not null)
|
||||
{
|
||||
packType = 2;
|
||||
break;
|
||||
} // MMC 整合包(优先于 manifest.json 判断,#4194)
|
||||
|
||||
if (archive.GetEntry("modrinth.index.json") is not null)
|
||||
{
|
||||
packType = 4;
|
||||
break;
|
||||
} // Modrinth 整合包
|
||||
|
||||
if (archive.GetEntry("manifest.json") is not null)
|
||||
{
|
||||
var json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(archive.GetEntry("manifest.json").Open(),
|
||||
Encoding.UTF8));
|
||||
if (json["addons"] is null)
|
||||
{
|
||||
packType = 0;
|
||||
break; // CurseForge 整合包
|
||||
}
|
||||
|
||||
packType = 3;
|
||||
break;
|
||||
// MCBBS 整合包
|
||||
}
|
||||
|
||||
if (archive.GetEntry("modpack.json") is not null)
|
||||
{
|
||||
packType = 1;
|
||||
break;
|
||||
} // HMCL 整合包
|
||||
|
||||
if (archive.GetEntry("modpack.zip") is not null || archive.GetEntry("modpack.mrpack") is not null)
|
||||
{
|
||||
packType = 9;
|
||||
break;
|
||||
} // 带启动器的压缩包
|
||||
|
||||
// 从一级目录判断整合包类型
|
||||
var exitTry = false;
|
||||
foreach (var Entry in archive.Entries)
|
||||
{
|
||||
var fullNames = Entry.FullName.Split("/");
|
||||
archiveBaseFolder = fullNames[0] + "/";
|
||||
// 确定为一级目录下
|
||||
if (fullNames.Count() != 2)
|
||||
continue;
|
||||
// 判断是否为关键文件
|
||||
if (fullNames[1] == "mcbbs.packmeta")
|
||||
{
|
||||
packType = 3;
|
||||
exitTry = true;
|
||||
break;
|
||||
} // MCBBS 整合包(优先于 manifest.json 判断)
|
||||
|
||||
if (fullNames[1] == "mmc-pack.json")
|
||||
{
|
||||
packType = 2;
|
||||
exitTry = true;
|
||||
break;
|
||||
} // MMC 整合包(优先于 manifest.json 判断,#4194)
|
||||
|
||||
if (fullNames[1] == "modrinth.index.json")
|
||||
{
|
||||
packType = 4;
|
||||
exitTry = true;
|
||||
break;
|
||||
} // Modrinth 整合包
|
||||
|
||||
if (fullNames[1] == "manifest.json")
|
||||
{
|
||||
var json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(Entry.Open(), Encoding.UTF8));
|
||||
if (json["addons"] is null)
|
||||
{
|
||||
packType = 0;
|
||||
exitTry = true;
|
||||
break; // CurseForge 整合包
|
||||
}
|
||||
|
||||
packType = 3;
|
||||
archiveBaseFolder = "overrides/";
|
||||
exitTry = true;
|
||||
break;
|
||||
// MCBBS 整合包
|
||||
}
|
||||
|
||||
if (fullNames[1] == "modpack.json")
|
||||
{
|
||||
packType = 1;
|
||||
exitTry = true;
|
||||
break;
|
||||
} // HMCL 整合包
|
||||
|
||||
if (fullNames[1] == "modpack.zip" || fullNames[1] == "modpack.mrpack")
|
||||
{
|
||||
packType = 9;
|
||||
exitTry = true;
|
||||
break;
|
||||
} // 带启动器的压缩包
|
||||
}
|
||||
|
||||
if (exitTry) break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message.Contains("Error.WinIOError"))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.OpenFailed"), ex);
|
||||
else if (file.EndsWithF(".rar", true))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.RarUnsupported"), ex);
|
||||
else
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnsupportedArchive"), ex);
|
||||
}
|
||||
} while (false);
|
||||
|
||||
// 执行对应的安装方法
|
||||
switch (packType)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:CurseForge");
|
||||
return InstallPackCurseForge(file, archive, archiveBaseFolder, instanceName, logo, resourceId,
|
||||
isOnlineInstall);
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:HMCL");
|
||||
return InstallPackHMCL(file, archive, archiveBaseFolder);
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:MMC");
|
||||
return InstallPackMMC(file, archive, archiveBaseFolder);
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:MCBBS");
|
||||
return InstallPackMCBBS(file, archive, archiveBaseFolder, instanceName);
|
||||
}
|
||||
case 4:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:Modrinth");
|
||||
return InstallPackModrinth(file, archive, archiveBaseFolder, instanceName, logo, resourceId,
|
||||
isOnlineInstall);
|
||||
}
|
||||
case 9:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:带启动器的压缩包");
|
||||
return InstallPackLauncherPack(file, archive, archiveBaseFolder);
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包");
|
||||
return InstallPackCompress(file, archive);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (archive is not null)
|
||||
archive.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader,
|
||||
double progressIncrement)
|
||||
{
|
||||
// 解压文件
|
||||
var retryCount = 1;
|
||||
var encode = Encoding.GetEncoding("GB18030");
|
||||
var initialProgress = loader.Progress;
|
||||
|
||||
while (retryCount <= 5)
|
||||
try
|
||||
{
|
||||
loader.Progress = initialProgress;
|
||||
|
||||
// 删除旧目录
|
||||
ModBase.DeleteDirectory(installTemp);
|
||||
|
||||
// 解压文件,ProgressIncrementHandler 通过 Lambda 更新进度
|
||||
ModBase.ExtractFile(fileAddress, installTemp, encode,
|
||||
delta => loader.Progress += delta * progressIncrement);
|
||||
|
||||
// 解压成功,更新进度并退出循环
|
||||
loader.Progress = initialProgress + progressIncrement;
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"第 {retryCount} 次解压尝试失败");
|
||||
|
||||
if (ex is ArgumentException || ex is IOException)
|
||||
{
|
||||
encode = Encoding.UTF8;
|
||||
ModBase.Log("[ModPack] 已切换压缩包解压编码为 UTF8");
|
||||
}
|
||||
|
||||
// 检查加载器状态,决定是否中止
|
||||
if (loader is not null && loader.LoadingState != MyLoading.MyLoadingState.Run)
|
||||
return;
|
||||
|
||||
// 增加重试次数
|
||||
retryCount++;
|
||||
|
||||
if (retryCount <= 5)
|
||||
// 等待一段时间再重试
|
||||
Thread.Sleep((retryCount - 1) * 2000);
|
||||
else
|
||||
throw new Exception("解压整合包文件失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从整合包的 override 目录复制文件,同时设置 PCL 的配置文件与版本隔离。
|
||||
/// 对路径末尾是否为 \ 没有要求。
|
||||
/// </summary>
|
||||
private static void CopyOverrideDirectory(string overridesFolder, string versionFolder, LoaderBase loader,
|
||||
double progressIncrement)
|
||||
{
|
||||
if (!overridesFolder.EndsWithF(@"\"))
|
||||
overridesFolder += @"\";
|
||||
if (!versionFolder.EndsWithF(@"\"))
|
||||
versionFolder += @"\";
|
||||
// 复制文件
|
||||
if (Directory.Exists(overridesFolder))
|
||||
{
|
||||
ModBase.Log($"[ModPack] 处理整合包覆写文件夹:{overridesFolder} → {versionFolder}");
|
||||
ModBase.CopyDirectory(overridesFolder, versionFolder,
|
||||
delta => loader.Progress += delta * progressIncrement);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[ModPack] 整合包中没有覆写文件夹:{overridesFolder}");
|
||||
loader.Progress += progressIncrement;
|
||||
}
|
||||
|
||||
// 设置 ini
|
||||
var overridesIni = $@"{overridesFolder}PCL\Setup.ini";
|
||||
var versionIni = $@"{versionFolder}PCL\Setup.ini";
|
||||
if (File.Exists(overridesIni))
|
||||
{
|
||||
ModBase.WriteIni(overridesIni, "VersionArgumentIndie", 1.ToString()); // 开启版本隔离
|
||||
ModBase.WriteIni(overridesIni, "VersionArgumentIndieV2", true.ToString());
|
||||
ModBase.CopyFile(overridesIni, versionIni); // 覆写已有的 ini
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.WriteIni(versionIni, "VersionArgumentIndie", 1.ToString()); // 开启版本隔离
|
||||
ModBase.WriteIni(versionIni, "VersionArgumentIndieV2", true.ToString());
|
||||
}
|
||||
|
||||
ModBase.IniClearCache(versionIni); // 重置缓存,避免被安装过程中写入的 ini 覆盖
|
||||
}
|
||||
|
||||
#region CurseForge
|
||||
|
||||
private static LoaderCombo<string> InstallPackCurseForge(string fileAddress, ZipArchive archive,
|
||||
string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null,
|
||||
bool isOnlineInstall = false)
|
||||
{
|
||||
// 读取 Json 文件
|
||||
JsonObject json;
|
||||
try
|
||||
{
|
||||
json = (JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "manifest.json").Open()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("CurseForge 整合包安装信息存在问题", ex);
|
||||
}
|
||||
|
||||
if (json["minecraft"] is null || json["minecraft"]["version"] is null)
|
||||
throw new Exception("CurseForge 整合包未提供 Minecraft 版本信息");
|
||||
|
||||
// 获取实例名
|
||||
if (instanceName is null)
|
||||
{
|
||||
instanceName = (string)(json["name"] ?? "");
|
||||
var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions"));
|
||||
if (!validate.Validate(instanceName).IsValid)
|
||||
instanceName = "";
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "",
|
||||
[validate]);
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 获取 Mod API 版本信息
|
||||
string forgeVersion = null;
|
||||
string neoForgeVersion = null;
|
||||
string fabricVersion = null;
|
||||
var modLoader = ModComp.CompLoaderType.Any;
|
||||
foreach (var Entry in (dynamic)json["minecraft"]["modLoaders"] ?? Array.Empty<JsonNode>())
|
||||
{
|
||||
string id = (Entry["id"] ?? "").ToString().ToLower();
|
||||
if (id.StartsWithF("forge-"))
|
||||
{
|
||||
// Forge 指定
|
||||
if (id.Contains("recommended"))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.TooOldUnsupported"));
|
||||
ModBase.Log("[ModPack] 整合包 Forge 版本:" + id);
|
||||
forgeVersion = id.Replace("forge-", "");
|
||||
modLoader = ModComp.CompLoaderType.Forge;
|
||||
}
|
||||
else if (id.StartsWithF("neoforge-"))
|
||||
{
|
||||
// NeoForge 指定
|
||||
ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + id);
|
||||
neoForgeVersion = id.Replace("neoforge-", "");
|
||||
modLoader = ModComp.CompLoaderType.NeoForge;
|
||||
}
|
||||
else if (id.StartsWithF("fabric-"))
|
||||
{
|
||||
// Fabric 指定
|
||||
try
|
||||
{
|
||||
ModBase.Log("[ModPack] 整合包 Fabric 版本:" + id);
|
||||
fabricVersion = id.Replace("fabric-", "");
|
||||
modLoader = ModComp.CompLoaderType.Fabric;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "读取整合包 Fabric 版本失败:" + id);
|
||||
}
|
||||
}
|
||||
else if (id.StartsWithF("quilt-"))
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported"));
|
||||
}
|
||||
}
|
||||
|
||||
// 解压
|
||||
var installTemp = ModMain.RequestTaskTempFolder();
|
||||
var installLoaders = new List<LoaderBase>();
|
||||
var overrideHome = (string)(json["overrides"] ?? "");
|
||||
if (!string.IsNullOrEmpty(overrideHome))
|
||||
installLoaders.Add(new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"),
|
||||
task =>
|
||||
{
|
||||
ExtractModpackFiles(installTemp, fileAddress, task, 0.6d);
|
||||
CopyOverrideDirectory(
|
||||
Path.Combine(installTemp, archiveBaseFolder, overrideHome == "." || overrideHome == "./" ? "" : overrideHome),
|
||||
$@"{ModFolder.mcFolderSelected}versions\{instanceName}", task, 0.4d);
|
||||
})
|
||||
{
|
||||
ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d,
|
||||
block = false
|
||||
}); // 每 6M 需要 1s
|
||||
// 获取 Mod 列表
|
||||
var modList = new List<int>();
|
||||
var modOptionalList = new List<int>();
|
||||
foreach (var ModEntry in (dynamic)json["files"] ?? Array.Empty<JsonNode>())
|
||||
{
|
||||
if (ModEntry["projectID"] is null || ModEntry["fileID"] is null)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.ModMissingRequiredInfoSkipped", ModEntry));
|
||||
continue;
|
||||
}
|
||||
|
||||
modList.Add((int)ModEntry["fileID"]);
|
||||
if (ModEntry["required"] is JsonNode requiredNode && !requiredNode.ToObject<bool>())
|
||||
modOptionalList.Add((int)ModEntry["fileID"]);
|
||||
}
|
||||
|
||||
if (modList.Any())
|
||||
{
|
||||
var modDownloadLoaders = new List<LoaderBase>();
|
||||
// 获取 Mod 下载信息
|
||||
modDownloadLoaders.Add(new LoaderTask<int, JsonArray>(
|
||||
Lang.Text("Minecraft.Download.Modpack.Stage.PrepareModsDownloadInfo"), task =>
|
||||
{
|
||||
var allowMirror = true;
|
||||
JsonArray ret;
|
||||
var tryCount = 0;
|
||||
do
|
||||
{
|
||||
tryCount += 1;
|
||||
ret = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
|
||||
"https://api.curseforge.com/v1/mods/files",
|
||||
"POST", "{\"fileIds\": [" + modList.Join(",") + "]}", "application/json",
|
||||
allowMirror)))["data"];
|
||||
if (modList.Count <= ret.Count)
|
||||
{
|
||||
ModBase.Log("[Modpack] 已获取到的模组数量足够,开始进行下一步");
|
||||
break;
|
||||
}
|
||||
|
||||
allowMirror = false;
|
||||
ModBase.Log($"[Modpack] 获取模组数量不达标,设置镜像源允许状态为: {allowMirror}");
|
||||
if (tryCount > 3) throw new Exception(Lang.Text("Minecraft.Download.Modpack.SomeModsDeleted"));
|
||||
} while (true);
|
||||
|
||||
task.output = ret;
|
||||
})
|
||||
{
|
||||
ProgressWeight = modList.Count / 10d
|
||||
}); // 每 10 Mod 需要 1s
|
||||
// 构造 NetFile
|
||||
modDownloadLoaders.Add(new LoaderTask<JsonArray, List<DownloadFile>>(
|
||||
Lang.Text("Minecraft.Download.Modpack.Stage.BuildModsDownloadInfo"), task =>
|
||||
{
|
||||
var fileList = new Dictionary<int, DownloadFile>();
|
||||
foreach (var ModJson in task.input)
|
||||
{
|
||||
var id = ModJson["id"].ToObject<int>();
|
||||
// 跳过重复的 Mod(疑似 CurseForge Bug)
|
||||
if (fileList.ContainsKey(id))
|
||||
continue;
|
||||
// 可选 Mod 提示
|
||||
if (modOptionalList.Contains(id))
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message", ModJson["displayName"]),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip")
|
||||
) == 2)
|
||||
continue;
|
||||
|
||||
// 根据 modules 和文件名后缀判断资源类型
|
||||
string targetFolder;
|
||||
ModComp.CompType type;
|
||||
if (ModJson["modules"].AsArray().Any()) // modules 可能返回 null(#1006)
|
||||
{
|
||||
var moduleNames = ((JsonArray)ModJson["modules"]).Select(l => l["name"].ToString()).ToList();
|
||||
if (moduleNames.Contains("META-INF") || moduleNames.Contains("mcmod.info") ||
|
||||
(ModJson?["FileName"]?.ToString()?.EndsWithF(".jar", true)).GetValueOrDefault())
|
||||
{
|
||||
targetFolder = "mods";
|
||||
type = ModComp.CompType.Mod;
|
||||
}
|
||||
else if (moduleNames.Contains("pack.mcmeta"))
|
||||
{
|
||||
targetFolder = "resourcepacks";
|
||||
type = ModComp.CompType.ResourcePack;
|
||||
}
|
||||
else if (moduleNames.Contains("level.dat"))
|
||||
{
|
||||
targetFolder = "saves";
|
||||
type = ModComp.CompType.World;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetFolder = "shaderpacks";
|
||||
type = ModComp.CompType.Shader;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetFolder = "mods";
|
||||
type = ModComp.CompType.Mod;
|
||||
}
|
||||
|
||||
// 建立 CompFile
|
||||
var file = new ModComp.CompFile((JsonObject)ModJson, type);
|
||||
if (!file.Available)
|
||||
continue;
|
||||
// 实际的添加
|
||||
fileList.Add(id,
|
||||
file.ToNetFile($@"{ModFolder.mcFolderSelected}versions\{instanceName}\{targetFolder}\",
|
||||
ModComp.DownloadReason.ModPack, json["minecraft"]!["version"]!.ToString(), modLoader));
|
||||
task.Progress += 1d / (1 + modList.Count);
|
||||
}
|
||||
|
||||
task.output = fileList.Values.ToList();
|
||||
})
|
||||
{
|
||||
ProgressWeight = modList.Count / 200d,
|
||||
show = false
|
||||
}); // 每 200 Mod 需要 1s
|
||||
// 下载 Mod 文件
|
||||
modDownloadLoaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods"), [])
|
||||
{ ProgressWeight = modList.Count * 1.5d }); // 每个 Mod 需要 1.5s
|
||||
// 构造加载器
|
||||
installLoaders.Add(
|
||||
new LoaderCombo<int>(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods.MainLoader"),
|
||||
modDownloadLoaders)
|
||||
{ show = false, ProgressWeight = modDownloadLoaders.Sum(l => l.ProgressWeight) });
|
||||
}
|
||||
|
||||
// 构造加载器
|
||||
var request = new ModDownloadLib.McInstallRequest
|
||||
{
|
||||
targetInstanceName = instanceName,
|
||||
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\",
|
||||
minecraftName = json["minecraft"]["version"].ToString(),
|
||||
forgeVersion = forgeVersion,
|
||||
neoForgeVersion = neoForgeVersion,
|
||||
fabricVersion = fabricVersion,
|
||||
};
|
||||
var mergeLoaders = ModDownloadLib.McInstallLoader(request);
|
||||
// 构造总加载器
|
||||
var loaders = new List<LoaderBase>();
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"),
|
||||
installLoaders)
|
||||
{ show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) });
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders)
|
||||
{ show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) });
|
||||
loaders.Add(new LoaderTask<string, string>(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task =>
|
||||
{
|
||||
// 设置图标
|
||||
var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\";
|
||||
if (logo is not null && File.Exists(logo))
|
||||
{
|
||||
File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true);
|
||||
States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png";
|
||||
States.Instance.IsLogoCustom[versionFolder] = true;
|
||||
ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo);
|
||||
}
|
||||
|
||||
// 删除原始整合包文件
|
||||
foreach (var Target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") })
|
||||
if (File.Exists(Target))
|
||||
{
|
||||
ModBase.Log("[ModPack] 删除原始整合包文件:" + Target);
|
||||
File.Delete(Target);
|
||||
}
|
||||
|
||||
if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack")
|
||||
{
|
||||
ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress);
|
||||
File.Delete(fileAddress);
|
||||
}
|
||||
|
||||
// 整合包版本
|
||||
if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString();
|
||||
States.Instance.ModpackSource[versionFolder] = "CurseForge";
|
||||
States.Instance.ModpackId[versionFolder] = resourceId;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]);
|
||||
if (projects.Count == 0)
|
||||
break;
|
||||
States.Instance.CustomInfo[versionFolder] = projects.First().Description;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败");
|
||||
}
|
||||
} while (false);
|
||||
})
|
||||
{
|
||||
ProgressWeight = 0.1d,
|
||||
show = false
|
||||
});
|
||||
|
||||
// 重复任务检查
|
||||
var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.CurseForgeInstall", instanceName);
|
||||
if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? "")))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 启动
|
||||
var loader = new LoaderCombo<string>(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState };
|
||||
loader.Start(request.targetInstanceFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
if (!isOnlineInstall)
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager));
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modrinth
|
||||
|
||||
private static LoaderCombo<string> InstallPackModrinth(string fileAddress, ZipArchive archive,
|
||||
string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null,
|
||||
bool isOnlineInstall = false)
|
||||
{
|
||||
// 读取 Json 文件
|
||||
JsonObject json;
|
||||
try
|
||||
{
|
||||
json = (JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modrinth.index.json").Open()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Modrinth 整合包安装信息存在问题", ex);
|
||||
}
|
||||
|
||||
if (json["dependencies"] is null || json["dependencies"]["minecraft"] is null)
|
||||
throw new Exception("Modrinth 整合包未提供 Minecraft 版本信息");
|
||||
// 获取 Mod API 版本信息
|
||||
string minecraftVersion = null;
|
||||
string forgeVersion = null;
|
||||
string neoForgeVersion = null;
|
||||
string fabricVersion = null;
|
||||
var modLoader = ModComp.CompLoaderType.Any;
|
||||
foreach (var Entry in json["dependencies"]?.AsObject() ?? new JsonObject())
|
||||
switch (Entry.Key.ToLower() ?? "")
|
||||
{
|
||||
case "minecraft":
|
||||
{
|
||||
minecraftVersion = Entry.Value?.ToObject<string>();
|
||||
break;
|
||||
}
|
||||
case "forge": // eg. 14.23.5.2859 / 1.19-41.1.0
|
||||
{
|
||||
forgeVersion = Entry.Value?.ToObject<string>();
|
||||
modLoader = ModComp.CompLoaderType.Forge;
|
||||
ModBase.Log("[ModPack] 整合包 Forge 版本:" + forgeVersion);
|
||||
break;
|
||||
}
|
||||
case "neoforge":
|
||||
case "neo-forge": // eg. 20.6.98-beta
|
||||
{
|
||||
neoForgeVersion = Entry.Value?.ToObject<string>();
|
||||
modLoader = ModComp.CompLoaderType.NeoForge;
|
||||
ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + neoForgeVersion);
|
||||
break;
|
||||
}
|
||||
case "fabric-loader": // eg. 0.14.14
|
||||
{
|
||||
fabricVersion = Entry.Value?.ToObject<string>();
|
||||
modLoader = ModComp.CompLoaderType.Fabric;
|
||||
ModBase.Log("[ModPack] 整合包 Fabric 版本:" + fabricVersion);
|
||||
break;
|
||||
}
|
||||
case "quilt-loader": // eg. 0.26.0
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported"));
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.UnknownLoader", Entry.Key, Entry.Value),
|
||||
HintType.Error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取实例名
|
||||
if (instanceName is null)
|
||||
{
|
||||
instanceName = (string)(json["name"] ?? "");
|
||||
var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions"));
|
||||
if (!validate.Validate(instanceName).IsValid)
|
||||
instanceName = "";
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "",
|
||||
[validate]);
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 解压
|
||||
var installTemp = ModMain.RequestTaskTempFolder();
|
||||
var installLoaders = new List<LoaderBase>();
|
||||
installLoaders.Add(new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"),
|
||||
task =>
|
||||
{
|
||||
ExtractModpackFiles(installTemp, fileAddress, task, 0.5d);
|
||||
CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "overrides"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d);
|
||||
CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "client-overrides"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.1d);
|
||||
})
|
||||
{
|
||||
ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d,
|
||||
block = false
|
||||
}); // 每 6M 需要 1s
|
||||
// 获取下载文件列表
|
||||
var fileList = new List<DownloadFile>();
|
||||
foreach (var File in (dynamic)json["files"] ?? Array.Empty<JsonNode>())
|
||||
{
|
||||
// 检查是否需要该文件
|
||||
if (File["env"] is not null)
|
||||
switch (File["env"]["client"].ToString() ?? "")
|
||||
{
|
||||
case "optional":
|
||||
{
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message",
|
||||
ModBase.GetFileNameFromPath(File["path"].ToString())),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"),
|
||||
Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip")
|
||||
) == 2) continue;
|
||||
|
||||
break;
|
||||
}
|
||||
case "unsupported":
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加下载文件
|
||||
var urls = ((JsonArray)File["downloads"])
|
||||
.OfType<JsonNode>()
|
||||
.Select(x => ModComp.CompFile.HandleCurseForgeDownloadUrls(x.ToString()))
|
||||
.ToList();
|
||||
// 镜像源
|
||||
urls = urls.SelectMany(x => ModDownload.DlSourceModDownloadGet(x)).ToList();
|
||||
var targetPath = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\{File["path"]}";
|
||||
if (!Path.GetFullPath(targetPath)
|
||||
.StartsWithF($@"{ModFolder.mcFolderSelected}versions\{instanceName}\", true))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Message", targetPath),
|
||||
Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Title"), isWarn: true);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
fileList.Add(new DownloadFile(
|
||||
ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion,
|
||||
modLoader), targetPath,
|
||||
new ModBase.FileChecker(actualSize: ((JsonNode)File["fileSize"]).ToObject<long>(),
|
||||
hash: File["hashes"]["sha1"].ToString()), true));
|
||||
}
|
||||
|
||||
if (fileList.Any())
|
||||
installLoaders.Add(
|
||||
new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadAdditions"), fileList)
|
||||
{ ProgressWeight = fileList.Count * 1.5d }); // 每个 Mod 需要 1.5s
|
||||
|
||||
// 构造加载器
|
||||
var request = new ModDownloadLib.McInstallRequest
|
||||
{
|
||||
targetInstanceName = instanceName,
|
||||
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\",
|
||||
minecraftName = minecraftVersion,
|
||||
forgeVersion = forgeVersion,
|
||||
neoForgeVersion = neoForgeVersion,
|
||||
fabricVersion = fabricVersion,
|
||||
};
|
||||
var mergeLoaders = ModDownloadLib.McInstallLoader(request);
|
||||
// 构造总加载器
|
||||
var loaders = new List<LoaderBase>();
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"),
|
||||
installLoaders)
|
||||
{ show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) });
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders)
|
||||
{ show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) });
|
||||
loaders.Add(new LoaderTask<string, string>(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task =>
|
||||
{
|
||||
// 设置图标
|
||||
var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\";
|
||||
if (logo is not null && File.Exists(logo))
|
||||
{
|
||||
File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true);
|
||||
States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png";
|
||||
States.Instance.IsLogoCustom[versionFolder] = true;
|
||||
ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo);
|
||||
}
|
||||
|
||||
// 删除原始整合包文件
|
||||
foreach (var Target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") })
|
||||
if (File.Exists(Target))
|
||||
{
|
||||
ModBase.Log("[ModPack] 删除原始整合包文件:" + Target);
|
||||
File.Delete(Target);
|
||||
}
|
||||
|
||||
if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack")
|
||||
{
|
||||
ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress);
|
||||
File.Delete(fileAddress);
|
||||
}
|
||||
|
||||
// 整合包版本
|
||||
if (json["versionId"] is not null)
|
||||
States.Instance.ModpackVersion[versionFolder] = json["versionId"].ToString();
|
||||
States.Instance.ModpackSource[versionFolder] = "Modrinth";
|
||||
States.Instance.ModpackId[versionFolder] = resourceId;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]);
|
||||
if (projects.Count == 0)
|
||||
break;
|
||||
States.Instance.CustomInfo[versionFolder] = projects.First().Description;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败");
|
||||
}
|
||||
} while (false);
|
||||
})
|
||||
{
|
||||
ProgressWeight = 0.1d,
|
||||
show = false
|
||||
});
|
||||
|
||||
// 重复任务检查
|
||||
var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.ModrinthInstall", instanceName);
|
||||
if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? "")))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 启动
|
||||
var loader = new LoaderCombo<string>(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState };
|
||||
loader.Start(request.targetInstanceFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
if (!isOnlineInstall)
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager));
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HMCL
|
||||
|
||||
private static LoaderCombo<string> InstallPackHMCL(string fileAddress, ZipArchive archive, string archiveBaseFolder)
|
||||
{
|
||||
// 读取 Json 文件
|
||||
JsonObject json;
|
||||
try
|
||||
{
|
||||
json = (JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modpack.json").Open(), Encoding.UTF8));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("HMCL 整合包安装信息存在问题", ex);
|
||||
}
|
||||
|
||||
// 获取实例名
|
||||
var instanceName = (string)(json["name"] ?? "");
|
||||
var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions"));
|
||||
if (!validate.Validate(instanceName).IsValid)
|
||||
instanceName = "";
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "",
|
||||
[validate]);
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
throw new ModBase.CancelledException();
|
||||
// 解压
|
||||
var installTemp = ModMain.RequestTaskTempFolder();
|
||||
var installLoaders = new List<LoaderBase>();
|
||||
installLoaders.Add(new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"),
|
||||
task =>
|
||||
{
|
||||
ExtractModpackFiles(installTemp, fileAddress, task, 0.6d);
|
||||
CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "minecraft"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d);
|
||||
})
|
||||
{
|
||||
ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d,
|
||||
block = false
|
||||
}); // 每 6M 需要 1s
|
||||
// 构造游戏本体安装加载器
|
||||
if (json["gameVersion"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Hmcl"));
|
||||
var request = new ModDownloadLib.McInstallRequest
|
||||
{
|
||||
targetInstanceName = instanceName,
|
||||
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\",
|
||||
minecraftName = json["gameVersion"].ToString()
|
||||
};
|
||||
var mergeLoaders = ModDownloadLib.McInstallLoader(request);
|
||||
// 构造总加载器
|
||||
var loaders = new List<LoaderBase>
|
||||
{
|
||||
new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), installLoaders)
|
||||
{ show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) },
|
||||
new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders)
|
||||
{ show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }
|
||||
};
|
||||
// 重复任务检查
|
||||
var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.HmclInstall", instanceName);
|
||||
if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? "")))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 启动
|
||||
var loader = new LoaderCombo<string>(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState };
|
||||
loader.Start(request.targetInstanceFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager));
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MCBBS
|
||||
|
||||
private static LoaderCombo<string> InstallPackMCBBS(string fileAddress, ZipArchive archive,
|
||||
string archiveBaseFolder, string instanceName = null)
|
||||
{
|
||||
// 读取 Json 文件
|
||||
JsonObject json;
|
||||
try
|
||||
{
|
||||
// VB 的 If(a, b) 在 C# 中如果是 null 合并则用 ??,如果是三元运算则用 ?:
|
||||
var entry = archive.GetEntry(archiveBaseFolder + "mcbbs.packmeta") ??
|
||||
archive.GetEntry(archiveBaseFolder + "manifest.json");
|
||||
using (var stream = entry.Open())
|
||||
{
|
||||
json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("MCBBS 整合包安装信息存在问题", ex);
|
||||
}
|
||||
|
||||
// 获取实例名
|
||||
if (instanceName is null)
|
||||
{
|
||||
instanceName = json["name"]?.ToString() ?? "";
|
||||
var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions"));
|
||||
|
||||
if (!validate.Validate(instanceName).IsValid) instanceName = "";
|
||||
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "",
|
||||
[validate]);
|
||||
|
||||
if (string.IsNullOrEmpty(instanceName)) throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 解压与路径准备
|
||||
var installTemp = ModMain.RequestTaskTempFolder();
|
||||
var versionFolder = $"{ModFolder.mcFolderSelected}versions\\{instanceName}";
|
||||
var installLoaders = new List<LoaderBase>();
|
||||
|
||||
// 解压整合包文件任务
|
||||
var unzipTask = new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"),
|
||||
task =>
|
||||
{
|
||||
ExtractModpackFiles(installTemp, fileAddress, task, 0.6);
|
||||
CopyOverrideDirectory(
|
||||
Path.Combine(installTemp, archiveBaseFolder, "overrides"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName),
|
||||
task, 0.4);
|
||||
|
||||
// JVM 参数处理
|
||||
if (json["launchInfo"] is not null)
|
||||
{
|
||||
var launchInfo = (JsonObject)json["launchInfo"];
|
||||
Config.Instance.JvmArgs[versionFolder] = string.Join(" ", launchInfo["javaArgument"]);
|
||||
Config.Instance.GameArgs[versionFolder] = string.Join(" ", launchInfo["launchArgument"]);
|
||||
}
|
||||
|
||||
// 整合包版本
|
||||
if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString();
|
||||
});
|
||||
|
||||
unzipTask.ProgressWeight = new FileInfo(fileAddress).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s
|
||||
unzipTask.block = false;
|
||||
installLoaders.Add(unzipTask);
|
||||
|
||||
// 构造加载器
|
||||
if (json["addons"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.McbbsAddons"));
|
||||
|
||||
var addons = new Dictionary<string, string>();
|
||||
foreach (var EntryNode in json["addons"].AsArray()) { var entry = EntryNode.AsObject(); addons.Add(entry["id"].ToString(), entry["version"].ToString()); }
|
||||
|
||||
if (!addons.ContainsKey("game"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic"), HintType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (addons.ContainsKey("quilt"))
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported"));
|
||||
|
||||
// 构造安装请求
|
||||
var request = new ModDownloadLib.McInstallRequest
|
||||
{
|
||||
targetInstanceName = instanceName,
|
||||
targetInstanceFolder = $"{ModFolder.mcFolderSelected}versions\\{instanceName}\\",
|
||||
minecraftName = addons["game"],
|
||||
optiFineVersion = addons.ContainsKey("optifine") ? addons["optifine"] : null,
|
||||
forgeVersion = addons.ContainsKey("forge") ? addons["forge"] : null,
|
||||
neoForgeVersion = addons.ContainsKey("neoforge") ? addons["neoforge"] : null,
|
||||
fabricVersion = addons.ContainsKey("fabric") ? addons["fabric"] : null,
|
||||
};
|
||||
|
||||
var mergeLoaders = ModDownloadLib.McInstallLoader(request);
|
||||
|
||||
// 构造总加载器
|
||||
var loaders = new List<LoaderBase>();
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"),
|
||||
installLoaders)
|
||||
{
|
||||
show = false,
|
||||
block = false,
|
||||
ProgressWeight = installLoaders.Sum(l => l.ProgressWeight)
|
||||
});
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders)
|
||||
{
|
||||
show = false,
|
||||
ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight)
|
||||
});
|
||||
|
||||
// 重复任务检查
|
||||
var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.McbbsInstall", instanceName);
|
||||
if (loaderTaskbar.Any(l => l.name == loaderName))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 启动任务
|
||||
var loader = new LoaderCombo<string>(loaderName, loaders);
|
||||
loader.OnStateChanged = ModDownloadLib.McInstallState;
|
||||
|
||||
loader.Start(request.targetInstanceFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager));
|
||||
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 带启动器的压缩包
|
||||
|
||||
private static LoaderCombo<string> InstallPackLauncherPack(string fileAddress, ZipArchive archive,
|
||||
string archiveBaseFolder)
|
||||
{
|
||||
// 获取解压路径
|
||||
ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"),
|
||||
Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true);
|
||||
var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title"));
|
||||
if (string.IsNullOrEmpty(targetFolder))
|
||||
throw new ModBase.CancelledException();
|
||||
if (Directory.GetFileSystemEntries(targetFolder).Length > 0)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 解压
|
||||
var loader = new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[]
|
||||
{
|
||||
new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task =>
|
||||
{
|
||||
ExtractModpackFiles(targetFolder, fileAddress, task, 0.9d);
|
||||
Thread.Sleep(400); // 避免文件争用
|
||||
// 查找解压后的 exe 文件
|
||||
string launcher = null;
|
||||
foreach (var ExeFile in Directory.GetFiles(targetFolder, "*.exe", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var info = FileVersionInfo.GetVersionInfo(ExeFile);
|
||||
ModBase.Log($"[Modpack] 文件 {ExeFile} 的产品名标识为 {info.ProductName}");
|
||||
if (info.ProductName == "Plain Craft Launcher")
|
||||
{
|
||||
launcher = ExeFile;
|
||||
ModBase.Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}");
|
||||
}
|
||||
else if ((info.ProductName.ContainsF("Launcher", true) || info.ProductName.ContainsF("启动", true)) &&
|
||||
!(info.ProductName == "Plain Craft Launcher Admin Manager"))
|
||||
{
|
||||
if (launcher is null)
|
||||
{
|
||||
launcher = ExeFile;
|
||||
ModBase.Log($"[Modpack] 发现整合包附带的疑似第三方启动器:{ExeFile}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task.Progress = 0.95d;
|
||||
// 尝试使用附带的启动器打开
|
||||
if (launcher is not null)
|
||||
{
|
||||
ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + launcher);
|
||||
if (ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Message", launcher),
|
||||
Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Title"),
|
||||
Lang.Text("Minecraft.Download.Modpack.BundledLauncher.UseBundled"),
|
||||
Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse")
|
||||
) == 1)
|
||||
{
|
||||
ModBase.OpenExplorer(targetFolder);
|
||||
ModBase.ShellOnly(launcher, "--wait"); // 要求等待已有的 PCL 退出
|
||||
ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序");
|
||||
ModMain.frmMain.EndProgram(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log("[Modpack] 未找到压缩包中附带的启动器");
|
||||
}
|
||||
|
||||
ModBase.OpenExplorer(targetFolder);
|
||||
// 加入文件夹列表
|
||||
var instanceName = ModBase.GetFolderNameFromPath(targetFolder);
|
||||
Directory.CreateDirectory(Path.Combine(targetFolder, ".minecraft"));
|
||||
PageSelectLeft.AddFolder(
|
||||
Path.Combine(targetFolder, ".minecraft", archiveBaseFolder.Replace("/", @"\").TrimStart('\\')), instanceName,
|
||||
false); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串)
|
||||
// 调用 modpack 文件进行安装
|
||||
var modpackFile = Directory.GetFiles(targetFolder, "modpack.*", SearchOption.AllDirectories).First();
|
||||
ModBase.Log("[Modpack] 调用 modpack 文件继续安装:" + modpackFile);
|
||||
ModpackInstall(modpackFile);
|
||||
})
|
||||
});
|
||||
loader.Start(targetFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 普通压缩包
|
||||
|
||||
private static LoaderCombo<string> InstallPackCompress(string fileAddress, ZipArchive archive)
|
||||
{
|
||||
// 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径
|
||||
Match match = null;
|
||||
var regex = new Regex(@"^.*\/(?=versions\/(?<ver>[^\/]+)\/(\k<ver>)\.json$)", RegexOptions.IgnoreCase);
|
||||
foreach (var Entry in archive.Entries)
|
||||
{
|
||||
var entryMatch = regex.Match("/" + Entry.FullName);
|
||||
if (entryMatch.Success)
|
||||
{
|
||||
match = entryMatch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); // 没有匹配
|
||||
var archiveBaseFolder = match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串)
|
||||
var instanceName = match.Groups[1].Value;
|
||||
ModBase.Log("[ModPack] 检测到压缩包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + instanceName);
|
||||
// 获取解压路径
|
||||
ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"),
|
||||
Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true);
|
||||
var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title"));
|
||||
if (string.IsNullOrEmpty(targetFolder))
|
||||
throw new ModBase.CancelledException();
|
||||
if (targetFolder.Contains("!") || targetFolder.Contains(";"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder),
|
||||
HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
if (Directory.GetFileSystemEntries(targetFolder).Length > 0)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 解压
|
||||
var loader = new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[]
|
||||
{
|
||||
new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task =>
|
||||
{
|
||||
ExtractModpackFiles(targetFolder, fileAddress, task, 0.95d);
|
||||
// 加入文件夹列表
|
||||
PageSelectLeft.AddFolder(Path.Combine(targetFolder, archiveBaseFolder), ModBase.GetFolderNameFromPath(targetFolder),
|
||||
false);
|
||||
Thread.Sleep(400); // 避免文件争用
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect));
|
||||
})
|
||||
})
|
||||
{
|
||||
OnStateChanged = ModDownloadLib.McInstallState
|
||||
};
|
||||
loader.Start(targetFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MultiMC
|
||||
|
||||
public class MMCPackInfo
|
||||
{
|
||||
public JsonObject additionalJson = new();
|
||||
public bool isCleanroomOverrided;
|
||||
public bool isFabricOverrided;
|
||||
public bool isForgeOverrided;
|
||||
public bool isMcArgsEdited;
|
||||
public bool isMinecraftOverrided;
|
||||
public bool isNeoForgeOverrided;
|
||||
public JsonArray jvmArgs = new();
|
||||
public JsonArray libraries = new();
|
||||
public JsonObject overridedJson = new();
|
||||
public string tweakers = null;
|
||||
}
|
||||
|
||||
private static LoaderCombo<string> InstallPackMMC(string fileAddress, ZipArchive archive, string archiveBaseFolder)
|
||||
{
|
||||
// 读取 Json 文件
|
||||
JsonObject packJson;
|
||||
string packInstance;
|
||||
MMCPackInfo packInfo = null;
|
||||
try
|
||||
{
|
||||
packJson = (JsonObject)ModBase.GetJson(
|
||||
ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "mmc-pack.json").Open(), Encoding.UTF8));
|
||||
packInstance = ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "instance.cfg").Open(), Encoding.UTF8);
|
||||
|
||||
#region JSON Patches
|
||||
|
||||
// 参考 https://github.com/MultiMC/Launcher/wiki/JSON-Patches
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!archive.Entries.Any(e =>
|
||||
e.FullName.Equals(archiveBaseFolder + "patches/", StringComparison.OrdinalIgnoreCase)))
|
||||
break;
|
||||
ModBase.Log("[ModPack] 安装的 MultiMC 整合包存在 JSON Patches");
|
||||
// 排序预处理
|
||||
var patches = new List<KeyValuePair<JsonObject, int>>();
|
||||
foreach (var entry in archive.Entries)
|
||||
if (!entry.FullName.EndsWith("/") && entry.FullName.StartsWith(archiveBaseFolder + "patches/"))
|
||||
{
|
||||
var patch = (JsonObject)ModBase.GetJson(ModBase.ReadFile(
|
||||
archive.GetEntry(entry.FullName).Open(), Encoding.UTF8));
|
||||
patches.Add(new KeyValuePair<JsonObject, int>(patch,
|
||||
(int)(patch["order"] is not null ? patch["order"] : 0)));
|
||||
}
|
||||
|
||||
var components = (JsonArray)packJson["components"];
|
||||
var componentUids = components
|
||||
.Select(c => c["uid"]?.ToString())
|
||||
.ToHashSet();
|
||||
|
||||
patches = patches
|
||||
.Where(p => componentUids.Contains(p.Key["uid"]?.ToString()))
|
||||
.OrderBy(p => p.Value)
|
||||
.ToList();
|
||||
// 应用 Patches
|
||||
packInfo = new MMCPackInfo();
|
||||
|
||||
string tweakers = null;
|
||||
JsonObject assetIndex = null;
|
||||
JsonObject javaVerJson = null;
|
||||
string mainClass = null;
|
||||
var gameArguments = new JsonArray();
|
||||
var jvmArguments = new JsonArray();
|
||||
var libJson = new JsonArray();
|
||||
var addLibJson = new JsonArray();
|
||||
foreach (var Patch in patches)
|
||||
{
|
||||
var patchJson = Patch.Key;
|
||||
if ((string)patchJson["uid"] == "net.minecraft")
|
||||
{
|
||||
packInfo.isMinecraftOverrided = true;
|
||||
}
|
||||
else if ((string)patchJson["uid"] == "net.minecraftforge")
|
||||
{
|
||||
if (patchJson["version"].ToString().StartsWithF("0."))
|
||||
packInfo.isCleanroomOverrided = true;
|
||||
else
|
||||
packInfo.isForgeOverrided = true;
|
||||
}
|
||||
else if ((string)patchJson["uid"] == "net.neoforged")
|
||||
{
|
||||
packInfo.isNeoForgeOverrided = true;
|
||||
}
|
||||
else if ((string)patchJson["uid"] == "net.fabricmc.fabric-loader")
|
||||
{
|
||||
packInfo.isFabricOverrided = true;
|
||||
}
|
||||
else if ((string)patchJson["uid"] == "org.quiltmc.quilt-loader")
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported"));
|
||||
}
|
||||
|
||||
// JVM 参数
|
||||
if (patchJson["+jvmArgs"] is not null)
|
||||
{
|
||||
jvmArguments.Merge(patchJson["+jvmArgs"]);
|
||||
ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 JVM 参数");
|
||||
}
|
||||
|
||||
// Libraries
|
||||
if (patchJson["libraries"] is not null || patchJson["+libraries"] is not null)
|
||||
{
|
||||
var libs = new JsonArray();
|
||||
if (patchJson["libraries"] is not null)
|
||||
foreach (var Library in patchJson["libraries"].AsArray())
|
||||
{
|
||||
if (Library is not JsonObject LibraryObj) continue;
|
||||
var libJobj = LibraryObj.DeepClone().AsObject();
|
||||
if (libJobj["MMC-hint"] is not null)
|
||||
{
|
||||
libJobj.Add("hint", libJobj["MMC-hint"]?.DeepClone());
|
||||
libJobj.Remove("MMC-hint");
|
||||
}
|
||||
|
||||
libs.Add(libJobj);
|
||||
}
|
||||
|
||||
if (patchJson["+libraries"] is not null)
|
||||
foreach (var Library in patchJson["+libraries"].AsArray()) // TODO: 此处处理不严谨,但也能用吧
|
||||
{
|
||||
if (Library is not JsonObject LibraryObj) continue;
|
||||
var libJobj = LibraryObj.DeepClone().AsObject();
|
||||
if (libJobj["MMC-hint"] is not null)
|
||||
{
|
||||
libJobj.Add("hint", libJobj["MMC-hint"]?.DeepClone());
|
||||
libJobj.Remove("MMC-hint");
|
||||
}
|
||||
|
||||
libs.Add(libJobj);
|
||||
}
|
||||
|
||||
libJson.Merge(libs);
|
||||
ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Libraries");
|
||||
}
|
||||
|
||||
// Tweakers
|
||||
if (patchJson["+tweakers"] is not null)
|
||||
{
|
||||
tweakers = (string)patchJson["+tweakers"][0];
|
||||
ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Tweakers");
|
||||
}
|
||||
|
||||
// AssetIndex
|
||||
if (patchJson["assetIndex"] is not null)
|
||||
{
|
||||
assetIndex = patchJson["assetIndex"]?.DeepClone().AsObject();
|
||||
ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 AssetIndex");
|
||||
}
|
||||
|
||||
// minecraftArguments -> arguments.game
|
||||
if (patchJson["minecraftArguments"] is not null)
|
||||
{
|
||||
foreach (var Arg in patchJson["minecraftArguments"].ToString().Split(" "))
|
||||
gameArguments.Add(Arg);
|
||||
packInfo.isMcArgsEdited = true;
|
||||
ModBase.Log(
|
||||
$"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 minecraftArguments 至 arguments.game");
|
||||
}
|
||||
|
||||
// mainClass
|
||||
if (patchJson["mainClass"] is not null)
|
||||
{
|
||||
mainClass = (string)patchJson["mainClass"];
|
||||
ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 mainClass");
|
||||
}
|
||||
|
||||
// Java 版本要求
|
||||
if (patchJson["compatibleJavaMajors"] is not null)
|
||||
{
|
||||
var javaVersion = 0;
|
||||
string javaComponent = null;
|
||||
var javaMajors = (JsonArray)patchJson["compatibleJavaMajors"];
|
||||
foreach (var Java in javaMajors)
|
||||
{
|
||||
if (javaVersion > ModBase.Val(Java))
|
||||
continue;
|
||||
// 优先选择主要的版本
|
||||
if (ModBase.Val(Java) == 21d)
|
||||
{
|
||||
javaVersion = 21;
|
||||
javaComponent = "java-runtime-delta";
|
||||
}
|
||||
else if (ModBase.Val(Java) == 17d)
|
||||
{
|
||||
javaVersion = 17;
|
||||
javaComponent = "java-runtime-gamma";
|
||||
}
|
||||
else if (ModBase.Val(Java) == 11d)
|
||||
{
|
||||
javaVersion = 11;
|
||||
javaComponent = null;
|
||||
}
|
||||
else if (ModBase.Val(Java) == 8d)
|
||||
{
|
||||
javaVersion = 8;
|
||||
javaComponent = "jre-legacy";
|
||||
}
|
||||
}
|
||||
|
||||
if (javaVersion == 0)
|
||||
{
|
||||
javaVersion = (int)javaMajors[0];
|
||||
javaComponent = null;
|
||||
}
|
||||
|
||||
javaVerJson = new JsonObject { { "majorVersion", javaVersion } };
|
||||
if (javaComponent is not null) javaVerJson.Add("component", javaComponent);
|
||||
ModBase.Log($"[ModPack] JSON-Patch {patchJson["uid"]} 要求 Java 版本: " + javaVersion);
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject jsonArguments = null;
|
||||
if (!string.IsNullOrWhiteSpace(tweakers))
|
||||
{
|
||||
gameArguments.Add("--tweakClass");
|
||||
gameArguments.Add(tweakers);
|
||||
}
|
||||
|
||||
if (gameArguments is not null || jvmArguments is not null)
|
||||
{
|
||||
jvmArguments.Insert(0, "-Djava.library.path=${natives_directory}");
|
||||
jvmArguments.Insert(1, "-Dminecraft.launcher.brand=${launcher_name}");
|
||||
jvmArguments.Insert(2, "-Dminecraft.launcher.version=${launcher_version}");
|
||||
jvmArguments.Insert(3, "-cp");
|
||||
jvmArguments.Insert(4, "${classpath}");
|
||||
jsonArguments = new JsonObject { { "game", gameArguments }, { "jvm", jvmArguments } };
|
||||
}
|
||||
|
||||
packInfo.overridedJson = new JsonObject();
|
||||
if (jsonArguments is not null)
|
||||
packInfo.overridedJson.Add("arguments", jsonArguments);
|
||||
if (mainClass is not null)
|
||||
packInfo.overridedJson.Add("mainClass", mainClass);
|
||||
if (assetIndex is not null)
|
||||
packInfo.overridedJson.Add("assetIndex", assetIndex);
|
||||
if (javaVerJson is not null)
|
||||
packInfo.overridedJson.Add("javaVersion", javaVerJson);
|
||||
if (libJson is not null)
|
||||
packInfo.overridedJson.Add("libraries", libJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "应用 MMC JSON-Patches 失败");
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("MMC 整合包安装信息存在问题", ex);
|
||||
}
|
||||
|
||||
// 获取实例名
|
||||
var instanceName = packInstance.RegexSeek(@"(?<=\nname\=)[^\n]+") ?? "";
|
||||
var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions"));
|
||||
if (!validate.Validate(instanceName).IsValid)
|
||||
instanceName = "";
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "",
|
||||
[validate]);
|
||||
if (string.IsNullOrEmpty(instanceName))
|
||||
throw new ModBase.CancelledException();
|
||||
// 解压
|
||||
var installTemp = ModMain.RequestTaskTempFolder();
|
||||
var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}";
|
||||
var installLoaders = new List<LoaderBase>();
|
||||
installLoaders.Add(new LoaderTask<string, int>(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"),
|
||||
task =>
|
||||
{
|
||||
ExtractModpackFiles(installTemp, fileAddress, task, 0.55d);
|
||||
CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "libraries"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName, "libraries"), task, 0.2d);
|
||||
CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, ".minecraft"),
|
||||
Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.2d);
|
||||
|
||||
#region instance.cfg
|
||||
|
||||
// 读取 MMC 设置文件(#2655)
|
||||
try
|
||||
{
|
||||
var mMCSetupFile = Path.Combine(installTemp, archiveBaseFolder, "instance.cfg");
|
||||
// 将其中的等号替换为冒号,以符合 ini 文件格式
|
||||
if (File.Exists(mMCSetupFile))
|
||||
{
|
||||
List<string> lines = [];
|
||||
foreach (var Line in ModBase.ReadFile(mMCSetupFile).Split(new[] { "\r", "\n" },
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!Line.Contains("="))
|
||||
continue;
|
||||
lines.Add(Line.BeforeFirst("=") + ":" + Line.AfterFirst("="));
|
||||
}
|
||||
|
||||
ModBase.WriteFile(mMCSetupFile, lines.Join("\r\n"));
|
||||
// 读取文件
|
||||
if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "OverrideCommands",
|
||||
false.ToString())))
|
||||
{
|
||||
var preLaunchCommand = ModBase.ReadIni(mMCSetupFile, "PreLaunchCommand");
|
||||
if (!string.IsNullOrEmpty(preLaunchCommand))
|
||||
{
|
||||
preLaunchCommand = preLaunchCommand.Replace(@"\""", "\"")
|
||||
.Replace("$INST_JAVA", "{java}java.exe").Replace(@"$INST_MC_DIR\", "{minecraft}")
|
||||
.Replace("$INST_MC_DIR", "{minecraft}").Replace(@"$INST_DIR\", "{verpath}")
|
||||
.Replace("$INST_DIR", "{verpath}").Replace("$INST_ID", "{name}")
|
||||
.Replace("$INST_NAME", "{name}");
|
||||
Config.Instance.PreLaunchCommand[versionFolder] = preLaunchCommand;
|
||||
ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:启动前执行命令:" + preLaunchCommand);
|
||||
}
|
||||
}
|
||||
|
||||
if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "JoinServerOnLaunch",
|
||||
false.ToString())))
|
||||
{
|
||||
var serverAddress = ModBase.ReadIni(mMCSetupFile, "JoinServerOnLaunchAddress")
|
||||
.Replace(@"\""", "\"");
|
||||
Config.Instance.ServerToEnter[versionFolder] = serverAddress;
|
||||
ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:自动进入服务器:" + serverAddress);
|
||||
}
|
||||
|
||||
if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "IgnoreJavaCompatibility",
|
||||
false.ToString())))
|
||||
{
|
||||
Config.Instance.IgnoreJavaCompatibility[versionFolder] = true;
|
||||
ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:忽略 Java 兼容性警告");
|
||||
}
|
||||
|
||||
var logo = Path.GetFileName(ModBase.ReadIni(mMCSetupFile, "iconKey"));
|
||||
if (!string.IsNullOrEmpty(logo) && File.Exists($"{installTemp}{archiveBaseFolder}{logo}.png"))
|
||||
{
|
||||
States.Instance.IsLogoCustom[versionFolder] = true;
|
||||
States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png";
|
||||
ModBase.CopyFile($"{installTemp}{archiveBaseFolder}{logo}.png",
|
||||
$@"{ModFolder.mcFolderSelected}versions\{instanceName}\PCL\Logo.png");
|
||||
ModBase.Log($"[ModPack] 迁移 MultiMC 实例独立设置:实例图标({logo}.png)");
|
||||
}
|
||||
|
||||
// JVM 参数
|
||||
var jvmArgs = ModBase.ReadIni(mMCSetupFile, "JvmArgs");
|
||||
if (!string.IsNullOrEmpty(jvmArgs))
|
||||
{
|
||||
if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "OverrideJavaArgs",
|
||||
false.ToString())))
|
||||
{
|
||||
Config.Instance.JvmArgs[versionFolder] = jvmArgs;
|
||||
ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(覆盖):" + jvmArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
jvmArgs = jvmArgs +
|
||||
" " +
|
||||
Config.Launch.JvmArgs;
|
||||
Config.Instance.JvmArgs[versionFolder] = jvmArgs;
|
||||
ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(追加):" + jvmArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"读取 MMC 配置文件失败({installTemp}{archiveBaseFolder}instance.cfg)");
|
||||
}
|
||||
|
||||
#endregion
|
||||
})
|
||||
{
|
||||
ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d,
|
||||
block = false
|
||||
}); // 每 6M 需要 1s
|
||||
// 构造实例安装请求
|
||||
if (packJson["components"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic"));
|
||||
var request = new ModDownloadLib.McInstallRequest
|
||||
{
|
||||
targetInstanceName = instanceName,
|
||||
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"
|
||||
};
|
||||
foreach (var Component in packJson["components"].AsArray())
|
||||
switch ((Component["uid"] ?? "").ToString() ?? "")
|
||||
{
|
||||
case "org.lwjgl":
|
||||
{
|
||||
ModBase.Log("[ModPack] 已跳过 LWJGL 项");
|
||||
break;
|
||||
}
|
||||
case "net.minecraft":
|
||||
{
|
||||
request.minecraftName = (string)Component["version"];
|
||||
break;
|
||||
}
|
||||
case "net.minecraftforge":
|
||||
{
|
||||
if (Component["version"].ToString().StartsWithF("0."))
|
||||
request.cleanroomVersion = (string)Component["version"];
|
||||
else
|
||||
request.forgeVersion = (string)Component["version"];
|
||||
|
||||
break;
|
||||
}
|
||||
case "net.neoforged":
|
||||
{
|
||||
request.neoForgeVersion = (string)Component["version"];
|
||||
break;
|
||||
}
|
||||
case "net.fabricmc.fabric-loader":
|
||||
{
|
||||
request.fabricVersion = (string)Component["version"];
|
||||
break;
|
||||
}
|
||||
case "org.quiltmc.quilt-loader":
|
||||
{
|
||||
throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported"));
|
||||
}
|
||||
}
|
||||
|
||||
if (packInfo is not null)
|
||||
request.mmcPackInfo = packInfo;
|
||||
// 构造加载器
|
||||
var mergeLoaders = ModDownloadLib.McInstallLoader(request);
|
||||
// 构造总加载器
|
||||
var loaders = new List<LoaderBase>();
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"),
|
||||
installLoaders)
|
||||
{ show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) });
|
||||
loaders.Add(new LoaderCombo<string>(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders)
|
||||
{ show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) });
|
||||
|
||||
// 重复任务检查
|
||||
var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.MmcInstall", instanceName);
|
||||
if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? "")))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error);
|
||||
throw new ModBase.CancelledException();
|
||||
}
|
||||
|
||||
// 启动
|
||||
var loader = new LoaderCombo<string>(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState };
|
||||
loader.Start(request.targetInstanceFolder);
|
||||
LoaderTaskbarAdd(loader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager));
|
||||
return loader;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PCL.Core.Utils.Secret;
|
||||
using PCL.Core.Utils.Validate;
|
||||
using PCL.Network;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.IO.Net;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前选定的档案
|
||||
/// </summary>
|
||||
public static McProfile selectedProfile;
|
||||
|
||||
/// <summary>
|
||||
/// 上次选定的档案编号
|
||||
/// </summary>
|
||||
public static int lastUsedProfile;
|
||||
|
||||
/// <summary>
|
||||
/// 档案列表
|
||||
/// </summary>
|
||||
public static List<McProfile> profileList = new();
|
||||
|
||||
public static bool isCreatingProfile;
|
||||
|
||||
/// <summary>
|
||||
/// 档案操作日志
|
||||
/// </summary>
|
||||
public static void ProfileLog(string content, ModBase.LogLevel level = ModBase.LogLevel.Normal)
|
||||
{
|
||||
var output = "[Profile] " + content;
|
||||
ModBase.Log(output, level);
|
||||
}
|
||||
|
||||
#region 获取正版档案 UUID
|
||||
|
||||
/// <summary>
|
||||
/// 根据用户名返回对应 UUID,需要多线程
|
||||
/// </summary>
|
||||
/// <param name="name">玩家 ID</param>
|
||||
public static object McLoginMojangUuid(string name, bool throwOnNotFound)
|
||||
{
|
||||
if (name.Trim().Length == 0)
|
||||
return ModBase.StrFill("", "0", 32);
|
||||
// 从缓存获取
|
||||
var uuid = ModBase.ReadIni(ModBase.pathTemp + @"Cache\Uuid\Mojang.ini", name);
|
||||
if ((uuid?.Length ?? 0) == 32)
|
||||
return uuid;
|
||||
// 从官网获取
|
||||
try
|
||||
{
|
||||
JsonObject gotJson = null;
|
||||
var finished = false;
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
gotJson = (JsonObject)ModNet.NetGetCodeByRequestRetry(
|
||||
"https://api.mojang.com/users/profiles/minecraft/" + name, isJson: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
finished = true;
|
||||
}
|
||||
}, $"{name} Uuid Get");
|
||||
while (!finished)
|
||||
Thread.Sleep(50);
|
||||
if (gotJson is null)
|
||||
throw new FileNotFoundException("正版玩家档案不存在(" + name + ")");
|
||||
uuid = (string)(gotJson["id"] ?? "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "从官网获取正版 UUID 失败(" + name + ")");
|
||||
if (!throwOnNotFound && ex is FileNotFoundException)
|
||||
uuid = GetOfflineUuid(name, isLegacy: true); // 玩家档案不存在
|
||||
else
|
||||
throw new Exception("从官网获取正版 UUID 失败", ex);
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
if ((uuid?.Length ?? 0) != 32)
|
||||
throw new Exception("获取的正版 UUID 长度不足(" + uuid + ")");
|
||||
ModBase.WriteIni(ModBase.pathTemp + @"Cache\Uuid\Mojang.ini", name, uuid);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 类型声明
|
||||
|
||||
public class McProfile
|
||||
{
|
||||
public string AccessToken;
|
||||
public string ClientToken;
|
||||
|
||||
/// <summary>
|
||||
/// 档案描述,暂时没做功能
|
||||
/// </summary>
|
||||
public string Desc;
|
||||
|
||||
/// <summary>
|
||||
/// 联网验证档案的验证有效期
|
||||
/// </summary>
|
||||
public long Expires;
|
||||
|
||||
/// <summary>
|
||||
/// 用于识别正版档案的 ID 标识符
|
||||
/// </summary>
|
||||
[Obsolete("暂时弃用,应当使用 AccessToken 与 RefreshToken")]
|
||||
public string IdentityId;
|
||||
|
||||
/// <summary>
|
||||
/// 登录用户名,用于第三方验证
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码,用于第三方验证
|
||||
/// </summary>
|
||||
public string Password;
|
||||
|
||||
/// <summary>
|
||||
/// 原始 JSON 数据,用于正版验证部分功能
|
||||
/// </summary>
|
||||
public string RawJson;
|
||||
|
||||
public string RefreshToken;
|
||||
|
||||
/// <summary>
|
||||
/// 验证服务器地址,用于第三方验证
|
||||
/// </summary>
|
||||
public string Server;
|
||||
|
||||
/// <summary>
|
||||
/// 验证服务器名称,来自第三方验证服务器返回的 Metadata
|
||||
/// </summary>
|
||||
public string ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// 用于档案列表头像显示的皮肤 ID
|
||||
/// </summary>
|
||||
public string SkinHeadId;
|
||||
|
||||
/// <summary>
|
||||
/// 档案类型
|
||||
/// </summary>
|
||||
public ModLaunch.McLoginType Type;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家 ID
|
||||
/// </summary>
|
||||
public string Username;
|
||||
|
||||
public string Uuid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 读写档案
|
||||
|
||||
/// <summary>
|
||||
/// 重新获取已有档案列表
|
||||
/// </summary>
|
||||
public static void GetProfile()
|
||||
{
|
||||
ProfileLog("开始获取本地档案");
|
||||
profileList.Clear();
|
||||
var profilePath = Path.Combine(ModBase.pathAppdataConfig, "profiles.json");
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(ModBase.pathAppdataConfig))
|
||||
Directory.CreateDirectory(ModBase.pathAppdataConfig);
|
||||
if (!File.Exists(profilePath))
|
||||
{
|
||||
File.Create(profilePath).Close();
|
||||
ModBase.WriteFile(profilePath, "{\"lastUsed\":0,\"profiles\":[]}"); // 创建档案列表文件
|
||||
}
|
||||
|
||||
var profileJobj = ModBase.GetJson(ModBase.ReadFile(profilePath));
|
||||
lastUsedProfile = (int)profileJobj["lastUsed"];
|
||||
var profileListJobj = (JsonArray)profileJobj["profiles"];
|
||||
foreach (var Profile in profileListJobj)
|
||||
{
|
||||
McProfile newProfile = null;
|
||||
if ((string)Profile["type"] == "microsoft")
|
||||
newProfile = new McProfile
|
||||
{
|
||||
Type = ModLaunch.McLoginType.Ms,
|
||||
Uuid = (string)Profile["uuid"],
|
||||
Username = (string)Profile["username"],
|
||||
AccessToken = EncryptHelper.SecretDecrypt((string?)Profile["accessToken"]),
|
||||
RefreshToken = EncryptHelper.SecretDecrypt((string?)Profile["refreshToken"]),
|
||||
Expires = (long)Profile["expires"],
|
||||
Desc = (string)Profile["desc"],
|
||||
RawJson = EncryptHelper.SecretDecrypt((string?)Profile["rawJson"]),
|
||||
SkinHeadId = (string)Profile["skinHeadId"]
|
||||
};
|
||||
else if ((string)Profile["type"] == "authlib")
|
||||
newProfile = new McProfile
|
||||
{
|
||||
Type = ModLaunch.McLoginType.Auth,
|
||||
Uuid = (string)Profile["uuid"],
|
||||
Username = (string)Profile["username"],
|
||||
AccessToken = EncryptHelper.SecretDecrypt((string?)Profile["accessToken"]),
|
||||
RefreshToken = EncryptHelper.SecretDecrypt((string?)Profile["refreshToken"]),
|
||||
Expires = (long)Profile["expires"],
|
||||
Server = (string)Profile["server"],
|
||||
ServerName = (string)Profile["serverName"],
|
||||
Name = EncryptHelper.SecretDecrypt((string?)Profile["name"]),
|
||||
Password = EncryptHelper.SecretDecrypt((string?)Profile["password"]),
|
||||
ClientToken = EncryptHelper.SecretDecrypt((string?)Profile["clientToken"]),
|
||||
Desc = (string)Profile["desc"],
|
||||
SkinHeadId = (string)Profile["skinHeadId"]
|
||||
};
|
||||
else
|
||||
newProfile = new McProfile
|
||||
{
|
||||
Type = ModLaunch.McLoginType.Legacy,
|
||||
Uuid = (string)Profile["uuid"],
|
||||
Username = (string)Profile["username"],
|
||||
Desc = (string)Profile["desc"],
|
||||
SkinHeadId = (string)Profile["skinHeadId"]
|
||||
};
|
||||
profileList.Add(newProfile);
|
||||
}
|
||||
|
||||
ProfileLog($"获取到 {profileList.Count} 个档案");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var profilePathBak =
|
||||
Path.Combine(ModBase.pathAppdataConfig, $"profiles.json.bak{DateTime.Now.ToBinary()}");
|
||||
File.Move(profilePath, profilePathBak);
|
||||
}
|
||||
catch (Exception ex1)
|
||||
{
|
||||
}
|
||||
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Profile.Error.Corrupted"),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Account.Profile.Error.Corrupted"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以当前的档案列表写入配置文件
|
||||
/// </summary>
|
||||
public static void SaveProfile(JsonArray listJson = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = new JsonObject();
|
||||
if (listJson is not null)
|
||||
{
|
||||
json = new JsonObject { { "lastUsed", lastUsedProfile }, { "profiles", listJson } };
|
||||
}
|
||||
else
|
||||
{
|
||||
var list = new JsonArray();
|
||||
foreach (var Profile in profileList)
|
||||
{
|
||||
JsonObject profileJobj = null;
|
||||
if (Profile.Type == ModLaunch.McLoginType.Ms)
|
||||
profileJobj = new JsonObject
|
||||
{
|
||||
{ "type", "microsoft" }, { "uuid", Profile.Uuid }, { "username", Profile.Username },
|
||||
{ "accessToken", EncryptHelper.SecretEncrypt(Profile.AccessToken) },
|
||||
{ "refreshToken", EncryptHelper.SecretEncrypt(Profile.RefreshToken) },
|
||||
{ "expires", Profile.Expires }, { "desc", Profile.Desc },
|
||||
{ "rawJson", EncryptHelper.SecretEncrypt(Profile.RawJson) },
|
||||
{ "skinHeadId", Profile.SkinHeadId }
|
||||
};
|
||||
else if (Profile.Type == ModLaunch.McLoginType.Auth)
|
||||
profileJobj = new JsonObject
|
||||
{
|
||||
{ "type", "authlib" }, { "uuid", Profile.Uuid }, { "username", Profile.Username },
|
||||
{ "accessToken", EncryptHelper.SecretEncrypt(Profile.AccessToken) },
|
||||
{ "refreshToken", EncryptHelper.SecretEncrypt(Profile.RefreshToken) },
|
||||
{ "expires", Profile.Expires }, { "server", Profile.Server },
|
||||
{ "serverName", Profile.ServerName }, { "name", EncryptHelper.SecretEncrypt(Profile.Name) },
|
||||
{ "password", EncryptHelper.SecretEncrypt(Profile.Password) },
|
||||
{ "clientToken", EncryptHelper.SecretEncrypt(Profile.ClientToken) },
|
||||
{ "desc", Profile.Desc }, { "skinHeadId", Profile.SkinHeadId }
|
||||
};
|
||||
else
|
||||
profileJobj = new JsonObject
|
||||
{
|
||||
{ "type", "offline" }, { "uuid", Profile.Uuid }, { "username", Profile.Username },
|
||||
{ "desc", Profile.Desc }, { "skinHeadId", Profile.SkinHeadId }
|
||||
};
|
||||
list.Add(profileJobj);
|
||||
}
|
||||
|
||||
ProfileLog($"开始保存档案,共 {list.Count} 个");
|
||||
json = new JsonObject { { "lastUsed", lastUsedProfile }, { "profiles", list } };
|
||||
}
|
||||
|
||||
var actualFile = Path.Combine(ModBase.pathAppdataConfig, "profiles.json");
|
||||
var tempFile = actualFile + ".tmp";
|
||||
var bakFile = actualFile + ".bak";
|
||||
File.WriteAllBytes(tempFile, Encoding.UTF8.GetBytes(json.ToJsonString()));
|
||||
if (File.Exists(actualFile))
|
||||
File.Replace(tempFile, actualFile, bakFile);
|
||||
else
|
||||
File.Move(tempFile, actualFile);
|
||||
ProfileLog("档案已保存");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Profile.Error.Write"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Launch.Account.Profile.Error.Write"));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 新建与编辑
|
||||
|
||||
/// <summary>
|
||||
/// 新建档案
|
||||
/// </summary>
|
||||
public static void CreateProfile()
|
||||
{
|
||||
int? selectedAuthTypeNum = default; // 验证类型序号
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
List<IMyRadio> authTypeList;
|
||||
#if DEBUG || DEBUGCI
|
||||
authTypeList = _GetAvailableProfileSelection(true);
|
||||
#else
|
||||
var hasMinecraftAccount = profileList.Any(x => x.Type == ModLaunch.McLoginType.Ms);
|
||||
var restricted = Lang.IsFeaturesUnrestricted && profileList.Count > 0;
|
||||
var hasNetwork = NetworkHelper.IsNetworkAvailable();
|
||||
if (hasMinecraftAccount || restricted || !hasNetwork)
|
||||
authTypeList = _GetAvailableProfileSelection(true);
|
||||
else
|
||||
authTypeList = _GetAvailableProfileSelection(false);
|
||||
|
||||
#endif
|
||||
|
||||
selectedAuthTypeNum = ModMain.MyMsgBoxSelect(authTypeList, Lang.Text("Launch.Account.Profile.Create.SelectAuthType.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"));
|
||||
});
|
||||
if (selectedAuthTypeNum is null)
|
||||
return;
|
||||
isCreatingProfile = true;
|
||||
if (selectedAuthTypeNum.HasValue && selectedAuthTypeNum.Value == 0) // 正版验证
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Ms));
|
||||
else if (selectedAuthTypeNum.HasValue && selectedAuthTypeNum.Value == 1) // 第三方验证
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth));
|
||||
else // 离线验证
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Legacy));
|
||||
}
|
||||
|
||||
private static List<IMyRadio> _GetAvailableProfileSelection(bool includeOfflineAndThirdParty) => includeOfflineAndThirdParty switch
|
||||
{
|
||||
true =>
|
||||
[
|
||||
new MyListItem
|
||||
{
|
||||
Title = Lang.Text("Launch.Account.Type.Microsoft"),
|
||||
Type = MyListItem.CheckType.RadioBox,
|
||||
SvgIcon = "lucide/shield-check"
|
||||
},
|
||||
|
||||
new MyListItem
|
||||
{
|
||||
Title = Lang.Text("Launch.Account.Type.ThirdParty"),
|
||||
Type = MyListItem.CheckType.RadioBox,
|
||||
SvgIcon = "lucide/network"
|
||||
},
|
||||
|
||||
new MyListItem
|
||||
{
|
||||
Title = Lang.Text("Launch.Account.Type.Offline"),
|
||||
Type = MyListItem.CheckType.RadioBox,
|
||||
SvgIcon = "lucide/link-2-off"
|
||||
}
|
||||
],
|
||||
_ =>
|
||||
[
|
||||
new MyListItem
|
||||
{
|
||||
Title = Lang.Text("Launch.Account.Type.Microsoft"),
|
||||
Type = MyListItem.CheckType.RadioBox,
|
||||
SvgIcon = "lucide/shield-check"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 编辑当前档案的 ID
|
||||
/// </summary>
|
||||
public static void EditProfileId()
|
||||
{
|
||||
if (selectedProfile.Type == ModLaunch.McLoginType.Ms)
|
||||
{
|
||||
string newUsername = null;
|
||||
ModBase.RunInUiWait(() => newUsername = ModMain.MyMsgBoxInput(Lang.Text("Launch.Account.Profile.EditPlayerId.Title"), Lang.Text("Launch.Account.Profile.EditPlayerId.MicrosoftWarning"),
|
||||
selectedProfile.Username,
|
||||
[new StringLengthValidator(3, 16), new RegexValidator("([A-z]|[0-9]|_)+")],
|
||||
Lang.Text("Launch.Account.Profile.EditPlayerId.Hint"), Lang.Text("Common.Action.Confirm")));
|
||||
if (string.IsNullOrEmpty(newUsername))
|
||||
return;
|
||||
if (string.IsNullOrWhiteSpace(newUsername))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.EditPlayerId.Empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.EditPlayerId.Confirm.Message"), Lang.Text("Launch.Account.Profile.EditPlayerId.Confirm.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 2)
|
||||
return;
|
||||
// 更新档案信息
|
||||
// 刷新页面信息
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var checkResult = (JsonObject)ModBase.GetJson(Requester.Fetch(
|
||||
$"https://api.minecraftservices.com/minecraft/profile/name/{newUsername}/available",
|
||||
new FetchParam
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{ { "Authorization", "Bearer " + selectedProfile.AccessToken } }
|
||||
}));
|
||||
if ((string)checkResult["status"] == "DUPLICATE")
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.EditPlayerId.Duplicate"), Lang.Text("Launch.Account.Profile.EditPlayerId.Failed.Title"), Lang.Text("Common.Action.Confirm"), isWarn: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((string)checkResult["status"] == "NOT_ALLOWED")
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.EditPlayerId.NotAllowed"), Lang.Text("Launch.Account.Profile.EditPlayerId.Failed.Title"), Lang.Text("Common.Action.Confirm"), isWarn: true);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = Requester.Fetch(
|
||||
$"https://api.minecraftservices.com/minecraft/profile/name/{newUsername}",
|
||||
new FetchParam
|
||||
{
|
||||
Method = "PUT",
|
||||
ContentType = "application/json",
|
||||
Headers = new Dictionary<string, string>
|
||||
{ { "Authorization", "Bearer " + selectedProfile.AccessToken } }
|
||||
});
|
||||
var resultJson = (JsonObject)ModBase.GetJson(result);
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.EditPlayerId.Success", resultJson["name"]), HintType.Success);
|
||||
profileList.Remove(selectedProfile);
|
||||
selectedProfile.Username = (string)resultJson["name"];
|
||||
profileList.Add(selectedProfile);
|
||||
lastUsedProfile = profileList.Count - 1;
|
||||
// 本方法在后台线程执行(阻塞网络请求),下面操作 WPF 控件,必须切回 UI 线程,
|
||||
// 否则触发线程亲和性异常——与本文件其他从后台线程刷新界面处一样用 RunInUi 包裹。
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
// 改名成功后正处于档案页(ProfileSkin),此时 RefreshPage 因目标页与当前页相同会提前
|
||||
// 返回、不刷新显示的玩家 ID,需显式 Reload 当前档案页,使新 ID 立即生效。
|
||||
ModMain.frmLoginProfileSkin?.Reload();
|
||||
ModMain.frmLaunchLeft.RefreshPage(true);
|
||||
});
|
||||
SaveProfile();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
var exSummary = ex.ToString();
|
||||
if (exSummary.Contains("403"))
|
||||
ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.EditPlayerId.Cooldown"), Lang.Text("Launch.Account.Profile.EditPlayerId.Failed.Title"), Lang.Text("Common.Action.Confirm"));
|
||||
else
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Profile.Error.ChangeId"),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Account.Profile.Error.ChangeId"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
else if (selectedProfile.Type == ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
var server = selectedProfile.Server;
|
||||
ModBase.OpenWebsite(server.Replace("/api/yggdrasil/authserver" + (server.EndsWithF("/") ? "/" : ""),
|
||||
"/user/profile"));
|
||||
}
|
||||
else
|
||||
{
|
||||
string newUsername = null;
|
||||
ModBase.RunInUiWait(() => newUsername = ModMain.MyMsgBoxInput(Lang.Text("Launch.Account.Profile.EditPlayerId.Title"),
|
||||
defaultInput: selectedProfile.Username,
|
||||
validateRules: [new StringLengthValidator(3, 16), new RegexValidator("([A-z]|[0-9]|_)+")],
|
||||
hintText: Lang.Text("Launch.Account.Profile.EditPlayerId.Hint"), button1: Lang.Text("Common.Action.Confirm"), button2: Lang.Text("Common.Action.Cancel")));
|
||||
if (string.IsNullOrEmpty(newUsername))
|
||||
return;
|
||||
EditOfflineUuid(selectedProfile, GetOfflineUuid(newUsername));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑离线档案的 UUID
|
||||
/// </summary>
|
||||
/// <param name="profile">目标档案</param>
|
||||
public static void EditOfflineUuid(McProfile profile, string uuid = null)
|
||||
{
|
||||
var profileIndex = profileList.IndexOf(profile);
|
||||
string newUuid;
|
||||
if (uuid is not null)
|
||||
{
|
||||
newUuid = uuid;
|
||||
goto Write;
|
||||
}
|
||||
|
||||
int uuidType;
|
||||
int? uuidTypeInput = default;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
var uuidTypeList = new List<IMyRadio>
|
||||
{
|
||||
new MyRadioBox { Text = Lang.Text("Launch.Account.Profile.Uuid.Standard") }, new MyRadioBox { Text = Lang.Text("Launch.Account.Profile.Uuid.Legacy") },
|
||||
new MyRadioBox { Text = Lang.Text("Common.Option.Customize") }
|
||||
};
|
||||
uuidTypeInput = ModMain.MyMsgBoxSelect(uuidTypeList, Lang.Text("Launch.Account.Profile.Uuid.SelectType.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"));
|
||||
});
|
||||
if (uuidTypeInput is null)
|
||||
return;
|
||||
uuidType = (int)uuidTypeInput;
|
||||
if (uuidType == 0)
|
||||
newUuid = GetOfflineUuid(profile.Username);
|
||||
else if (uuidType == 1)
|
||||
newUuid = GetOfflineUuid(profile.Username, isLegacy: true);
|
||||
else
|
||||
newUuid = ModMain.MyMsgBoxInput(Lang.Text("Launch.Account.Profile.Uuid.ChangeTitle", profile.Username), defaultInput: profile.Uuid,
|
||||
hintText: Lang.Text("Launch.Account.Profile.Uuid.Hint"),
|
||||
validateRules:
|
||||
[new StringLengthValidator(32, 32), new RegexValidator("([A-z]|[0-9]){32}", Lang.Text("Launch.Account.Profile.Uuid.InvalidChars"))],
|
||||
button1: Lang.Text("Common.Action.Continue"), button2: Lang.Text("Common.Action.Cancel"));
|
||||
if (string.IsNullOrEmpty(newUuid))
|
||||
return;
|
||||
Write: ;
|
||||
|
||||
profileList[profileIndex].Uuid = newUuid;
|
||||
selectedProfile = profileList[profileIndex];
|
||||
SaveProfile();
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.Saved"), HintType.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑指定档案的验证服务器显示名称
|
||||
/// </summary>
|
||||
public static void EditAuthServerName(McProfile profile, string serverName)
|
||||
{
|
||||
var profileIndex = profileList.IndexOf(profile);
|
||||
profileList[profileIndex].ServerName = serverName;
|
||||
SaveProfile();
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.Saved"), HintType.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除特定档案
|
||||
/// </summary>
|
||||
/// <param name="profile">目标档案</param>
|
||||
public static void RemoveProfile(McProfile profile)
|
||||
{
|
||||
profileList.Remove(profile);
|
||||
lastUsedProfile = default;
|
||||
SaveProfile();
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.Deleted"), HintType.Success);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 离线 UUID 获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取离线 UUID
|
||||
/// </summary>
|
||||
/// <param name="userName">玩家 ID</param>
|
||||
/// <param name="isSplited">返回的 UUID 是否有连字符分割</param>
|
||||
/// <param name="isLegacy">是否使用旧版 PCL 生成方式,若为 True 则返回的 UUID 总是不带连字符</param>
|
||||
public static string GetOfflineUuid(string userName, bool isSplited = false, bool isLegacy = false)
|
||||
{
|
||||
if (isLegacy)
|
||||
{
|
||||
var fullUuid = ModBase.StrFill(userName.Length.ToString("X"), "0", 16) +
|
||||
ModBase.StrFill(ModBase.GetHash(userName).ToString("X"), "0", 16);
|
||||
return fullUuid.Substring(0, 12) + "3" + fullUuid.Substring(13, 3) + "9" + fullUuid.Substring(17, 15);
|
||||
}
|
||||
|
||||
var md5Hash = MD5.Create();
|
||||
var hash = md5Hash.ComputeHash(Encoding.UTF8.GetBytes("OfflinePlayer:" + userName));
|
||||
hash[6] = (byte)(hash[6] & 0xF);
|
||||
hash[6] = (byte)(hash[6] | 0x30);
|
||||
hash[8] = (byte)(hash[8] & 0x3F);
|
||||
hash[8] = (byte)(hash[8] | 0x80);
|
||||
var parsed = new Guid(ToUuidString(hash));
|
||||
ProfileLog("获取到离线 UUID: " + parsed);
|
||||
if (isSplited) return parsed.ToString();
|
||||
|
||||
return parsed.ToString().Replace("-", "");
|
||||
}
|
||||
|
||||
private static string ToUuidString(byte[] bytes)
|
||||
{
|
||||
var msb = 0L;
|
||||
var lsb = 0L;
|
||||
for (var i = 0; i <= 7; i++)
|
||||
msb = (msb << 8) | (bytes[i] & 0xFF);
|
||||
for (var i = 8; i <= 15; i++)
|
||||
lsb = (lsb << 8) | (bytes[i] & 0xFF);
|
||||
return $"{Digits(msb >> 32, 8)}-{Digits(msb >> 16, 4)}-{Digits(msb, 4)}-{Digits(lsb >> 48, 4)}-{Digits(lsb, 12)}";
|
||||
}
|
||||
|
||||
private static object Digits(long val, int digs)
|
||||
{
|
||||
var hi = 1L << (digs * 4);
|
||||
return (hi | (val & (hi - 1L))).ToString("X").Substring(1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 档案信息获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取档案详情信息用于显示
|
||||
/// </summary>
|
||||
/// <param name="profile">目标档案</param>
|
||||
/// <returns>显示的详情信息</returns>
|
||||
public static object GetProfileInfo(McProfile profile)
|
||||
{
|
||||
string info = null;
|
||||
if (profile.Type == ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
info += Lang.Text("Launch.Account.Type.ThirdParty");
|
||||
if (!string.IsNullOrWhiteSpace(profile.ServerName))
|
||||
info += $" / {profile.ServerName}";
|
||||
}
|
||||
else if (profile.Type == ModLaunch.McLoginType.Ms)
|
||||
{
|
||||
info += Lang.Text("Launch.Account.Type.Microsoft");
|
||||
}
|
||||
else
|
||||
{
|
||||
info += Lang.Text("Launch.Account.Type.Offline");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profile.Desc))
|
||||
info += $",{profile.Desc}";
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前档案的验证信息。
|
||||
/// <param name="targetAuthType">验证类型,若为新档案需填</param>
|
||||
/// </summary>
|
||||
public static ModLaunch.McLoginData GetLoginData(ModLaunch.McLoginType targetAuthType = default)
|
||||
{
|
||||
ModLaunch.McLoginType authType = default;
|
||||
if (selectedProfile is null) // 新档案
|
||||
{
|
||||
if (targetAuthType != default)
|
||||
authType = targetAuthType;
|
||||
else
|
||||
authType = ModLaunch.McLoginType.Legacy;
|
||||
if (authType == ModLaunch.McLoginType.Auth)
|
||||
return new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
Description = "Authlib-Injector",
|
||||
LoginType = ModLaunch.McLoginType.Auth,
|
||||
IsExist = ModMain.frmLoginAuth is null
|
||||
};
|
||||
|
||||
if (authType == ModLaunch.McLoginType.Ms) return new ModLaunch.McLoginMs();
|
||||
|
||||
return new ModLaunch.McLoginLegacy();
|
||||
}
|
||||
|
||||
// 已有档案
|
||||
authType = selectedProfile.Type;
|
||||
if (authType == ModLaunch.McLoginType.Auth)
|
||||
return new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
BaseUrl = selectedProfile.Server,
|
||||
UserName = selectedProfile.Name,
|
||||
Password = selectedProfile.Password,
|
||||
Description = "Authlib-Injector",
|
||||
LoginType = ModLaunch.McLoginType.Auth,
|
||||
IsExist = ModMain.frmLoginAuth is null
|
||||
};
|
||||
|
||||
if (authType == ModLaunch.McLoginType.Ms)
|
||||
{
|
||||
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Finished)
|
||||
return new ModLaunch.McLoginMs
|
||||
{
|
||||
OAuthRefreshToken = selectedProfile.RefreshToken,
|
||||
UserName = selectedProfile.Username,
|
||||
AccessToken = selectedProfile.AccessToken,
|
||||
Uuid = selectedProfile.Uuid,
|
||||
ProfileJson = selectedProfile.RawJson
|
||||
};
|
||||
|
||||
return new ModLaunch.McLoginMs
|
||||
{ OAuthRefreshToken = selectedProfile.RefreshToken, UserName = selectedProfile.Name };
|
||||
}
|
||||
|
||||
return new ModLaunch.McLoginLegacy { UserName = selectedProfile.Username, Uuid = selectedProfile.Uuid };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查当前档案是否有效
|
||||
/// </summary>
|
||||
/// <returns>若档案验证有效,则返回空字符串,否则返回错误原因</returns>
|
||||
public static string IsProfileValid()
|
||||
{
|
||||
switch (selectedProfile.Type)
|
||||
{
|
||||
case ModLaunch.McLoginType.Legacy:
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedProfile.Username.Trim()))
|
||||
return Lang.Text("Launch.Account.Profile.Validation.EmptyUsername");
|
||||
if (selectedProfile.Username.Contains("\""))
|
||||
return Lang.Text("Launch.Account.Profile.Validation.QuoteInUsername");
|
||||
if (ModInstanceList.McMcInstanceSelected is not null && ModInstanceList.McMcInstanceSelected.Info.Drop >= 203 &&
|
||||
selectedProfile.Username.Trim().Length > 16) return Lang.Text("Launch.Account.Profile.Validation.UsernameTooLong");
|
||||
return "";
|
||||
}
|
||||
case ModLaunch.McLoginType.Ms:
|
||||
{
|
||||
return "";
|
||||
}
|
||||
case ModLaunch.McLoginType.Auth:
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return Lang.Text("Launch.Account.Profile.Validation.UnknownAuthType");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 皮肤
|
||||
|
||||
private static bool _isMsSkinChanging;
|
||||
|
||||
public static void ChangeSkinMs()
|
||||
{
|
||||
// 检查条件,获取新皮肤
|
||||
if (_isMsSkinChanging)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Change.Busy"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModLaunch.mcLoginLoader.State == ModBase.LoadState.Failed)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Change.LoginFailed"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var skinInfo = ModSkin.McSkinSelect();
|
||||
if (!skinInfo.IsVaild)
|
||||
return;
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Change.Starting"));
|
||||
_isMsSkinChanging = true;
|
||||
// 开始实际获取
|
||||
|
||||
// 获取登录信息
|
||||
|
||||
// 获取新皮肤地址
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Retry: ;
|
||||
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Loading)
|
||||
ModLaunch.mcLoginMsLoader.WaitForExit();
|
||||
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
|
||||
ModLaunch.mcLoginMsLoader.WaitForExit(GetLoginData());
|
||||
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Change.LoginFailed"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var accessToken = selectedProfile.AccessToken;
|
||||
var headers = new Dictionary<string, string>();
|
||||
headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
headers.Add("Accept", "*/*");
|
||||
headers.Add("User-Agent", "MojangSharp/0.1");
|
||||
var contents = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(skinInfo.IsSlim ? "slim" : "classic"), "variant" },
|
||||
{
|
||||
new ByteArrayContent(ModBase.ReadFileBytes(skinInfo.LocalFile)), "file",
|
||||
ModBase.GetFileNameFromPath(skinInfo.LocalFile)
|
||||
}
|
||||
};
|
||||
var res = Requester.Fetch("https://api.minecraftservices.com/minecraft/profile/skins",
|
||||
new FetchParam
|
||||
{
|
||||
Method = "POST",
|
||||
Content = contents,
|
||||
Headers = headers
|
||||
});
|
||||
if (res.Contains("request requires user authentication"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Change.Reauthenticating"));
|
||||
ModLaunch.mcLoginMsLoader.Start(GetLoginData(), true);
|
||||
goto Retry;
|
||||
}
|
||||
|
||||
if (res.Contains("\"error\""))
|
||||
{
|
||||
HintService.Hint(
|
||||
Lang.Text(
|
||||
"Launch.Skin.Change.FailedWithDetail",
|
||||
((JsonObject)ModBase.GetJson(res))["error"]?.ToString() ?? res),
|
||||
HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModBase.Log("[Skin] 皮肤修改返回值:" + "\r\n" + res);
|
||||
var resultJson = (JsonObject)ModBase.GetJson(res);
|
||||
if (resultJson.ContainsKey("errorMessage")) throw new Exception(resultJson["errorMessage"].ToString());
|
||||
foreach (var skinNode in resultJson["skins"].AsArray()) { var skin = skinNode.AsObject();
|
||||
if (skin["state"].ToString() == "ACTIVE")
|
||||
{
|
||||
MySkin.ReloadCache((string)skin["url"]);
|
||||
return;
|
||||
} }
|
||||
|
||||
throw new Exception("未知错误(" + res + ")");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.GetType().Equals(typeof(TaskCanceledException)))
|
||||
HintService.Hint(
|
||||
Lang.Text("Launch.Skin.Change.Timeout.WithDetail", ex.ToString()),
|
||||
HintType.Error);
|
||||
else
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Profile.Error.ChangeSkin"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Account.Profile.Error.ChangeSkin"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isMsSkinChanging = false;
|
||||
}
|
||||
}, "Ms Skin Upload"); // 等待登录结束
|
||||
// #5309
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.VisualBasic;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModSkin
|
||||
{
|
||||
public struct McSkinInfo
|
||||
{
|
||||
public bool IsSlim;
|
||||
public string LocalFile;
|
||||
public bool IsVaild;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 要求玩家选择一个皮肤文件,并进行相关校验。
|
||||
/// </summary>
|
||||
public static McSkinInfo McSkinSelect()
|
||||
{
|
||||
var fileName = SystemDialogs.SelectFile(Lang.Text("Launch.Skin.FileDialog.Filter"), Lang.Text("Launch.Skin.FileDialog.Title"));
|
||||
|
||||
// 验证有效性
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
return new McSkinInfo { IsVaild = false };
|
||||
try
|
||||
{
|
||||
var image = new MyBitmap(fileName);
|
||||
if (image.pic.Width != 64 || !(image.pic.Height == 32 || image.pic.Height == 64))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.InvalidSize"), HintType.Error);
|
||||
return new McSkinInfo { IsVaild = false };
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(fileName);
|
||||
if (fileInfo.Length > 24 * 1024)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.FileTooLarge", Lang.Number(fileInfo.Length / 1024d, "N2")),
|
||||
HintType.Error);
|
||||
return new McSkinInfo { IsVaild = false };
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.File.Error"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.File.Error"));
|
||||
return new McSkinInfo { IsVaild = false };
|
||||
}
|
||||
|
||||
// 获取皮肤种类
|
||||
var isSlim = ModMain.MyMsgBox(Lang.Text("Launch.Skin.Model.SelectMessage"), Lang.Text("Launch.Skin.Model.SelectTitle"), Lang.Text("Launch.Skin.Model.Steve"), Lang.Text("Launch.Skin.Model.Alex"), Lang.Text("Common.Option.IDontKnow"),
|
||||
highLight: false);
|
||||
if (isSlim == 3)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Model.UnknownHint"));
|
||||
return new McSkinInfo { IsVaild = false };
|
||||
}
|
||||
|
||||
return new McSkinInfo { IsVaild = true, IsSlim = isSlim == 2, LocalFile = fileName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。
|
||||
/// </summary>
|
||||
public static string McSkinGetAddress(string uuid, string type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uuid))
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.UuidEmpty"));
|
||||
|
||||
if (uuid.StartsWith("00000"))
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.OfflineNoSkin"));
|
||||
|
||||
// 尝试读取缓存
|
||||
var cachePath = Path.Combine(ModBase.pathTemp, $"Cache\\Skin\\Index{type}.ini");
|
||||
var cacheSkinAddress = ModBase.ReadIni(cachePath, uuid);
|
||||
if (!string.IsNullOrEmpty(cacheSkinAddress))
|
||||
return cacheSkinAddress;
|
||||
|
||||
// 获取皮肤地址
|
||||
var url = type switch
|
||||
{
|
||||
"Mojang" => "https://sessionserver.mojang.com/session/minecraft/profile/",
|
||||
"Ms" => "https://sessionserver.mojang.com/session/minecraft/profile/",
|
||||
"Auth" => ModProfile.selectedProfile.Server.Replace("/authserver", "") +
|
||||
"/sessionserver/session/minecraft/profile/",
|
||||
_ => throw new ArgumentException(Lang.Text("Minecraft.Skin.Error.InvalidSkinType", type ?? "null"))
|
||||
};
|
||||
|
||||
var skinString = ModNet.NetGetCodeByRequestRetry(url + uuid);
|
||||
if (string.IsNullOrEmpty((string?)skinString))
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.SkinReturnEmpty"));
|
||||
|
||||
// 解析皮肤 Property
|
||||
string skinValue = null;
|
||||
try
|
||||
{
|
||||
var json = (JsonObject)ModBase.GetJson((string)skinString);
|
||||
foreach (var property in json["properties"].AsArray())
|
||||
if (property["name"]?.ToString() == "textures")
|
||||
{
|
||||
skinValue = property["value"]?.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
if (skinValue is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.PropertyNotFound"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex,
|
||||
$"无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:{skinString}",
|
||||
ModBase.LogLevel.Developer);
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.NoSkinData"), ex);
|
||||
}
|
||||
|
||||
// 解码 Base64 并解析 JSON
|
||||
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(skinValue));
|
||||
var skinJson = (JsonObject)ModBase.GetJson(decoded.ToLowerInvariant());
|
||||
|
||||
if (skinJson["textures"]?["skin"]?["url"] is null)
|
||||
throw new Exception(Lang.Text("Minecraft.Skin.Error.NoCustomSkin"));
|
||||
|
||||
var skinUrl = skinJson["textures"]["skin"]["url"].ToString();
|
||||
skinUrl = skinUrl.Contains("minecraft.net/") ? skinUrl.Replace("http://", "https://") : skinUrl;
|
||||
|
||||
// 保存缓存
|
||||
ModBase.WriteIni(cachePath, uuid, skinUrl);
|
||||
ModBase.Log($"[Skin] UUID {uuid} 对应的皮肤文件为 {skinUrl}");
|
||||
|
||||
return skinUrl;
|
||||
}
|
||||
|
||||
private static readonly object mcSkinDownloadLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 从 Url 下载皮肤。返回本地文件路径,失败将抛出异常。
|
||||
/// </summary>
|
||||
public static string McSkinDownload(string address)
|
||||
{
|
||||
var skinName = ModBase.GetFileNameFromPath(address);
|
||||
var fileAddress = ModBase.pathTemp + @"Cache\Skin\" + ModBase.GetHash(address) + ".png";
|
||||
lock (mcSkinDownloadLock)
|
||||
{
|
||||
if (!File.Exists(fileAddress))
|
||||
{
|
||||
FileDownloader.DownloadAsync(address, fileAddress + ModNet.netDownloadEnd).GetAwaiter().GetResult();
|
||||
File.Delete(fileAddress);
|
||||
FileSystem.Rename(fileAddress + ModNet.netDownloadEnd, fileAddress);
|
||||
ModBase.Log("[Minecraft] 皮肤下载成功:" + fileAddress);
|
||||
}
|
||||
|
||||
return fileAddress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Uuid 对应的皮肤,返回"Steve"或"Alex"。
|
||||
/// </summary>
|
||||
public static string McSkinSex(string uuid)
|
||||
{
|
||||
if (uuid.Length != 32)
|
||||
return "Steve";
|
||||
var a = int.Parse(uuid[7].ToString(), NumberStyles.AllowHexSpecifier);
|
||||
var b = int.Parse(uuid[15].ToString(), NumberStyles.AllowHexSpecifier);
|
||||
var c = int.Parse(uuid[23].ToString(), NumberStyles.AllowHexSpecifier);
|
||||
var d = int.Parse(uuid[31].ToString(), NumberStyles.AllowHexSpecifier);
|
||||
return ((a ^ b ^ c ^ d) % 2) != 0 ? "Alex" : "Steve";
|
||||
// Math.floorMod(uuid.hashCode(), 18)
|
||||
|
||||
// Public Function hashCode(ByVal str As String) As Integer
|
||||
// Dim hash As Integer = 0
|
||||
// Dim n As Integer = str.Length
|
||||
// If n = 0 Then
|
||||
// Return hash
|
||||
// End If
|
||||
// For i As Integer = 0 To n - 1
|
||||
// hash = hash + Asc(str(i)) * (1 << (n - i - 1))
|
||||
// Next
|
||||
// Return hash
|
||||
// End Function
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using PCL.Core.UI.Controls;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
internal static class ModStyle
|
||||
{
|
||||
// Partly generated by claude-sonnet-4-20250514
|
||||
public class TimerRun : Run, IDisposable
|
||||
{
|
||||
// 定时器事件
|
||||
public delegate void TimerTickDelegate(TimerRun sender);
|
||||
|
||||
// 定义依赖属性
|
||||
public static readonly DependencyProperty UpdateIntervalProperty =
|
||||
DependencyProperty.Register(nameof(UpdateInterval), typeof(TimeSpan), typeof(TimerRun),
|
||||
new PropertyMetadata(TimeSpan.FromSeconds(1d)));
|
||||
|
||||
private object _isDisposed = false;
|
||||
|
||||
private DispatcherTimer _timer;
|
||||
|
||||
public TimerRun(TimeSpan interval = default, bool autoStart = false)
|
||||
{
|
||||
_timer = new DispatcherTimer();
|
||||
_timer.Tick += _TimerTick;
|
||||
UpdateInterval = interval == default ? TimeSpan.FromSeconds(1d) : interval;
|
||||
AutoStart = autoStart;
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
private object _isTimerRunning => _timer is not null && _timer.IsEnabled;
|
||||
|
||||
// UpdateInterval 属性
|
||||
public TimeSpan UpdateInterval
|
||||
{
|
||||
get => (TimeSpan)GetValue(UpdateIntervalProperty);
|
||||
set
|
||||
{
|
||||
if (value > TimeSpan.Zero) SetValue(UpdateIntervalProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoStart { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if ((bool)_isDisposed)
|
||||
return;
|
||||
_isDisposed = true;
|
||||
// 资源释放
|
||||
_timer.Tick -= _TimerTick;
|
||||
_timer?.Stop();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
// 属性变化处理
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (ReferenceEquals(e.Property, UpdateIntervalProperty) && _timer is not null)
|
||||
_timer.Interval = UpdateInterval;
|
||||
}
|
||||
|
||||
public event TimerTickDelegate? TimerTick;
|
||||
|
||||
private void _TimerTick(object sender, EventArgs e)
|
||||
{
|
||||
TimerTick?.Invoke(this);
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AutoStart)
|
||||
StartTimer();
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StopTimer();
|
||||
}
|
||||
|
||||
public void StartTimer()
|
||||
{
|
||||
if (Dispatcher is null)
|
||||
{
|
||||
ModBase.Log(
|
||||
"[TimerRun] Dispatcher is null, unable to run",
|
||||
ModBase.LogLevel.Critical,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.DispatcherUnavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(bool)_isTimerRunning)
|
||||
_timer?.Start();
|
||||
}
|
||||
|
||||
public void StopTimer()
|
||||
{
|
||||
if ((bool)_isTimerRunning)
|
||||
_timer?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public class MinecraftFormatter
|
||||
{
|
||||
private static readonly Dictionary<string, string> colorMap = new()
|
||||
{
|
||||
{ "black", "0" }, { "dark_blue", "1" }, { "dark_green", "2" }, { "dark_aqua", "3" }, { "dark_red", "4" },
|
||||
{ "dark_purple", "5" }, { "gold", "6" }, { "gray", "7" }, { "dark_gray", "8" }, { "blue", "9" },
|
||||
{ "green", "a" }, { "aqua", "b" }, { "red", "c" }, { "light_purple", "d" }, { "yellow", "e" },
|
||||
{ "white", "f" }
|
||||
};
|
||||
|
||||
private static readonly Random random = new();
|
||||
|
||||
private static readonly string randomChars =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?/~";
|
||||
|
||||
public static string ConvertToMinecraftFormat(JsonObject data)
|
||||
{
|
||||
var result = "";
|
||||
foreach (var item in data["extra"].AsArray())
|
||||
result += ProcessElement((JsonObject)item, new List<string>());
|
||||
return result.Replace("§§", "§");
|
||||
}
|
||||
|
||||
private static string ProcessElement(JsonObject element, List<string> currentFormat)
|
||||
{
|
||||
var text = "";
|
||||
var formats = new List<string>(currentFormat);
|
||||
|
||||
// 处理格式
|
||||
if (element.ContainsKey("bold") && element["bold"].ToObject<bool>()) formats.Add("l");
|
||||
|
||||
if (element.ContainsKey("color"))
|
||||
{
|
||||
var color = element["color"].ToString();
|
||||
var colorCode = "f";
|
||||
if (colorMap.ContainsKey(color)) colorCode = colorMap[color];
|
||||
formats.Insert(0, colorCode); // 颜色代码在前
|
||||
}
|
||||
|
||||
// 应用格式
|
||||
if (formats.Count > 0) text += "§" + string.Join("§", formats);
|
||||
|
||||
// 添加文本内容
|
||||
if (element.ContainsKey("text")) text += element["text"].ToString();
|
||||
|
||||
// 处理子元素
|
||||
if (element.ContainsKey("extra"))
|
||||
foreach (var child in element["extra"].AsArray())
|
||||
text += ProcessElement((JsonObject)child, new List<string>(formats));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft 文本格式化代码,用于显示不同颜色的文本
|
||||
/// </summary>
|
||||
/// <param name="text">要格式化的文本</param>
|
||||
/// <param name="lab">控件</param>
|
||||
public static void SetColorfulTextLab(string text, TextBlock lab, bool isDarkMode = true)
|
||||
{
|
||||
if (lab is null)
|
||||
{
|
||||
ModBase.Log("[Style] SetColorfulTextLab: lab is null");
|
||||
return;
|
||||
}
|
||||
|
||||
lab.Inlines.Clear();
|
||||
|
||||
var hasItalicProperty = false; // 斜体
|
||||
var hasDeleteLineProperty = false; // 删除线
|
||||
var hasStrickThroughProperty = false; // 下划线
|
||||
var hasBlodProperty = false; // 粗体
|
||||
var isRandomText = false; // 随机文本模式
|
||||
|
||||
var color = isDarkMode ? "#FFFFFF" : "#888888";
|
||||
var isColorCode = false;
|
||||
var curRun = new TimerRun();
|
||||
lab.Inlines.Add(curRun);
|
||||
|
||||
// 用于存储需要随机化的文本段
|
||||
var randomTextRuns = new List<TimerRun>();
|
||||
|
||||
foreach (var c in text)
|
||||
{
|
||||
if (c.ToString() == "§") // 下一字符是格式化代码
|
||||
{
|
||||
isColorCode = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isColorCode)
|
||||
{
|
||||
var prevColor = color;
|
||||
if (!MotdRenderer.TryGetColorFromCode(c.ToString(), isDarkMode, out color))
|
||||
{
|
||||
color = prevColor; // out 会将 color 置为 null,统一恢复为之前的颜色
|
||||
switch (c)
|
||||
{
|
||||
// 格式化代码
|
||||
case 'k':
|
||||
case 'K': // 随机字符
|
||||
{
|
||||
isRandomText = true;
|
||||
// 开始新的Run用于随机文本
|
||||
if (!string.IsNullOrEmpty(curRun.Text))
|
||||
{
|
||||
curRun = new TimerRun();
|
||||
lab.Inlines.Add(curRun);
|
||||
}
|
||||
|
||||
curRun.AutoStart = true;
|
||||
randomTextRuns.Add(curRun);
|
||||
break;
|
||||
}
|
||||
case 'l': // 粗体
|
||||
{
|
||||
hasBlodProperty = true;
|
||||
break;
|
||||
}
|
||||
case 'o': // 斜体
|
||||
{
|
||||
hasItalicProperty = true;
|
||||
break;
|
||||
}
|
||||
case 'n': // 下划线
|
||||
{
|
||||
hasStrickThroughProperty = true;
|
||||
break;
|
||||
}
|
||||
case 'm': // 删除线
|
||||
{
|
||||
hasDeleteLineProperty = true;
|
||||
break;
|
||||
}
|
||||
case 'r': // 重置
|
||||
{
|
||||
color = isDarkMode ? "#FFFFFF" : "#888888";
|
||||
hasBlodProperty = false;
|
||||
hasItalicProperty = false;
|
||||
hasStrickThroughProperty = false;
|
||||
hasDeleteLineProperty = false;
|
||||
isRandomText = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(curRun.Text) && c.ToString() != "k" && c.ToString() != "K") // 遇到格式代码但是有文本,重开一个Run
|
||||
{
|
||||
curRun = new TimerRun();
|
||||
lab.Inlines.Add(curRun);
|
||||
}
|
||||
|
||||
curRun.Foreground = new SolidColorBrush(new ModBase.MyColor(color));
|
||||
curRun.FontWeight = hasBlodProperty ? FontWeights.Bold : FontWeights.Normal;
|
||||
curRun.FontStyle = hasItalicProperty ? FontStyles.Italic : FontStyles.Normal;
|
||||
curRun.TextDecorations = hasStrickThroughProperty ? TextDecorations.Strikethrough : null;
|
||||
curRun.TextDecorations = hasDeleteLineProperty ? TextDecorations.Underline : null;
|
||||
}
|
||||
else if (isRandomText)
|
||||
{
|
||||
// 随机模式下,添加随机字符
|
||||
curRun.Text += randomChars[random.Next(randomChars.Length)].ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
curRun.Text += c.ToString();
|
||||
}
|
||||
|
||||
if (isColorCode)
|
||||
isColorCode = false;
|
||||
}
|
||||
|
||||
// 设置定时器来更新随机文本
|
||||
if (randomTextRuns.Count > 0)
|
||||
foreach (var run in randomTextRuns)
|
||||
{
|
||||
run.UpdateInterval = TimeSpan.FromMilliseconds(20d);
|
||||
run.TimerTick += sender =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sender.Text))
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0, loopTo = sender.Text.Length - 1; i <= loopTo; i++)
|
||||
sb.Append(randomChars[random.Next(randomChars.Length)]);
|
||||
sender.Text = sb.ToString();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,824 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModWatcher
|
||||
{
|
||||
// 对全体的监视
|
||||
public static List<Watcher> mcWatcherList = new();
|
||||
private static bool isWatcherRunning;
|
||||
public static bool hasRunningMinecraft;
|
||||
|
||||
private static void WatcherStateChanged()
|
||||
{
|
||||
var isRunning = false;
|
||||
var triggerLauncherShutdown = true;
|
||||
foreach (var Watcher in mcWatcherList)
|
||||
{
|
||||
if (Watcher.State == Watcher.MinecraftState.Loading || Watcher.State == Watcher.MinecraftState.Running)
|
||||
{
|
||||
isRunning = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Watcher.State == Watcher.MinecraftState.Crashed || Watcher.State == Watcher.MinecraftState.Canceled)
|
||||
triggerLauncherShutdown = false;
|
||||
}
|
||||
|
||||
if (isWatcherRunning == isRunning)
|
||||
return;
|
||||
isWatcherRunning = isRunning;
|
||||
if (isWatcherRunning)
|
||||
MinecraftStart();
|
||||
else
|
||||
MinecraftStop(triggerLauncherShutdown);
|
||||
}
|
||||
|
||||
private static void MinecraftStart()
|
||||
{
|
||||
ModLaunch.McLaunchLog("[全局] 出现运行中的 Minecraft");
|
||||
hasRunningMinecraft = true;
|
||||
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
|
||||
}
|
||||
|
||||
private static void MinecraftStop(bool triggerLauncherShutdown)
|
||||
{
|
||||
ModLaunch.McLaunchLog("[全局] 已无运行中的 Minecraft");
|
||||
hasRunningMinecraft = false;
|
||||
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
|
||||
// 音乐播放
|
||||
if (Config.Preference.Music.StopInGame)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在结束后开始音乐播放");
|
||||
});
|
||||
else if (Config.Preference.Music.StartInGame)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在结束后暂停音乐播放");
|
||||
});
|
||||
// 开始视频背景播放
|
||||
ModVideoBack.IsGaming = false;
|
||||
ModVideoBack.VideoPlay();
|
||||
// 启动器可见性
|
||||
switch (Config.Launch.LauncherVisibility)
|
||||
{
|
||||
case LauncherVisibility.HideAndExit:
|
||||
// 直接关闭
|
||||
if (triggerLauncherShutdown)
|
||||
ModBase.RunInUi(() => ModMain.frmMain.EndProgram(false));
|
||||
else
|
||||
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
|
||||
break;
|
||||
case LauncherVisibility.HideAndReopen:
|
||||
// 恢复
|
||||
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static GameLogLevel GetLevel(string line, GameLogLevel lastLevel)
|
||||
{
|
||||
Func<string, SolidColorBrush> getColorBrush =
|
||||
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
|
||||
var starting = line.Split(": ")[0];
|
||||
if (starting.ContainsF("FATAL"))
|
||||
return GameLogLevel.Fatal;
|
||||
if (starting.ContainsF("ERROR"))
|
||||
return GameLogLevel.Error;
|
||||
if (starting.ContainsF("WARN"))
|
||||
return GameLogLevel.Warn;
|
||||
if (starting.ContainsF("INFO"))
|
||||
return GameLogLevel.Info;
|
||||
if (starting.ContainsF("DEBUG"))
|
||||
return GameLogLevel.Debug;
|
||||
if (line.StartsWithF("Exception in thread \""))
|
||||
return GameLogLevel.Error;
|
||||
if ((line.ContainsF("Exception") || line.ContainsF("Realms authentication error with message ")) &&
|
||||
lastLevel >= GameLogLevel.Warn)
|
||||
return lastLevel;
|
||||
if (line.StartsWithF(" at ") && lastLevel >= GameLogLevel.Warn)
|
||||
return lastLevel;
|
||||
return GameLogLevel.Info;
|
||||
}
|
||||
|
||||
private static SolidColorBrush GetColor(GameLogLevel level)
|
||||
{
|
||||
Func<string, SolidColorBrush> getColorBrush =
|
||||
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
|
||||
switch (level)
|
||||
{
|
||||
case GameLogLevel.Debug:
|
||||
{
|
||||
return getColorBrush("ColorBrushDebug");
|
||||
}
|
||||
case GameLogLevel.Info:
|
||||
{
|
||||
getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
|
||||
break;
|
||||
}
|
||||
case GameLogLevel.Warn:
|
||||
{
|
||||
return getColorBrush("ColorBrushWarn");
|
||||
}
|
||||
case GameLogLevel.Error:
|
||||
{
|
||||
return getColorBrush("ColorBrushError");
|
||||
}
|
||||
case GameLogLevel.Fatal:
|
||||
{
|
||||
return getColorBrush("ColorBrushFatal");
|
||||
}
|
||||
}
|
||||
|
||||
return getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
|
||||
}
|
||||
|
||||
// 实时日志处理
|
||||
public class LogOutputEventArgs : EventArgs
|
||||
{
|
||||
public SolidColorBrush color;
|
||||
public string logText;
|
||||
|
||||
public LogOutputEventArgs(string logText, SolidColorBrush color)
|
||||
{
|
||||
this.logText = logText;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private enum GameLogLevel
|
||||
{
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3,
|
||||
Fatal = 4
|
||||
}
|
||||
|
||||
// 对单个进程的监视
|
||||
public class Watcher
|
||||
{
|
||||
public delegate void GameExitEventHandler();
|
||||
|
||||
public delegate void LogOutputEventHandler(Watcher sender, LogOutputEventArgs e);
|
||||
|
||||
public enum MinecraftState
|
||||
{
|
||||
Loading,
|
||||
Running,
|
||||
Crashed,
|
||||
Ended,
|
||||
Canceled
|
||||
}
|
||||
|
||||
private readonly int pid;
|
||||
|
||||
/// <summary>
|
||||
/// 是否处理实时日志。
|
||||
/// </summary>
|
||||
private readonly bool realTime;
|
||||
|
||||
private readonly object waitingLogLock = new();
|
||||
public uint countDebug;
|
||||
public uint countError;
|
||||
public uint countFatal;
|
||||
public uint countInfo;
|
||||
public uint countWarn;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏的所有日志输出,只有处理实时日志的情况下才会记录。
|
||||
/// </summary>
|
||||
public List<string> fullLog = new();
|
||||
|
||||
// 初始化
|
||||
public Process gameProcess;
|
||||
|
||||
// 窗口检查
|
||||
private bool isWindowAppeared;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。
|
||||
/// </summary>
|
||||
private bool isWindowFinished;
|
||||
|
||||
public string jStackPath;
|
||||
|
||||
/// <summary>
|
||||
/// 上一行日志级别。
|
||||
/// </summary>
|
||||
private GameLogLevel lastLevel = GameLogLevel.Info;
|
||||
|
||||
public Queue<string> latestLog = new();
|
||||
public ModLoader.LoaderTask<Process, int> loader;
|
||||
|
||||
// 进度更新
|
||||
private int logProgress;
|
||||
public McInstance version;
|
||||
|
||||
// 日志
|
||||
public List<string> waitingLog = new(1000);
|
||||
private nint windowHandle;
|
||||
private string windowTitle = "";
|
||||
|
||||
public Watcher(ModLoader.LoaderTask<Process, int> loader, McInstance version, string windowTitle,
|
||||
string jStackPath, bool outputRealTime = false)
|
||||
{
|
||||
this.loader = loader;
|
||||
this.version = version;
|
||||
this.windowTitle = windowTitle;
|
||||
realTime = outputRealTime;
|
||||
pid = loader.input.Id;
|
||||
this.jStackPath = jStackPath;
|
||||
|
||||
WatcherLog(Lang.Text("Watcher.Start"));
|
||||
if (string.IsNullOrWhiteSpace(windowTitle))
|
||||
WatcherLog("要求窗口标题:" + windowTitle);
|
||||
|
||||
// 更改列表
|
||||
var newWatcherList = new List<Watcher>();
|
||||
foreach (var Watch in mcWatcherList)
|
||||
{
|
||||
if (Watch.State == MinecraftState.Crashed || Watch.State == MinecraftState.Ended ||
|
||||
Watch.State == MinecraftState.Canceled)
|
||||
continue;
|
||||
newWatcherList.Add(Watch);
|
||||
}
|
||||
|
||||
newWatcherList.Add(this);
|
||||
mcWatcherList = newWatcherList;
|
||||
WatcherStateChanged();
|
||||
|
||||
// 初始化进程与日志读取
|
||||
gameProcess = loader.input;
|
||||
gameProcess.BeginOutputReadLine();
|
||||
gameProcess.BeginErrorReadLine();
|
||||
gameProcess.OutputDataReceived += LogReceived;
|
||||
gameProcess.ErrorDataReceived += LogReceived;
|
||||
|
||||
// 初始化时钟
|
||||
// 设置窗口标题
|
||||
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (State != MinecraftState.Ended && State != MinecraftState.Crashed &&
|
||||
State != MinecraftState.Canceled && loader.State != ModBase.LoadState.Aborted)
|
||||
{
|
||||
TimerWindow();
|
||||
TimerLog();
|
||||
if (!string.IsNullOrWhiteSpace(windowTitle))
|
||||
for (var i = 1; i <= 3; i++)
|
||||
{
|
||||
if (State == MinecraftState.Running && !gameProcess.HasExited)
|
||||
{
|
||||
var realTitle = windowTitle.Replace("{date}", Lang.Date(DateTime.Now, "d"))
|
||||
.Replace("{time}", Lang.Date(DateTime.Now, "T"));
|
||||
SetWindowText(windowHandle, realTitle);
|
||||
}
|
||||
|
||||
Thread.Sleep(64);
|
||||
}
|
||||
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
WatcherLog(Lang.Text("Watcher.Exited"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"Minecraft 日志监控主循环出错",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
||||
State = MinecraftState.Ended;
|
||||
}
|
||||
}, "Minecraft Watcher PID " + pid);
|
||||
}
|
||||
|
||||
public MinecraftState State
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
field = value;
|
||||
WatcherStateChanged();
|
||||
}
|
||||
} = MinecraftState.Loading;
|
||||
|
||||
/// <summary>
|
||||
/// 是否处理实时日志。
|
||||
/// </summary>
|
||||
public bool RealTimeLog => realTime;
|
||||
|
||||
// 状态
|
||||
/// <summary>
|
||||
/// 游戏退出时触发。
|
||||
/// </summary>
|
||||
public event GameExitEventHandler? GameExit;
|
||||
|
||||
private void LogReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
lock (waitingLogLock)
|
||||
{
|
||||
waitingLog.Add(e.Data);
|
||||
}
|
||||
|
||||
if (realTime)
|
||||
{
|
||||
LogRealTime(e.Data, ref lastLevel);
|
||||
if (e.Data is not null)
|
||||
fullLog.Add(e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发日志改变事件,并统计日志行数。
|
||||
/// </summary>
|
||||
private void LogRealTime(string line, ref GameLogLevel level)
|
||||
{
|
||||
if (line is null)
|
||||
return; // 杀游戏进程时有概率传 null
|
||||
level = line.StartsWithF(" at ") || line.StartsWithF("Caused by: ") || line.StartsWithF(" ... ")
|
||||
? level
|
||||
: GetLevel(line, level);
|
||||
|
||||
// “ ... 4 more”
|
||||
var color = GetColor(level);
|
||||
switch (level)
|
||||
{
|
||||
case GameLogLevel.Debug:
|
||||
{
|
||||
countDebug = (uint)(countDebug + 1L);
|
||||
break;
|
||||
}
|
||||
case GameLogLevel.Info:
|
||||
{
|
||||
countInfo = (uint)(countInfo + 1L);
|
||||
break;
|
||||
}
|
||||
case GameLogLevel.Warn:
|
||||
{
|
||||
countWarn = (uint)(countWarn + 1L);
|
||||
break;
|
||||
}
|
||||
case GameLogLevel.Error:
|
||||
{
|
||||
countError = (uint)(countError + 1L);
|
||||
break;
|
||||
}
|
||||
case GameLogLevel.Fatal:
|
||||
{
|
||||
countFatal = (uint)(countFatal + 1L);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LogOutput?.Invoke(this, new LogOutputEventArgs(line, color));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 有新的日志输出,日志计数器发生改变时触发。
|
||||
/// </summary>
|
||||
public event LogOutputEventHandler? LogOutput;
|
||||
|
||||
private void TimerLog()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 输出文本
|
||||
var copyed = new List<string>();
|
||||
lock (waitingLogLock)
|
||||
{
|
||||
if (!waitingLog.Any())
|
||||
return;
|
||||
copyed = waitingLog;
|
||||
waitingLog = new List<string>(1000);
|
||||
}
|
||||
|
||||
foreach (var Str in copyed)
|
||||
GameLog(Str);
|
||||
if (State == MinecraftState.Loading)
|
||||
ProgressUpdate();
|
||||
// 游戏退出检查
|
||||
if (gameProcess.HasExited)
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode));
|
||||
// 实时日志输出
|
||||
if (realTime)
|
||||
{
|
||||
var arglevel = GameLogLevel.Info;
|
||||
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
|
||||
}
|
||||
|
||||
GameExit?.Invoke();
|
||||
// If Process.ExitCode = 1 Then
|
||||
// '返回值为 1,考虑是任务管理器结束
|
||||
// WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1
|
||||
// State = MinecraftState.Ended
|
||||
// Else
|
||||
if (State == MinecraftState.Loading)
|
||||
{
|
||||
// 窗口未出现
|
||||
WatcherLog(Lang.Text("Watcher.Crash.Suspected"));
|
||||
Crashed();
|
||||
}
|
||||
else if (gameProcess.ExitCode != 0 && State == MinecraftState.Running &&
|
||||
version.releaseTime.Year >= 2012)
|
||||
{
|
||||
// 返回值不为 0 且未结束
|
||||
WatcherLog(Lang.Text("Watcher.Crash.AbnormalExit"));
|
||||
Crashed();
|
||||
}
|
||||
else if (State != MinecraftState.Crashed)
|
||||
{
|
||||
// 正常关闭
|
||||
State = MinecraftState.Ended;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"输出 Minecraft 日志失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void GameLog(string text)
|
||||
{
|
||||
// 预处理
|
||||
if (text is null)
|
||||
return;
|
||||
text = text.Replace("\r\n", "\r").Replace("\n", "\r")
|
||||
.Replace("\r", "\r\n");
|
||||
// If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
|
||||
// 加入预存储
|
||||
latestLog.Enqueue(text);
|
||||
if (latestLog.Count >= 501)
|
||||
latestLog.Dequeue();
|
||||
// 进度处理
|
||||
if (logProgress < 1)
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Progress.LogAppeared"));
|
||||
logProgress = 1;
|
||||
} // 可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版)
|
||||
|
||||
if (logProgress < 2 && text.Contains("Setting user:"))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Progress.UserSet")); // 仅确保支持 Minecraft 1.7+
|
||||
logProgress = 2;
|
||||
}
|
||||
else if (logProgress < 3 && text.ContainsF("lwjgl version", true))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Progress.LwjglConfirmed"));
|
||||
logProgress = 3;
|
||||
}
|
||||
else if (logProgress < 4 &&
|
||||
(text.Contains("OpenAL initialized") || text.Contains("Starting up SoundSystem")))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Progress.OpenAlLoaded")); // 仅确保支持 Minecraft 1.7+
|
||||
logProgress = 4;
|
||||
}
|
||||
else if (logProgress < 5 &&
|
||||
((text.Contains("Created") && text.Contains("textures") && text.Contains("-atlas")) ||
|
||||
text.Contains("Found animation info")))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Progress.TexturesLoaded")); // 仅确保支持 Minecraft 1.7+
|
||||
logProgress = 5;
|
||||
}
|
||||
|
||||
// 输出日志
|
||||
// Log(Text)
|
||||
// 关闭与崩溃检测
|
||||
if (!text.Contains("[CHAT]"))
|
||||
{
|
||||
if (text.Contains("Someone is closing me!") ||
|
||||
text.Contains("Restarting Minecraft with command")) // #1258
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Log.CloseDetected", text));
|
||||
State = MinecraftState.Ended;
|
||||
}
|
||||
else if (text.Contains("Crash report saved to") ||
|
||||
text.Contains("This crash report has been saved to:"))
|
||||
{
|
||||
// Text.Contains("Minecraft ran into a problem! Report saved to:") Then
|
||||
// Minecraft 崩溃,忽略 VanillaFix
|
||||
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
||||
Crashed();
|
||||
}
|
||||
else if (text.Contains("Could not save crash report to"))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
||||
Crashed();
|
||||
}
|
||||
else if (text.Contains("/ERROR]: Unable to launch") ||
|
||||
text.Contains("An exception was thrown, the game will display an error screen and halt."))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
||||
Crashed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WatcherLog(string text)
|
||||
{
|
||||
ModLaunch.McLaunchLog("[" + pid + "] " + text);
|
||||
}
|
||||
|
||||
private void ProgressUpdate()
|
||||
{
|
||||
double currentProgress;
|
||||
if (isWindowAppeared || logProgress >= 4)
|
||||
{
|
||||
currentProgress = 0.95d;
|
||||
WatcherLog(Lang.Text("Watcher.LoadComplete"));
|
||||
State = MinecraftState.Running;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentProgress = Math.Min(logProgress, 3) / 3d * 0.9d;
|
||||
}
|
||||
|
||||
loader.Progress = currentProgress;
|
||||
}
|
||||
|
||||
private void TimerWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (gameProcess.HasExited)
|
||||
return;
|
||||
if (isWindowFinished)
|
||||
return;
|
||||
// 获取全部窗口,检查是否有新增的
|
||||
KeyValuePair<nint, string>? minecraftWindow = default;
|
||||
try
|
||||
{
|
||||
minecraftWindow = TryGetMinecraftWindow();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
// 拒绝访问(#1062)
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Watcher.SecurityBlocked"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Watcher.SecurityBlocked"));
|
||||
isWindowFinished = true;
|
||||
}
|
||||
|
||||
if (minecraftWindow is null)
|
||||
return;
|
||||
var minecraftWindowName = minecraftWindow.Value.Value;
|
||||
var minecraftWindowHandle = minecraftWindow.Value.Key;
|
||||
// 已找到窗口
|
||||
if (!minecraftWindowName.StartsWithF("FML") && !minecraftWindowName.StartsWithF("Quilt Loader"))
|
||||
{
|
||||
// 已找到 Minecraft 窗口
|
||||
windowHandle = minecraftWindowHandle;
|
||||
WatcherLog(Lang.Text("Watcher.WindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
|
||||
isWindowFinished = true;
|
||||
// 最大化
|
||||
if (Config.Launch.GameWindowMode == GameWindowSizeMode.Maximized)
|
||||
// 如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
|
||||
// ……虽然我很想这样说,但总有人反馈,算了
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
ShowWindow(windowHandle, 3U);
|
||||
WatcherLog(Lang.Text("Watcher.WindowMaximized", minecraftWindowHandle.ToInt64()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "最大化 Minecraft 窗口时出现错误");
|
||||
}
|
||||
}, "MinecraftWindowMaximize");
|
||||
}
|
||||
else if (!isWindowAppeared)
|
||||
{
|
||||
// 已找到 FML 窗口
|
||||
WatcherLog(Lang.Text("Watcher.FmlWindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
|
||||
}
|
||||
|
||||
isWindowAppeared = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"检查 Minecraft 窗口失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。
|
||||
/// Nothing 代表未找到。
|
||||
/// </summary>
|
||||
private KeyValuePair<nint, string>? TryGetMinecraftWindow()
|
||||
{
|
||||
KeyValuePair<nint, string>? tryGetMinecraftWindowRet = default;
|
||||
tryGetMinecraftWindowRet = default;
|
||||
EnumWindows((hwnd, lParam) =>
|
||||
{
|
||||
if (tryGetMinecraftWindowRet is not null)
|
||||
return false; // 找到后停止枚举
|
||||
|
||||
var str = new StringBuilder(512);
|
||||
GetClassName(hwnd, str, str.Capacity);
|
||||
var className = str.ToString();
|
||||
|
||||
if (!(className == "GLFW30" || className == "LWJGL" || className == "SunAwtFrame"))
|
||||
return true;
|
||||
|
||||
// 获取窗口标题名
|
||||
str = new StringBuilder(512);
|
||||
GetWindowText(hwnd, str, str.Capacity);
|
||||
var windowText = str.ToString();
|
||||
|
||||
// 部分版本会搞个 GLFW message window 出来所以得反选
|
||||
if (!(windowText.StartsWithF("FML") ||
|
||||
(windowText != "PopupMessageWindow" && !windowText.StartsWithF("GLFW"))))
|
||||
return true;
|
||||
|
||||
// 获取窗口关联的进程
|
||||
var processId = default(int);
|
||||
GetWindowThreadProcessId(hwnd, ref processId);
|
||||
try
|
||||
{
|
||||
if (processId != gameProcess.Id)
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 找到目标,赋值并停止枚举
|
||||
tryGetMinecraftWindowRet = new KeyValuePair<nint, string>(hwnd, windowText);
|
||||
return false;
|
||||
}, nint.Zero);
|
||||
return tryGetMinecraftWindowRet;
|
||||
}
|
||||
|
||||
[DllImport("user32")]
|
||||
private static extern bool EnumWindows(EnumWindowsSub lpEnumFunc, nint lParam);
|
||||
|
||||
[DllImport("user32", EntryPoint = "GetClassNameW", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32", EntryPoint = "GetWindowTextW", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32", EntryPoint = "SetWindowTextW", CharSet = CharSet.Unicode)]
|
||||
private static extern bool SetWindowText(nint hWnd, string lpString);
|
||||
|
||||
[DllImport("user32")]
|
||||
private static extern bool ShowWindow(nint hWnd, uint cmdWindow);
|
||||
|
||||
[DllImport("user32")]
|
||||
private static extern int GetWindowThreadProcessId(nint hWnd, ref int lpdwProcessId);
|
||||
|
||||
// 崩溃处理
|
||||
private void Crashed()
|
||||
{
|
||||
if (State is MinecraftState.Crashed or MinecraftState.Ended)
|
||||
return;
|
||||
State = MinecraftState.Crashed;
|
||||
// 崩溃分析
|
||||
if (!Config.Launch.DisableCrashAnalysis)
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Crash.Detected"));
|
||||
HintService.Hint(Lang.Text("Watcher.Crash.Hint"));
|
||||
|
||||
ModBase.FeedbackInfo();
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
WatcherLog(Lang.Text("Watcher.Crash.AnalysisStart"));
|
||||
var analyzer = new CrashAnalyzer(pid);
|
||||
analyzer.Collect(version.PathIndie, latestLog.ToList());
|
||||
analyzer.Prepare();
|
||||
analyzer.Analyze(version);
|
||||
analyzer.Output(
|
||||
false,
|
||||
[
|
||||
version.PathInstance + version.Name + ".json",
|
||||
LogWrapper.CurrentLogger.CurrentLogFiles.Last(),
|
||||
ModBase.exePath + @"PCL\LatestLaunch.bat"
|
||||
]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"崩溃分析失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Crash.Analysis.Error.Failed"));
|
||||
}
|
||||
}, "Crash Analyzer");
|
||||
}
|
||||
else
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Crash.DetectedDisabled"));
|
||||
}
|
||||
}
|
||||
|
||||
// 强制关闭
|
||||
public bool CheckAlive(Process p)
|
||||
{
|
||||
if (!p.HasExited)
|
||||
return true;
|
||||
var exists = Array.Exists(Process.GetProcesses(), item => item.Id == p.Id);
|
||||
if (exists)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
State = MinecraftState.Canceled;
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Kill.Attempt"));
|
||||
try
|
||||
{
|
||||
if (CheckAlive(gameProcess))
|
||||
gameProcess.Kill();
|
||||
gameProcess.WaitForExit(5000);
|
||||
if (CheckAlive(gameProcess))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Kill.TaskkillAttempt"));
|
||||
var taskkillInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "taskkill.exe",
|
||||
Arguments = $"/PID {gameProcess.Id} /F /T",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
var taskkillProcess = Process.Start(taskkillInfo);
|
||||
var output = taskkillProcess.StandardOutput.ReadToEnd();
|
||||
WatcherLog(Lang.Text("Watcher.Kill.TaskkillResult", output));
|
||||
gameProcess.WaitForExit(5000);
|
||||
if (CheckAlive(gameProcess))
|
||||
{
|
||||
WatcherLog(Lang.Text("Watcher.Kill.Timeout"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WatcherLog(Lang.Text("Watcher.Kill.Success"));
|
||||
if (realTime)
|
||||
{
|
||||
var arglevel = GameLogLevel.Info;
|
||||
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
|
||||
}
|
||||
|
||||
GameExit?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Watcher.Kill.Failed"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Watcher.Kill.Failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出运行栈
|
||||
public List<string> ExportStackDump(string savePath)
|
||||
{
|
||||
var dump = new List<string>();
|
||||
for (var i = 1; i <= 3; i++)
|
||||
{
|
||||
dump.Add(ModBase.ShellAndGetOutput(jStackPath, "-l -e " + gameProcess.Id));
|
||||
Thread.Sleep(3000);
|
||||
}
|
||||
|
||||
return dump;
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsSub(nint hwnd, nint lParam);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user