MainForm.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. using Microsoft.Extensions.Logging.Console;
  2. using MySql.Data.MySqlClient;
  3. using SinMaiLauncher.ChildProcessHolder;
  4. using SinMaiLauncher.Interops;
  5. using SinMaiLauncher.Models;
  6. namespace SinMaiLauncher;
  7. public partial class MainForm : Form
  8. {
  9. private ILogger logger;
  10. private readonly IEnumerator<string> txtAni = TextAnimation.LoadingTextSeq(8).GetEnumerator();
  11. private SettingReader setting;
  12. private Dictionary<ChildProcessKind, ChildProcessControlGroup> childProcessControlGroups;
  13. private string mainControlStatusText = "摸鱼中";
  14. private bool mainControlStatusTextAnimation = false;
  15. public MainForm()
  16. {
  17. InitializeComponent();
  18. }
  19. private async void MainForm_Shown(object sender, EventArgs e)
  20. {
  21. TopMost = false;
  22. TopMost = true;
  23. tcMain.SelectedTab = tpConsoles;
  24. ConsoleInterops.CurrentProcess.ConsoleAllocate();
  25. ConsoleInterops.CurrentProcess.ConsoleDisableQuickEdit();
  26. ConsoleInterops.CurrentProcess.ConsoleWindowSetParent(tpConsoleLauncher);
  27. ConsoleInterops.CurrentProcess.ConsoleWindowFill(tpConsoleLauncher);
  28. tpConsoleLauncher.Resize += delegate { ConsoleInterops.CurrentProcess.ConsoleWindowFill(tpConsoleLauncher); };
  29. //start host application
  30. var builder = Host.CreateDefaultBuilder();
  31. builder.ConfigureServices((ctx, services) =>
  32. {
  33. //控制台日志格式
  34. services.AddLogging(opt =>
  35. {
  36. opt.AddSimpleConsole(p =>
  37. {
  38. p.TimestampFormat = "[dd HH:mm:ss] ";
  39. p.SingleLine = true;
  40. p.ColorBehavior = LoggerColorBehavior.Enabled;
  41. })
  42. .AddDebug()
  43. //.AddProvider(loggingIntoEventBus)
  44. ;
  45. services.AddSingleton(opt);
  46. });
  47. services.AddSingleton(this);
  48. });
  49. var host = builder.Build();
  50. var services = host.Services;
  51. host.Start();
  52. logger = services.GetRequiredService<ILogger<MainForm>>();
  53. var conf = services.GetRequiredService<IConfiguration>();
  54. var settingModel = new SinMaiLauncherSettingModel();
  55. conf.GetSection("SinMaiLauncher").Bind(settingModel);
  56. setting = new(settingModel);
  57. childProcessControlGroups = new Dictionary<ChildProcessKind, ChildProcessControlGroup>
  58. {
  59. {
  60. ChildProcessKind.MariaDb, new ChildProcessControlGroup
  61. {
  62. StateBag = new ProcessStateBagMariaDb(services.GetRequiredService<ILogger<ProcessStateBagMariaDb>>(), setting),
  63. ConsoleTab = tpConsoleMaria, PidLabel = lblPidMaria, StatusLabel = lblSubControlMaria,
  64. StartButton = btnSubControlMariaStart, StopButton = btnSubControlMariaStop
  65. }
  66. },
  67. {
  68. ChildProcessKind.AquaDx, new ChildProcessControlGroup
  69. {
  70. StateBag = new ProcessStateBagAquaDx(services.GetRequiredService<ILogger<ProcessStateBagAquaDx>>(), setting),
  71. Dep = ChildProcessKind.MariaDb,
  72. ConsoleTab = tpConsoleAquaDx, PidLabel = lblPidAquaDx, StatusLabel = lblSubControlAquaDx,
  73. StartButton = btnSubControlAquaDxStart, StopButton = btnSubControlAquaDxStop
  74. }
  75. },
  76. {
  77. ChildProcessKind.Injector, new ChildProcessControlGroup
  78. {
  79. StateBag = new ChildProcessStateBagForConsole(ChildProcessKind.Injector, setting.Injector),
  80. Dep = ChildProcessKind.AquaDx,
  81. ConsoleTab = tpConsoleInjector, PidLabel = lblPidInjector, StatusLabel = lblSubControlInjector,
  82. StartButton = btnSubControlInjectorStart, StopButton = btnSubControlInjectorStop
  83. }
  84. },
  85. {
  86. ChildProcessKind.SinMai, new ChildProcessControlGroup
  87. {
  88. StateBag = new ChildProcessStateBagForWin(ChildProcessKind.SinMai, setting.SinMai),
  89. Dep = ChildProcessKind.Injector,
  90. ConsoleTab = null, PidLabel = lblPidSinMai, StatusLabel = lblSubControlSinMai,
  91. StartButton = btnSubControlSinMaiStart, StopButton = btnSubControlSinMaiStop
  92. }
  93. }
  94. };
  95. foreach (var gr in childProcessControlGroups.Values)
  96. {
  97. var tab = gr.ConsoleTab;
  98. var cpb = gr.StateBag;
  99. var btnStart = gr.StartButton;
  100. var btnStop = gr.StopButton;
  101. var lblPid = gr.PidLabel;
  102. var lblStatus = gr.StatusLabel;
  103. //绑定tab控制台窗口填充
  104. if (cpb is ChildProcessStateBagForConsole con)
  105. {
  106. tab.Resize += delegate
  107. {
  108. if (con.HWndConsole.HasValue) WindowInterops.FillWindow(con.HWndConsole.Value, tab);
  109. };
  110. }
  111. btnStart.Click += async delegate
  112. {
  113. if (cpb.IsAlive)
  114. {
  115. MessageBox.Show("它在运行了,别重复点启动啊");
  116. return;
  117. }
  118. if (gr.Dep.HasValue && childProcessControlGroups[gr.Dep.Value].StateBag.Status != ChildProcessStatus.Ready)
  119. {
  120. MessageBox.Show("依赖的进程未就绪,启动不了的,等就绪了再启动。要按顺序启动。");
  121. return;
  122. }
  123. await cpb.StartAsync();
  124. };
  125. btnStop.Click += async delegate
  126. {
  127. if (cpb.IsAlive == false)
  128. {
  129. MessageBox.Show("都没启动,咋停止啊?");
  130. return;
  131. }
  132. await cpb.StopAsync(TimeSpan.FromSeconds(5));
  133. };
  134. cpb.StatusUpdated += async delegate
  135. {
  136. void UpdateUi()
  137. {
  138. lblPid.Text = cpb.Pid.HasValue ? cpb.Pid.Value.ToString() : "-";
  139. lblStatus.Text = cpb.Status switch
  140. {
  141. ChildProcessStatus.NoLaunched => "未启动",
  142. ChildProcessStatus.PreLaunching => "准备启动",
  143. ChildProcessStatus.MissingWorkingDir => "丢失工作目录",
  144. ChildProcessStatus.MissingExeFile => "丢失exe文件",
  145. ChildProcessStatus.Launching => "正在启动",
  146. ChildProcessStatus.WaitingReady => "等待就绪...",
  147. ChildProcessStatus.Ready => "就绪",
  148. ChildProcessStatus.Stopping => "正在停止",
  149. ChildProcessStatus.Stopped => "已停止",
  150. _ => "?"
  151. };
  152. if (cpb.Status == ChildProcessStatus.WaitingReady && cpb is ChildProcessStateBagForConsole con && cpb.IsAlive && cpb.Pid.HasValue && con.HWndConsole.HasValue)
  153. {
  154. if (tab != null)
  155. {
  156. //console 收纳
  157. Program.DisableQuickEdit(cpb.Pid.Value);
  158. WindowInterops.RemoveBorderVisible(con.HWndConsole.Value);
  159. WindowInterops.SetParent(con.HWndConsole.Value, tab.Handle);
  160. WindowInterops.FillWindow(con.HWndConsole.Value, tab);
  161. Height++;
  162. Height--;
  163. }
  164. }
  165. if (cpb.Status == ChildProcessStatus.Ready && cpb is ChildProcessStateBagForWin win)
  166. {
  167. //chkExplorerAuto
  168. }
  169. }
  170. if (InvokeRequired) await InvokeAsync(UpdateUi);
  171. else UpdateUi();
  172. };
  173. }
  174. logger.LogInformation("Started");
  175. Application.DoEvents();
  176. await Task.Delay(1000);
  177. tcMain.Enabled = true;
  178. //#if DEBUG
  179. // tcMain.SelectedTab = tpSubControls;
  180. //#else
  181. tcMain.SelectedTab = tpMainControl;
  182. //#endif
  183. }
  184. private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
  185. {
  186. if (
  187. childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.Status == ChildProcessStatus.Ready
  188. || childProcessControlGroups[ChildProcessKind.AquaDx].StateBag.Status == ChildProcessStatus.Ready
  189. )
  190. {
  191. e.Cancel = MessageBox.Show($"基础设施进程仍在运行。{Environment.NewLine}为了避免进度数据裂开,请确保安全停止再关闭。{Environment.NewLine}要强制关闭吗?", "关闭启动器", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.No;
  192. }
  193. }
  194. private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
  195. {
  196. //Kill all child process
  197. childProcessControlGroups[ChildProcessKind.SinMai].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  198. childProcessControlGroups[ChildProcessKind.Injector].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  199. childProcessControlGroups[ChildProcessKind.AquaDx].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  200. childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  201. }
  202. private void tcConsoles_SelectedIndexChanged(object sender, EventArgs e)
  203. {
  204. Height++;
  205. Height--;
  206. }
  207. private class ChildProcessControlGroup
  208. {
  209. public ChildProcessStateBag StateBag { get; set; }
  210. public ChildProcessKind? Dep { get; set; }
  211. public TabPage? ConsoleTab { get; set; } // 可空,窗口进程为 null
  212. public Label PidLabel { get; set; }
  213. public Label StatusLabel { get; set; }
  214. public Button StartButton { get; set; }
  215. public Button StopButton { get; set; }
  216. }
  217. private async void btnMainControlStart_Click(object sender, EventArgs e)
  218. {
  219. btnMainControlStart.Enabled = false;
  220. mainControlStatusTextAnimation = true;
  221. var orderedProcesses = new List<(ChildProcessKind kind, TimeSpan timeout)>
  222. {
  223. (ChildProcessKind.MariaDb, TimeSpan.FromSeconds(30)),
  224. (ChildProcessKind.AquaDx,TimeSpan.FromSeconds(90)),
  225. (ChildProcessKind.Injector,TimeSpan.FromSeconds(10)),
  226. (ChildProcessKind.SinMai,TimeSpan.FromSeconds(10)),
  227. };
  228. var startedProcesses = new List<ChildProcessKind>();
  229. try
  230. {
  231. foreach (var (kind, timeout) in orderedProcesses)
  232. {
  233. var gr = childProcessControlGroups[kind];
  234. var cpb = gr.StateBag;
  235. if (cpb.IsAlive)
  236. {
  237. logger.LogWarning("{Kind} is already running, skipping.", kind);
  238. startedProcesses.Add(kind); // 已运行的也记录,便于回滚
  239. continue;
  240. }
  241. mainControlStatusText = $"正在启动 {kind}";
  242. await cpb.StartAsync();
  243. await WaitForReadyAsync(cpb, timeout);
  244. if (cpb.Status != ChildProcessStatus.Ready)
  245. {
  246. logger.LogError($"{kind} failed to reach Ready state (current: {cpb.Status}), rolling back.");
  247. await RollbackAsync(startedProcesses);
  248. return;
  249. }
  250. startedProcesses.Add(kind);
  251. }
  252. mainControlStatusText = $"已启动!";
  253. btnMainControlStop.Enabled = true;
  254. mainControlStatusTextAnimation = false;
  255. }
  256. catch (Exception exception)
  257. {
  258. logger.LogError(exception, "Error during startup, rolling back.");
  259. await RollbackAsync(startedProcesses);
  260. }
  261. async Task WaitForReadyAsync(ChildProcessStateBag cpb, TimeSpan timeout)
  262. {
  263. if (cpb.Status == ChildProcessStatus.Ready
  264. || cpb.Status == ChildProcessStatus.MissingWorkingDir
  265. || cpb.Status == ChildProcessStatus.MissingExeFile
  266. || cpb.Status == ChildProcessStatus.Stopped) return;
  267. var tcs = new TaskCompletionSource<bool>();
  268. EventHandler<EventArgs> handler = null;
  269. handler = delegate
  270. {
  271. if (cpb.Status == ChildProcessStatus.Ready
  272. || cpb.Status == ChildProcessStatus.MissingWorkingDir
  273. || cpb.Status == ChildProcessStatus.MissingExeFile
  274. || cpb.Status == ChildProcessStatus.Stopped
  275. )
  276. {
  277. cpb.StatusUpdated -= handler;
  278. tcs.TrySetResult(true);
  279. }
  280. };
  281. cpb.StatusUpdated += handler;
  282. try
  283. {
  284. // 等待 Ready 或超时
  285. await Task.WhenAny(tcs.Task, Task.Delay(timeout));
  286. if (!tcs.Task.IsCompleted)
  287. {
  288. cpb.StatusUpdated -= handler;
  289. throw new TimeoutException($"{cpb.Kind} did not reach Ready state within {timeout.TotalSeconds} seconds.");
  290. }
  291. }
  292. finally
  293. {
  294. cpb.StatusUpdated -= handler; // 确保清理
  295. }
  296. }
  297. async Task RollbackAsync(List<ChildProcessKind> startedProcesses)
  298. {
  299. // 按相反顺序停止
  300. foreach (var kind in startedProcesses.AsEnumerable().Reverse())
  301. {
  302. mainControlStatusText = $"正在停止 {kind}";
  303. var group = childProcessControlGroups[kind];
  304. var cpb = group.StateBag;
  305. if (cpb.IsAlive)
  306. {
  307. logger.LogInformation("Stopping {Kind} during rollback...", kind);
  308. await InvokeAsync(() => group.StatusLabel.Text = "正在停止...");
  309. await cpb.StopAsync(TimeSpan.FromSeconds(5));
  310. }
  311. }
  312. mainControlStatusText = "启动失败,已回滚到启动之前";
  313. btnMainControlStart.Enabled = true;
  314. mainControlStatusTextAnimation = false;
  315. }
  316. }
  317. private async void btnMainControlStop_Click(object sender, EventArgs e)
  318. {
  319. var orderedProcesses = new List<(ChildProcessKind kind, TimeSpan timeout)>
  320. {
  321. (ChildProcessKind.SinMai,TimeSpan.FromSeconds(5)),
  322. (ChildProcessKind.Injector,TimeSpan.FromSeconds(10)),
  323. (ChildProcessKind.AquaDx,TimeSpan.FromSeconds(90)),
  324. (ChildProcessKind.MariaDb, TimeSpan.FromSeconds(30)),
  325. };
  326. foreach (var (kind, timeout) in orderedProcesses)
  327. {
  328. mainControlStatusText = $"正在停止 {kind}";
  329. var gr = childProcessControlGroups[kind];
  330. if (gr.StateBag.IsAlive)
  331. {
  332. await gr.StateBag.StopAsync(timeout);
  333. }
  334. }
  335. btnMainControlStart.Enabled = true;
  336. btnMainControlStop.Enabled = false;
  337. mainControlStatusText = $"已停止";
  338. mainControlStatusTextAnimation = false;
  339. }
  340. private DateTime lastCheckSinMaiMainWinPos = DateTime.Now;
  341. private DateTime lastCheckSysVolume = DateTime.Now;
  342. private void tmrMain_Tick(object sender, EventArgs e)
  343. {
  344. if (mainControlStatusTextAnimation)
  345. {
  346. txtAni.MoveNext();
  347. lblMainControlStatus.Text = $"{mainControlStatusText} {txtAni.Current}";
  348. }
  349. else
  350. {
  351. lblMainControlStatus.Text = $"{mainControlStatusText}";
  352. }
  353. //窗口调整
  354. if (chkWinTrickAuto.Checked && lastCheckSinMaiMainWinPos < DateTime.Now.AddSeconds(-5))
  355. {
  356. lastCheckSinMaiMainWinPos = DateTime.Now;
  357. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  358. if (pb.Status == ChildProcessStatus.Ready && pb.HWndMainWindow.HasValue)
  359. {
  360. MoveSinMaiMainWindow();
  361. }
  362. }
  363. //显示系统音量
  364. if (lastCheckSysVolume < DateTime.Now.AddSeconds(-1))
  365. {
  366. lastCheckSinMaiMainWinPos = DateTime.Now;
  367. var sv = SystemAudioVolumeInterop.GetVolume();
  368. lblSysVolume.Text = sv.HasValue ? $"{sv:N2}%" : "-";
  369. }
  370. }
  371. private void btnAimeWrite_Click(object sender, EventArgs e)
  372. {
  373. var input = cboAimeCard.Text;
  374. if (string.IsNullOrEmpty(input))
  375. {
  376. MessageBox.Show("倒是输入aime卡号啊?");
  377. return;
  378. }
  379. if (Directory.Exists(setting.AimeFleSinMaiDir) == false)
  380. {
  381. MessageBox.Show("找不到写aime卡文件的目录");
  382. return;
  383. }
  384. if (Directory.Exists(setting.AimeFleSinMaiPath))
  385. {
  386. MessageBox.Show("找不到写aime卡的文件");
  387. return;
  388. }
  389. var aime = input.Replace("-", "");
  390. File.WriteAllText(setting.AimeFleSinMaiPath, aime);
  391. MessageBox.Show($"已将选定的卡号已写入 aime.txt !{Environment.NewLine}在游戏登录时按回车键读卡{Environment.NewLine}(按一次回车不行就多按几次回车键直到读卡成功🙄)");
  392. }
  393. private void btnWinTrick_Click(object sender, EventArgs e)
  394. {
  395. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  396. if (pb.Status != ChildProcessStatus.Ready || !pb.HWndMainWindow.HasValue)
  397. {
  398. MessageBox.Show("游戏启动之后再点这个按钮");
  399. return;
  400. }
  401. var moveWindowResult = MoveSinMaiMainWindow();
  402. switch (moveWindowResult)
  403. {
  404. case WindowInterops.MoveWindowResult.FailGet:
  405. MessageBox.Show("操作不成功:读取失败");
  406. break;
  407. case WindowInterops.MoveWindowResult.FailSet:
  408. MessageBox.Show("操作不成功:设置失败");
  409. break;
  410. case WindowInterops.MoveWindowResult.Success:
  411. MessageBox.Show("操作成功");
  412. break;
  413. case WindowInterops.MoveWindowResult.NoChange:
  414. MessageBox.Show("没有改变");
  415. break;
  416. }
  417. }
  418. private WindowInterops.MoveWindowResult MoveSinMaiMainWindow()
  419. {
  420. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  421. //WS_CLIPSIBLINGS WS_POPUPWINDOW WS_VISIBLE
  422. var rc = setting.Settings.AutoRect;
  423. return WindowInterops.MoveWindow(pb.HWndMainWindow.Value, rc.Left, rc.Top, rc.Width, rc.Height);
  424. }
  425. private void btnAimeRefresh_Click(object sender, EventArgs e)
  426. {
  427. if (childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.Status != ChildProcessStatus.Ready)
  428. {
  429. MessageBox.Show("启动数据库先啊!");
  430. return;
  431. }
  432. var conf = setting.Settings.Infra.MariaDb;
  433. var connectionString = $"Server={conf.Host};Port={conf.Port};Uid={conf.Usr};Pwd={conf.Pwd};";
  434. using var conn = new MySqlConnection(connectionString);
  435. conn.Open();
  436. using var cmd = conn.CreateCommand();
  437. cmd.CommandText = "SELECT `luid` FROM `aqua`.`sega_card`";
  438. using var dr = cmd.ExecuteReader();
  439. cboAimeCard.Items.Clear();
  440. while (dr.Read())
  441. {
  442. var aimeR = dr["luid"].ToString();
  443. var aimeH = string.Join("-",
  444. aimeR.Substring(0, 4),
  445. aimeR.Substring(4, 4),
  446. aimeR.Substring(8, 4),
  447. aimeR.Substring(12, 4),
  448. aimeR.Substring(16, 4));
  449. cboAimeCard.Items.Add(aimeH);
  450. }
  451. MessageBox.Show($"从数据库中读取到 {cboAimeCard.Items.Count} 个卡号已载入下拉列表");
  452. }
  453. private void btnExplorerKill_Click(object sender, EventArgs e)
  454. {
  455. MessageBox.Show("还没做!");
  456. }
  457. private void btnExplorerRestore_Click(object sender, EventArgs e)
  458. {
  459. MessageBox.Show("还没做!");
  460. }
  461. private void btnProcessLoopBackVirtualNic_Click(object sender, EventArgs e)
  462. {
  463. MessageBox.Show("还没做!");
  464. }
  465. private void btnAppDataOverwrite_Click(object sender, EventArgs e)
  466. {
  467. MessageBox.Show("还没做!");
  468. }
  469. private void btnSysVolumeDec_Click(object sender, EventArgs e)
  470. {
  471. var sv = SystemAudioVolumeInterop.GetVolume();
  472. if (!sv.HasValue)
  473. {
  474. MessageBox.Show("无法获取当前音量");
  475. return;
  476. }
  477. sv -= 5;
  478. if (sv < 0) sv = 0;
  479. if (SystemAudioVolumeInterop.SetVolume(sv.Value) == false) MessageBox.Show("音量调整失败");
  480. }
  481. private void btnSysVolumeInc_Click(object sender, EventArgs e)
  482. {
  483. var sv = SystemAudioVolumeInterop.GetVolume();
  484. if (!sv.HasValue)
  485. {
  486. MessageBox.Show("无法获取当前音量");
  487. return;
  488. }
  489. sv += 5;
  490. if (sv > 100) sv = 100;
  491. if (SystemAudioVolumeInterop.SetVolume(sv.Value) == false) MessageBox.Show("音量调整失败");
  492. }
  493. }