diff --git a/IO/NetFile.cs b/IO/NetFile.cs new file mode 100644 index 00000000..08a6e361 --- /dev/null +++ b/IO/NetFile.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using PCL.Core.Net; +using PCL.Core.Utils.Exts; +using PCL.Core.Utils.Hash; + +namespace PCL.Core.IO; + +public class NetFile +{ + public required string Path { get; set; } + public int Size = -1; + public HashAlgorithm Algorithm = HashAlgorithm.sha1; + public string Hash = ""; + public required string[] Url { get; set; } + + public bool CheckFile() + { + if (!File.Exists(Path)) return false; + if (!string.IsNullOrEmpty(Hash)) + { + using var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read, 16384, true); + var hash = Algorithm switch + { + HashAlgorithm.md5 => MD5Provider.Instance.ComputeHash(fs), + HashAlgorithm.sha1 => SHA1Provider.Instance.ComputeHash(fs), + HashAlgorithm.sha256 => SHA256Provider.Instance.ComputeHash(fs), + HashAlgorithm.sha512 => SHA512Provider.Instance.ComputeHash(fs), + _ => throw new NotSupportedException($"Unsupport algorithm: {Algorithm}") + }; + return hash == Hash; + } + return true; + } + // 这个方法存在的意义就是为了让 Downloader 支持换源重试 + /// + /// 获取当前对象的 DownloadItem 列表。 + /// + /// + public List GetDownloadItem() + { + var list = new List(); + foreach (var url in Url) + { + var item = new DownloadItem(url.ToUri(), Path); + item.Finished += () => CheckFile(); + list.Add(item); + } + return list; + } +} + +public enum HashAlgorithm { + md5, + sha1, + sha256, + sha512 +} \ No newline at end of file diff --git a/Minecraft/Instance/Clients/ClientBase.cs b/Minecraft/Instance/Clients/ClientBase.cs new file mode 100644 index 00000000..1005587f --- /dev/null +++ b/Minecraft/Instance/Clients/ClientBase.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using PCL.Core.IO; + +namespace PCL.Core.Minecraft.Instance.Clients; + +public class ClientBase : IClient +{ + + public static Task GetVersionInfoAsync(string version) + { + throw new NotImplementedException(); + } + + public static Task ParseAsync(string version) + { + throw new NotImplementedException(); + } + + public static Task UpdateVersionIndexAsync() + { + throw new NotImplementedException(); + } + + public virtual Task> AnalyzeLibraryAsync() + { + throw new NotImplementedException(); + } + + public virtual Task> AnalyzeMissingLibraryAsync() + { + throw new NotImplementedException(); + } + + public virtual Task ExecuteInstallerAsync(string path) + { + throw new NotImplementedException(); + } + + public virtual Task GetJsonAsync() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Minecraft/Instance/Clients/ForgeClient.cs b/Minecraft/Instance/Clients/ForgeClient.cs new file mode 100644 index 00000000..3b54ec5b --- /dev/null +++ b/Minecraft/Instance/Clients/ForgeClient.cs @@ -0,0 +1,6 @@ +namespace PCL.Core.Minecraft.Instance.Clients; + +public class ForgeClient : ClientBase +{ + +} \ No newline at end of file diff --git a/Minecraft/Instance/Clients/MinecraftClient.cs b/Minecraft/Instance/Clients/MinecraftClient.cs new file mode 100644 index 00000000..47f903c6 --- /dev/null +++ b/Minecraft/Instance/Clients/MinecraftClient.cs @@ -0,0 +1,191 @@ +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using System.Net.Http; +using PCL.Core.Net; +using PCL.Core.App; +using PCL.Core.Logging; +using System.Linq; +using System.Data; +using PCL.Core.Utils.Hash; +using System.Collections.Generic; +using System.Management; +using System; +using System.Runtime.InteropServices; +using PCL.Core.Utils.Exts; +using PCL.Core.Minecraft.Instance; +using PCL.Core.IO; + +namespace PCL.Core.Minecraft.Instance.Clients; + +public class MinecraftClient : ClientBase +{ + public static JsonNode? VersionList; + private Version? _version; + private JsonNode? _versionJson; + private string? _jsonUrl; + private string? _jsonHash; + private const string Official = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; + private const string BmclApi = "https://bmclapi2.bangbang93.com/mc/game/version_manifest_v2.json"; + private const string AssetsBaseUri = "https://resources.download.minecraft.net"; + private static string[] _GetVersionSource() => Config.ToolConfigGroup.DownloadConfigGroup.VersionSourceSolution switch + { + 0 => [Official, Official, BmclApi], + 1 => [Official, BmclApi, Official], + 2 => [BmclApi, BmclApi, Official] + }; + private static string[] _GetFileSource(string uri) + { + var mirror = uri + .Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com") + .Replace("libraries.minecraft.net", "bmclapi2.bangbang93.com/maven") + .Replace("pistom-data.mojang.com", "bmclapi2.bangbang93.com") + .Replace(AssetsBaseUri, "https://bmclapi2.bangbang93.com/assets"); + return Config.ToolConfigGroup.DownloadConfigGroup.VersionSourceSolution switch + { + 0 => [uri, uri, mirror], + 1 => [uri, mirror, uri], + 2 => [mirror, mirror, uri] + }; + } + + public new static async Task GetVersionInfoAsync(string mcVersion) + { + if (VersionList is null) await UpdateVersionIndexAsync(); + return VersionList!["versions"]?.AsArray().Where(value => value?["id"]?.ToString() == mcVersion).First(); + } + public new static async Task UpdateVersionIndexAsync() + { + foreach (var source in _GetVersionSource()) + { + try + { + using var handler = await HttpRequestBuilder.Create(source, HttpMethod.Get).SendAsync(true); + VersionList = await handler.AsJsonAsync(); + } + catch (HttpRequestException ex) + { + LogWrapper.Error(ex, "Minecraft", "Failed to get version list"); + } + } + } + public override async Task GetJsonAsync() + { + + foreach (var source in _GetFileSource(_jsonUrl!.ToString())) + { + try + { + var response = await HttpRequestBuilder.Create(source, HttpMethod.Get).SendAsync(true); + var content = await response.AsStringAsync(); + if (!string.IsNullOrEmpty(_jsonHash)) + { + var hashResult = SHA1Provider.Instance.ComputeHash(content); + if (string.Equals(hashResult, _jsonHash, StringComparison.OrdinalIgnoreCase)) continue; + } + return content; + } + catch (HttpRequestException ex) + { + LogWrapper.Error(ex, "Minecraft", "下载版本 Json 失败"); + } + } + throw new HttpRequestException("Failed to download version json:All of source unavailable"); + } + public override async Task> AnalyzeLibraryAsync() + { + var list = new List(); + if (_versionJson is null) + { + _versionJson = JsonNode.Parse(await GetJsonAsync()); + } + foreach (var library in _versionJson!["libraries"]!.AsArray()) + { + var rules = library?["rules"]; + // skip check when rules is null + if (rules is not null) foreach (var rule in rules.AsArray()) + { + // do nothing when allow/disallow (it skipped by continue) + switch (rule!["action"]!.ToString()) + { + case "disallow": + var os = rule["os"]; + var osName = os!["name"]?.ToString(); + var arch = os!["arch"]?.ToString(); + if (!string.IsNullOrEmpty(osName) && + RuntimeInformation.IsOSPlatform(OSPlatform.Create(osName.ToUpper()))) continue; + var currentArchitecture = Architecture.X86; + + if (!Enum.TryParse(arch!.Capitalize(), out currentArchitecture)) continue; + if (!string.IsNullOrEmpty(arch) && + RuntimeInformation.OSArchitecture == currentArchitecture) continue; + break; + case "allow": + default: + break; + } + } + var artifact = library?["downloads"]?["artifact"]; + var classifiers = library?["downloads"]?["classifiers"]; + if (artifact is not null) + { + list.Add(new NetFile() + { + Path = "", + Url = [""], + Size = 0, + Algorithm = HashAlgorithm.sha1, + Hash = "" + }); + } + if (classifiers is not null) + { + // get key by os type + var nativeKey = library?["natives"]?[Environment.OSVersion.Platform.ToString()]?.ToString(); + if (string.IsNullOrEmpty(nativeKey)) continue; + if (nativeKey.Contains("arch")) + nativeKey = nativeKey.Replace("${arch}", $"{(RuntimeInformation.OSArchitecture == Architecture.X86 ? "86" : "64")}"); + + } + } + return list; + } + public override Task> AnalyzeMissingLibraryAsync() + { + return base.AnalyzeMissingLibraryAsync(); + } + public async Task> AnalyzeAssetsAsync(JsonNode versionJson) + { + await GetJsonAsync(); + var list = new List(); + foreach (var asset in versionJson["object"]!.AsObject()) + { + var hash = asset!.Value!["hash"]!.ToString(); + var size = asset!.Value!["size"]!.GetValue(); + var pathSuffix = $"{hash.Substring(0, 1)}/{hash}"; + var assetUri = $"{AssetsBaseUri}/{pathSuffix}"; + var path = $"assets/objects/{pathSuffix}"; + list.Add(new NetFile() + { + Url = _GetFileSource(assetUri), + Path = path, + Algorithm = HashAlgorithm.sha1, + Hash = hash + }); + } + return list; + } + public static async Task ParseAsync(string mcVersion) + { + if (VersionList is null) await UpdateVersionIndexAsync(); + var info = GetVersionInfoAsync(mcVersion); + var client = new MinecraftClient() + { + _version = new Version(mcVersion) + }; + return client; + } + public override async Task ExecuteInstallerAsync(string path) + { + + } +} \ No newline at end of file diff --git a/Minecraft/Instance/IClient.cs b/Minecraft/Instance/IClient.cs new file mode 100644 index 00000000..1b4aad64 --- /dev/null +++ b/Minecraft/Instance/IClient.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using PCL.Core.Net; +using System.Text.Json.Nodes; +using System.Collections.Generic; +using PCL.Core.IO; + +namespace PCL.Core.Minecraft.Instance; + +public interface IClient +{ + static abstract Task GetVersionInfoAsync(string version); + static abstract Task UpdateVersionIndexAsync(); + abstract Task> AnalyzeLibraryAsync(); + abstract Task GetJsonAsync(); + static abstract Task ParseAsync(string version); + abstract Task ExecuteInstallerAsync(string path); + abstract Task> AnalyzeMissingLibraryAsync(); +} diff --git a/Minecraft/Instance/IVersion.cs b/Minecraft/Instance/IVersion.cs new file mode 100644 index 00000000..39770628 --- /dev/null +++ b/Minecraft/Instance/IVersion.cs @@ -0,0 +1 @@ +namespace PCL.Core.Minecraft.Instance.Clients; \ No newline at end of file diff --git a/Minecraft/Instance/InstanceInstallHandler.cs b/Minecraft/Instance/InstanceInstallHandler.cs new file mode 100644 index 00000000..73e436c0 --- /dev/null +++ b/Minecraft/Instance/InstanceInstallHandler.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PCL.Core.Minecraft.Instance.Clients; + +namespace PCL.Core.Minecraft.Instance; + +public static class InstanceInstallHandler +{ + public static async Task StartClientInstallAsync(IEnumerable clients,string path) + { + var task = new List(); + foreach (var client in clients){ + task.Add(client.GetJsonAsync()); + } + } + +} \ No newline at end of file diff --git a/Minecraft/JavaHelper.cs b/Minecraft/JavaHelper.cs new file mode 100644 index 00000000..39965122 --- /dev/null +++ b/Minecraft/JavaHelper.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using PCL.Core.Net; + +namespace PCL.Core.Minecraft; + +public static class JavaHelper +{ + private static string[] _GetJavaIndexUrl() => [ + "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json", + "https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json" + ]; + private static JsonNode? _JavaIndex; + public static async Task UpdateJavaIndex() + { + foreach (var url in _GetJavaIndexUrl()) + { + var result = await HttpRequestBuilder.Create(url, HttpMethod.Get).SendAsync(false); + if (!result.IsSuccess) continue; + _JavaIndex = await result.AsJsonAsync(); + } + throw new HttpRequestException("Failed to download version json:All of source unavailable"); + } + public static string GetIndexUrlByVersion(int mojarVersaion) { + foreach (var kvp in _JavaIndex!.AsObject()) + { + if (kvp.Key == "gamecore") continue; + var os = kvp.Key; + if (os.Contains("-")) os = os.Split("-")[0]; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create(os.ToUpper()))) continue; + foreach (var javas in kvp.Value!.AsObject()) + { + + } + } + return string.Empty; + } + public static string GetIndexUrlByName(string name) { + return string.Empty; + } +} \ No newline at end of file diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 46439a0c..00000000 --- a/NOTICE +++ /dev/null @@ -1,16 +0,0 @@ -This product uses the following open source components: - -- SharpZipLib (MIT License) - https://github.com/icsharpcode/SharpZipLib - -- LiteDB (MIT License) - https://github.com/litedb-org/LiteDB - -- Microsoft.Net.Compilers.Toolset (MIT License) - https://github.com/dotnet/roslyn - -- Microsoft.Toolkit.Uwp.Notifications (MIT License) - https://github.com/CommunityToolkit/WindowsCommunityToolkit - -- PolySharp (MIT License) - https://github.com/Sergio0694/PolySharp/ \ No newline at end of file diff --git a/PCL.Core.slnx b/PCL.Core.slnx index 65157cbf..fc059954 100644 --- a/PCL.Core.slnx +++ b/PCL.Core.slnx @@ -1,4 +1,4 @@ - + diff --git a/Utils/Exts/StringExtension.cs b/Utils/Exts/StringExtension.cs index f971117f..be3c6c02 100644 --- a/Utils/Exts/StringExtension.cs +++ b/Utils/Exts/StringExtension.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; namespace PCL.Core.Utils.Exts; @@ -41,7 +42,7 @@ public static class StringExtension if (targetType.IsEnum) return Enum.Parse(targetType, value, ignoreCase: true); - var parse = targetType.GetMethod("Parse", + var parse = targetType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, binder: null, types: [typeof(string)], modifiers: null); if (parse is not null) return parse.Invoke(null, [value]); @@ -203,7 +204,7 @@ public static bool IsASCII(this string str) { return str.All(c => c < 128); } - + public static bool StartsWithF(this string str, string prefix, bool ignoreCase = false) => str.StartsWith(prefix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); @@ -224,4 +225,9 @@ public static int LastIndexOfF(this string str, string subStr, bool ignoreCase = public static int LastIndexOfF(this string str, string subStr, int startIndex, bool ignoreCase = false) => str.LastIndexOf(subStr, startIndex, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + + public static string Capitalize(this string text) + => Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(text); + public static Uri ToUri(this string url) => new Uri(url); + } diff --git a/Utils/OS/EnvironmentInterop.cs b/Utils/OS/EnvironmentInterop.cs index f7cc71a4..fd6a0fdc 100644 --- a/Utils/OS/EnvironmentInterop.cs +++ b/Utils/OS/EnvironmentInterop.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using PCL.Core.Logging; using PCL.Core.Utils.Exts;