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.

441 lines
18 KiB

  1. using IPA.Patcher;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Runtime.InteropServices;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11. using System.Windows.Forms;
  12. using IPA.ArgParsing;
  13. namespace IPA {
  14. public class Program {
  15. public enum Architecture {
  16. x86,
  17. x64,
  18. Unknown
  19. }
  20. private static Version Version => Assembly.GetEntryAssembly().GetName().Version;
  21. public static ArgumentFlag ArgHelp = new ArgumentFlag("--help", "-h") { DocString = "prints this message" };
  22. public static ArgumentFlag ArgWaitFor = new ArgumentFlag("--waitfor", "-w") { DocString = "waits for the specified PID to exit", ValueString = "PID" };
  23. public static ArgumentFlag ArgForce = new ArgumentFlag("--force", "-f") { DocString = "forces the operation to go through" };
  24. public static ArgumentFlag ArgRevert = new ArgumentFlag("--revert", "-r") { DocString = "reverts the IPA installation" };
  25. public static ArgumentFlag ArgNoWait = new ArgumentFlag("--nowait", "-n") { DocString = "doesn't wait for user input after the operation" };
  26. public static ArgumentFlag ArgStart = new ArgumentFlag("--start", "-s") { DocString = "uses value as arguments to start the game after the patch/unpatch", ValueString = "ARGUMENTS" };
  27. public static ArgumentFlag ArgLaunch = new ArgumentFlag("--launch", "-l") { DocString = "uses positional parameters as arguments to start the game after patch/unpatch" };
  28. static void Main(string[] args)
  29. {
  30. Arguments.CmdLine.Flags(ArgHelp, ArgWaitFor, ArgForce, ArgRevert, ArgNoWait, ArgStart, ArgLaunch).Process();
  31. if (ArgHelp)
  32. {
  33. Arguments.CmdLine.PrintHelp();
  34. return;
  35. }
  36. try
  37. {
  38. if (ArgWaitFor.HasValue)
  39. { // wait for process if necessary
  40. int pid = int.Parse(ArgWaitFor.Value);
  41. try
  42. { // wait for beat saber to exit (ensures we can modify the file)
  43. var parent = Process.GetProcessById(pid);
  44. Console.WriteLine($"Waiting for parent ({pid}) process to die...");
  45. parent.WaitForExit();
  46. }
  47. catch (Exception) { }
  48. }
  49. PatchContext context;
  50. var argExeName = Arguments.CmdLine.PositionalArgs.FirstOrDefault(s => s.EndsWith(".exe"));
  51. if (argExeName == null)
  52. context = PatchContext.Create(new DirectoryInfo(Directory.GetCurrentDirectory()).GetFiles()
  53. .First(o => o.Extension == ".exe" && o.FullName != Assembly.GetCallingAssembly().Location)
  54. .FullName);
  55. else
  56. context = PatchContext.Create(argExeName);
  57. // Sanitizing
  58. Validate(context);
  59. if (ArgRevert || Keyboard.IsKeyDown(Keys.LMenu))
  60. Revert(context);
  61. else
  62. {
  63. Install(context);
  64. StartIfNeedBe(context);
  65. }
  66. }
  67. catch (Exception e) {
  68. Fail(e.Message);
  69. }
  70. WaitForEnd();
  71. }
  72. private static void WaitForEnd()
  73. {
  74. if (!ArgNoWait)
  75. {
  76. Console.ForegroundColor = ConsoleColor.DarkYellow;
  77. Console.WriteLine("[Press any key to continue]");
  78. Console.ResetColor();
  79. Console.ReadLine();
  80. }
  81. }
  82. private static void Validate(PatchContext c) {
  83. if (!Directory.Exists(c.DataPathDst) || !File.Exists(c.EngineFile)) {
  84. Fail("Game does not seem to be a Unity project. Could not find the libraries to patch.");
  85. Console.WriteLine($"DataPath: {c.DataPathDst}");
  86. Console.WriteLine($"EngineFile: {c.EngineFile}");
  87. }
  88. }
  89. private static void Install(PatchContext context) {
  90. try {
  91. var backup = new BackupUnit(context);
  92. #region Patch Version Check
  93. var patchedModule = PatchedModule.Load(context.EngineFile);
  94. var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) > 0;
  95. Console.WriteLine($"Current: {Version} Patched: {patchedModule.Data.Version}");
  96. if (isCurrentNewer) {
  97. Console.ForegroundColor = ConsoleColor.White;
  98. Console.WriteLine(
  99. $"Preparing for update, {(patchedModule.Data.Version == null ? "UnPatched" : patchedModule.Data.Version.ToString())} => {Version}");
  100. Console.WriteLine("--- Starting ---");
  101. Revert(context);
  102. Console.ResetColor();
  103. #region File Copying
  104. Console.ForegroundColor = ConsoleColor.Magenta;
  105. Console.WriteLine("Updating files... ");
  106. var nativePluginFolder = Path.Combine(context.DataPathDst, "Plugins");
  107. bool isFlat = Directory.Exists(nativePluginFolder) &&
  108. Directory.GetFiles(nativePluginFolder).Any(f => f.EndsWith(".dll"));
  109. bool force = !BackupManager.HasBackup(context) || ArgForce;
  110. var architecture = DetectArchitecture(context.Executable);
  111. Console.WriteLine("Architecture: {0}", architecture);
  112. CopyAll(new DirectoryInfo(context.DataPathSrc), new DirectoryInfo(context.DataPathDst), force,
  113. backup,
  114. (from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
  115. architecture));
  116. CopyAll(new DirectoryInfo(context.LibsPathSrc), new DirectoryInfo(context.LibsPathDst), force,
  117. backup,
  118. (from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
  119. architecture));
  120. Console.WriteLine("Successfully updated files!");
  121. #endregion
  122. }
  123. else {
  124. Console.ForegroundColor = ConsoleColor.Red;
  125. Console.WriteLine($"Files up to date @ Version {Version}!");
  126. Console.ResetColor();
  127. }
  128. #endregion
  129. #region Create Plugin Folder
  130. if (!Directory.Exists(context.PluginsFolder)) {
  131. Console.ForegroundColor = ConsoleColor.DarkYellow;
  132. Console.WriteLine("Creating plugins folder... ");
  133. Directory.CreateDirectory(context.PluginsFolder);
  134. Console.ResetColor();
  135. }
  136. #endregion
  137. #region Patching
  138. if (!patchedModule.Data.IsPatched || isCurrentNewer) {
  139. Console.ForegroundColor = ConsoleColor.Yellow;
  140. Console.WriteLine($"Patching UnityEngine.dll with Version {Application.ProductVersion}... ");
  141. backup.Add(context.EngineFile);
  142. patchedModule.Patch(Version);
  143. Console.WriteLine("Done!");
  144. Console.ResetColor();
  145. }
  146. #endregion
  147. #region Virtualizing
  148. if (File.Exists(context.AssemblyFile)) {
  149. var virtualizedModule = VirtualizedModule.Load(context.AssemblyFile);
  150. if (!virtualizedModule.IsVirtualized) {
  151. Console.ForegroundColor = ConsoleColor.Green;
  152. Console.WriteLine("Virtualizing Assembly-Csharp.dll... ");
  153. backup.Add(context.AssemblyFile);
  154. virtualizedModule.Virtualize();
  155. Console.WriteLine("Done!");
  156. Console.ResetColor();
  157. }
  158. }
  159. #endregion
  160. #region Creating shortcut
  161. if(!File.Exists(context.ShortcutPath))
  162. {
  163. Console.ForegroundColor = ConsoleColor.DarkGreen;
  164. Console.WriteLine("Creating shortcut to IPA ({0})... ", context.IPA);
  165. try
  166. {
  167. Shortcut.Create(
  168. fileName: context.ShortcutPath,
  169. targetPath: context.IPA,
  170. arguments: Args(context.Executable, "-ln"),
  171. workingDirectory: context.ProjectRoot,
  172. description: "Launches the game and makes sure it's in a patched state",
  173. hotkey: "",
  174. iconPath: context.Executable
  175. );
  176. } catch (Exception)
  177. {
  178. Console.ForegroundColor = ConsoleColor.Red;
  179. Console.Error.WriteLine("Failed to create shortcut, but game was patched!");
  180. }
  181. Console.ResetColor();
  182. }
  183. #endregion
  184. }
  185. catch (Exception e)
  186. {
  187. Console.ForegroundColor = ConsoleColor.Red;
  188. Fail("Oops! This should not have happened.\n\n" + e);
  189. }
  190. Console.ResetColor();
  191. }
  192. private static void Revert(PatchContext context) {
  193. Console.ForegroundColor = ConsoleColor.Cyan;
  194. Console.Write("Restoring backup... ");
  195. if (BackupManager.Restore(context)) {
  196. Console.WriteLine("Done!");
  197. }
  198. else {
  199. Console.WriteLine("Already vanilla or you removed your backups!");
  200. }
  201. if (File.Exists(context.ShortcutPath)) {
  202. Console.WriteLine("Deleting shortcut...");
  203. File.Delete(context.ShortcutPath);
  204. }
  205. Console.WriteLine("");
  206. Console.WriteLine("--- Done reverting ---");
  207. Console.ResetColor();
  208. }
  209. private static void StartIfNeedBe(PatchContext context) {
  210. if (ArgStart.HasValue)
  211. {
  212. Process.Start(context.Executable, ArgStart.Value);
  213. }
  214. else
  215. {
  216. var argList = Arguments.CmdLine.PositionalArgs.ToList();
  217. argList.Remove(context.Executable);
  218. if (ArgLaunch)
  219. {
  220. Process.Start(context.Executable, Args(argList.ToArray()));
  221. }
  222. }
  223. }
  224. public static IEnumerable<FileInfo> NativePluginInterceptor(FileInfo from, FileInfo to,
  225. DirectoryInfo nativePluginFolder, bool isFlat, Architecture preferredArchitecture) {
  226. if (to.FullName.StartsWith(nativePluginFolder.FullName)) {
  227. var relevantBit = to.FullName.Substring(nativePluginFolder.FullName.Length + 1);
  228. // Goes into the plugin folder!
  229. bool isFileFlat = !relevantBit.StartsWith("x86");
  230. if (isFlat && !isFileFlat) {
  231. // Flatten structure
  232. bool is64Bit = relevantBit.StartsWith("x86_64");
  233. if (!is64Bit && preferredArchitecture == Architecture.x86) {
  234. // 32 bit
  235. yield return new FileInfo(Path.Combine(nativePluginFolder.FullName,
  236. relevantBit.Substring("x86".Length + 1)));
  237. }
  238. else if (is64Bit && (preferredArchitecture == Architecture.x64 ||
  239. preferredArchitecture == Architecture.Unknown)) {
  240. // 64 bit
  241. yield return new FileInfo(Path.Combine(nativePluginFolder.FullName,
  242. relevantBit.Substring("x86_64".Length + 1)));
  243. }
  244. else {
  245. // Throw away
  246. yield break;
  247. }
  248. }
  249. else if (!isFlat && isFileFlat) {
  250. // Deepen structure
  251. yield return new FileInfo(Path.Combine(Path.Combine(nativePluginFolder.FullName, "x86"),
  252. relevantBit));
  253. yield return new FileInfo(Path.Combine(Path.Combine(nativePluginFolder.FullName, "x86_64"),
  254. relevantBit));
  255. }
  256. else {
  257. yield return to;
  258. }
  259. }
  260. else {
  261. yield return to;
  262. }
  263. }
  264. public static void ClearLine()
  265. {
  266. Console.SetCursorPosition(0, Console.CursorTop);
  267. int tpos = Console.CursorTop;
  268. Console.Write(new string(' ', Console.WindowWidth));
  269. Console.SetCursorPosition(0, tpos);
  270. }
  271. private static IEnumerable<FileInfo> PassThroughInterceptor(FileInfo from, FileInfo to) {
  272. yield return to;
  273. }
  274. public static void CopyAll(DirectoryInfo source, DirectoryInfo target, bool aggressive, BackupUnit backup,
  275. Func<FileInfo, FileInfo, IEnumerable<FileInfo>> interceptor = null) {
  276. if (interceptor == null) {
  277. interceptor = PassThroughInterceptor;
  278. }
  279. // Copy each file into the new directory.
  280. foreach (FileInfo fi in source.GetFiles()) {
  281. foreach (var targetFile in interceptor(fi, new FileInfo(Path.Combine(target.FullName, fi.Name)))) {
  282. if (!targetFile.Exists || targetFile.LastWriteTimeUtc < fi.LastWriteTimeUtc || aggressive) {
  283. targetFile.Directory.Create();
  284. Console.CursorTop--;
  285. ClearLine();
  286. Console.WriteLine(@"Copying {0}", targetFile.FullName);
  287. backup.Add(targetFile);
  288. fi.CopyTo(targetFile.FullName, true);
  289. }
  290. }
  291. }
  292. // Copy each subdirectory using recursion.
  293. foreach (DirectoryInfo diSourceSubDir in source.GetDirectories()) {
  294. DirectoryInfo nextTargetSubDir = new DirectoryInfo(Path.Combine(target.FullName, diSourceSubDir.Name));
  295. CopyAll(diSourceSubDir, nextTargetSubDir, aggressive, backup, interceptor);
  296. }
  297. }
  298. static void Fail(string message) {
  299. Console.Error.Write("ERROR: " + message);
  300. WaitForEnd();
  301. Environment.Exit(1);
  302. }
  303. public static string Args(params string[] args) {
  304. return string.Join(" ", args.Select(EncodeParameterArgument).ToArray());
  305. }
  306. /// <summary>
  307. /// Encodes an argument for passing into a program
  308. /// </summary>
  309. /// <param name="original">The value that should be received by the program</param>
  310. /// <returns>The value which needs to be passed to the program for the original value
  311. /// to come through</returns>
  312. public static string EncodeParameterArgument(string original) {
  313. if (string.IsNullOrEmpty(original))
  314. return original;
  315. string value = Regex.Replace(original, @"(\\*)" + "\"", @"$1\$0");
  316. value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");
  317. return value;
  318. }
  319. public static Architecture DetectArchitecture(string assembly) {
  320. using (var reader = new BinaryReader(File.OpenRead(assembly))) {
  321. var header = reader.ReadUInt16();
  322. if (header == 0x5a4d) {
  323. reader.BaseStream.Seek(60, SeekOrigin.Begin); // this location contains the offset for the PE header
  324. var peOffset = reader.ReadUInt32();
  325. reader.BaseStream.Seek(peOffset + 4, SeekOrigin.Begin);
  326. var machine = reader.ReadUInt16();
  327. if (machine == 0x8664) // IMAGE_FILE_MACHINE_AMD64
  328. return Architecture.x64;
  329. else if (machine == 0x014c) // IMAGE_FILE_MACHINE_I386
  330. return Architecture.x86;
  331. else if (machine == 0x0200) // IMAGE_FILE_MACHINE_IA64
  332. return Architecture.x64;
  333. else
  334. return Architecture.Unknown;
  335. }
  336. else {
  337. // Not a supported binary
  338. return Architecture.Unknown;
  339. }
  340. }
  341. }
  342. public abstract class Keyboard {
  343. [Flags]
  344. private enum KeyStates {
  345. None = 0,
  346. Down = 1,
  347. Toggled = 2
  348. }
  349. [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
  350. private static extern short GetKeyState(int keyCode);
  351. private static KeyStates GetKeyState(Keys key) {
  352. KeyStates state = KeyStates.None;
  353. short retVal = GetKeyState((int) key);
  354. //If the high-order bit is 1, the key is down
  355. //otherwise, it is up.
  356. if ((retVal & 0x8000) == 0x8000)
  357. state |= KeyStates.Down;
  358. //If the low-order bit is 1, the key is toggled.
  359. if ((retVal & 1) == 1)
  360. state |= KeyStates.Toggled;
  361. return state;
  362. }
  363. public static bool IsKeyDown(Keys key) {
  364. return KeyStates.Down == (GetKeyState(key) & KeyStates.Down);
  365. }
  366. public static bool IsKeyToggled(Keys key) {
  367. return KeyStates.Toggled == (GetKeyState(key) & KeyStates.Toggled);
  368. }
  369. }
  370. }
  371. }