diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.SuikaiLauncher.Core/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.SuikaiLauncher.Core/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000..ef20cb0
--- /dev/null
+++ b/.idea/.idea.SuikaiLauncher.Core/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/.idea.SuikaiLauncher.Core/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml b/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml
new file mode 100644
index 0000000..66c3b6e
--- /dev/null
+++ b/.idea/.idea.SuikaiLauncher.Core/.idea/workspace.xml
@@ -0,0 +1,123 @@
+
+
+
+ SuikaiLauncher.Core.Modpack/SuikaiLauncher.Core.Modpack.csproj
+ SuikaiLauncher.Core.Modpack/SuikaiLauncher.Core.Modpack.csproj
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1749966088508
+
+
+ 1749966088508
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SuikaiLauncher.Core.Account/Modules/Microsoft.cs b/SuikaiLauncher.Core.Account/Modules/Microsoft.cs
index 6dac874..52010be 100644
--- a/SuikaiLauncher.Core.Account/Modules/Microsoft.cs
+++ b/SuikaiLauncher.Core.Account/Modules/Microsoft.cs
@@ -1,10 +1,10 @@
-using SuikaiLauncher.Core.Account.JsonModel;
+using SuikaiLauncher.Core.Account.JsonModel;
using SuikaiLauncher.Core.Base;
using SuikaiLauncher.Core.Override;
using System.Runtime.CompilerServices;
-namespace SuikaiLauncher.Core.Account.Modules
+namespace SuikaiLauncher.Core.Account
{
public class Microsoft
{
diff --git a/SuikaiLauncher.Core.Account/Modules/Profile.cs b/SuikaiLauncher.Core.Account/Modules/Profile.cs
new file mode 100644
index 0000000..8ea19fc
--- /dev/null
+++ b/SuikaiLauncher.Core.Account/Modules/Profile.cs
@@ -0,0 +1,149 @@
+using System.Text;
+using SuikaiLauncher.Core.Base;
+
+namespace SuikaiLauncher.Core.Account;
+///
+/// 档案类
+///
+public class Profile
+{
+ private static StringBuilder StrBuilder = new("*",30);
+ ///
+ /// 用户名
+ ///
+ public required string Name { get; set; }
+ ///
+ /// UUID
+ ///
+ public required string uuid { get; set; }
+ ///
+ /// 访问令牌
+ ///
+ public required string accessToken { get; set; }
+ ///
+ /// 刷新令牌(可能为 null)
+ ///
+ public string? refreshToken { get; set; }
+ ///
+ /// 用户皮肤的下载地址,没有设置则为 null
+ ///
+ public string? Skin { get; set; }
+ ///
+ /// 披风下载地址,没有则为 null
+ ///
+ public string? Cape { get; set; }
+ ///
+ /// 账户类型
+ ///
+ public McLoginType LoginType {get; set;}
+ ///
+ /// 档案过期时间(Access Token)
+ ///
+ public long ExpiresIn { get; set; }
+ ///
+ /// 完全过期时间(Refresh Token)
+ ///
+ public long ExpiredAt { get; set; }
+ ///
+ /// 档案创建时间
+ ///
+ public long CreateAt { get; set; }
+
+ public static string RemoveSecret(string SecretText)
+ {
+ return SecretText.Substring(0, 5) + StrBuilder + SecretText.Substring(SecretText.Length - 5,SecretText.Length -1);
+ }
+}
+///
+/// 档案管理器
+///
+public static class ProfileManager
+{
+ public static Profile CurrentProfile
+ {
+ private set
+ {
+
+ }
+ get
+ {
+
+ }
+ }
+ private static List? profiles;
+ ///
+ /// 初始化档案数据库
+ ///
+ private static void InitializeProfilesDatabase()
+ {
+
+ }
+ ///
+ /// 保存档案
+ ///
+ public static void SaveProfile()
+ {
+
+ }
+ ///
+ /// 清空档案数据库缓存(被移除的档案会在下次加载时恢复,除非数据库没有整这个档案)
+ ///
+ public static void Clear()
+ {
+
+ }
+ ///
+ /// 获取某一个档案
+ ///
+ /// 档案索引
+ /// 代表这个档案的 Profile 类
+ public static Profile GetProfile(int ProfileId)
+ {
+
+ }
+ ///
+ /// 删除某个档案(这会导致档案永久丢失)
+ ///
+ /// 要删除的档案
+ public static void DeleteProfile(int ProfileId)
+ {
+
+ }
+ ///
+ /// 删除整个档案数据库(这会导致存储在数据库的全部档案丢失)
+ ///
+ public static void DeleteProfiles()
+ {
+
+ }
+ ///
+ /// 刷新档案,如果档案过期,则会尝试重新登录
+ ///
+ public static void RefreshProfile()
+ {
+
+ }
+}
+
+///
+/// 账户类型
+///
+public enum McLoginType
+{
+ ///
+ /// 离线
+ ///
+ Offline = 0,
+ ///
+ /// 微软(正版)
+ ///
+ Microsoft = 1,
+ ///
+ /// Yggdrasil 第三方登录
+ ///
+ Auth = 2,
+ ///
+ /// 统一通行证
+ ///
+ Nide = 3
+}
\ No newline at end of file
diff --git a/SuikaiLauncher.Core.Base/Modules/FileIO.cs b/SuikaiLauncher.Core.Base/Modules/FileIO.cs
index 075c67f..8988d9f 100644
--- a/SuikaiLauncher.Core.Base/Modules/FileIO.cs
+++ b/SuikaiLauncher.Core.Base/Modules/FileIO.cs
@@ -7,6 +7,7 @@
using System.IO.Compression;
using System.Linq.Expressions;
using System.Net;
+using System.Formats.Tar;
using System.Security.Cryptography;
using System.Text;
@@ -217,6 +218,7 @@ public class ArchiveFile : IDisposable
{
private bool _dispose;
private string FilePath;
+
private dynamic? Handler;
private GZipStream? DataStream;
public bool disposed
@@ -316,6 +318,5 @@ public async Task WriteFile(string ArchiveEntry,Stream FileReadStream)
}
}
}
-
}
}
\ No newline at end of file
diff --git a/SuikaiLauncher.Core.Base/Modules/Network.cs b/SuikaiLauncher.Core.Base/Modules/Network.cs
index cd2ff19..332c315 100644
--- a/SuikaiLauncher.Core.Base/Modules/Network.cs
+++ b/SuikaiLauncher.Core.Base/Modules/Network.cs
@@ -1,611 +1,611 @@
-#pragma warning disable SYSLIB0014
-using SuikaiLauncher.Core.Base;
-using System.Net;
-using System.Net.Security;
-using System.Net.Sockets;
-using System.Security.Cryptography.X509Certificates;
-using SuikaiLauncher.Core.Override;
-using System.Net.NetworkInformation;
-using System.Runtime.CompilerServices;
-using System.Reflection.Metadata;
-
-// 这里是一堆和网络有关系的工具,包括网络请求,代理,Ping,域名解析
-// 虽然看起来很乱然而我没空整理,就先这样吧(逃
-
-namespace SuikaiLauncher.Core.Base
-{
- public class SocketConnect
- {
- public required Socket Socket;
- }
- public class HttpRequestBuilder
- {
- public static readonly Dictionary ConnectionPool = new();
- public static readonly object ConnectionLock = new object();
- public class SslInfomation
- {
- public required object RequestMessage;
- public required X509Certificate? Cert;
- public required X509Chain? Chain;
- public required SslPolicyErrors SslPolicyError;
- }
- private int timeout;
- public required HttpRequestMessage Req;
- public HttpResponseMessage? Resp;
- private static readonly HttpProxy RequestProxyFactory = new();
- private string? ConnectAddress;
- private int ConnectPort = 0;
- private static bool CheckSsl = true;
- public static readonly object HttpRequestBuilderPropertyChangeLock = new object();
- private static Func? CustomSslValidateCallback;
- private static readonly SocketsHttpHandler SocketHandler = new()
- {
- UseProxy = true,
- Proxy = RequestProxyFactory,
- AllowAutoRedirect = false,
- AutomaticDecompression = DecompressionMethods.All,
- SslOptions = new SslClientAuthenticationOptions()
- {
- RemoteCertificateValidationCallback = (object httpRequestMessage, X509Certificate? cert, X509Chain? certChain, SslPolicyErrors sslPolicyError) =>
- {
- if (CustomSslValidateCallback is not null)
- {
- return CustomSslValidateCallback(new SslInfomation()
- {
- RequestMessage = httpRequestMessage,
- Cert = cert,
- Chain = certChain,
- SslPolicyError = sslPolicyError
- });
- }
- if (sslPolicyError != SslPolicyErrors.None && CheckSsl)
- {
- return false;
- }
- return true;
- }
- },
- // 长连接实现
- ConnectCallback = async (SocketsHttpConnectionContext context, CancellationToken token) =>
- {
- var host = context.DnsEndPoint.Host;
- var port = context.DnsEndPoint.Port;
- SocketConnect? socketConnect = null;
- lock (ConnectionLock)
- {
- if (ConnectionPool.TryGetValue($"{host}:{port}", out socketConnect))
- {
- if (socketConnect.Socket != null && socketConnect.Socket.Connected)
- {
- // 检查 socket 是否可写
- try
- {
- bool poll = socketConnect.Socket.Poll(0, SelectMode.SelectWrite);
- if (poll)
- {
- // 返回现有连接
- return new NetworkStream(socketConnect.Socket, ownsSocket: false);
- }
- }
- catch
- {
- // 连接失效,移除
- ConnectionPool.Remove($"{host}:{port}");
- socketConnect = null;
- }
- }
- else
- {
- ConnectionPool.Remove($"{host}:{port}");
- socketConnect = null;
- }
- }
- }
-
- // 新建连接
- var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
- {
- NoDelay = true
- };
- socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
- await socket.ConnectAsync(host, port, token);
-
- lock (ConnectionLock)
- {
- ConnectionPool[$"{host}:{port}"] = new SocketConnect() { Socket = socket };
- }
-
- return new NetworkStream(socket, ownsSocket: false);
- }
- };
- private static readonly HttpClient Client = new(SocketHandler);
- private static bool FirstRequest;
- public static string UserAgent = "SuikaiLauncher.Core/0.0.1";
- ///
- /// 创建 HttpRequestBuilder 的新实例
- ///
- /// 目标服务器地址
- /// HttpRequestBuilder
- public static HttpRequestBuilder Create(string url, HttpMethod method)
- {
- return new HttpRequestBuilder() { Req = new HttpRequestMessage(method,url) };
- }
- ///
- /// 设置默认证书验证函数
- ///
- /// 证书验证函数
- public static void SetCustomSslValidateCallback(Func CustomCallback)
- {
- lock (HttpRequestBuilderPropertyChangeLock)
- {
- CustomSslValidateCallback = CustomCallback;
- }
- }
- ///
- /// 设置请求的 IP 地址,用于覆盖默认查询结果
- ///
- /// IP 地址
- /// HttpRequestBuilder
- public HttpRequestBuilder SetSourceAddress(string Address)
- {
- return this;
- }
- ///
- /// 设置请求端口,用于覆盖默认端口
- ///
- /// 端口号
- /// HttpRequestBuilder
- public HttpRequestBuilder SetConnectPort(int Port)
- {
- return this;
- }
- ///
- /// 是否在默认验证逻辑中忽略 SSL 证书错误
- ///
- /// HttpRequestBuilder
- public HttpRequestBuilder IgnoreSslError()
- {
- if (!CheckSsl) return this;
- lock (HttpRequestBuilderPropertyChangeLock)
- {
- CheckSsl = true;
- }
- return this;
- }
- ///
- /// 设置请求使用的代理
- ///
- /// 代理服务器
- /// HttpRequestBuilder
- public HttpRequestBuilder UseProxy(string? Proxy = null)
- {
- if (!string.IsNullOrWhiteSpace(Proxy))
- {
- lock (RequestProxyFactory.ProxyChangeLock)
- {
- RequestProxyFactory.ProxyAddress = Proxy;
- RequestProxyFactory.RequiredReloadProxyServer = true;
- }
- }
- return this;
-
- }
- public HttpRequestBuilder WithRequestData(MemoryStream Data)
- {
- this.Req.Content = new ByteArrayContent(Data.ToArray());
- return this;
- }
- ///
- /// 设置标头
- ///
- /// 标头
- /// 值
- /// HttpRequestBuilder
- public HttpRequestBuilder SetHeader(string Name, string Value)
- {
- if (Name.ContainsF("Content") && Req.Content is not null) Req.Content.Headers.Add(Name, Value);
- Req.Headers.Add(Name, Value);
- return this;
- }
- ///
- /// 发送网络请求并自动处理重定向
- ///
- ///
- public async Task Invoke()
- {
- await (await this.SendRequest()).ResolveHttpRedirect();
- return this;
- }
- ///
- /// 获取响应
- ///
- /// HttpResponseMessage
- public HttpResponseMessage GetResponse(bool EnsureSuccessStatusCode = true)
- {
- if (this.Resp is null) throw new InvalidOperationException("尝试在服务器响应之前获取响应对象");
- if (EnsureSuccessStatusCode) this.Resp.EnsureSuccessStatusCode();
- return this.Resp;
- }
- ///
- /// 发送单次网络请求,不处理重定向
- ///
- /// HttpRequestBuilder
- public async Task SendRequest()
- {
- using (CancellationTokenSource CTS = new(this.timeout))
- {
- this.Resp = await Client.SendAsync(this.Req,HttpCompletionOption.ResponseHeadersRead,CTS.Token);
- return this;
- }
- }
- ///
- /// 处理网络请求的重定向,直到响应码不处于 300~399 范围内(不包括 304)
- ///
- ///
- public async Task ResolveHttpRedirect()
- {
-
- if (this.Resp!.StatusCode is > (HttpStatusCode)300 and < (HttpStatusCode)400 && this.Resp.StatusCode != (HttpStatusCode)304)
- {
- HttpRequestMessage RedirectReq = new();
- foreach(var Header in this.Req.Headers)
- {
- Req.Headers.Add(Header.Key, Header.Value);
- }
- if (this.Req.Content is not null)
- {
- MemoryStream ReqStream = new();
- await (await this.Req.Content.ReadAsStreamAsync()).CopyToAsync(ReqStream);
- if (ReqStream is null) goto SkipContent;
- RedirectReq.Content = new ByteArrayContent(ReqStream.ToArray());
- ReqStream.Dispose();
- foreach (var Header in this.Req.Content.Headers)
- {
- RedirectReq.Content.Headers.Add(Header.Key, Header.Value);
- }
- }
- SkipContent:
- RedirectReq.RequestUri = this.Req.Headers.GetValues("location").First().ToURI();
- this.Req.Dispose();
- this.Resp.Dispose();
- this.Req = RedirectReq;
- await this.Invoke();
- }
- return this;
- }
- }
- public class Localhost {
- public class PingResult
- {
- public object? CustomResult;
- public List Result { get { throw new InvalidOperationException("无法读取此属性"); } set
- {
- TotalSend = value.Count;
- value.Select(k =>
- {
- switch (k.Status)
- {
- case IPStatus.Success:
- if (Fastest == -1) Fastest = k.RoundtripTime;
- else if (Slowest == -1) Slowest = k.RoundtripTime;
- else if (Slowest < k.RoundtripTime) Slowest = k.RoundtripTime;
- else if (Fastest > k.RoundtripTime) Fastest = k.RoundtripTime;
- TotalUsage += k.RoundtripTime;
- return null;
- case IPStatus.TimedOut:
- Failed++;
- Logger.Log($"[Network] Ping {k.Address} 失败:请求超时");
- return null;
- case IPStatus.DestinationHostUnreachable:
- Failed++;
- Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址不可达");
- return null;
- case IPStatus.DestinationNetworkUnreachable:
- Failed++;
- Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址所处的网络不可达");
- return null;
- case IPStatus.DestinationPortUnreachable:
- Failed++;
- Logger.Log($"[Network] Ping {k.Address} 失败:远程地址所指定的端口不可达");
- return null;
- case IPStatus.NoResources:
- Failed++;
- Logger.Log($"[Network] Ping {k.Address} 失败:网络资源不足");
- return null;
-
- }
- if (k.Status == IPStatus.Success) Success++;
- else
- {
- if (k.Status == IPStatus.TimedOut)
- Failed++;
- return null;
- }
- return null;
- });
- Average = TotalUsage / TotalSend;
- }
- }
- public long Fastest = -1;
- public long Slowest = -1;
- public long Average = -1;
- public long TotalUsage = -1;
- public long Success = 0;
- public long Failed = 0;
- public int TotalSend = 0;
- }
- private static bool SupportIPv6;
- private static Ping ICMPClient = new();
- public class PingInfomation
- {
- public required string Address;
- public int port = 25565;
- public int MaxTry = 1;
- public int Timeout = 2500;
- }
- ///
- /// 并行发送多个 ICMP/TCP 包来测试本地网络到目标服务器的连通性
- ///
- /// 目标服务器地址
- /// 端口号(仅 Tcping 模式下可用)
- /// 是否使用 Tcping 模式
- /// 允许的最大超时
- /// 发送的 ICMP/TCP 包数量(并行发送过多包可能导致额外开销)
- /// 自定义处理类
- public async static Task Ping(string Address, int port = 80, bool UseTcping = false, int MaxTimeout = 2500, int MaxTry = 4,Func? CustomResolver = null)
- {
- if (CustomResolver is not null) return new PingResult() { CustomResult = CustomResolver(new PingInfomation() { Address = Address, port = port, Timeout = MaxTimeout, MaxTry = MaxTry }) };
- Logger.Log($"[Network] 开始 Ping {Address}(0.0.0.0),具有 32 字节的数据。");
- var Operation = new List>();
- Operation.AddRange(Enumerable.Range(0, MaxTry).Select(avalue => ICMPClient.SendPingAsync(IPAddress.Parse(Address), MaxTimeout, new byte[32])));
- var ReplyResult = await Task.WhenAll(Operation);
-
- var Result = new PingResult()
- {
- Result = ReplyResult.ToList()
- };
- Logger.Log($"[Network] {Address}(0.0.0.0)的 Ping 统计结果:\n\n已发送:{Result.TotalSend} 已接收:{Result.Success} 丢包率:{Math.Round((double)(Result.Success / Result.TotalSend), 0)}% \n\n最长:{Result.Slowest}ms 最短:{Result.Fastest} 平均:{Result.Average}ms");
- return Result;
- }
- public static bool CheckIPv6Support()
- {
- foreach(NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces())
- {
- if (adapter.OperationalStatus != OperationalStatus.Up) continue;
- foreach (UnicastIPAddressInformation IP in adapter.GetIPProperties().UnicastAddresses)
- {
- if (IP.Address.AddressFamily is AddressFamily.InterNetworkV6)
- {
- SupportIPv6 = true;
- break;
- }
- }
- }
- TestIPv6:
- PingResult Result = Ping("[2400:3200:baba::1]").GetAwaiter().GetResult();
- Finally:
- return SupportIPv6;
- }
- }
- ///
- /// 支持 DNS Over HTTPS 的域名解析查询类
- ///
- public class DNSResolver {
- private static readonly Dictionary DnsQueryCache = new();
- public class DNSResolveResult
- {
- public List? Address;
- public List? IPAddress;
- }
- private static Dictionary DnsQueryResult = new();
- public static string? DOHServerAddress;
- ///
- ///
- ///
- ///
- ///
- ///
- public async static Task GetResolveResultUsingLocalDns(string RequestUrl, int ResolveTimeout = 500)
- {
- try
- {
- // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显)
- using (CancellationTokenSource CTS = new(ResolveTimeout))
- {
- IPHostEntry ResolveResult = await Dns.GetHostEntryAsync(RequestUrl);
- return new DNSResolveResult()
- {
- IPAddress = ResolveResult.AddressList.Select(ip => ip.ToString()).ToList(),
- Address = ResolveResult.Aliases.ToList()!
- };
- }
- }
- catch (TaskCanceledException ex)
- {
- throw new TimeoutException("操作超时", ex);
- }
- catch (SocketException ex)
- {
- throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex);
- }
- catch (ArgumentException)
- {
- throw new ArgumentException("此 URI 格式不正确或为空字符串");
- }
- }
- public async static Task GetResolveResultUsingDOH(string RequestUrl, int ResolveTimeout = 500)
- {
- try
- {
- // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显)
- using (CancellationTokenSource CTS = new(ResolveTimeout))
- {
- await HttpRequestBuilder
- .Create(DOHServerAddress! + "?name=" + RequestUrl + "&type=A", HttpMethod.Get)
- .SetSourceAddress(DNSResolver.GetResolveResultUsingLocalDns(RequestUrl).Result.IPAddress![0])
- .SetConnectPort(443)
- .SetHeader("Accept", "application/dns-json")
- .UseProxy()
- .Invoke();
- return null;
- }
- }
- catch (TaskCanceledException ex)
- {
- throw new TimeoutException("操作超时", ex);
- }
- catch (SocketException ex)
- {
- throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex);
- }
- catch (ArgumentException)
- {
- throw new ArgumentException("此 URI 格式不正确或为空字符串");
- }
- }
- }
- public class HttpProxy : IWebProxy
- {
- public ICredentials? Credentials { get; set; }
- private IWebProxy SystemProxy = HttpClient.DefaultProxy;
- private WebProxy? CurrentProxy;
- public object ProxyChangeLock = new object[1];
- public bool RequiredReloadProxyServer;
- public bool UseSystemProxy = true;
- public string? ProxyAddress;
- public Uri? GetProxy(Uri RequestHost)
- {
- return GetProxy(RequestHost.AbsoluteUri)?.Address;
- }
- public WebProxy? GetProxy(string Host)
- {
- try
- {
- Logger.Log("Success!");
- WebProxy CurrentSystemProxy = new WebProxy(SystemProxy.GetProxy(new Uri(Host)), true);
- if (CurrentProxy is not null && !RequiredReloadProxyServer) return CurrentProxy;
- if (RequiredReloadProxyServer)
- {
- Logger.Log("[Network] 已要求刷新代理配置,开始重载代理配置");
- if (UseSystemProxy && ProxyAddress.IsNullOrWhiteSpaceF())
- {
- Logger.Log("[Network] 当前代理配置:跟随系统代理设置");
- lock (ProxyChangeLock)
- {
- CurrentProxy = CurrentSystemProxy;
- RequiredReloadProxyServer = false;
- }
- }
- else if (!UseSystemProxy && !ProxyAddress.IsNullOrWhiteSpaceF())
- {
- Logger.Log("[Network] 当前代理配置:自定义");
- lock (ProxyChangeLock)
- {
- CurrentProxy = new WebProxy(ProxyAddress, true);
- RequiredReloadProxyServer = false;
- }
- }
- else
- {
- // 直接返回
- Logger.Log("[Network] 当前代理配置:禁用");
- return null;
- }
- return CurrentProxy;
- }
- return null;
- }
- catch (UriFormatException)
- {
- Logger.Log("[Network] 检测到可能错误的配置,已清空自定义代理配置并使用默认值。");
- ProxyAddress = null;
- return CurrentProxy;
- }
- }
- public bool IsBypassed(Uri RequestUri)
- {
- return CurrentProxy?.GetProxy(RequestUri) == RequestUri;
- }
- }
-
- public class Download
- {
- internal static SemaphoreSlim MaxDownloadThread = new SemaphoreSlim(64);
-
- public static int MaxThread
- {
- set
- {
- if (value > 384) throw new ArgumentException("给定的线程数过多");
- var old = MaxDownloadThread;
- MaxDownloadThread = new SemaphoreSlim(value);
- old?.Dispose();
- }
- get => MaxDownloadThread.CurrentCount;
- }
-
- public static WebProxy? ProxyServer = null;
- public static bool ParallelDownload = true;
-
- public class FileMetaData
- {
- public string? path { get; set; }
- public string? hash { get; set; }
- public string? algorithm { get; set; }
- public long? size { get; set; }
- public string? url { get; set; }
- public long Start;
- public bool ValidatePathContains(string path)
- {
- if (this.path.IsNullOrWhiteSpaceF() || path.IsNullOrWhiteSpaceF()) return false;
- return Path.GetFullPath(this.path).StartsWith(path);
- }
- }
-
- public static readonly object FileListLock = new object[1];
- public static long TotalFileCount = 0;
- public static long CompleteFileCount = 0;
-
- public static async Task NetCopyFileAsync(List DlTasks, CancellationToken? Token = null, int MaxThreadCount = 64)
- {
- var token = Token ?? CancellationToken.None;
- SemaphoreSlim semaphore = MaxDownloadThread;
-
- lock (FileListLock)
- {
- TotalFileCount += DlTasks.Count;
- }
-
- var tasks = DlTasks.Select(async t =>
- {
- await semaphore.WaitAsync(token);
- try
- {
- if (t.url is null || t.path is null) Logger.Crash();
- Logger.Log($"[Network] 直接下载文件:{t.url}");
- var data = await Network.NetworkRequest(t.url, Token: token);
- await FileIO.WriteData(data, t.path, token);
- lock (FileListLock)
- {
- CompleteFileCount++;
- }
- }
- catch (OperationCanceledException ex)
- {
- Logger.Log(ex, "[Network] 下载已取消");
- }
- catch (Exception ex)
- {
- Logger.Log(ex, "[Network] 下载文件失败");
- throw;
- }
- finally
- {
- semaphore.Release();
- }
- });
-
- await Task.WhenAll(tasks);
- }
- }
+#pragma warning disable SYSLIB0014
+using SuikaiLauncher.Core.Base;
+using System.Net;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Security.Cryptography.X509Certificates;
+using SuikaiLauncher.Core.Override;
+using System.Net.NetworkInformation;
+using System.Runtime.CompilerServices;
+using System.Reflection.Metadata;
+
+// 这里是一堆和网络有关系的工具,包括网络请求,代理,Ping,域名解析
+// 虽然看起来很乱然而我没空整理,就先这样吧(逃
+
+namespace SuikaiLauncher.Core.Base
+{
+ public class SocketConnect
+ {
+ public required Socket Socket;
+ }
+ public class HttpRequestBuilder
+ {
+ public static readonly Dictionary ConnectionPool = new();
+ public static readonly object ConnectionLock = new object();
+ public class SslInfomation
+ {
+ public required object RequestMessage;
+ public required X509Certificate? Cert;
+ public required X509Chain? Chain;
+ public required SslPolicyErrors SslPolicyError;
+ }
+ private int timeout;
+ public required HttpRequestMessage Req;
+ public HttpResponseMessage? Resp;
+ private static readonly HttpProxy RequestProxyFactory = new();
+ private string? ConnectAddress;
+ private int ConnectPort = 0;
+ private static bool CheckSsl = true;
+ public static readonly object HttpRequestBuilderPropertyChangeLock = new object();
+ private static Func? CustomSslValidateCallback;
+ private static readonly SocketsHttpHandler SocketHandler = new()
+ {
+ UseProxy = true,
+ Proxy = RequestProxyFactory,
+ AllowAutoRedirect = false,
+ AutomaticDecompression = DecompressionMethods.All,
+ SslOptions = new SslClientAuthenticationOptions()
+ {
+ RemoteCertificateValidationCallback = (object httpRequestMessage, X509Certificate? cert, X509Chain? certChain, SslPolicyErrors sslPolicyError) =>
+ {
+ if (CustomSslValidateCallback is not null)
+ {
+ return CustomSslValidateCallback(new SslInfomation()
+ {
+ RequestMessage = httpRequestMessage,
+ Cert = cert,
+ Chain = certChain,
+ SslPolicyError = sslPolicyError
+ });
+ }
+ if (sslPolicyError != SslPolicyErrors.None && CheckSsl)
+ {
+ return false;
+ }
+ return true;
+ }
+ },
+ // 长连接实现
+ ConnectCallback = async (SocketsHttpConnectionContext context, CancellationToken token) =>
+ {
+ var host = context.DnsEndPoint.Host;
+ var port = context.DnsEndPoint.Port;
+ SocketConnect? socketConnect = null;
+ lock (ConnectionLock)
+ {
+ if (ConnectionPool.TryGetValue($"{host}:{port}", out socketConnect))
+ {
+ if (socketConnect.Socket != null && socketConnect.Socket.Connected)
+ {
+ // 检查 socket 是否可写
+ try
+ {
+ bool poll = socketConnect.Socket.Poll(0, SelectMode.SelectWrite);
+ if (poll)
+ {
+ // 返回现有连接
+ return new NetworkStream(socketConnect.Socket, ownsSocket: false);
+ }
+ }
+ catch
+ {
+ // 连接失效,移除
+ ConnectionPool.Remove($"{host}:{port}");
+ socketConnect = null;
+ }
+ }
+ else
+ {
+ ConnectionPool.Remove($"{host}:{port}");
+ socketConnect = null;
+ }
+ }
+ }
+
+ // 新建连接
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
+ {
+ NoDelay = true
+ };
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
+ await socket.ConnectAsync(host, port, token);
+
+ lock (ConnectionLock)
+ {
+ ConnectionPool[$"{host}:{port}"] = new SocketConnect() { Socket = socket };
+ }
+
+ return new NetworkStream(socket, ownsSocket: false);
+ }
+ };
+ private static readonly HttpClient Client = new(SocketHandler);
+ private static bool FirstRequest;
+ public static string UserAgent = "SuikaiLauncher.Core/0.0.1";
+ ///
+ /// 创建 HttpRequestBuilder 的新实例
+ ///
+ /// 目标服务器地址
+ /// HttpRequestBuilder
+ public static HttpRequestBuilder Create(string url, HttpMethod method)
+ {
+ return new HttpRequestBuilder() { Req = new HttpRequestMessage(method,url) };
+ }
+ ///
+ /// 设置默认证书验证函数
+ ///
+ /// 证书验证函数
+ public static void SetCustomSslValidateCallback(Func CustomCallback)
+ {
+ lock (HttpRequestBuilderPropertyChangeLock)
+ {
+ CustomSslValidateCallback = CustomCallback;
+ }
+ }
+ ///
+ /// 设置请求的 IP 地址,用于覆盖默认查询结果
+ ///
+ /// IP 地址
+ /// HttpRequestBuilder
+ public HttpRequestBuilder SetSourceAddress(string Address)
+ {
+ return this;
+ }
+ ///
+ /// 设置请求端口,用于覆盖默认端口
+ ///
+ /// 端口号
+ /// HttpRequestBuilder
+ public HttpRequestBuilder SetConnectPort(int Port)
+ {
+ return this;
+ }
+ ///
+ /// 是否在默认验证逻辑中忽略 SSL 证书错误
+ ///
+ /// HttpRequestBuilder
+ public HttpRequestBuilder IgnoreSslError()
+ {
+ if (!CheckSsl) return this;
+ lock (HttpRequestBuilderPropertyChangeLock)
+ {
+ CheckSsl = true;
+ }
+ return this;
+ }
+ ///
+ /// 设置请求使用的代理
+ ///
+ /// 代理服务器
+ /// HttpRequestBuilder
+ public HttpRequestBuilder UseProxy(string? Proxy = null)
+ {
+ if (!string.IsNullOrWhiteSpace(Proxy))
+ {
+ lock (RequestProxyFactory.ProxyChangeLock)
+ {
+ RequestProxyFactory.ProxyAddress = Proxy;
+ RequestProxyFactory.RequiredReloadProxyServer = true;
+ }
+ }
+ return this;
+
+ }
+ public HttpRequestBuilder WithRequestData(MemoryStream Data)
+ {
+ this.Req.Content = new ByteArrayContent(Data.ToArray());
+ return this;
+ }
+ ///
+ /// 设置标头
+ ///
+ /// 标头
+ /// 值
+ /// HttpRequestBuilder
+ public HttpRequestBuilder SetHeader(string Name, string Value)
+ {
+ if (Name.ContainsF("Content") && Req.Content is not null) Req.Content.Headers.Add(Name, Value);
+ Req.Headers.Add(Name, Value);
+ return this;
+ }
+ ///
+ /// 发送网络请求并自动处理重定向
+ ///
+ ///
+ public async Task Invoke()
+ {
+ await (await this.SendRequest()).ResolveHttpRedirect();
+ return this;
+ }
+ ///
+ /// 获取响应
+ ///
+ /// HttpResponseMessage
+ public HttpResponseMessage GetResponse(bool EnsureSuccessStatusCode = true)
+ {
+ if (this.Resp is null) throw new InvalidOperationException("尝试在服务器响应之前获取响应对象");
+ if (EnsureSuccessStatusCode) this.Resp.EnsureSuccessStatusCode();
+ return this.Resp;
+ }
+ ///
+ /// 发送单次网络请求,不处理重定向
+ ///
+ /// HttpRequestBuilder
+ public async Task SendRequest()
+ {
+ using (CancellationTokenSource CTS = new(this.timeout))
+ {
+ this.Resp = await Client.SendAsync(this.Req,HttpCompletionOption.ResponseHeadersRead,CTS.Token);
+ return this;
+ }
+ }
+ ///
+ /// 处理网络请求的重定向,直到响应码不处于 300~399 范围内(不包括 304)
+ ///
+ ///
+ public async Task ResolveHttpRedirect()
+ {
+
+ if (this.Resp!.StatusCode is > (HttpStatusCode)300 and < (HttpStatusCode)400 && this.Resp.StatusCode != (HttpStatusCode)304)
+ {
+ HttpRequestMessage RedirectReq = new();
+ foreach(var Header in this.Req.Headers)
+ {
+ Req.Headers.Add(Header.Key, Header.Value);
+ }
+ if (this.Req.Content is not null)
+ {
+ MemoryStream ReqStream = new();
+ await (await this.Req.Content.ReadAsStreamAsync()).CopyToAsync(ReqStream);
+ if (ReqStream is null) goto SkipContent;
+ RedirectReq.Content = new ByteArrayContent(ReqStream.ToArray());
+ ReqStream.Dispose();
+ foreach (var Header in this.Req.Content.Headers)
+ {
+ RedirectReq.Content.Headers.Add(Header.Key, Header.Value);
+ }
+ }
+ SkipContent:
+ RedirectReq.RequestUri = this.Req.Headers.GetValues("location").First().ToURI();
+ this.Req.Dispose();
+ this.Resp.Dispose();
+ this.Req = RedirectReq;
+ await this.Invoke();
+ }
+ return this;
+ }
+ }
+ public class Localhost {
+ public class PingResult
+ {
+ public object? CustomResult;
+ public List Result { get { throw new InvalidOperationException("无法读取此属性"); } set
+ {
+ TotalSend = value.Count;
+ value.Select(k =>
+ {
+ switch (k.Status)
+ {
+ case IPStatus.Success:
+ if (Fastest == -1) Fastest = k.RoundtripTime;
+ else if (Slowest == -1) Slowest = k.RoundtripTime;
+ else if (Slowest < k.RoundtripTime) Slowest = k.RoundtripTime;
+ else if (Fastest > k.RoundtripTime) Fastest = k.RoundtripTime;
+ TotalUsage += k.RoundtripTime;
+ return null;
+ case IPStatus.TimedOut:
+ Failed++;
+ Logger.Log($"[Network] Ping {k.Address} 失败:请求超时");
+ return null;
+ case IPStatus.DestinationHostUnreachable:
+ Failed++;
+ Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址不可达");
+ return null;
+ case IPStatus.DestinationNetworkUnreachable:
+ Failed++;
+ Logger.Log($"[Network] Ping {k.Address} 失败:此远程地址所处的网络不可达");
+ return null;
+ case IPStatus.DestinationPortUnreachable:
+ Failed++;
+ Logger.Log($"[Network] Ping {k.Address} 失败:远程地址所指定的端口不可达");
+ return null;
+ case IPStatus.NoResources:
+ Failed++;
+ Logger.Log($"[Network] Ping {k.Address} 失败:网络资源不足");
+ return null;
+
+ }
+ if (k.Status == IPStatus.Success) Success++;
+ else
+ {
+ if (k.Status == IPStatus.TimedOut)
+ Failed++;
+ return null;
+ }
+ return null;
+ });
+ Average = TotalUsage / TotalSend;
+ }
+ }
+ public long Fastest = -1;
+ public long Slowest = -1;
+ public long Average = -1;
+ public long TotalUsage = -1;
+ public long Success = 0;
+ public long Failed = 0;
+ public int TotalSend = 0;
+ }
+ private static bool SupportIPv6;
+ private static Ping ICMPClient = new();
+ public class PingInfomation
+ {
+ public required string Address;
+ public int port = 25565;
+ public int MaxTry = 1;
+ public int Timeout = 2500;
+ }
+ ///
+ /// 并行发送多个 ICMP/TCP 包来测试本地网络到目标服务器的连通性
+ ///
+ /// 目标服务器地址
+ /// 端口号(仅 Tcping 模式下可用)
+ /// 是否使用 Tcping 模式
+ /// 允许的最大超时
+ /// 发送的 ICMP/TCP 包数量(并行发送过多包可能导致额外开销)
+ /// 自定义处理类
+ public async static Task Ping(string Address, int port = 80, bool UseTcping = false, int MaxTimeout = 2500, int MaxTry = 4,Func? CustomResolver = null)
+ {
+ if (CustomResolver is not null) return new PingResult() { CustomResult = CustomResolver(new PingInfomation() { Address = Address, port = port, Timeout = MaxTimeout, MaxTry = MaxTry }) };
+ Logger.Log($"[Network] 开始 Ping {Address}(0.0.0.0),具有 32 字节的数据。");
+ var Operation = new List>();
+ Operation.AddRange(Enumerable.Range(0, MaxTry).Select(avalue => ICMPClient.SendPingAsync(IPAddress.Parse(Address), MaxTimeout, new byte[32])));
+ var ReplyResult = await Task.WhenAll(Operation);
+
+ var Result = new PingResult()
+ {
+ Result = ReplyResult.ToList()
+ };
+ Logger.Log($"[Network] {Address}(0.0.0.0)的 Ping 统计结果:\n\n已发送:{Result.TotalSend} 已接收:{Result.Success} 丢包率:{Math.Round((double)(Result.Success / Result.TotalSend), 0)}% \n\n最长:{Result.Slowest}ms 最短:{Result.Fastest} 平均:{Result.Average}ms");
+ return Result;
+ }
+ public static bool CheckIPv6Support()
+ {
+ foreach(NetworkInterface adapter in NetworkInterface.GetAllNetworkInterfaces())
+ {
+ if (adapter.OperationalStatus != OperationalStatus.Up) continue;
+ foreach (UnicastIPAddressInformation IP in adapter.GetIPProperties().UnicastAddresses)
+ {
+ if (IP.Address.AddressFamily is AddressFamily.InterNetworkV6)
+ {
+ SupportIPv6 = true;
+ break;
+ }
+ }
+ }
+ TestIPv6:
+ PingResult Result = Ping("[2400:3200:baba::1]").GetAwaiter().GetResult();
+ Finally:
+ return SupportIPv6;
+ }
+ }
+ ///
+ /// 支持 DNS Over HTTPS 的域名解析查询类
+ ///
+ public class DNSResolver {
+ private static readonly Dictionary DnsQueryCache = new();
+ public class DNSResolveResult
+ {
+ public List? Address;
+ public List? IPAddress;
+ }
+ private static Dictionary DnsQueryResult = new();
+ public static string? DOHServerAddress;
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async static Task GetResolveResultUsingLocalDns(string RequestUrl, int ResolveTimeout = 500)
+ {
+ try
+ {
+ // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显)
+ using (CancellationTokenSource CTS = new(ResolveTimeout))
+ {
+ IPHostEntry ResolveResult = await Dns.GetHostEntryAsync(RequestUrl);
+ return new DNSResolveResult()
+ {
+ IPAddress = ResolveResult.AddressList.Select(ip => ip.ToString()).ToList(),
+ Address = ResolveResult.Aliases.ToList()!
+ };
+ }
+ }
+ catch (TaskCanceledException ex)
+ {
+ throw new TimeoutException("操作超时", ex);
+ }
+ catch (SocketException ex)
+ {
+ throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex);
+ }
+ catch (ArgumentException)
+ {
+ throw new ArgumentException("此 URI 格式不正确或为空字符串");
+ }
+ }
+ public async static Task GetResolveResultUsingDOH(string RequestUrl, int ResolveTimeout = 500)
+ {
+ try
+ {
+ // DNS 查询不像网络请求,过长的查询时间会让下载速度缓慢(尤其是从很多不同服务器下载文件,这种情况下 DNS 查询导致的缓慢会更加明显)
+ using (CancellationTokenSource CTS = new(ResolveTimeout))
+ {
+ await HttpRequestBuilder
+ .Create(DOHServerAddress! + "?name=" + RequestUrl + "&type=A", HttpMethod.Get)
+ .SetSourceAddress(DNSResolver.GetResolveResultUsingLocalDns(RequestUrl).Result.IPAddress![0])
+ .SetConnectPort(443)
+ .SetHeader("Accept", "application/dns-json")
+ .UseProxy()
+ .Invoke();
+ return null;
+ }
+ }
+ catch (TaskCanceledException ex)
+ {
+ throw new TimeoutException("操作超时", ex);
+ }
+ catch (SocketException ex)
+ {
+ throw new TaskCanceledException($"未能解析此远程名称 {RequestUrl}", ex);
+ }
+ catch (ArgumentException)
+ {
+ throw new ArgumentException("此 URI 格式不正确或为空字符串");
+ }
+ }
+ }
+ public class HttpProxy : IWebProxy
+ {
+ public ICredentials? Credentials { get; set; }
+ private IWebProxy SystemProxy = HttpClient.DefaultProxy;
+ private WebProxy? CurrentProxy;
+ public object ProxyChangeLock = new object[1];
+ public bool RequiredReloadProxyServer;
+ public bool UseSystemProxy = true;
+ public string? ProxyAddress;
+ public Uri? GetProxy(Uri RequestHost)
+ {
+ return GetProxy(RequestHost.AbsoluteUri)?.Address;
+ }
+ public WebProxy? GetProxy(string Host)
+ {
+ try
+ {
+ Logger.Log("Success!");
+ WebProxy CurrentSystemProxy = new WebProxy(SystemProxy.GetProxy(new Uri(Host)), true);
+ if (CurrentProxy is not null && !RequiredReloadProxyServer) return CurrentProxy;
+ if (RequiredReloadProxyServer)
+ {
+ Logger.Log("[Network] 已要求刷新代理配置,开始重载代理配置");
+ if (UseSystemProxy && ProxyAddress.IsNullOrWhiteSpaceF())
+ {
+ Logger.Log("[Network] 当前代理配置:跟随系统代理设置");
+ lock (ProxyChangeLock)
+ {
+ CurrentProxy = CurrentSystemProxy;
+ RequiredReloadProxyServer = false;
+ }
+ }
+ else if (!UseSystemProxy && !ProxyAddress.IsNullOrWhiteSpaceF())
+ {
+ Logger.Log("[Network] 当前代理配置:自定义");
+ lock (ProxyChangeLock)
+ {
+ CurrentProxy = new WebProxy(ProxyAddress, true);
+ RequiredReloadProxyServer = false;
+ }
+ }
+ else
+ {
+ // 直接返回
+ Logger.Log("[Network] 当前代理配置:禁用");
+ return null;
+ }
+ return CurrentProxy;
+ }
+ return null;
+ }
+ catch (UriFormatException)
+ {
+ Logger.Log("[Network] 检测到可能错误的配置,已清空自定义代理配置并使用默认值。");
+ ProxyAddress = null;
+ return CurrentProxy;
+ }
+ }
+ public bool IsBypassed(Uri RequestUri)
+ {
+ return CurrentProxy?.GetProxy(RequestUri) == RequestUri;
+ }
+ }
+
+ public class Download
+ {
+ internal static SemaphoreSlim MaxDownloadThread = new SemaphoreSlim(64);
+
+ public static int MaxThread
+ {
+ set
+ {
+ if (value > 384) throw new ArgumentException("给定的线程数过多");
+ var old = MaxDownloadThread;
+ MaxDownloadThread = new SemaphoreSlim(value);
+ old?.Dispose();
+ }
+ get => MaxDownloadThread.CurrentCount;
+ }
+
+ public static WebProxy? ProxyServer = null;
+ public static bool ParallelDownload = true;
+
+ public class FileMetaData
+ {
+ public string? path { get; set; }
+ public string? hash { get; set; }
+ public string? algorithm { get; set; }
+ public long? size { get; set; }
+ public string? url { get; set; }
+ public long Start;
+ public bool ValidatePathContains(string path)
+ {
+ if (this.path.IsNullOrWhiteSpaceF() || path.IsNullOrWhiteSpaceF()) return false;
+ return Path.GetFullPath(this.path).StartsWith(path);
+ }
+ }
+
+ public static readonly object FileListLock = new object[1];
+ public static long TotalFileCount = 0;
+ public static long CompleteFileCount = 0;
+
+ public static async Task NetCopyFileAsync(List DlTasks, CancellationToken? Token = null, int MaxThreadCount = 64)
+ {
+ var token = Token ?? CancellationToken.None;
+ SemaphoreSlim semaphore = MaxDownloadThread;
+
+ lock (FileListLock)
+ {
+ TotalFileCount += DlTasks.Count;
+ }
+
+ var tasks = DlTasks.Select(async t =>
+ {
+ await semaphore.WaitAsync(token);
+ try
+ {
+ if (t.url is null || t.path is null) Logger.Crash();
+ Logger.Log($"[Network] 直接下载文件:{t.url}");
+ var data = await Network.NetworkRequest(t.url, Token: token);
+ await FileIO.WriteData(data, t.path, token);
+ lock (FileListLock)
+ {
+ CompleteFileCount++;
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.Log(ex, "[Network] 下载已取消");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex, "[Network] 下载文件失败");
+ throw;
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ });
+
+ await Task.WhenAll(tasks);
+ }
+ }
}
\ No newline at end of file
diff --git a/SuikaiLauncher.Core.Base/Modules/PEReader.cs b/SuikaiLauncher.Core.Base/Modules/PEReader.cs
index 0eeb0fc..c0a5e64 100644
--- a/SuikaiLauncher.Core.Base/Modules/PEReader.cs
+++ b/SuikaiLauncher.Core.Base/Modules/PEReader.cs
@@ -1,39 +1,39 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection.PortableExecutable;
-using System.Text;
-using System.Threading.Tasks;
-
-
-namespace SuikaiLauncher.Core.Base
-{
- // 通用 PE 头读取器
- public class PEReader
- {
- private PEHeaders? Headers;
- private FileStream? FileReadStream;
- public void OpenFile(string FilePath)
- {
- try
- {
- if (!File.Exists(FilePath)) throw new FileNotFoundException("未找到指定文件");
- using (this.FileReadStream = new(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- Headers = new PEHeaders(this.FileReadStream);
- }
- }catch (BadImageFormatException ex)
- {
- this.OnFailed(ex);
- }catch (InvalidOperationException ex)
- {
- this.OnFailed(ex);
- }
- }
- public void OnFailed(Exception ex)
- {
- Logger.Log(ex, "[Runtime] 读取文件 PE 头失败。");
- throw new TaskCanceledException("此 PE 文件的格式无效");
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.PortableExecutable;
+using System.Text;
+using System.Threading.Tasks;
+
+
+namespace SuikaiLauncher.Core.Base
+{
+ // 通用 PE 头读取器
+ public class PEReader
+ {
+ private PEHeaders? Headers;
+ private FileStream? FileReadStream;
+ public void OpenFile(string FilePath)
+ {
+ try
+ {
+ if (!File.Exists(FilePath)) throw new FileNotFoundException("未找到指定文件");
+ using (this.FileReadStream = new(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ Headers = new PEHeaders(this.FileReadStream);
+ }
+ }catch (BadImageFormatException ex)
+ {
+ this.OnFailed(ex);
+ }catch (InvalidOperationException ex)
+ {
+ this.OnFailed(ex);
+ }
+ }
+ public void OnFailed(Exception ex)
+ {
+ Logger.Log(ex, "[Runtime] 读取文件 PE 头失败。");
+ throw new TaskCanceledException("此 PE 文件的格式无效");
+ }
+ }
+}
diff --git a/SuikaiLauncher.Core.Base/Modules/Process.cs b/SuikaiLauncher.Core.Base/Modules/Process.cs
index 8123f48..98673d5 100644
--- a/SuikaiLauncher.Core.Base/Modules/Process.cs
+++ b/SuikaiLauncher.Core.Base/Modules/Process.cs
@@ -1,100 +1,100 @@
-using SuikaiLauncher.Core.Override;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using static System.Runtime.InteropServices.JavaScript.JSType;
-
-namespace SuikaiLauncher.Core.Base
-{
- public class ProcessBuilder
- {
- private Action? CrashCallback;
- private Action? ExitCallback;
- private CancellationTokenSource CTS = new();
- private List Arguments = [];
- private FileStream OutputStream = new(Environments.ApplicationDataPath + "SuikaiLauncher/Core/ProcessBuilder/Logs/" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 8192, true);
- private Process process = new();
- private MemoryStream buffer = new();
- private readonly object StreamLock = new object[1];
- private bool HasStart = false;
- public static ProcessBuilder Create()
- {
- return new ProcessBuilder();
- }
- public ProcessBuilder Executable(string exec)
- {
- this.process.StartInfo.FileName = exec;
- return this;
- }
- public ProcessBuilder RequireEncoding(Encoding? encoding = null)
- {
- this.process.StartInfo.StandardErrorEncoding = this.process.StartInfo.StandardOutputEncoding = encoding ?? Encoding.UTF8;
- return this;
- }
- public ProcessBuilder WithArgument(string Argument)
- {
- this.process.StartInfo.ArgumentList.Add(Argument);
- return this;
- }
- public ProcessBuilder RunAsAdmin()
- {
- this.process.StartInfo.Verb = "runas";
- return this;
- }
- public ProcessBuilder UseShell()
- {
- this.process.StartInfo.UseShellExecute = true;
- return this;
- }
- public ProcessBuilder Invoke()
- {
- if (this.HasStart) throw new InvalidOperationException("不可启动已经启动的 ProcessBuilder 对象");
- this.HasStart = true;
- process.OutputDataReceived += (sender, e) =>
- {
- if (e.Data is null) return;
- this.buffer.Write(e.Data.GetBytes());
- };
- process.Start();
- Task.Run(async () =>
- {
- while (!process.HasExited)
- {
- await Task.Delay(TimeSpan.FromSeconds(10));
- lock (StreamLock)
- {
-
- this.buffer.CopyToAsync(this.OutputStream);
-
- }
- await this.OutputStream.FlushAsync();
- // 清空 Buffer 防止内存爆炸
- this.buffer.SetLength(0);
- if (CTS.Token.IsCancellationRequested) throw new TaskCanceledException("进程监控已退出");
- }
- TaskCanceledException TaskEX = new();
- TaskEX.Data["RawOutput"] = null;
- if (process.ExitCode != 0 && this.CrashCallback is not null) this.CrashCallback(TaskEX);
- else if(this.ExitCallback is not null) this.ExitCallback();
- });
- return this;
- }
- public void SetCustomProcessCrashCallback(Action Callback)
- {
- this.CrashCallback = Callback;
- }
- public void SetCustomProcessExitCallback(Action Callback)
- {
- this.ExitCallback = Callback;
- }
- public async Task StopProcess()
- {
- if (this.process.HasExited) return;
- if (this.process.MainWindowTitle.IsNullOrWhiteSpaceF()) this.process.Kill();
- this.process.CloseMainWindow();
- this.process.WaitForExit(5000);
- if (!this.process.HasExited) this.process.Kill();
- }
- }
-}
+using SuikaiLauncher.Core.Override;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace SuikaiLauncher.Core.Base
+{
+ public class ProcessBuilder
+ {
+ private Action? CrashCallback;
+ private Action? ExitCallback;
+ private CancellationTokenSource CTS = new();
+ private List Arguments = [];
+ private FileStream OutputStream = new(Environments.ApplicationDataPath + "SuikaiLauncher/Core/ProcessBuilder/Logs/" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 8192, true);
+ private Process process = new();
+ private MemoryStream buffer = new();
+ private readonly object StreamLock = new object[1];
+ private bool HasStart = false;
+ public static ProcessBuilder Create()
+ {
+ return new ProcessBuilder();
+ }
+ public ProcessBuilder Executable(string exec)
+ {
+ this.process.StartInfo.FileName = exec;
+ return this;
+ }
+ public ProcessBuilder RequireEncoding(Encoding? encoding = null)
+ {
+ this.process.StartInfo.StandardErrorEncoding = this.process.StartInfo.StandardOutputEncoding = encoding ?? Encoding.UTF8;
+ return this;
+ }
+ public ProcessBuilder WithArgument(string Argument)
+ {
+ this.process.StartInfo.ArgumentList.Add(Argument);
+ return this;
+ }
+ public ProcessBuilder RunAsAdmin()
+ {
+ this.process.StartInfo.Verb = "runas";
+ return this;
+ }
+ public ProcessBuilder UseShell()
+ {
+ this.process.StartInfo.UseShellExecute = true;
+ return this;
+ }
+ public ProcessBuilder Invoke()
+ {
+ if (this.HasStart) throw new InvalidOperationException("不可启动已经启动的 ProcessBuilder 对象");
+ this.HasStart = true;
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (e.Data is null) return;
+ this.buffer.Write(e.Data.GetBytes());
+ };
+ process.Start();
+ Task.Run(async () =>
+ {
+ while (!process.HasExited)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(10));
+ lock (StreamLock)
+ {
+
+ this.buffer.CopyToAsync(this.OutputStream);
+
+ }
+ await this.OutputStream.FlushAsync();
+ // 清空 Buffer 防止内存爆炸
+ this.buffer.SetLength(0);
+ if (CTS.Token.IsCancellationRequested) throw new TaskCanceledException("进程监控已退出");
+ }
+ TaskCanceledException TaskEX = new();
+ TaskEX.Data["RawOutput"] = null;
+ if (process.ExitCode != 0 && this.CrashCallback is not null) this.CrashCallback(TaskEX);
+ else if(this.ExitCallback is not null) this.ExitCallback();
+ });
+ return this;
+ }
+ public void SetCustomProcessCrashCallback(Action Callback)
+ {
+ this.CrashCallback = Callback;
+ }
+ public void SetCustomProcessExitCallback(Action Callback)
+ {
+ this.ExitCallback = Callback;
+ }
+ public async Task StopProcess()
+ {
+ if (this.process.HasExited) return;
+ if (this.process.MainWindowTitle.IsNullOrWhiteSpaceF()) this.process.Kill();
+ this.process.CloseMainWindow();
+ this.process.WaitForExit(5000);
+ if (!this.process.HasExited) this.process.Kill();
+ }
+ }
+}
diff --git a/SuikaiLauncher.Core.Base/Modules/Setup.cs b/SuikaiLauncher.Core.Base/Modules/Setup.cs
index 9fba4af..83a0cc8 100644
--- a/SuikaiLauncher.Core.Base/Modules/Setup.cs
+++ b/SuikaiLauncher.Core.Base/Modules/Setup.cs
@@ -74,403 +74,8 @@ internal class ModData
- public class Config
+ public static class Setup
{
- private static XDocument XmlConfig;
- private static XDocument XmlBackupConfig;
- private static object SetupChangeLock = new object[1];
- private static bool SetupChanged;
- private static Thread SetupWatcher;
-
- public static void InitConfig()
- {
- if (File.Exists(Environments.ConfigPath))
- {
- throw new FieldAccessException($"目标文件已存在: {Environments.ConfigPath}");
- }
-
- string? DirectoryPath = Path.GetDirectoryName(Environments.ConfigPath);
- if (DirectoryPath.IsNullOrWhiteSpaceF() && !Directory.Exists(DirectoryPath))
- {
- Directory.CreateDirectory(DirectoryPath);
- }
-
- XDocument config = new XDocument(new XElement("Setup"));
- config.Root?.Add(new XElement("System"));
- config.Root?.Add(new XElement("Account"));
- config.Root?.Add(new XElement("Versions"));
-
- try
- {
- config.Save(Environments.ConfigPath);
- config.Save(Environments.ConfigPath.Replace(".xml", ".xml.backup"));
- }
- catch (Exception ex)
- {
- throw new Exception($"初始化配置文件时发生错误:",ex);
- }
- }
- public static void ReleaseSetup()
- {
- lock (SetupChangeLock)
- {
- SetupWatcher.Interrupt();
- }
- }
- public static void LoadConfig(bool ForceReload = false)
- {
- // 必须包括全部代码,以避免同一时间有多个线程尝试加载和修改值导致线程冲突
- lock (SetupChangeLock)
- {
- // 确保单例
- if (!ForceReload && XmlConfig is not null) return;
- if (File.Exists(Environments.ConfigPath))
- {
- try
- {
- XmlConfig = XDocument.Load(Environments.ConfigPath);
- SetupWatcher = new Thread(StartSetupWatcher);
- SetupWatcher.Start();
- }
- catch (Exception ex)
- {
- XmlConfig = null;
- throw new Exception($"加载配置文件时发生错误", ex);
- }
- }
- else
- {
- XmlConfig = null;
- throw new FileNotFoundException($"配置文件不存在: {Environments.ConfigPath}");
- }
-
- if (File.Exists(Environments.ConfigPath.Replace(".xml", ".xml.backup")))
- {
- try
- {
- XmlBackupConfig = XDocument.Load(Environments.ConfigPath.Replace(".xml", ".xml.backup"));
- }
- catch (Exception ex)
- {
- XmlBackupConfig = null;
- throw new Exception($"加载备份配置文件时发生错误:", ex);
- }
- }
- else
- {
- XmlBackupConfig = null;
- }
- }
- }
-
- private static void StartSetupWatcher()
- {
- try
- {
- while (true)
- {
- // 减轻同步锁造成的性能影响
- Thread.Sleep(10000);
- if (SetupChanged)
- {
- lock (SetupChangeLock)
- {
- XmlConfig.Save(Environments.ConfigPath);
- }
- }
- }
- }catch (ThreadInterruptedException)
- {
- if (SetupChanged)
- {
- lock (SetupChangeLock)
- {
- XmlConfig.Save(Environments.ConfigPath);
- }
- }
- }
- }
-
- ///
- /// 设置命名空间内某个设置项的值
- ///
- /// 设置项名称
- /// 值
- /// 主命名空间
- /// 子命名空间
- public static void Set(string key, string value, string XmlNameSpace = "System", string SubNameSpace = "")
- {
- if (XmlConfig is null) LoadConfig();
- if (XmlConfig?.Root == null)
- {
- throw new InvalidOperationException("配置文件未加载或根节点不存在。");
- }
-
- if (key.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException(nameof(key), "设置项名称不能为空。");
- }
-
- if (XmlNameSpace.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- if (namespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- if (SubNameSpace.IsNullOrWhiteSpaceF())
- {
- XElement keyElement = namespaceElement.Element(key);
- if (keyElement == null)
- {
- lock (SetupChangeLock)
- {
- namespaceElement.Add(new XElement(key, value));
- }
- }
- else
- {
- lock (SetupChangeLock)
- {
- keyElement.Value = value;
- }
- }
- SaveConfig();
- return;
- }
-
- XElement subNamespaceElement = namespaceElement.Element(SubNameSpace);
- if (subNamespaceElement == null)
- {
- lock (SetupChangeLock)
- {
- namespaceElement.Add(new XElement(SubNameSpace, new XElement(key, value)));
- }
- }
- else
- {
- XElement keyElement = subNamespaceElement.Element(key);
- if (keyElement == null)
- {
- lock (SetupChangeLock)
- {
- subNamespaceElement.Add(new XElement(key, value));
- }
- }
- else
- {
- lock (SetupChangeLock)
- {
- keyElement.Value = value;
- }
- }
- }
- SaveConfig();
- }
-
- ///
- /// 获取命名空间内某个设置项的值
- ///
- /// 设置项名称
- /// 主命名空间
- /// 子命名空间
- /// object
- public static object? Get(string key, string XmlNameSpace = "System", string SubNameSpace = "")
- {
- if (XmlConfig is null) LoadConfig();
- if (XmlConfig?.Root == null)
- {
- throw new InvalidOperationException("配置文件未加载或根节点不存在。");
- }
-
- if (key.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException(nameof(key), "设置项名称不能为空。");
- }
-
- if (XmlNameSpace.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- if (namespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- if (SubNameSpace.IsNullOrWhiteSpaceF())
- {
- return namespaceElement.Element(key)?.Value;
- }
-
- XElement subNamespaceElement = namespaceElement.Element(SubNameSpace);
- if (subNamespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace));
- }
-
- return subNamespaceElement.Element(key)?.Value;
- }
-
- ///
- /// 重置某个设置项
- ///
- /// 设置项名称
- /// 主命名空间
- /// 子命名空间
- public static void Reset(string key, string XmlNameSpace = "System", string SubNameSpace = "")
- {
- if (XmlConfig is null) LoadConfig();
- if (XmlConfig?.Root == null)
- {
- throw new InvalidOperationException("配置文件未加载或根节点不存在。");
- }
-
- if (key.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException(nameof(key), "设置项名称不能为空。");
- }
-
- if (XmlNameSpace.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- if (namespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- if (SubNameSpace.IsNullOrWhiteSpaceF())
- {
- if (namespaceElement.Element(key) == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(key));
- }
- namespaceElement.Element(key)?.Remove();
- SaveConfig();
- return;
- }
-
- XElement subNamespaceElement = namespaceElement.Element(SubNameSpace);
- if (subNamespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace));
- }
- if (subNamespaceElement.Element(key) == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(key));
- }
- subNamespaceElement.Element(key)?.Remove();
- SaveConfig();
- }
-
-
- ///
- /// 清空设置项
- ///
- /// 设置项名称
- /// 设置项所处的命名空间
- /// 设置项所处的子命名空间
- public static void Clean(string key, string XmlNameSpace = "System", string SubNameSpace = "")
- {
- Set(key, string.Empty, XmlNameSpace, SubNameSpace);
- }
-
- ///
- /// 创建设置项命名空间
- ///
- /// 主命名空间
- /// 子命名空间
- public static void CreateXmlNameSpace(string XmlNameSpace, string SubNameSpace = "")
- {
- if (XmlConfig is null) LoadConfig();
- if (XmlConfig?.Root == null)
- {
- throw new InvalidOperationException("配置文件未加载或根节点不存在。");
- }
-
- if (XmlNameSpace.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- if (namespaceElement == null)
- {
- XmlConfig.Root.Add(new XElement(XmlNameSpace));
- namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- }
-
- if (!SubNameSpace.IsNullOrWhiteSpaceF() && namespaceElement != null && namespaceElement.Element(SubNameSpace) == null)
- {
- namespaceElement.Add(new XElement(SubNameSpace));
- }
- SaveConfig();
- }
-
- ///
- /// 删除设置项命名空间
- ///
- /// 主命名空间
- /// 子命名空间
- public static void DeleteXmlNameSpace(string XmlNameSpace, string SubNameSpace = "")
- {
- if (XmlConfig is null) LoadConfig();
- if (XmlConfig?.Root == null)
- {
- throw new InvalidOperationException("配置文件未加载或根节点不存在。");
- }
-
- if (XmlNameSpace.IsNullOrWhiteSpaceF())
- {
- throw new ArgumentNullException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
-
- if (SubNameSpace.IsNullOrWhiteSpaceF())
- {
- if (XmlConfig.Root.Element(XmlNameSpace) == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
- XmlConfig.Root.Element(XmlNameSpace)?.Remove();
- }
- else
- {
- XElement namespaceElement = XmlConfig.Root.Element(XmlNameSpace);
- if (namespaceElement == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(XmlNameSpace));
- }
- if (namespaceElement.Element(SubNameSpace) == null)
- {
- throw new ArgumentException("给定关键字不存在于设置项中", nameof(SubNameSpace));
- }
- namespaceElement.Element(SubNameSpace)?.Remove();
- }
- SaveConfig();
- }
-
- private static void SaveConfig()
- {
- try
- {
- if (SetupChanged) return;
- lock (SetupChangeLock)
- {
- SetupChanged = true;
- }
- }
- catch (Exception ex)
- {
- throw new Exception($"保存配置文件时发生错误",ex);
- }
- }
+
}
}
diff --git a/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj b/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj
index 589b982..6901f2e 100644
--- a/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj
+++ b/SuikaiLauncher.Core.Base/SuikaiLauncher.Core.Base.csproj
@@ -4,6 +4,8 @@
net8.0
enable
enable
+ SuikaiProject
+ SuikaiLauncher.Core
diff --git a/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs b/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs
new file mode 100644
index 0000000..ea73fc8
--- /dev/null
+++ b/SuikaiLauncher.Core.Minecraft/Modules/Launch.cs
@@ -0,0 +1,69 @@
+using System.Text;
+using SuikaiLauncher.Core.Account;
+using SuikaiLauncher.Core.Base;
+
+namespace SuikaiLauncher.Core.Minecraft.Modules;
+
+public class Launch
+{
+ public required McVersion? McVersion { get; set; }
+
+ private Profile LaunchProfile = ProfileManager.CurrentProfile;
+
+ public async Task LaunchGame()
+ {
+ Logger.Log("[Minecraft] 获取启动档案成功");
+ Logger.Log("[Minecraft] 启动预检查阶段开始");
+ this.PreCheck();
+ Logger.Log("[Minecraft] 环境预检通过");
+ Task[] LaunchTask =
+ [
+ this.CheckFile(),
+ this.CheckRuntime()
+ ];
+ await Task.WhenAll(LaunchTask);
+ Logger.Log("[Minecraft] ====== 启动信息 ======");
+ Logger.Log($"[Minecraft] 游戏版本:{McVersion.Version}");
+ Logger.Log($"[Minecraft] 游戏用户名:{LaunchProfile.Name}");
+ Logger.Log($"[Minecraft] ");
+ ProcessBuilder PBuilder = ProcessBuilder
+ .Create()
+ .Executable("java.exe")
+ .RequireEncoding(Encoding.UTF8)
+ .WithArgument(await this.GetJvmArgument())
+
+
+
+ }
+ ///
+ /// 检查运行环境是否正确
+ ///
+ public async Task CheckRuntime()
+ {
+
+ }
+ ///
+ /// 预检运行环境和游戏信息
+ ///
+ public void PreCheck()
+ {
+
+ }
+ ///
+ /// 检查文件完整性
+ ///
+ private async Task CheckFile()
+ {
+
+ }
+
+ private async Task GetJvmArgument()
+ {
+ return "";
+ }
+
+ private async Task CrashCallback(TaskCanceledException ex)
+ {
+ Logger.Log("[Minecraft] Minecraft 已崩溃");
+ }
+}
\ No newline at end of file
diff --git a/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj b/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj
index ed07bda..8c1ba44 100644
--- a/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj
+++ b/SuikaiLauncher.Core.Minecraft/SuikaiLauncher.Core.Minecraft.csproj
@@ -7,6 +7,7 @@
+