diff --git a/MinecraftLaunch.Test/Program.cs b/MinecraftLaunch.Test/Program.cs
index 61f617b..e8e4db1 100644
--- a/MinecraftLaunch.Test/Program.cs
+++ b/MinecraftLaunch.Test/Program.cs
@@ -1,14 +1,53 @@
-//
-using MinecraftLaunch.Components.Fetcher;
-using MinecraftLaunch.Components.Launcher;
+using MinecraftLaunch.Components.Launcher;
using MinecraftLaunch.Components.Resolver;
+using MinecraftLaunch.Classes.Models.Launch;
using MinecraftLaunch.Components.Authenticator;
+using System.IO.Compression;
-var resolver = new GameResolver("C:\\Users\\w\\Desktop\\temp\\.minecraft");
-Launcher launcher = new(resolver, new(new OfflineAuthenticator("Yang114").Authenticate()) {
- JvmConfig = new(new JavaFetcher().Fetch().FirstOrDefault().JavaPath) {
+//new() {
+// "https://download.mcbbs.net/version/1.7.10/client",
+// "https://download.mcbbs.net/version/1.12.2/client",
+// "https://download.mcbbs.net/version/1.16.5/client",
+// "https://download.mcbbs.net/version/1.20.1/client",
+// "https://download.mcbbs.net/version/1.20.2/client",
+// "https://download.mcbbs.net/version/1.20.3/client",
+// "https://download.mcbbs.net/version/1.20.4/client",
+//}
+
+var file = new DownloadFile() {
+ DownloadPath = "C:\\Users\\w\\Desktop\\temp\\natives",
+ FileName = "test.jar",
+ DownloadUri = "https://download.mcbbs.net/version/1.20.4/client",
+};
+
+file.Changed += (_, x) => {
+ Console.WriteLine($"{x.ProgressPercentage * 100:0.00} - {x.Speed}");
+};
+
+await DownloadHelper.AdvancedDownloadFile(file,DownloadSettings.Default);
+
+Console.ReadKey();
+return;
+var account = new OfflineAuthenticator("Yang114").Authenticate();
+var resolver = new GameResolver(".minecraft");
+
+var config = new LaunchConfig {
+ Account = account,
+ IsEnableIndependencyCore = true,
+ JvmConfig = new(@"C:\Program Files\Java\jdk1.8.0_301\bin\javaw.exe") {
MaxMemory = 1024,
}
-});
+};
+
+Launcher launcher = new(resolver, config);
+var gameProcessWatcher = await launcher.LaunchAsync("1.12.2");
+
+//获取输出日志
+gameProcessWatcher.OutputLogReceived += (sender, args) => {
+ Console.WriteLine(args.Text);
+};
-await launcher.LaunchAsync("1.12.2");
\ No newline at end of file
+//检测游戏退出
+gameProcessWatcher.Exited += (sender, args) => {
+ Console.WriteLine("exit");
+};
\ No newline at end of file
diff --git a/MinecraftLaunch.Test/downloader.cs b/MinecraftLaunch.Test/downloader.cs
new file mode 100644
index 0000000..b60a937
--- /dev/null
+++ b/MinecraftLaunch.Test/downloader.cs
@@ -0,0 +1,625 @@
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Net.Http.Headers;
+using System.Net;
+using System.Threading.Tasks.Dataflow;
+using System.Security.AccessControl;
+using System.Security.Cryptography;
+using MinecraftLaunch.Extensions;
+
+public static class DownloadHelper {
+ ///
+ /// 下载线程
+ ///
+ public static int DownloadThread { get; set; } = 8;
+
+ const int DefaultBufferSize = 1024 * 1024 * 4;
+ static HttpClient Head => new();
+ static HttpClient Data => new();
+ static HttpClient MultiPart => new();
+
+ #region 下载数据
+
+ ///
+ /// 下载文件(通过线程池)
+ ///
+ ///
+ ///
+ ///
+ public static async Task DownloadData(DownloadFile downloadFile, DownloadSettings? downloadSettings = null) {
+ downloadSettings ??= DownloadSettings.Default;
+
+ var filePath = Path.Combine(downloadFile.DownloadPath, downloadFile.FileName);
+ var exceptions = new List();
+
+ for (var i = 0; i <= downloadSettings.RetryCount; i++) {
+ using var cts = new CancellationTokenSource(downloadSettings.Timeout * Math.Max(1, i + 1));
+
+ try {
+ using var request = new HttpRequestMessage(HttpMethod.Get, downloadFile.DownloadUri);
+
+ if (downloadSettings.Authentication != null)
+ request.Headers.Authorization = downloadSettings.Authentication;
+ if (!string.IsNullOrEmpty(downloadSettings.Host))
+ request.Headers.Host = downloadSettings.Host;
+
+ using var res =
+ await Data.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token);
+
+ res.EnsureSuccessStatusCode();
+
+ await using var stream = await res.Content.ReadAsStreamAsync(cts.Token);
+ await using var outputStream = File.Create(filePath);
+
+ var responseLength = res.Content.Headers.ContentLength ?? 0;
+ var downloadedBytesCount = 0L;
+ var sw = new Stopwatch();
+
+ var tSpeed = 0d;
+ var cSpeed = 0;
+
+ using var rentMemory = Pool.Rent(DefaultBufferSize);
+
+ while (true) {
+ sw.Restart();
+ var bytesRead = await stream.ReadAsync(rentMemory.Memory, cts.Token);
+ sw.Stop();
+
+ if (bytesRead == 0) break;
+
+ await outputStream.WriteAsync(rentMemory.Memory[..bytesRead], cts.Token);
+
+ downloadedBytesCount += bytesRead;
+
+ var elapsedTime = sw.Elapsed.TotalSeconds == 0 ? 1 : sw.Elapsed.TotalSeconds;
+ var speed = bytesRead / elapsedTime;
+
+ tSpeed += speed;
+ cSpeed++;
+
+ downloadFile.OnChanged(
+ speed,
+ (double)downloadedBytesCount / responseLength,
+ downloadedBytesCount,
+ responseLength);
+ }
+
+ sw.Stop();
+
+ if (downloadSettings.CheckFile && !string.IsNullOrEmpty(downloadFile.CheckSum)) {
+ await outputStream.FlushAsync(cts.Token);
+ outputStream.Seek(0, SeekOrigin.Begin);
+
+ var checkSum = (await downloadSettings.HashDataAsync(outputStream, cts.Token)).BytesToString();
+
+ if (!(checkSum?.Equals(downloadFile.CheckSum, StringComparison.OrdinalIgnoreCase) ?? false)) {
+ downloadFile.RetryCount++;
+ continue;
+ }
+ }
+
+ var aSpeed = tSpeed / cSpeed;
+ downloadFile.OnCompleted(true, null, aSpeed);
+
+ return;
+ }
+ catch (Exception e) {
+ await Task.Delay(250, cts.Token);
+
+ downloadFile.RetryCount++;
+ exceptions.Add(e);
+ }
+ }
+
+ downloadFile.OnCompleted(false, new AggregateException(exceptions), 0);
+ }
+
+ #endregion
+
+ public static bool SetMaxThreads() {
+ ThreadPool.GetMaxThreads(out var maxWorkerThreads,
+ out var maxConcurrentActiveRequests);
+
+ var changeSucceeded = ThreadPool.SetMaxThreads(
+ maxWorkerThreads, maxConcurrentActiveRequests);
+
+ return changeSucceeded;
+ }
+
+ public static string AutoFormatSpeedString(double speedInBytePerSecond) {
+ var speed = AutoFormatSpeed(speedInBytePerSecond);
+ var unit = speed.Unit switch {
+ SizeUnit.B => "B / s",
+ SizeUnit.Kb => "Kb / s",
+ SizeUnit.Mb => "Mb / s",
+ SizeUnit.Gb => "Gb / s",
+ SizeUnit.Tb => "Tb / s",
+ _ => "B / s"
+ };
+
+ return $"{speed.Speed:F} {unit}";
+ }
+
+ public static (double Speed, SizeUnit Unit) AutoFormatSpeed(double transferSpeed) {
+ const double baseNum = 1024;
+
+ // Auto choose the unit
+ var unit = SizeUnit.B;
+
+ if (transferSpeed > baseNum) {
+ unit = SizeUnit.Kb;
+ if (transferSpeed > Math.Pow(baseNum, 2)) {
+ unit = SizeUnit.Mb;
+ if (transferSpeed > Math.Pow(baseNum, 3)) {
+ unit = SizeUnit.Gb;
+ if (transferSpeed > Math.Pow(baseNum, 4)) {
+ unit = SizeUnit.Tb;
+ }
+ }
+ }
+ }
+
+ var convertedSpeed = unit switch {
+ SizeUnit.Kb => transferSpeed / baseNum,
+ SizeUnit.Mb => transferSpeed / Math.Pow(baseNum, 2),
+ SizeUnit.Gb => transferSpeed / Math.Pow(baseNum, 3),
+ SizeUnit.Tb => transferSpeed / Math.Pow(baseNum, 4),
+ _ => transferSpeed
+ };
+
+ return (convertedSpeed, unit);
+ }
+
+ #region 下载一个列表中的文件(自动确定是否使用分片下载)
+
+ ///
+ /// 下载文件方法(自动确定是否使用分片下载)
+ ///
+ ///
+ ///
+ public static Task AdvancedDownloadFile(DownloadFile df, DownloadSettings downloadSettings) {
+ if (!Directory.Exists(df.DownloadPath))
+ Directory.CreateDirectory(df.DownloadPath);
+
+ if (df.FileSize is >= 1048576 or 0)
+ return MultiPartDownloadTaskAsync(df, downloadSettings);
+
+ return DownloadData(df, downloadSettings);
+ }
+
+ ///
+ /// 下载文件方法(自动确定是否使用分片下载)
+ ///
+ /// 文件列表
+ ///
+ public static async Task AdvancedDownloadListFile(
+ IEnumerable fileEnumerable,
+ DownloadSettings downloadSettings) {
+ SetMaxThreads();
+
+ var actionBlock = new ActionBlock(
+ d => AdvancedDownloadFile(d, downloadSettings),
+ new ExecutionDataflowBlockOptions {
+ BoundedCapacity = DownloadThread * 2,
+ MaxDegreeOfParallelism = DownloadThread
+ });
+
+ foreach (var downloadFile in fileEnumerable) {
+ await actionBlock.SendAsync(downloadFile);
+ }
+
+ actionBlock.Complete();
+ await actionBlock.Completion;
+ }
+
+ #endregion
+
+ #region 分片下载
+
+ static readonly MemoryPool Pool = MemoryPool.Shared;
+
+ ///
+ /// 分片下载方法(异步)
+ ///
+ ///
+ ///
+ ///
+ public static async Task MultiPartDownloadTaskAsync(
+ DownloadFile? downloadFile,
+ DownloadSettings? downloadSettings = null) {
+ if (downloadFile == null) return;
+
+ downloadSettings ??= DownloadSettings.Default;
+
+ if (downloadSettings.DownloadParts <= 0)
+ downloadSettings.DownloadParts = Environment.ProcessorCount;
+
+ var exceptions = new List();
+ var filePath = Path.Combine(downloadFile.DownloadPath, downloadFile.FileName);
+ var timeout = TimeSpan.FromMilliseconds(downloadSettings.Timeout * 2);
+
+ var isLatestFileCheckSucceeded = true;
+ List? readRanges = null;
+
+ for (var r = 0; r <= downloadSettings.RetryCount; r++) {
+ using var cts = new CancellationTokenSource(timeout * Math.Max(1, r + 1));
+
+ try {
+ #region Get file size
+
+ using var headReq = new HttpRequestMessage(HttpMethod.Head, downloadFile.DownloadUri);
+
+ if (downloadSettings.Authentication != null)
+ headReq.Headers.Authorization = downloadSettings.Authentication;
+ if (!string.IsNullOrEmpty(downloadSettings.Host))
+ headReq.Headers.Host = downloadSettings.Host;
+
+ using var headRes = await Head.SendAsync(headReq, cts.Token);
+
+ headRes.EnsureSuccessStatusCode();
+
+ var responseLength = headRes.Content.Headers.ContentLength ?? 0;
+ var hasAcceptRanges = headRes.Headers.AcceptRanges.Count != 0;
+
+ using var rangeGetMessage = new HttpRequestMessage(HttpMethod.Get, downloadFile.DownloadUri);
+ rangeGetMessage.Headers.Range = new RangeHeaderValue(0, 0);
+
+ if (downloadSettings.Authentication != null)
+ rangeGetMessage.Headers.Authorization = downloadSettings.Authentication;
+ if (!string.IsNullOrEmpty(downloadSettings.Host))
+ rangeGetMessage.Headers.Host = downloadSettings.Host;
+
+ using var rangeGetRes = await Head.SendAsync(rangeGetMessage, cts.Token);
+
+ var parallelDownloadSupported =
+ responseLength != 0 &&
+ hasAcceptRanges &&
+ rangeGetRes.StatusCode == HttpStatusCode.PartialContent &&
+ (rangeGetRes.Content.Headers.ContentRange?.HasRange ?? false) &&
+ rangeGetRes.Content.Headers.ContentLength == 1;
+
+ if (!parallelDownloadSupported) {
+ await DownloadData(downloadFile, downloadSettings);
+ return;
+ }
+
+ #endregion
+
+ if (!Directory.Exists(downloadFile.DownloadPath))
+ Directory.CreateDirectory(downloadFile.DownloadPath);
+
+ #region Calculate ranges
+
+ readRanges = [];
+ var partSize = responseLength / downloadSettings.DownloadParts;
+ var totalSize = responseLength;
+
+ while (totalSize > 0) {
+ //计算分片
+ var to = totalSize;
+ var from = totalSize - partSize;
+
+ if (from < 0) from = 0;
+
+ totalSize -= partSize;
+
+ readRanges.Add(new DownloadRange {
+ Start = from,
+ End = to,
+ TempFileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())
+ });
+ }
+
+ #endregion
+
+ #region Parallel download
+
+ var downloadedBytesCount = 0L;
+ var tasksDone = 0;
+ var doneRanges = new ConcurrentBag();
+
+ var streamBlock =
+ new TransformBlock(
+ async p => {
+ using var request = new HttpRequestMessage(HttpMethod.Get, downloadFile.DownloadUri);
+
+ if (downloadSettings.Authentication != null)
+ request.Headers.Authorization = downloadSettings.Authentication;
+ if (!string.IsNullOrEmpty(downloadSettings.Host))
+ request.Headers.Host = downloadSettings.Host;
+
+ request.Headers.Range = new RangeHeaderValue(p.Start, p.End);
+
+ var downloadTask = await MultiPart.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead,
+ cts.Token);
+
+ return (downloadTask, p);
+ }, new ExecutionDataflowBlockOptions {
+ BoundedCapacity = downloadSettings.DownloadParts,
+ MaxDegreeOfParallelism = downloadSettings.DownloadParts
+ });
+
+ var tSpeed = 0D;
+ var cSpeed = 0;
+
+ var writeActionBlock = new ActionBlock<(HttpResponseMessage, DownloadRange)>(async t => {
+ using var res = t.Item1;
+
+ await using var stream = await res.Content.ReadAsStreamAsync(cts.Token);
+ await using var fileToWriteTo = File.Create(t.Item2.TempFileName);
+ using var rentMemory = Pool.Rent(DefaultBufferSize);
+
+ var sw = new Stopwatch();
+
+ while (true) {
+ sw.Restart();
+ var bytesRead = await stream.ReadAsync(rentMemory.Memory, cts.Token);
+ sw.Stop();
+
+ if (bytesRead == 0)
+ break;
+
+ await fileToWriteTo.WriteAsync(rentMemory.Memory[..bytesRead], cts.Token);
+
+ Interlocked.Add(ref downloadedBytesCount, bytesRead);
+
+ var elapsedTime = Math.Ceiling(sw.Elapsed.TotalSeconds);
+ var speed = bytesRead / elapsedTime;
+
+ tSpeed += speed;
+ cSpeed++;
+
+ downloadFile.OnChanged(
+ speed,
+ (double)downloadedBytesCount / responseLength,
+ downloadedBytesCount,
+ responseLength);
+ }
+
+ sw.Stop();
+
+ Interlocked.Add(ref tasksDone, 1);
+ doneRanges.Add(t.Item2);
+ }, new ExecutionDataflowBlockOptions {
+ BoundedCapacity = downloadSettings.DownloadParts,
+ MaxDegreeOfParallelism = downloadSettings.DownloadParts,
+ CancellationToken = cts.Token
+ });
+
+ var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
+
+ var filesBlock =
+ new TransformManyBlock, DownloadRange>(chunk => chunk,
+ new ExecutionDataflowBlockOptions());
+
+ filesBlock.LinkTo(streamBlock, linkOptions);
+ streamBlock.LinkTo(writeActionBlock, linkOptions);
+ filesBlock.Post(readRanges);
+
+ filesBlock.Complete();
+
+ await writeActionBlock.Completion;
+
+ var aSpeed = tSpeed / cSpeed;
+
+ if (doneRanges.Count != readRanges.Count) {
+ downloadFile.RetryCount++;
+ streamBlock.Complete();
+ writeActionBlock.Complete();
+ continue;
+ }
+
+ await using (var outputStream = File.Create(filePath)) {
+ foreach (var inputFilePath in readRanges) {
+ await using var inputStream = File.OpenRead(inputFilePath.TempFileName);
+ outputStream.Seek(inputFilePath.Start, SeekOrigin.Begin);
+
+ await inputStream.CopyToAsync(outputStream, cts.Token);
+ }
+
+ await outputStream.FlushAsync(cts.Token);
+ outputStream.Seek(0, SeekOrigin.Begin);
+
+ if (downloadSettings.CheckFile && !string.IsNullOrEmpty(downloadFile.CheckSum)) {
+ var checkSum = (await downloadSettings.HashDataAsync(outputStream, cts.Token)).BytesToString();
+
+ if (!checkSum.Equals(downloadFile.CheckSum, StringComparison.OrdinalIgnoreCase)) {
+ downloadFile.RetryCount++;
+ isLatestFileCheckSucceeded = false;
+ continue;
+ }
+
+ isLatestFileCheckSucceeded = true;
+ }
+ }
+
+ streamBlock.Complete();
+ writeActionBlock.Complete();
+
+ #endregion
+
+ downloadFile.OnCompleted(true, null, aSpeed);
+ return;
+ }
+ catch (Exception ex) {
+ if (readRanges != null)
+ foreach (var piece in readRanges.Where(piece => File.Exists(piece.TempFileName)))
+ try {
+ File.Delete(piece.TempFileName);
+ }
+ catch (Exception e) {
+ Debug.WriteLine(e);
+ }
+
+ downloadFile.RetryCount++;
+ exceptions.Add(ex);
+ // downloadFile.OnCompleted(false, ex, 0);
+ }
+ }
+
+ if (exceptions.Count > 0) {
+ downloadFile.OnCompleted(false, new AggregateException(exceptions), 0);
+ return;
+ }
+
+ if (!isLatestFileCheckSucceeded) {
+ downloadFile.OnCompleted(false, null, 0);
+ return;
+ }
+
+ downloadFile.OnCompleted(true, null, 0);
+ }
+
+ #endregion
+}
+
+public enum HashType {
+ MD5,
+ SHA1,
+ SHA256,
+ SHA384,
+ SHA512
+}
+
+public class DownloadSettings {
+ public static DownloadSettings Default => new() {
+ RetryCount = 0,
+ CheckFile = false,
+ Timeout = (int)TimeSpan.FromMinutes(5).TotalMilliseconds,
+ DownloadParts = 16
+ };
+
+ public int RetryCount { get; init; }
+ public bool CheckFile { get; init; }
+ public int Timeout { get; init; }
+ public int DownloadParts { get; set; }
+ public HashType HashType { get; init; }
+
+ ///
+ /// 认证
+ ///
+ public AuthenticationHeaderValue? Authentication { get; init; }
+
+ ///
+ /// 请求源
+ ///
+ public string? Host { get; init; }
+
+ public async ValueTask HashDataAsync(Stream stream, CancellationToken? token) {
+ token ??= CancellationToken.None;
+
+ return HashType switch {
+ HashType.MD5 => await MD5.HashDataAsync(stream, token.Value),
+ HashType.SHA1 => await SHA1.HashDataAsync(stream, token.Value),
+ HashType.SHA256 => await SHA256.HashDataAsync(stream, token.Value),
+ HashType.SHA384 => await SHA384.HashDataAsync(stream, token.Value),
+ HashType.SHA512 => await SHA512.HashDataAsync(stream, token.Value),
+ _ => throw new NotSupportedException()
+ };
+ }
+}
+
+public class DownloadFile {
+ ///
+ /// 下载Uri
+ ///
+ public required string DownloadUri { get; init; }
+
+ ///
+ /// 下载路径
+ ///
+ public required string DownloadPath { get; init; }
+
+ ///
+ /// 保存的文件名
+ ///
+ public required string FileName { get; init; }
+
+ ///
+ /// 最大重试计数
+ ///
+ public int RetryCount { get; internal set; }
+
+ ///
+ /// 文件类型(仅在Lib/Asset补全时可用)
+ ///
+ public ResourceType FileType { get; init; }
+
+ ///
+ /// 文件大小
+ ///
+ public long FileSize { get; init; }
+
+ ///
+ /// 文件检验码
+ ///
+ public string? CheckSum { get; init; }
+
+ ///
+ /// 下载完成事件
+ ///
+ public event EventHandler? Completed;
+
+ ///
+ /// 下载改变事件
+ ///
+ public event EventHandler? Changed;
+
+ public void OnChanged(double speed, double progress, long bytesReceived, long totalBytes) {
+ Changed?.Invoke(this, new DownloadFileChangedEventArgs {
+ Speed = speed,
+ ProgressPercentage = progress,
+ BytesReceived = bytesReceived,
+ TotalBytes = totalBytes
+ });
+ }
+
+ public void OnCompleted(bool? success, Exception? ex, double averageSpeed) {
+ Completed?.Invoke(this, new DownloadFileCompletedEventArgs(success, ex, averageSpeed));
+ }
+}
+
+public enum SizeUnit {
+ B,
+ Kb,
+ Mb,
+ Gb,
+ Tb
+}
+
+[DebuggerDisplay("[{Start}-{End}]")]
+public readonly struct DownloadRange {
+ ///
+ /// 开始字节
+ ///
+ public required long Start { get; init; }
+
+ ///
+ /// 结束字节
+ ///
+ public required long End { get; init; }
+
+ ///
+ /// 临时文件名称
+ ///
+ public required string TempFileName { get; init; }
+}
+
+public class DownloadFileCompletedEventArgs(bool? success, Exception? ex, double averageSpeed) : EventArgs {
+ public double AverageSpeed { get; set; } = averageSpeed;
+ public bool? Success { get; } = success;
+ public Exception? Error { get; } = ex;
+}
+
+public class DownloadFileChangedEventArgs : EventArgs {
+ ///
+ /// 速度:字节 /秒
+ ///
+ public double Speed { get; set; }
+ public double ProgressPercentage { get; set; }
+ public long BytesReceived { get; set; }
+ public long? TotalBytes { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch.sln b/MinecraftLaunch.sln
index 19c0de3..4cc31cd 100644
--- a/MinecraftLaunch.sln
+++ b/MinecraftLaunch.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftLaunch", "Minecraf
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftLaunch.Simple", "MinecraftLaunch.Test\MinecraftLaunch.Simple.csproj", "{76E3BD50-5A2C-43D0-A5B5-CD139EB60C94}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjBobcat", "..\..\..\Code\ProjBobcat\ProjBobcat\ProjBobcat\ProjBobcat.csproj", "{3DF75100-069C-4D5A-9A06-12E006DB2EBF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +33,14 @@ Global
{76E3BD50-5A2C-43D0-A5B5-CD139EB60C94}.Release|Any CPU.Build.0 = Release|Any CPU
{76E3BD50-5A2C-43D0-A5B5-CD139EB60C94}.Release|x64.ActiveCfg = Release|Any CPU
{76E3BD50-5A2C-43D0-A5B5-CD139EB60C94}.Release|x64.Build.0 = Release|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Debug|x64.Build.0 = Debug|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Release|x64.ActiveCfg = Release|Any CPU
+ {3DF75100-069C-4D5A-9A06-12E006DB2EBF}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MinecraftLaunch/Classes/Models/Launch/LaunchConfig.cs b/MinecraftLaunch/Classes/Models/Launch/LaunchConfig.cs
index 5cb8960..ed91d98 100644
--- a/MinecraftLaunch/Classes/Models/Launch/LaunchConfig.cs
+++ b/MinecraftLaunch/Classes/Models/Launch/LaunchConfig.cs
@@ -1,12 +1,7 @@
using MinecraftLaunch.Classes.Models.Auth;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace MinecraftLaunch.Classes.Models.Launch {
- public record LaunchConfig {
+ public record LaunchConfig() {
public Account Account { get; set; }
public JvmConfig JvmConfig { get; set; }
@@ -22,23 +17,23 @@ public record LaunchConfig {
public bool IsEnableIndependencyCore { get; set; } = true;
public GameWindowConfig GameWindowConfig { get; set; } = new();
-
- public LaunchConfig(Account account) {
+
+ public LaunchConfig(Account account) : this() {
Account = account;
}
- public LaunchConfig(Account account, JvmConfig jvmConfig) {
+ public LaunchConfig(Account account, JvmConfig jvmConfig) : this() {
Account = account;
JvmConfig = jvmConfig;
}
- public LaunchConfig(Account account, JvmConfig jvmConfig, GameWindowConfig gameWindowConfig) {
+ public LaunchConfig(Account account, JvmConfig jvmConfig, GameWindowConfig gameWindowConfig) : this() {
Account = account;
JvmConfig = jvmConfig;
GameWindowConfig = gameWindowConfig;
}
- public LaunchConfig(Account account, JvmConfig jvmConfig, GameWindowConfig gameWindowConfig, ServerConfig serverConfig) {
+ public LaunchConfig(Account account, JvmConfig jvmConfig, GameWindowConfig gameWindowConfig, ServerConfig serverConfig) : this() {
Account = account;
JvmConfig = jvmConfig;
GameWindowConfig = gameWindowConfig;
diff --git a/MinecraftLaunch/Components/Installer/OptifineInstaller.cs b/MinecraftLaunch/Components/Installer/OptifineInstaller.cs
new file mode 100644
index 0000000..de43ad0
--- /dev/null
+++ b/MinecraftLaunch/Components/Installer/OptifineInstaller.cs
@@ -0,0 +1,13 @@
+using MinecraftLaunch.Classes.Interfaces;
+
+namespace MinecraftLaunch.Components.Installer;
+
+public class OptifineInstaller : InstallerBase {
+ public override ValueTask InstallAsync() {
+ throw new NotImplementedException();
+ }
+
+ public static void EnumerableFromVersionAsync(string mcVersion) {
+
+ }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Components/Resolver/GameResolver.cs b/MinecraftLaunch/Components/Resolver/GameResolver.cs
index d002143..9a192b7 100644
--- a/MinecraftLaunch/Components/Resolver/GameResolver.cs
+++ b/MinecraftLaunch/Components/Resolver/GameResolver.cs
@@ -11,9 +11,13 @@ namespace MinecraftLaunch.Components.Resolver {
///
/// Minecraft 核心解析器
///
- public class GameResolver(string root) : IGameResolver, IResolver {
- public DirectoryInfo Root => new(root);
+ public class GameResolver() : IGameResolver, IResolver {
+ public DirectoryInfo Root { set; get; }
+ public GameResolver(string path) : this() {
+ Root = new(path);
+ }
+
///
/// 获取特定游戏实体信息
///
@@ -28,14 +32,14 @@ public GameEntry GetGameEntity(string id) {
var gameEntity = new GameEntry {
Id = entity.Id,
Type = entity.Type,
- GameFolderPath = root,
+ GameFolderPath = Root.FullName,
IsInheritedFrom = false,
MainClass = entity.MainClass,
MainLoaderType = entity.GetGameLoaderType(),
JavaVersion = entity.JavaVersion?.GetInt32("majorVersion") ?? 8,
};
- var assetsIndexFile = Path.Combine(root, "assets", "indexes", $"{entity.AssetIndex?.Id}.json");
+ var assetsIndexFile = Path.Combine(Root.FullName, "assets", "indexes", $"{entity.AssetIndex?.Id}.json");
var jarFile = Path.Combine(gameEntity.OfVersionDirectoryPath(),
$"{id}.jar");
@@ -57,7 +61,7 @@ public GameEntry GetGameEntity(string id) {
gameEntity.BehindArguments = HandleMinecraftArguments(entity.MinecraftArguments);
}
- if (entity.Arguments != null && entity.Arguments.Game != null) {
+ if (entity.Arguments is { Game: not null }) {
IEnumerable behindArguments;
if (gameEntity.BehindArguments != null) {
behindArguments = gameEntity.BehindArguments.Union(HandleGameArguments(entity.Arguments));
@@ -68,7 +72,7 @@ public GameEntry GetGameEntity(string id) {
gameEntity.BehindArguments = behindArguments;
}
- if (entity.Arguments != null && entity.Arguments.Jvm != null) {
+ if (entity.Arguments is { Jvm: not null }) {
gameEntity.FrontArguments = HandleJvmArguments(entity.Arguments);
} else {
gameEntity.FrontArguments = ["-Djava.library.path=${natives_directory}",
@@ -100,7 +104,7 @@ public IEnumerable GetGameEntitys() {
}
public GameJsonEntry Resolve(string id) {
- var path = Path.Combine(root, "versions", id, $"{id}.json");
+ var path = Path.Combine(Root.FullName, "versions", id, $"{id}.json");
if (!File.Exists(path)) {
return null!;
}
@@ -118,10 +122,12 @@ private IEnumerable HandleMinecraftArguments(string minecraftArguments)
=> GroupArguments(minecraftArguments.Replace(" ", " ").Split(' '));
private IEnumerable HandleGameArguments(ArgumentsJsonEntry entity)
- => GroupArguments(entity.Game.Where(x => x.ValueKind == JsonValueKind.String).Select(x => x.GetString()!.ToPath()));
+ => GroupArguments(entity.Game.Where(x => x.ValueKind == JsonValueKind.String)
+ .Select(x => x.GetString()!.ToPath()));
private IEnumerable HandleJvmArguments(ArgumentsJsonEntry entity)
- => GroupArguments(entity.Jvm.Where(x => x.ValueKind == JsonValueKind.String).Select(x => x.GetString()!.ToPath()));
+ => GroupArguments(entity.Jvm.Where(x => x.ValueKind == JsonValueKind.String)
+ .Select(x => x.GetString()!.ToPath()));
private static IEnumerable GroupArguments(IEnumerable arguments) {
List cache = new List();
diff --git a/MinecraftLaunch/Extensions/CryptoExtension.cs b/MinecraftLaunch/Extensions/CryptoExtension.cs
index 6dd1286..284e743 100644
--- a/MinecraftLaunch/Extensions/CryptoExtension.cs
+++ b/MinecraftLaunch/Extensions/CryptoExtension.cs
@@ -6,6 +6,10 @@
namespace MinecraftLaunch.Extensions {
public static class CryptoExtension {
+ public static string BytesToString(this byte[] bytes) {
+ return BitConverter.ToString(bytes).Replace("-", string.Empty);
+ }
+
public static IEnumerable Remove(this ReadOnlySpan data) {
if (data.Length == 0 || data[0] != 239 || data[1] != 187 || data[2] != 191) {
return data.ToArray();
diff --git a/MinecraftLaunch/MinecraftLaunch.csproj b/MinecraftLaunch/MinecraftLaunch.csproj
index 329cd9f..f4b5161 100644
--- a/MinecraftLaunch/MinecraftLaunch.csproj
+++ b/MinecraftLaunch/MinecraftLaunch.csproj
@@ -1,6 +1,6 @@
- 3.0.0-preview6
+ 3.0.0-preview7