Techblox Mod Manager / Launcher
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

217 lines
9.4KB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO.Compression;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Threading.Tasks;
  9. using System.Windows.Forms;
  10. namespace TBMM
  11. {
  12. partial class MainForm
  13. {
  14. public GameState CheckIfPatched() => CheckIfPatched(out _);
  15. public GameState CheckIfPatched(out bool patched)
  16. {
  17. Dictionary<GameState, (string Status, string Extra, string Play)> statusTexts = new()
  18. {
  19. { GameState.NotFound, ("Game not found", "Specify the game's location in settings", "") },
  20. { GameState.InGame, ("Game is running", "", "In-game") },
  21. { GameState.NoPatcher, ("Patcher missing", "Clicking Play will install it", "") },
  22. { GameState.OldPatcher, ("Patcher outdated", "nClicking play will update it", "") },
  23. { GameState.Unpatched, ("Unpatched", "", "") },
  24. { GameState.Patched, ("Patched", "", "") }
  25. };
  26. void SetStatusText(GameState state, bool patched)
  27. {
  28. var (statusText, extra, play) = statusTexts[state];
  29. if (extra.Length == 0) extra = patched ? "Cannot join online mode" : "Online mode available";
  30. if (play.Length == 0) play = "Play modded";
  31. status.Text = $"Status: {statusText}\n{extra}";
  32. if (Configuration.KeepPatched)
  33. status.Text += "\nUnpatch on exit disabled";
  34. playbtn.Text = play;
  35. }
  36. if (GetExe() == null)
  37. {
  38. patched = false;
  39. SetStatusText(GameState.NotFound, false);
  40. return GameState.NotFound;
  41. }
  42. bool gameIsRunning = CheckIfGameIsRunning();
  43. if (gameIsRunning)
  44. {
  45. UpdateButton(playbtn, false); //Don't allow (un)installing mods if game is running
  46. UpdateButton(installbtn, false);
  47. UpdateButton(uninstallbtn, false);
  48. modlist.Enabled = false;
  49. }
  50. else
  51. {
  52. if (!working) UpdateButton(playbtn, true);
  53. modlist.Enabled = true;
  54. }
  55. GameState GetPatchedState()
  56. {
  57. if (!File.Exists(GamePath(@"\IPA.exe")))
  58. return GameState.NoPatcher;
  59. if (gcipa.Updatable && !(gcipa.Version == new Version(1, 0, 0, 0) &&
  60. gcipa.LatestVersion == new Version(4, 0, 0, 0))
  61. && !gameIsRunning)
  62. return GameState.OldPatcher;
  63. string gc = GetExe(withExtension: false);
  64. string backups = GamePath(@"\IPA\Backups\" + gc);
  65. if (!Directory.Exists(backups))
  66. return GameState.Unpatched;
  67. string backup = Directory.EnumerateDirectories(backups)
  68. .OrderByDescending(Directory.GetLastWriteTimeUtc).FirstOrDefault();
  69. if (backup == null)
  70. return GameState.Unpatched;
  71. if (File.GetLastWriteTime(GamePath($@"\{gc}_Data\Managed\Assembly-CSharp.dll"))
  72. > //If the file was updated at least 2 minutes after patching
  73. Directory.GetLastWriteTime(backup).AddMinutes(2)
  74. || !File.Exists(GamePath($@"\{gc}_Data\Managed\IllusionInjector.dll")))
  75. return GameState.Unpatched;
  76. return GameState.Patched;
  77. }
  78. var patchedState = GetPatchedState();
  79. var finalState = gameIsRunning ? GameState.InGame : patchedState;
  80. patched = patchedState == GameState.Patched;
  81. SetStatusText(finalState, patchedState == GameState.Patched);
  82. return finalState;
  83. }
  84. public async Task<bool?> PatchStartGame(string command = null)
  85. {
  86. if (!BeginWork()) return false;
  87. foreach (ListViewItem item in modlist.SelectedItems)
  88. item.Selected = false;
  89. bool? retOpenedWindowShouldStay = null;
  90. void EnsureShown(bool stay)
  91. {
  92. if (!Visible)
  93. {
  94. Show();
  95. retOpenedWindowShouldStay = stay;
  96. TopMost = true; //It opens in the background otherwise - should be fine since it only shows for a couple seconds
  97. }
  98. }
  99. var status = CheckIfPatched();
  100. //bool justDownloadedPatcherSoDontWarnAboutIncompatibility = false;
  101. switch (status)
  102. {
  103. case GameState.NotFound:
  104. MessageBox.Show("Techblox not found! Set the correct path in Settings.");
  105. EndWork(status, false);
  106. return retOpenedWindowShouldStay;
  107. case GameState.NoPatcher:
  108. case GameState.OldPatcher:
  109. {
  110. EnsureShown(false);
  111. if (MessageBox.Show((status == GameState.NoPatcher
  112. ? "The patcher (GCIPA) is not found. It's necessary to load the mods."
  113. : "There is a patcher update available!"
  114. ) + "\n\nIt will be downloaded from https://git.exmods.org/modtainers/GCIPA/releases and ran to patch the game. You can validate the game to restore the original game files or simply disable mods at any time.",
  115. "Patcher download needed", MessageBoxButtons.OKCancel) == DialogResult.Cancel)
  116. {
  117. EndWork(status);
  118. return retOpenedWindowShouldStay;
  119. }
  120. this.status.Text = "Status: Patching...";
  121. int C = 0;
  122. while (gcipa.DownloadURL == null && C < 20)
  123. await Task.Delay(500); //The EnsureShown() call should download info about GCIPA
  124. if (gcipa.DownloadURL == null)
  125. {
  126. MessageBox.Show("Could not get information about GCIPA in time. Please run TBMM manually.");
  127. return retOpenedWindowShouldStay;
  128. }
  129. using (WebClient client = GetClient())
  130. {
  131. string url = gcipa.DownloadURL;
  132. await client.DownloadFileTaskAsync(url, "IPA.zip");
  133. using (var fs = new FileStream("IPA.zip", FileMode.Open))
  134. using (var za = new ZipArchive(fs))
  135. za.ExtractToDirectory(Configuration.GamePath, true); //Overwrite files that were left from a previous install of the patcher
  136. File.Delete("IPA.zip");
  137. }
  138. }
  139. GetInstalledMods(); //Update patcher state, should be fine for this rare event
  140. status = CheckIfPatched();
  141. break;
  142. }
  143. switch (status)
  144. {
  145. case GameState.NoPatcher: //Make sure it actually worked
  146. case GameState.OldPatcher:
  147. EndWork(status, false);
  148. return retOpenedWindowShouldStay;
  149. case GameState.Unpatched:
  150. { //TODO: Wine
  151. EnsureShown(false);
  152. var (handler, task) = CheckStartGame(command);
  153. var process = ExecutePatcher(true);
  154. process.Exited += handler;
  155. await task;
  156. }
  157. break;
  158. case GameState.Patched:
  159. {
  160. //CheckStartGame(command)(null, null);
  161. var (handler, task) = CheckStartGame(command);
  162. handler(null, null);
  163. await task;
  164. }
  165. break;
  166. }
  167. return retOpenedWindowShouldStay;
  168. }
  169. private Process ExecutePatcher(bool patch)
  170. {
  171. var psi = new ProcessStartInfo(GamePath(@"\IPA.exe"), $"{GetExe()} --nowait {(patch ? "" : "--revert")}")
  172. {
  173. UseShellExecute = false,
  174. RedirectStandardError = true,
  175. RedirectStandardOutput = true,
  176. WorkingDirectory = Configuration.GamePath,
  177. CreateNoWindow = true
  178. };
  179. var process = Process.Start(psi);
  180. process.BeginErrorReadLine();
  181. process.BeginOutputReadLine();
  182. process.EnableRaisingEvents = true;
  183. modinfobox.Text = "";
  184. DataReceivedEventHandler onoutput = (sender, e) =>
  185. {
  186. Invoke((Action)(() => modinfobox.Text += e.Data + Environment.NewLine));
  187. };
  188. process.OutputDataReceived += onoutput;
  189. process.ErrorDataReceived += onoutput;
  190. return process;
  191. }
  192. public enum GameState
  193. {
  194. NotFound,
  195. NoPatcher,
  196. OldPatcher,
  197. Unpatched,
  198. Patched,
  199. /// <summary>
  200. /// Doesn't matter if patched, don't do anything if the game is running
  201. /// </summary>
  202. InGame
  203. }
  204. }
  205. }