1827 lines
70 KiB
C#
1827 lines
70 KiB
C#
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<ServerNode> { 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<long> 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<string> { "" };
|
||
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<HostsResponse>(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<bool> CheckAndApplySelfUpdate()
|
||
{
|
||
Console.WriteLine("正在检查启动器更新...");
|
||
try
|
||
{
|
||
string json = await HttpClient.GetStringAsync(LauncherManifestUrl);
|
||
var manifest = JsonSerializer.Deserialize<UpdateManifest>(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<FileNameListResponse>(
|
||
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<string>();
|
||
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<FileLinksResponse>(
|
||
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<string?> 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<int>? 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<long> 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<Config>(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<GamePermissions?> 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<GamePermissions>(
|
||
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<UpdateFile>? Dll { get; set; }
|
||
|
||
[JsonPropertyName("Launcher")]
|
||
public LauncherInfo? Launcher { get; set; }
|
||
|
||
[JsonPropertyName("opt")]
|
||
public List<UpdateFile>? Opt { get; set; }
|
||
|
||
[JsonPropertyName("option")]
|
||
public List<UpdateFile>? Option { get; set; }
|
||
|
||
[JsonPropertyName("file")]
|
||
public List<UpdateFile>? 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<string, FileNodeInfo>? 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<string, FileLinkInfo?>? 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; }
|
||
}
|
||
} |