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