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;