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; } } }