From ce464007539aab73c5a3f0ca5a285ce7f9d0d710 Mon Sep 17 00:00:00 2001 From: xiaoxin <2932869213@qq.com> Date: Wed, 21 Jan 2026 19:14:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 1827 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1827 insertions(+) create mode 100644 Program.cs diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..49e620c --- /dev/null +++ b/Program.cs @@ -0,0 +1,1827 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Net.Security; +using System.Numerics; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace SinmaiLauncher +{ + public static class AntiDebug + { + private static readonly string[] BadProcessNames = + { + "dnspy", + "dnspy-x86", + "dnspy-x64", + "ilspy", + "dotpeek", + "reflector", + "x64dbg", + "x32dbg", + "ollydbg", + "idaq", + "idaq64", + "idaw", + "idaw64", + "charles", + "fiddler", + "wireshark", + "cheatengine", + }; + + public static void PerformChecks() + { + try + { + if (Debugger.IsAttached) + { + Environment.Exit(0xBAD); + } + if (IsAnyBadProcessRunning()) + { + Environment.Exit(0xDEAD); + } + } + catch (Exception) + { + Environment.Exit(0xE); + } + } + + private static bool IsAnyBadProcessRunning() + { + try + { + Process[] allProcesses = Process.GetProcesses(); + foreach (var process in allProcesses) + { + try + { + string processName = process.ProcessName.ToLowerInvariant(); + foreach (var badName in BadProcessNames) + { + if (processName.Contains(badName)) + { + return true; + } + } + } + catch (Exception) { } + } + } + catch (Exception) { } + return false; + } + } + #endregion + + + public class ServerNode + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public string BaseUrl { get; set; } = ""; + public string Hash { get; set; } = ""; + } + + class Program + { + private static readonly ServerNode NodeBeijing = new ServerNode + { + Id = 1, + Name = "ZheJiang-10Gb", + BaseUrl = "", + Hash = "", + }; + + private static readonly ServerNode NodeTokyo = new ServerNode + { + Id = 2, + Name = "Tokyo-200Mb", + BaseUrl = "", + Hash = "", + }; + + private static readonly ServerNode NodeShanghai = new ServerNode + { + Id = 3, + Name = "Beijing-1Mb", + BaseUrl = "", + Hash = "", + }; + + private static ServerNode CurrentNode = NodeTokyo; + + private enum GameMode + { + Mai, + Chuni, + Ongeki, + Unknown, + } + + private static GameMode CurrentGameMode = GameMode.Unknown; + + private static string ApiVersionPrefix => (CurrentNode != null && CurrentNode.Id == 1) ? "" : ""; + private static string LauncherManifestUrl => $"{CurrentNode.BaseUrl}/{ApiVersionPrefix}/"; + private static string ApiVerificationUrl => $"{CurrentNode.BaseUrl}/{ApiVersionPrefix}/"; + private static string ApiDownloadUrl => $"{CurrentNode.BaseUrl}/{ApiVersionPrefix}/"; + private static string AnnouncementUrl => $"{CurrentNode.BaseUrl}/{ApiVersionPrefix}/"; + + private const string LocalSecretKey = + ""; + private static string ExpectedPublicKeyHash = + ""; + + private static readonly string ExecutablePath = Assembly.GetExecutingAssembly().Location; + private static readonly string LauncherDir = AppContext.BaseDirectory; + private static readonly string GameDir = Path.GetFullPath(Path.Combine(LauncherDir, "..")); + private static readonly string ConfigPath = Path.Combine(LauncherDir, "config.yml"); + + private static readonly string IcfDir = Path.Combine(GameDir, "amfs"); + + private static readonly HttpClient HttpClient = CreateHttpClientWithPinning(); + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + + public static void Main(string[] args) + { + AntiDebug.PerformChecks(); + ServicePointManager.SecurityProtocol = + SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; + MainAsync().GetAwaiter().GetResult(); + } + + private static async Task SelectServerNodeAsync() + { + Console.WriteLine("正在测试节点延迟..."); + Console.WriteLine(); + + var nodes = new List { NodeBeijing, NodeTokyo, NodeShanghai }; + + var pingTasks = nodes.Select(async node => + { + long latency = await PingHostAsync(node); + return (node, latency); + }).ToList(); + + var results = await Task.WhenAll(pingTasks); + + Console.WriteLine("┌─────┬──────────────────────────────┬─────────────┐"); + Console.WriteLine("│ ID │ 节点名称 │ 延迟状态 │"); + Console.WriteLine("├─────┼──────────────────────────────┼─────────────┤"); + + foreach (var (node, latency) in results) + { + string latencyStr; + ConsoleColor color; + + if (latency < 0) + { + latencyStr = "超时"; + color = ConsoleColor.Red; + } + else if (latency < 100) + { + latencyStr = $"{latency} ms"; + color = ConsoleColor.Green; + } + else if (latency < 200) + { + latencyStr = $"{latency} ms"; + color = ConsoleColor.Yellow; + } + else + { + latencyStr = $"{latency} ms"; + color = ConsoleColor.Red; + } + + int nameWidth = GetVisualWidth(node.Name); + int padding = 28 - nameWidth; + string paddedName = node.Name + new string(' ', Math.Max(0, padding)); + + Console.Write("│ "); + Console.Write($"{node.Id,-3}"); + Console.Write(" │ "); + Console.Write(paddedName); + Console.Write(" │ "); + + Console.ForegroundColor = color; + Console.Write($"{latencyStr,-11}"); + Console.ResetColor(); + Console.WriteLine(" │"); + } + Console.WriteLine("└─────┴──────────────────────────────┴─────────────┘"); + Console.WriteLine(" F1 | 国服绿网"); + Console.WriteLine("\n按数字选择节点(默认 2 = Tokyo):"); + + ConsoleKeyInfo key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.F1) + { + await RunGreenNetTool(); + PauseAndExit(); + return; + } + + ServerNode selected = key.Key switch + { + ConsoleKey.D1 => NodeBeijing, + ConsoleKey.NumPad1 => NodeBeijing, + ConsoleKey.D2 => NodeTokyo, + ConsoleKey.NumPad2 => NodeTokyo, + ConsoleKey.D3 => NodeShanghai, + ConsoleKey.NumPad3 => NodeShanghai, + _ => NodeTokyo, + }; + + if (selected.Id == 1) + { + selected.BaseUrl = NodeTokyo.BaseUrl; + selected.Hash = NodeTokyo.Hash; + } + + CurrentNode = selected; + ExpectedPublicKeyHash = selected.Hash; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"\n已选择节点:{selected.Name}"); + Console.ResetColor(); + + await Task.Delay(800); + Console.Clear(); + } + private static int GetVisualWidth(string s) + { + int len = 0; + foreach (var c in s) + { + len += (c < 128) ? 1 : 2; + } + return len; + } + private static async Task PingHostAsync(ServerNode node) + { + try + { + var uri = new Uri(node.BaseUrl); + string host = uri.Host; + if (node.Id == 1) + { + try + { + var sw = Stopwatch.StartNew(); + using (var client = new TcpClient()) + { + var connectTask = client.ConnectAsync(host, 443); + var timeoutTask = Task.Delay(5000); + + if (await Task.WhenAny(connectTask, timeoutTask) == connectTask) + { + await connectTask; + if (client.Connected) + { + sw.Stop(); + return sw.ElapsedMilliseconds; + } + } + } + } + catch + { + return -1; + } + return -1; + } + + using (var ping = new Ping()) + { + PingReply reply = await ping.SendPingAsync(host, 5000); + if (reply.Status == IPStatus.Success) + { + return reply.RoundtripTime; + } + } + } + catch { } + return -1; + } + + static async Task MainAsync() + { + Console.ForegroundColor = ConsoleColor.White; + Console.Clear(); + string currentVersion = + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "N/A"; + + DetectGameMode(); + await SelectServerNodeAsync(); + if (CurrentGameMode == GameMode.Unknown) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("baka~baka~"); + Console.WriteLine("工具放错位置了哦"); + Console.ResetColor(); + PauseAndExit(); + return; + } + + string gameModeName; + switch (CurrentGameMode) + { + case GameMode.Mai: + gameModeName = "maimai"; + break; + case GameMode.Chuni: + gameModeName = "CHUNITHM"; + break; + case GameMode.Ongeki: + gameModeName = "ONGEKI Re:Fresh"; + break; + default: + gameModeName = "未知"; + break; + } + Console.WriteLine($"当前运行游戏: {gameModeName}"); + + string gameId = GetGameIdString(CurrentGameMode); + if (gameId == "SDEZ") + { + Console.WriteLine("当前运行游戏SDEZ喵"); + } + else if (gameId == "SDGB") + { + Console.WriteLine("当前运行游戏SDGB喵"); + } + + Console.Title = $"启动器 v{currentVersion} - [{gameModeName}] - [{gameId}]"; + + if (await CheckAndApplySelfUpdate()) + { + return; + } + await ShowAnnouncement(); + + try + { + Config cfg = LoadOrCreateConfig(); + Console.WriteLine("正在验证机器码ing..."); + + GamePermissions? permissions = await VerifyApiAsync(cfg.Mc); + + if (permissions == null) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("啊嘞?机器码或通信验证失败惹"); + Console.WriteLine("请联系群主授权喵"); + Console.ResetColor(); + PauseAndExit(); + return; + } + + Console.WriteLine("验证成功"); + + bool hasPermission = false; + switch (gameId) + { + case "SDGB": + hasPermission = permissions.SDGB; + break; + case "SDEZ": + hasPermission = permissions.SDEZ; + break; + case "SDHD": + hasPermission = permissions.SDHD; + break; + case "SDDT": + hasPermission = permissions.SDDT; + break; + } + + if (!hasPermission) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine( + $"啊哦,您的账户没有 [{gameId}] ({gameModeName}) 的游戏权限喵" + ); + Console.WriteLine("请联系群主开通哦"); + Console.ResetColor(); + PauseAndExit(); + return; + } + + await CheckAndApplyGameUpdates(cfg.Mc, gameId); + Console.WriteLine("\n正在启动游戏..."); + StartGame(cfg); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("\n启动器运行期间发生严重错误"); + LogExceptionDetails(ex); + Console.ResetColor(); + PauseAndExit(); + } + } + + private static async Task RunGreenNetTool() + { + Console.Clear(); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("=== 国服绿网工具 ==="); + Console.ResetColor(); + + Config cfg = LoadOrCreateConfig(); + Console.WriteLine("正在验证权限..."); + + try + { + GamePermissions? permissions = await VerifyApiAsync(cfg.Mc); + if (permissions == null || !permissions.LV) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("权限验证失败喵"); + Console.WriteLine("5r喵~需要请联系群主喵"); + Console.ResetColor(); + Console.WriteLine("\n按任意键返回..."); + Console.ReadKey(true); + return; + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"验证过程出错: {ex.Message}"); + Console.ResetColor(); + Console.WriteLine("\n按任意键返回..."); + Console.ReadKey(true); + return; + } + + Console.WriteLine("权限验证通过,开始修改 hosts..."); + try + { + await ModifyHostsFile(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\n操作成功完成!"); + Console.WriteLine("请重启游戏或电脑以确保生效。"); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n操作失败: {ex.Message}"); + if (ex is UnauthorizedAccessException) + { + Console.WriteLine("请尝试以管理员身份运行此程序。"); + } + Console.ResetColor(); + } + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(true); + } + + private static async Task ModifyHostsFile() + { + Config cfg = LoadOrCreateConfig(); + string hostsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @""); + + if (!File.Exists(hostsPath)) + { + throw new FileNotFoundException("未找到系统 hosts 文件", hostsPath); + } + var lines = File.ReadAllLines(hostsPath).ToList(); + var domainsToRemove = new HashSet { "" }; + int removedCount = lines.RemoveAll(line => + { + if (string.IsNullOrWhiteSpace(line)) return false; + if (line.TrimStart().StartsWith("#")) return false; + + foreach (var domain in domainsToRemove) + { + if (line.Contains(domain)) return true; + } + return false; + }); + + Console.WriteLine($"已清理 {removedCount} 条旧记录"); + Console.WriteLine($"正在从服务器获取最新数据..."); + + string localR2f = GenerateLocalR2FCode(cfg.Mc); + var hostsRequestPayload = new + { + r2f_code = localR2f, + request_hosts = true, + mc = cfg.Mc + }; + + string jsonPayload = JsonSerializer.Serialize(hostsRequestPayload); + string encryptedPayload = AuthCipher.Encrypt(jsonPayload, cfg.Mc); + + var requestBody = new + { + mc = cfg.Mc, + payload = encryptedPayload + }; + + var content = new StringContent( + JsonSerializer.Serialize(requestBody), + Encoding.UTF8, + "application/json" + ); + + string newData = ""; + try + { + HttpResponseMessage response = await HttpClient.PostAsync(ApiDownloadUrl, content); + response.EnsureSuccessStatusCode(); + string encryptedResponse = await response.Content.ReadAsStringAsync(); + string decryptedJson = AuthCipher.Decrypt(encryptedResponse, cfg.Mc); + if (decryptedJson == null) + { + try + { + using (JsonDocument doc = JsonDocument.Parse(encryptedResponse)) + { + if (doc.RootElement.TryGetProperty("message", out JsonElement msg)) + { + throw new Exception($"服务器返回错误: {msg.GetString()}"); + } + } + } + catch (JsonException) { } + throw new Exception("解密服务器响应失败"); + } + HostsResponse? hostsResp = JsonSerializer.Deserialize(decryptedJson, JsonOptions); + if (hostsResp == null || !hostsResp.Success) + { + throw new Exception(hostsResp?.Message ?? "未知错误"); + } + newData = hostsResp.HostsContent ?? ""; + } + catch (Exception ex) + { + throw new Exception($"获取 hosts 数据失败: {ex.Message}"); + } + var newLines = newData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + int addedCount = 0; + foreach (var line in newLines) + { + var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) + { + string p1 = parts[0]; + string p2 = parts[1]; + string ip = ""; + string domain = ""; + if (IPAddress.TryParse(p2, out _)) + { + domain = p1; + ip = p2; + } + else if (IPAddress.TryParse(p1, out _)) + { + ip = p1; + domain = p2; + } + if (!string.IsNullOrEmpty(ip) && !string.IsNullOrEmpty(domain)) + { + lines.Add($"{ip} {domain}"); + addedCount++; + } + } + } + if (addedCount == 0) + { + Console.WriteLine("警告: 未能从服务器数据中解析出有效的 hosts 记录"); + } + File.WriteAllLines(hostsPath, lines); + Console.WriteLine("hosts 文件写入完成"); + try + { + Process.Start(new ProcessStartInfo + { + FileName = "ipconfig", + Arguments = "/flushdns", + CreateNoWindow = true, + UseShellExecute = false + })?.WaitForExit(); + Console.WriteLine("DNS 缓存已刷新"); + } + catch { } + } + private static async Task ShowAnnouncement() + { + string lastGgPath = Path.Combine(LauncherDir, ""); + try + { + Console.WriteLine("\n正在获取公告信息..."); + string announcement = await HttpClient.GetStringAsync(AnnouncementUrl); + announcement = announcement.Trim(); + if (string.IsNullOrWhiteSpace(announcement)) + return; + string last = ""; + if (File.Exists(lastGgPath)) + last = File.ReadAllText(lastGgPath).Trim(); + if (last == announcement) + return; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("\n" + new string('-', 70)); + Console.WriteLine("公告"); + Console.WriteLine(new string('-', 70)); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(announcement); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(new string('-', 70)); + Console.ResetColor(); + Console.WriteLine("\n按任意键继续..."); + Console.ReadKey(); + Console.Clear(); + File.WriteAllText(lastGgPath, announcement); + } + catch + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("无法获取公告信息,跳过显示"); + Console.ResetColor(); + } + } + private static HttpClient CreateHttpClientWithPinning() + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => + { + if (CurrentNode != null && CurrentNode.Id == 3) + { + return true; + } + if (CurrentNode != null && request != null) + { + try + { + string requestHost = request.RequestUri.Host; + string apiHost = new Uri(CurrentNode.BaseUrl).Host; + if (!string.Equals(requestHost, apiHost, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + catch { } + } + if (errors != SslPolicyErrors.None || cert == null) + { + Console.WriteLine($"SSL错误: {errors}"); + return false; + } + try + { + using var sha256 = SHA256.Create(); + var publicKeyHash = sha256.ComputeHash(cert.GetPublicKey()); + var hashString = BitConverter + .ToString(publicKeyHash) + .Replace("-", "") + .ToLowerInvariant(); + if (hashString == ExpectedPublicKeyHash) + { + return true; + } + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n[SSL 验证失败] 证书 Hash 不匹配!"); + Console.ResetColor(); + return false; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[SSL 验证异常]: {ex.Message}"); + Console.ResetColor(); + return false; + } + }, + }; + var client = new HttpClient(handler); + client.DefaultRequestHeaders.UserAgent.ParseAdd("xiaoxin/baka"); + return client; + } + private static async Task CheckAndApplySelfUpdate() + { + Console.WriteLine("正在检查启动器更新..."); + try + { + string json = await HttpClient.GetStringAsync(LauncherManifestUrl); + var manifest = JsonSerializer.Deserialize(json, JsonOptions); + if ( + manifest?.Launcher == null + || string.IsNullOrEmpty(manifest.Launcher.Name) + || string.IsNullOrEmpty(manifest.Launcher.Hash) + || string.IsNullOrEmpty(manifest.Launcher.Url) + ) + { + Console.WriteLine("启动器清单文件中缺少信息"); + return false; + } + string fileToCheck = manifest.Launcher.Name; + string serverHash = manifest.Launcher.Hash; + string localFilePath = Path.Combine(LauncherDir, fileToCheck); + Console.WriteLine($" -> 正在校验本地核心文件: {localFilePath}"); + bool needsUpdate = false; + if (!File.Exists(localFilePath)) + { + needsUpdate = true; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(" -> 状态: 核心文件不存在,需要更新"); + Console.ResetColor(); + } + else if (!VerifyFileHash(localFilePath, serverHash)) + { + needsUpdate = true; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(" -> 状态: 有新版本啦~"); + Console.ResetColor(); + } + if (needsUpdate) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("正在准备下载ing..."); + Console.ResetColor(); + await ApplySelfUpdate(manifest.Launcher); + return true; + } + else + { + Console.WriteLine("已经是最新版了哦"); + return false; + } + } + catch (HttpRequestException ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("无法连接到更新服务器,请检查网络配置"); + LogExceptionDetails(ex); + Console.ResetColor(); + PauseAndExit(); + return false; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"呜哇!检查更新时发生错误惹:"); + LogExceptionDetails(ex); + Console.ResetColor(); + return false; + } + } + private static async Task ApplySelfUpdate(LauncherInfo newLauncher) + { + string batPath = Path.Combine(LauncherDir, "updater.bat"); + string downloadPath = Path.Combine(LauncherDir, "update_new.dll"); + + try + { + await DownloadFileAsync( + newLauncher.Url, + downloadPath, + "新版核心 update.dll", + newLauncher.Hash + ); + string batchContent = + @""; + File.WriteAllText(batPath, batchContent, Encoding.Default); + + Process.Start( + new ProcessStartInfo(batPath) { CreateNoWindow = true, UseShellExecute = false } + ); + Environment.Exit(0); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("自动更新失败"); + LogExceptionDetails(ex); + Console.ResetColor(); + PauseAndExit(); + } + } + private static async Task CheckAndApplyGameUpdates(string mc, string gameId) + { + if (string.IsNullOrEmpty(gameId) || gameId == "unknown") + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("内部错误:无法识别的游戏ID,无法更新。"); + Console.ResetColor(); + return; + } + try + { + string localR2f = GenerateLocalR2FCode(mc); + var treeRequestPayload = new { r2f_code = localR2f, request_tree = gameId }; + string decryptedTreeJson = await PostToDownloadApiAsync(mc, treeRequestPayload); + if (string.IsNullOrEmpty(decryptedTreeJson)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("获取文件列表失败"); + Console.ResetColor(); + return; + } + var treeResponse = JsonSerializer.Deserialize( + decryptedTreeJson, + JsonOptions + ); + if (treeResponse == null || !treeResponse.Success || treeResponse.FileTree == null) + { + Console.ForegroundColor = ConsoleColor.Red; + if ( + treeResponse?.Message != null + && treeResponse.Message.Contains("permission denied") + ) + { + Console.WriteLine("获取文件列表失败: 游戏权限被服务器拒绝。"); + } + else + { + Console.WriteLine( + "获取文件列表失败: " + (treeResponse?.Message ?? "未知错误") + ); + } + Console.ResetColor(); + return; + } + Console.WriteLine( + $"成功获取到 {treeResponse.FileTree.Count} 个文件信息,正在比对本地文件..." + ); + var filesToRequestBag = new ConcurrentBag(); + int totalFiles = treeResponse.FileTree.Count; + int processedFiles = 0; + try { Console.CursorVisible = false; } catch { } + Parallel.ForEach(treeResponse.FileTree, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }, entry => + { + string serverFileName = entry.Key; + FileNodeInfo? nodeInfo = entry.Value; + string? serverHash = nodeInfo?.Hash; + string? category = nodeInfo?.Category; + if (!string.IsNullOrEmpty(serverFileName)) + { + string localPath = GetLocalPathForFile(serverFileName, category); + bool needsUpdate = false; + if (!File.Exists(localPath)) + { + needsUpdate = true; + } + else if (!string.IsNullOrEmpty(serverHash)) + { + string localHash = GetFileHash(localPath); + if (!localHash.Equals(serverHash, StringComparison.OrdinalIgnoreCase)) + { + needsUpdate = true; + } + } + if (needsUpdate) + { + filesToRequestBag.Add(serverFileName); + } + } + int current = Interlocked.Increment(ref processedFiles); + if (current % 5 == 0 || current == totalFiles) + { + Console.Write($"\r -> 校验进度: {current}/{totalFiles} ({(int)((float)current / totalFiles * 100)}%)"); + } + }); + try { Console.CursorVisible = true; } catch { } + Console.WriteLine(); + var filesToRequest = filesToRequestBag.ToList(); + if (filesToRequest.Count == 0) + { + Console.WriteLine("\n所有必要的游戏文件都存在哦"); + return; + } + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine( + $"\n发现 {filesToRequest.Count} 个本地缺失或需要更新的文件,正在请求下载链接..." + ); + Console.ResetColor(); + localR2f = GenerateLocalR2FCode(mc); + var linksRequestPayload = new + { + r2f_code = localR2f, + request_files = new { game = gameId, files = filesToRequest }, + }; + string decryptedLinksJson = await PostToDownloadApiAsync(mc, linksRequestPayload); + if (string.IsNullOrEmpty(decryptedLinksJson)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("获取下载链接失败"); + Console.ResetColor(); + return; + } + var linksResponse = JsonSerializer.Deserialize( + decryptedLinksJson, + JsonOptions + ); + if ( + linksResponse == null + || !linksResponse.Success + || linksResponse.FileLinks == null + ) + { + Console.ForegroundColor = ConsoleColor.Red; + if ( + linksResponse?.Message != null + && linksResponse.Message.Contains("permission denied") + ) + { + Console.WriteLine("获取下载链接失败: 游戏权限被服务器拒绝。"); + } + else + { + Console.WriteLine( + "获取下载链接失败: " + (linksResponse?.Message ?? "未知错误") + ); + } + Console.ResetColor(); + return; + } + long totalSize = 0; + long totalDownloaded = 0; + Console.WriteLine("正在计算总下载大小..."); + var sizeTasks = linksResponse.FileLinks.Select(async kvp => + { + if(kvp.Value?.Url != null) + { + long size = await GetFileSizeAsync(kvp.Value.Url); + Interlocked.Add(ref totalSize, size); + } + }); + await Task.WhenAll(sizeTasks); + Console.WriteLine($"总计大小: {FormatBytes(totalSize)},开始下载..."); + try { Console.CursorVisible = false; } catch { } + var stopwatch = Stopwatch.StartNew(); + int updatedCount = 0; + foreach (var entry in linksResponse.FileLinks) + { + string fileName = entry.Key; + FileLinkInfo? linkInfo = entry.Value; + string? category = null; + if (treeResponse.FileTree.TryGetValue(fileName, out var originalNode)) + { + category = originalNode.Category; + } + string localPath = GetLocalPathForFile(fileName, category); + if (linkInfo == null || string.IsNullOrEmpty(linkInfo.Url)) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" -> 服务器未返回文件 {fileName} 的有效 URL,跳过"); + Console.ResetColor(); + continue; + } + string? expectedHash = linkInfo.Hash; + if (string.IsNullOrEmpty(expectedHash) && originalNode != null) + { + expectedHash = originalNode.Hash; + } + try + { + await DownloadFileAsync(linkInfo.Url, localPath, fileName, expectedHash, (readBytes) => + { + long newTotal = Interlocked.Add(ref totalDownloaded, readBytes); + DrawGlobalProgressBar(newTotal, totalSize, stopwatch.Elapsed); + }); + updatedCount++; + } + catch (Exception ex) + { + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" -> 下载 {fileName} 失败: {ex.Message}"); + Console.ResetColor(); + } + } + try { Console.CursorVisible = true; } catch { } + Console.WriteLine(); + Console.WriteLine( + $"\n游戏文件更新完成,总计下载了 {updatedCount} 个缺失文件呢,哼哼ᗜˬᗜ" + ); + } + catch (JsonException jsonEx) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"解析服务器响应时出错 (可能是通信密钥不同步): {jsonEx.Message}"); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("检查游戏更新时发生严重错误QAQ"); + LogExceptionDetails(ex); + Console.ResetColor(); + } + } + private static string GetLocalPathForFile(string name, string? category) + { + string relativePath = name.Replace('/', Path.DirectorySeparatorChar); + + if (string.IsNullOrEmpty(category)) + { + return Path.Combine(GameDir, relativePath); + } + switch (category.ToLowerInvariant()) + { + case "option": + return Path.Combine(GameDir, "option", relativePath); + case "file": + return Path.Combine(GameDir, relativePath); + case "amfs": + return Path.Combine(GameDir, "amfs", relativePath); + case "dll": + return Path.Combine(GameDir, "Sinmai_Data", "Managed", relativePath); + case "opt": + return Path.Combine(GameDir, "Sinmai_Data", "StreamingAssets", relativePath); + default: + return Path.Combine(GameDir, relativePath); + } + } + private static async Task PostToDownloadApiAsync(string mc, object payloadObject) + { + try + { + string payloadJson = JsonSerializer.Serialize(payloadObject); + string encryptedPayload = AuthCipher.Encrypt(payloadJson, mc); + var apiRequest = new ApiAuthRequest { Mc = mc, Payload = encryptedPayload }; + string jsonRequest = JsonSerializer.Serialize(apiRequest); + var httpContent = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); + var response = await HttpClient.PostAsync(ApiDownloadUrl, httpContent); + response.EnsureSuccessStatusCode(); + string encryptedResponse = await response.Content.ReadAsStringAsync(); + string decryptedJson = AuthCipher.Decrypt(encryptedResponse, mc); + return decryptedJson; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[Download API] 请求失败: {ex.Message}"); + Console.ResetColor(); + return null; + } + } + private static async Task DownloadFileAsync( + string? url, + string localPath, + string? displayName, + string? expectedHash, + Action? progressCallback = null + ) + { + if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(displayName)) + { + return; + } + bool quiet = progressCallback != null; + if (!quiet) Console.WriteLine($" -> 准备下载: {displayName}"); + string? directory = Path.GetDirectoryName(localPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + try + { + using var response = await HttpClient.GetAsync( + url, + HttpCompletionOption.ResponseHeadersRead + ); + response.EnsureSuccessStatusCode(); + using var stream = await response.Content.ReadAsStreamAsync(); + Stopwatch? sw = quiet ? null : Stopwatch.StartNew(); + using ( + var fileStream = new FileStream( + localPath, + FileMode.Create, + FileAccess.Write, + FileShare.None + ) + ) + { + byte[] buffer = new byte[8192]; + long totalBytes = response.Content.Headers.ContentLength ?? -1; + long totalRead = 0; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await fileStream.WriteAsync(buffer, 0, bytesRead); + totalRead += bytesRead; + if (quiet) + { + progressCallback?.Invoke(bytesRead); + } + else if (totalBytes > 0) + { + var percentage = (int)((double)totalRead * 100 / totalBytes); + Console.Write( + $"\r -> 正在下载... {percentage}% ({totalRead / 1024 / 1024}MB / {totalBytes / 1024 / 1024}MB)" + ); + } + } + } + sw?.Stop(); + if (!quiet) + { + Console.WriteLine( + $"\r -> 下载完成: {displayName} ({sw?.Elapsed.TotalSeconds:F2}s) " + ); + } + if (!string.IsNullOrEmpty(expectedHash)) + { + if (!VerifyFileHash(localPath, expectedHash, false)) + { + throw new Exception("文件校验失败,可能已损坏惹QAQ"); + } + if (!quiet) Console.WriteLine(" -> 文件校验成功"); + } + else + {} + } + catch (Exception ex) + { + if (!quiet) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n -> 下载 {displayName} 时发生错误QAQ: {ex.Message}"); + Console.ResetColor(); + } + if (File.Exists(localPath)) + { + try + { + File.Delete(localPath); + } + catch { } + } + throw; + } + } + private static async Task GetFileSizeAsync(string url) + { + try + { + using var request = new HttpRequestMessage(HttpMethod.Head, url); + using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + if (response.IsSuccessStatusCode) + { + return response.Content.Headers.ContentLength ?? 0; + } + } + catch { } + return 0; + } + private static void DrawGlobalProgressBar(long currentBytes, long totalBytes, TimeSpan elapsed) + { + if (totalBytes <= 0) return; + double progress = (double)currentBytes / totalBytes; + progress = Math.Min(1.0, Math.Max(0.0, progress)); + int barWidth = 30; + int filledChars = (int)(progress * barWidth); + double seconds = elapsed.TotalSeconds; + string speedStr = "0 B/s"; + if (seconds > 0) + { + double speed = currentBytes / seconds; + speedStr = $"{FormatBytes((long)speed)}/s"; + } + string bar = new string('=', filledChars) + new string(' ', barWidth - filledChars); + if (filledChars > 0 && filledChars < barWidth) + { + bar = bar.Remove(filledChars - 1, 1).Insert(filledChars - 1, ">"); + } + else if (filledChars == barWidth) + { + bar = new string('=', barWidth); + } + string arrowBar = new string('=', filledChars); + if (filledChars < barWidth) arrowBar += ">"; + string barDisplay = arrowBar.PadRight(barWidth + 1); + Console.Write($"\r{barDisplay} {FormatBytes(currentBytes)}/{FormatBytes(totalBytes)} {speedStr} "); + } + private static string FormatBytes(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB", "TB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + private static bool VerifyFileHash( + string filePath, + string? expectedHash, + bool quiet = false + ) + { + if (string.IsNullOrEmpty(expectedHash)) + { + return true; + } + if (expectedHash.StartsWith("在此填入")) + { + if (!quiet) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" -> 跳过校验: {Path.GetFileName(filePath)}"); + Console.ResetColor(); + } + return true; + } + string actualHash = GetFileHash(filePath); + bool isMatch = actualHash.Equals(expectedHash, StringComparison.OrdinalIgnoreCase); + if (!isMatch && !quiet) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($" -> 文件不匹配!"); + Console.WriteLine($" 本地: {actualHash}"); + Console.ResetColor(); + } + return isMatch; + } + private static Config LoadOrCreateConfig() + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + var serializer = new SerializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + Config cfg; + string newMc = GenerateMachineCode(); + if (File.Exists(ConfigPath)) + { + string yaml = File.ReadAllText(ConfigPath); + cfg = deserializer.Deserialize(yaml); + bool configUpdated = false; + if (cfg.Mc != newMc) + { + cfg.Mc = newMc; + configUpdated = true; + } + if (cfg.MaiArgs == null) + { + cfg.MaiArgs = + ""; + configUpdated = true; + } + if (configUpdated) + { + string newYaml = serializer.Serialize(cfg); + File.WriteAllText(ConfigPath, newYaml); + } + } + else + { + cfg = new Config + { + MaiArgs = + "", + Mc = newMc, + }; + string yaml = serializer.Serialize(cfg); + File.WriteAllText(ConfigPath, yaml); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("首次运行:已在启动器目录创建 config.yml"); + Console.WriteLine("请将您的机器码提供给猫猫进行授权"); + Console.WriteLine("价格请看Q群公告"); + Console.WriteLine("机器码:"); + Console.ResetColor(); + Console.WriteLine(cfg.Mc); + PauseAndExit(); + } + return cfg; + } + private static string GenerateMachineCode() + { + try + { + string boardSerial = ""; + string cpuId = ""; + using ( + var searcher = new ManagementObjectSearcher( + "SELECT SerialNumber FROM Win32_BaseBoard" + ) + ) + { + foreach (var obj in searcher.Get()) + { + boardSerial = obj["SerialNumber"]?.ToString() ?? ""; + break; + } + } + using ( + var searcher = new ManagementObjectSearcher( + "SELECT ProcessorId FROM Win32_Processor" + ) + ) + { + foreach (var obj in searcher.Get()) + { + cpuId = obj["ProcessorId"]?.ToString() ?? ""; + break; + } + } + string combined = boardSerial + cpuId; + using var sha256 = SHA256.Create(); + byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined)); + string finalMc = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + return finalMc; + } + catch (Exception) + { + return "wmi_error_" + Guid.NewGuid().ToString("N"); + } + } + static async Task VerifyApiAsync(string mc) + { + try + { + string url = ApiVerificationUrl; + string staticPayload = ""; + string encryptedPayload = AuthCipher.Encrypt(staticPayload, mc); + var requestData = new ApiAuthRequest { Mc = mc, Payload = encryptedPayload }; + string jsonRequest = JsonSerializer.Serialize(requestData); + var httpContent = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); + + var response = await HttpClient.PostAsync(url, httpContent); + response.EnsureSuccessStatusCode(); + string encryptedResponse = await response.Content.ReadAsStringAsync(); + string decryptedJson = AuthCipher.Decrypt(encryptedResponse, mc); + var jsonDoc = JsonDocument.Parse( + decryptedJson, + new JsonDocumentOptions { AllowTrailingCommas = true } + ); + var root = jsonDoc.RootElement; + if ( + !root.TryGetProperty("success", out var successProp) + || !successProp.GetBoolean() + ) + { + return null; + } + if ( + !root.TryGetProperty("r2f_code", out var r2fCodeProp) + || r2fCodeProp.ValueKind != JsonValueKind.String + ) + { + return null; + } + string serverR2fCode = r2fCodeProp.GetString() ?? ""; + bool isR2fValid = false; + for (int offset = -1; offset <= 1; offset++) + { + string localR2fCode = GenerateLocalR2FCode(mc, offset); + if (serverR2fCode == localR2fCode) + { + isR2fValid = true; + break; + } + } + if (!isR2fValid) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("错误: 认证码校验失败 (可能是本地时间与服务器差异过大)"); + Console.ResetColor(); + return null; + } + if (!root.TryGetProperty("permissions", out var permsProp)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("警告: 验证服务器未返回权限信息"); + Console.ResetColor(); + return null; + } + var permissions = JsonSerializer.Deserialize( + permsProp.GetRawText(), + JsonOptions + ); + if (permissions == null) + { + return null; + } + return permissions; + } + catch (HttpRequestException) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("API 验证请求失败喵,请检查网络配置"); + Console.ResetColor(); + return null; + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"API 验证期间发生错误喵"); + Console.ResetColor(); + return null; + } + } + private static string GenerateLocalR2FCode(string machineCode, int timeStepOffset = 0) + { + var key = Encoding.UTF8.GetBytes(LocalSecretKey); + using var hmac = new HMACSHA1(key); + long timeStep = (DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 30) + timeStepOffset; + var timeStepBytes = BitConverter.GetBytes(timeStep); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(timeStepBytes); + } + var machineCodeBytes = Encoding.UTF8.GetBytes(machineCode); + var combinedBytes = new byte[timeStepBytes.Length + machineCodeBytes.Length]; + Buffer.BlockCopy(timeStepBytes, 0, combinedBytes, 0, timeStepBytes.Length); + Buffer.BlockCopy( + machineCodeBytes, + 0, + combinedBytes, + timeStepBytes.Length, + machineCodeBytes.Length + ); + var hash = hmac.ComputeHash(combinedBytes); + int offset = hash[19] & 0x0A; + var chunk = new byte[10]; + Buffer.BlockCopy(hash, offset, chunk, 0, 10); + Array.Reverse(chunk); + var positiveChunk = new byte[chunk.Length + 1]; + Buffer.BlockCopy(chunk, 0, positiveChunk, 0, chunk.Length); + positiveChunk[chunk.Length] = 0x00; + var binary = new BigInteger(positiveChunk); + var modulo = BigInteger.Pow(10, 20); + var otp = BigInteger.Remainder(binary, modulo); + if (otp.Sign < 0) + { + otp += modulo; + } + string finalOtp = otp.ToString(); + return finalOtp; + } + static void StartGame(Config cfg) + { + try + { + string batPath = Path.Combine(GameDir, "start_temp.bat"); + string batchContent; + + if (CurrentGameMode == GameMode.Chuni) + { + batchContent = + $@""; + } + else if (CurrentGameMode == GameMode.Ongeki) + { + batchContent = + $@""; + } + else + { + batchContent = + $@""; + } + File.WriteAllText(batPath, batchContent, Encoding.Default); + var psi = new ProcessStartInfo(batPath) + { + WorkingDirectory = GameDir, + UseShellExecute = true, + }; + Process.Start(psi); + Environment.Exit(0); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("启动游戏时发生错误"); + LogExceptionDetails(ex); + Console.ResetColor(); + } + } + private static void DetectGameMode() + { + if (File.Exists(Path.Combine(GameDir, "chusanhook.dll"))) + { + CurrentGameMode = GameMode.Chuni; + } + else if (File.Exists(Path.Combine(GameDir, "mai2hook.dll"))) + { + CurrentGameMode = GameMode.Mai; + } + else if (File.Exists(Path.Combine(GameDir, "mu3hook.dll"))) + { + CurrentGameMode = GameMode.Ongeki; + } + else + { + CurrentGameMode = GameMode.Unknown; + } + } + private static string GetGameIdString(GameMode mode) + { + switch (mode) + { + case GameMode.Chuni: + return "SDHD"; + case GameMode.Ongeki: + return "SDDT"; + case GameMode.Mai: + string caPemPath = Path.Combine(GameDir, "ca.pem"); + if (File.Exists(caPemPath)) + { + return "SDEZ"; + } + else + { + return "SDGB"; + } + default: + return "unknown"; + } + } + private static string GetFileHash(string filePath) + { + if (!File.Exists(filePath)) + { + return "文件不存在"; + } + try + { + using var sha256 = SHA256.Create(); + using var stream = File.OpenRead(filePath); + byte[] hash = sha256.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + catch (Exception ex) + { + return $"无法计算哈希: {ex.Message}"; + } + } + private static void PauseAndExit() + { + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + Environment.Exit(1); + } + private static void LogExceptionDetails(Exception ex, string indent = "") + { + if (ex == null) + return; + string errorType = $"{indent}类型: {ex.GetType().FullName}"; + string errorMessage = $"{indent}信息: {ex.Message}"; + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine($"{indent}--- 详细错误日志 ---"); + Console.WriteLine(errorType); + Console.WriteLine(errorMessage); + Console.ResetColor(); + if (ex.InnerException != null) + { + LogExceptionDetails(ex.InnerException, indent + " "); + } + } + public static class AuthCipher + { + private const int AesKeySize = 32; + private const int AesIvSize = 16; + + private static byte[] GenerateSessionToken(string mc) + { + try + { + string combinedString = LocalSecretKey + mc; + using (SHA256 sha256 = SHA256.Create()) + { + byte[] combinedBytes = Encoding.UTF8.GetBytes(combinedString); + return sha256.ComputeHash(combinedBytes); + } + } + catch (Exception) + { + return new byte[AesKeySize]; + } + } + public static string Encrypt(string plainText, string mc) + { + byte[] key = GenerateSessionToken(mc); + byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); + using (Aes aes = Aes.Create()) + { + aes.Key = key; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.GenerateIV(); + byte[] iv = aes.IV; + using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, iv)) + using (MemoryStream ms = new MemoryStream()) + { + ms.Write(iv, 0, iv.Length); + using ( + CryptoStream cs = new CryptoStream( + ms, + encryptor, + CryptoStreamMode.Write + ) + ) + { + cs.Write(plainBytes, 0, plainBytes.Length); + cs.FlushFinalBlock(); + } + return Convert.ToBase64String(ms.ToArray()); + } + } + } + public static string Decrypt(string cipherTextBase64, string mc) + { + byte[] key = GenerateSessionToken(mc); + byte[] combinedBytes = Convert.FromBase64String(cipherTextBase64); + if (combinedBytes.Length < AesIvSize) + { + throw new CryptographicException("无效"); + } + using (Aes aes = Aes.Create()) + { + aes.Key = key; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + byte[] iv = new byte[AesIvSize]; + Buffer.BlockCopy(combinedBytes, 0, iv, 0, iv.Length); + aes.IV = iv; + int cipherBytesLength = combinedBytes.Length - AesIvSize; + byte[] cipherBytes = new byte[cipherBytesLength]; + Buffer.BlockCopy(combinedBytes, AesIvSize, cipherBytes, 0, cipherBytesLength); + using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) + using (MemoryStream msDecrypt = new MemoryStream(cipherBytes)) + using ( + CryptoStream csDecrypt = new CryptoStream( + msDecrypt, + decryptor, + CryptoStreamMode.Read + ) + ) + using (StreamReader srDecrypt = new StreamReader(csDecrypt)) + { + return srDecrypt.ReadToEnd(); + } + } + } + } + } + public class Config + { + [YamlMember(Alias = "sinmaiargs")] + public string MaiArgs { get; set; } = ""; + + [YamlMember(Alias = "mc")] + public string Mc { get; set; } = ""; + public int server_line { get; set; } = 2; + } + public class UpdateManifest + { + [JsonPropertyName("dll")] + public List? Dll { get; set; } + + [JsonPropertyName("Launcher")] + public LauncherInfo? Launcher { get; set; } + + [JsonPropertyName("opt")] + public List? Opt { get; set; } + + [JsonPropertyName("option")] + public List? Option { get; set; } + + [JsonPropertyName("file")] + public List? File { get; set; } + + [JsonPropertyName("amfs")] + public JsonElement Amfs { get; set; } + } + public class LauncherInfo + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("hash")] + public string? Hash { get; set; } + } + public class UpdateFile + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("hash")] + public string? Hash { get; set; } + } + public class ApiAuthRequest + { + [JsonPropertyName("mc")] + public string Mc { get; set; } = ""; + + [JsonPropertyName("payload")] + public string Payload { get; set; } = ""; + } + + public class FileNameListResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("file_tree")] + public Dictionary? FileTree { get; set; } + } + + public class FileNodeInfo + { + [JsonPropertyName("hash")] + public string? Hash { get; set; } + + [JsonPropertyName("category")] + public string? Category { get; set; } + } + + public class HostsResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("hosts_content")] + public string? HostsContent { get; set; } + } + + public class FileLinksResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("file_links")] + public Dictionary? FileLinks { get; set; } + } + + public class FileLinkInfo + { + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("hash")] + public string? Hash { get; set; } + } + + public class GamePermissions + { + [JsonPropertyName("SDGB")] + public bool SDGB { get; set; } + + [JsonPropertyName("SDEZ")] + public bool SDEZ { get; set; } + + [JsonPropertyName("SDHD")] + public bool SDHD { get; set; } + + [JsonPropertyName("SDDT")] + public bool SDDT { get; set; } + + [JsonPropertyName("lv")] + public bool LV { get; set; } + } +} \ No newline at end of file