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.

546 lines
22 KiB

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