Jelajahi Sumber

Version 3.1.0

 * FEATURE - Support for games with 'netstandard2.0' API surface through config option 'EnableExperimentalHooks'
 * BUG FIX - Bug fixes and improvements to Utage hooking implementation - EnableUtage config option also removed (always on now)
 * BUG FIX - Rich text parser bug fixes when only a single tag with no ending was used
 * BUG FIX - Fixed potential NullReferenceException in TextGetterCompatibilityMode
 * BUG FIX - Load translator assemblies even if a '#' is present in file path
 * MISC - Determine whether to disable certificate checks at config initialization based on scripting backend and unity version
randoman 6 tahun lalu
induk
melakukan
fce123cea2
47 mengubah file dengan 2043 tambahan dan 185 penghapusan
  1. 4 1
      .gitignore
  2. 2 1
      CHANGELOG.md
  3. 30 0
      README.md
  4. 142 0
      XUnity.AutoTranslator.sln
  5. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  6. 2 2
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  7. 4 0
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  8. 2 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  9. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs
  10. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  11. 1 2
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/ExceptionExtensions.cs
  12. 64 46
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/HarmonyInstanceExtensions.cs
  13. 35 11
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs
  14. 3 10
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs
  15. 0 52
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/JumpedMethodCaller.cs
  16. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/UtageHooks.cs
  17. 0 31
      src/XUnity.AutoTranslator.Plugin.Core/Kernel32.cs
  18. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextGetterCompatModeHelper.cs
  19. 2 1
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  20. 2 2
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  21. 2 2
      src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj
  22. 2 1
      src/XUnity.AutoTranslator.Setup/Program.cs
  23. 20 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.Designer.cs
  24. 6 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.resx
  25. 1 1
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj
  26. 169 0
      src/XUnity.RuntimeHooker.Core/HookCallbackInvoker.cs
  27. 25 0
      src/XUnity.RuntimeHooker.Core/HookMethod.cs
  28. 15 0
      src/XUnity.RuntimeHooker.Core/HookPriority.cs
  29. 21 0
      src/XUnity.RuntimeHooker.Core/JumpedMethodInfo.cs
  30. 18 0
      src/XUnity.RuntimeHooker.Core/TrampolineData.cs
  31. 82 0
      src/XUnity.RuntimeHooker.Core/TrampolineHandler.cs
  32. 13 0
      src/XUnity.RuntimeHooker.Core/Unmanaged/Kernel32.cs
  33. 34 0
      src/XUnity.RuntimeHooker.Core/Unmanaged/Protection.cs
  34. 81 0
      src/XUnity.RuntimeHooker.Core/Utilities/ExpressionHelper.cs
  35. 15 18
      src/XUnity.RuntimeHooker.Core/Utilities/MemoryHelper.cs
  36. 8 0
      src/XUnity.RuntimeHooker.Core/XUnity.RuntimeHooker.Core.csproj
  37. 142 0
      src/XUnity.RuntimeHooker.Trampolines/TrampolineInitializer.cs
  38. 555 0
      src/XUnity.RuntimeHooker.Trampolines/Trampolines.cs
  39. 12 0
      src/XUnity.RuntimeHooker.Trampolines/XUnity.RuntimeHooker.Trampolines.csproj
  40. 73 0
      src/XUnity.RuntimeHooker/Properties/Resources.Designer.cs
  41. 124 0
      src/XUnity.RuntimeHooker/Properties/Resources.resx
  42. 147 0
      src/XUnity.RuntimeHooker/RuntimeMethodPatcher.cs
  43. 30 0
      src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj
  44. 53 0
      test/XUnity.RuntimeHooker.Benchmark/Program.cs
  45. 12 0
      test/XUnity.RuntimeHooker.Benchmark/XUnity.RuntimeHooker.Benchmark.csproj
  46. 72 0
      test/XUnity.RuntimeHooker.ConsoleTests/Program.cs
  47. 13 0
      test/XUnity.RuntimeHooker.ConsoleTests/XUnity.RuntimeHooker.ConsoleTests.csproj

+ 4 - 1
.gitignore

@@ -1,4 +1,4 @@
-## Ignore Visual Studio temporary files, build results, and
+## Ignore Visual Studio temporary files, build results, and
 ## files generated by popular Visual Studio add-ons.
 
 # User-specific files
@@ -252,3 +252,6 @@ paket-files/
 
 # Distribution
 dist/
+
+# Trampoline dll
+**/b979b301af8b4ef48f224de6dcf2ddf6.dll

+ 2 - 1
CHANGELOG.md

@@ -1,4 +1,5 @@
-### 3.0.3
+### 3.1.0
+ * FEATURE - Support for games with 'netstandard2.0' API surface through config option 'EnableExperimentalHooks'
  * BUG FIX - Bug fixes and improvements to Utage hooking implementation - EnableUtage config option also removed (always on now)
  * BUG FIX - Rich text parser bug fixes when only a single tag with no ending was used
  * BUG FIX - Fixed potential NullReferenceException in TextGetterCompatibilityMode

+ 30 - 0
README.md

@@ -8,6 +8,7 @@
  * [Text Frameworks](#text-frameworks)
  * [Plugin Frameworks](#plugin-frameworks)
  * [Configuration](#configuration)
+ * [Frequently Asked Questions](#frequently-asked-questions)
  * [Translating Mods](#translating-mods)
  * [Manual Translations](#manual-translations)
  * [Regarding Redistribution](#regarding-redistribution)
@@ -235,6 +236,7 @@ GameLogTextPaths=                ;Indicates specific paths for game objects that
 RomajiPostProcessing=ReplaceMacronWithCircumflex;RemoveApostrophes ;Indicates what type of post processing to do on 'translated' romaji texts. This can be important in certain games because the font used does not support various diacritics properly. This is a list seperated by ';'. Possible values: ["RemoveAllDiacritics", "ReplaceMacronWithCircumflex", "RemoveApostrophes"]
 TranslationPostProcessing=ReplaceMacronWithCircumflex ;Indicates what type of post processing to do on translated texts (not romaji). Possible values: ["RemoveAllDiacritics", "ReplaceMacronWithCircumflex", "RemoveApostrophes"]
 EnableExperimentalHooks=False    ;Indicates whether to use experimental hooks to improve the hooking capability of the plugin (currently being tested)
+ForceExperimentalHooks=False     ;Indicates that the plugin must use experimental hooks instead of harmony hooks. Only used for debugging
 
 [Texture]
 TextureDirectory=Translation\Texture ;Directory to dump textures to, and root of directories to load images from. Can use placeholder: {GameExeName}
@@ -333,12 +335,39 @@ To rememdy this, post processing can be applied to translations when 'romaji' is
 
 This type of post processing is also applied to normal translations, but instead uses the option `TranslationPostProcessing`, which can use the same values.
 
+#### Experimental Hooks
+Experimental hooks are hooks are created at runtime, but not through the Harmony dependency. Harmony has two primary problems that these hooks attempt to solve:
+ * Harmony cannot hook methods with no body.
+ * Harmony cannot hook methods under the `netstandard2.0` API surface, which later versions of Unity can be build under.
+
+The experimental hooks implemented in this library can do this. However, unless it is absolutely required by the game, it is not recommended to enable them.
+
+The following configuration controls the experimental hooks:
+ * `EnableExperimentalHooks`: Will allow the plugin to use these hooks, if a standard Harmony hook cannot be applied.
+ * `ForceExperimentalHooks`: Forces the plugin to use experimental hooks over Harmony hooks. `EnableExperimentalHooks` must also be enabled for this to work. Only used for debugging.
+
 #### Other Options
  * `TextGetterCompatibilityMode`: This mode fools the game into thinking that the text displayed is not translated. This is required if the game uses text displayed to the user to determine what logic to execute. You can easily determine if this is required if you can see the functionality works fine if you toggle the translation off (hotkey: ALT+T).
  * `IgnoreTextStartingWith`: Disable translation for any texts starting with values in this ';-separated' setting. The [default value](https://www.charbase.com/180e-unicode-mongolian-vowel-separator) is an invisible character that takes up no space.
  * `CopyToClipboard`: Copy text to translate to the clipboard to support tools such as Translation Aggregator.
  * `Delay`: Required delay from a text appears until a translation request is queued in seconds. IMGUI not supported.
 
+## Frequently Asked Questions
+> **Q: Why doesn't this plugin work in game X?**  
+A: If the plugin does not work in a game, you can try to set the following configuration parameter `DisableCertificateValidation=True`. If it still does not work, it likely uses a technique to display text that this plugin is not aware of. In that case you can make an issue and maybe it will get fixed in a later version.
+
+> **Q: The game stops working when this plugin applies translations.**  
+A: Try setting the following configuration parameter `TextGetterCompatibilityMode=True`.
+
+> **Q: Can this plugin translate other plugins/mods?**  
+A: Likely yes, see [here](#translating-mods).
+
+> **Q: How do I use CustomTranslate?**  
+A: If you have to ask, you probably can't. CustomTranslate is intended for developers of a translation service. They would be able to expose an API that conforms to CustomTranslate's API specification without needing to implement a custom ITranslateEndpoint in this plugin as well.
+
+> **Q: Please provide support for translation service X.**  
+A: For now, additional support for services that does not require some form of authentication is unlikely. Do note though, that it is possible to implement custom translators independently of this plugin. And it takes remarkably little code to do so.
+
 ## Translating Mods
 Often other mods UI are implemented through IMGUI. As you can see above, this is disabled by default. By changing the "EnableIMGUI" value to "True", it will start translating IMGUI as well, which likely means that other mods UI will be translated.
 
@@ -355,6 +384,7 @@ In this context, the `Translation\_AutoGeneratedTranslations.{lang}.txt` (Output
 Redistributing this plugin for various games is absolutely encouraged. However, if you do so, please keep the following in mind:
  * **Distribute the _AutoGeneratedTranslations.{lang}.txt file along with the redistribution with as many translations as possible to ensure the online translator is hit as little as possible.**
  * **Do not redistribute the mod with the configuration option `EnableIMGUI=True`.**
+ * **Test your redistribution with logging/console enabled to ensure the game does not exhibit undesirable behaviour such as spamming the endpoints.**
  * Ensure you keep the plugin up-to-date, as much as reasonably possible.
  * If you use image loading feature, make sure you read [this section](#texture-translation).
 

+ 142 - 0
XUnity.AutoTranslator.sln

@@ -68,92 +68,229 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Setup
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Setup.Build-x86", "src\XUnity.AutoTranslator.Setup.Build-x86\XUnity.AutoTranslator.Setup.Build-x86.csproj", "{1C574D77-FCC8-4249-8890-0B00EF092705}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker", "src\XUnity.RuntimeHooker\XUnity.RuntimeHooker.csproj", "{15A1D255-25F3-4D10-8053-F24B32C1CB7B}"
+	ProjectSection(ProjectDependencies) = postProject
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A} = {BE7ED338-106C-4CC6-BA19-8ADB2534326A}
+	EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Core", "src\XUnity.RuntimeHooker.Core\XUnity.RuntimeHooker.Core.csproj", "{921151C6-C532-44E3-92A1-283E5094171F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Trampolines", "src\XUnity.RuntimeHooker.Trampolines\XUnity.RuntimeHooker.Trampolines.csproj", "{BE7ED338-106C-4CC6-BA19-8ADB2534326A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.ConsoleTests", "test\XUnity.RuntimeHooker.ConsoleTests\XUnity.RuntimeHooker.ConsoleTests.csproj", "{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Benchmark", "test\XUnity.RuntimeHooker.Benchmark\XUnity.RuntimeHooker.Benchmark.csproj", "{E2F50278-9134-4DC8-9C50-4C0A52063A89}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
+		Debug|x86 = Debug|x86
 		Release|Any CPU = Release|Any CPU
+		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|x86.Build.0 = Debug|Any CPU
 		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|Any CPU.Build.0 = Release|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|x86.ActiveCfg = Release|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|x86.Build.0 = Release|Any CPU
 		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|x86.Build.0 = Debug|Any CPU
 		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|x86.ActiveCfg = Release|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|x86.Build.0 = Release|Any CPU
 		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|x86.Build.0 = Debug|Any CPU
 		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|x86.ActiveCfg = Release|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|x86.Build.0 = Release|Any CPU
 		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|x86.Build.0 = Debug|Any CPU
 		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|x86.ActiveCfg = Release|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|x86.Build.0 = Release|Any CPU
 		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|x86.Build.0 = Debug|Any CPU
 		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|x86.ActiveCfg = Release|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|x86.Build.0 = Release|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Debug|x86.Build.0 = Debug|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|Any CPU.Build.0 = Release|Any CPU
+		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|x86.ActiveCfg = Release|Any CPU
+		{12F1D16B-B8E1-4A9D-B65A-044650F15440}.Release|x86.Build.0 = Release|Any CPU
 		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Debug|x86.Build.0 = Debug|Any CPU
 		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|x86.ActiveCfg = Release|Any CPU
+		{7493BA4A-C688-4103-B161-7E578FBB6C0E}.Release|x86.Build.0 = Release|Any CPU
 		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Debug|x86.Build.0 = Debug|Any CPU
 		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|x86.ActiveCfg = Release|Any CPU
+		{5D9A4F4D-D2D2-4D2B-A58A-F7529DDAF1D0}.Release|x86.Build.0 = Release|Any CPU
 		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Debug|x86.Build.0 = Debug|Any CPU
 		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|Any CPU.Build.0 = Release|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|x86.ActiveCfg = Release|Any CPU
+		{95C83913-8D7F-4FA1-877E-288251A2A461}.Release|x86.Build.0 = Release|Any CPU
 		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Debug|x86.Build.0 = Debug|Any CPU
 		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|x86.ActiveCfg = Release|Any CPU
+		{DDE97F5D-E021-45C0-87B6-FAD35BC8BCFB}.Release|x86.Build.0 = Release|Any CPU
 		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Debug|x86.Build.0 = Debug|Any CPU
 		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|x86.ActiveCfg = Release|Any CPU
+		{F49BA6D1-FAFB-414C-97B1-28B77FC21286}.Release|x86.Build.0 = Release|Any CPU
 		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Debug|x86.Build.0 = Debug|Any CPU
 		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|x86.ActiveCfg = Release|Any CPU
+		{CEA7D2D8-C2CF-4FA3-8184-DD485160CDAC}.Release|x86.Build.0 = Release|Any CPU
 		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Debug|x86.Build.0 = Debug|Any CPU
 		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|x86.ActiveCfg = Release|Any CPU
+		{42DEC139-7E8E-4E2E-9BC9-0B5661F444C5}.Release|x86.Build.0 = Release|Any CPU
 		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Debug|x86.Build.0 = Debug|Any CPU
 		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|x86.ActiveCfg = Release|Any CPU
+		{A74F9486-2BA0-42FF-8BBC-9F07ECFBDCE5}.Release|x86.Build.0 = Release|Any CPU
 		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Debug|x86.Build.0 = Debug|Any CPU
 		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|x86.ActiveCfg = Release|Any CPU
+		{AE28F88E-E877-456B-98AB-BD03A59A3E44}.Release|x86.Build.0 = Release|Any CPU
 		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|Any CPU.ActiveCfg = Debug|x86
 		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|Any CPU.Build.0 = Debug|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|x86.ActiveCfg = Debug|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Debug|x86.Build.0 = Debug|x86
 		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|Any CPU.ActiveCfg = Release|x86
 		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|Any CPU.Build.0 = Release|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|x86.ActiveCfg = Release|x86
+		{AD0C7EF2-D394-43B5-9CCE-FA8A0A820076}.Release|x86.Build.0 = Release|x86
 		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Debug|x86.Build.0 = Debug|Any CPU
 		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|Any CPU.Build.0 = Release|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|x86.ActiveCfg = Release|Any CPU
+		{41B612A5-2974-4988-A82E-84EEF2212A61}.Release|x86.Build.0 = Release|Any CPU
 		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Debug|x86.Build.0 = Debug|Any CPU
 		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|Any CPU.Build.0 = Release|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|x86.ActiveCfg = Release|Any CPU
+		{65500234-7AD4-48B8-B51B-945ADBFF1133}.Release|x86.Build.0 = Release|Any CPU
 		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Debug|x86.Build.0 = Debug|Any CPU
 		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|x86.ActiveCfg = Release|Any CPU
+		{848D741E-A4F2-4680-AFD2-74C8723CE3C4}.Release|x86.Build.0 = Release|Any CPU
 		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|Any CPU.ActiveCfg = Debug|x86
 		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|Any CPU.Build.0 = Debug|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|x86.ActiveCfg = Debug|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Debug|x86.Build.0 = Debug|x86
 		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|Any CPU.ActiveCfg = Release|x86
 		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|Any CPU.Build.0 = Release|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|x86.ActiveCfg = Release|x86
+		{1C574D77-FCC8-4249-8890-0B00EF092705}.Release|x86.Build.0 = Release|x86
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Debug|x86.Build.0 = Debug|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Release|x86.ActiveCfg = Release|Any CPU
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B}.Release|x86.Build.0 = Release|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Debug|x86.Build.0 = Debug|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Release|x86.ActiveCfg = Release|Any CPU
+		{921151C6-C532-44E3-92A1-283E5094171F}.Release|x86.Build.0 = Release|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Debug|x86.Build.0 = Debug|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Release|x86.ActiveCfg = Release|Any CPU
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A}.Release|x86.Build.0 = Release|Any CPU
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Debug|x86.ActiveCfg = Debug|x86
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Debug|x86.Build.0 = Debug|x86
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Release|x86.ActiveCfg = Release|x86
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3}.Release|x86.Build.0 = Release|x86
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Debug|x86.Build.0 = Debug|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Release|x86.ActiveCfg = Release|Any CPU
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -180,6 +317,11 @@ Global
 		{65500234-7AD4-48B8-B51B-945ADBFF1133} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{848D741E-A4F2-4680-AFD2-74C8723CE3C4} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{1C574D77-FCC8-4249-8890-0B00EF092705} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{15A1D255-25F3-4D10-8053-F24B32C1CB7B} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{921151C6-C532-44E3-92A1-283E5094171F} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{BE7ED338-106C-4CC6-BA19-8ADB2534326A} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{20E57B16-F3C7-4D80-88FC-09D6C5A4CCC3} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
+		{E2F50278-9134-4DC8-9C50-4C0A52063A89} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}

+ 1 - 1
src/XUnity.AutoTranslator.Patcher/Patcher.cs

@@ -29,7 +29,7 @@ namespace XUnity.AutoTranslator.Patcher
       {
          get
          {
-            return "3.0.2";
+            return "3.1.0";
          }
       }
 

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.0.2</Version>
+      <Version>3.1.0</Version>
    </PropertyGroup>
 
    <ItemGroup>
@@ -31,7 +31,7 @@
       <ItemGroup>
          <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
       </ItemGroup>
-      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\BepIn\BepInEx' -DestinationPath '$(SolutionDir)dist\BepIn\XUnity.AutoTranslator-BepIn-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\BepIn\BepInEx' -DestinationPath '$(SolutionDir)dist\BepIn\XUnity.AutoTranslator-BepIn-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
    </Target>
 
 </Project>

+ 4 - 0
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -1675,6 +1675,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
                            SetTranslatedText( ui, translation, info );
                            return translation;
                         }
+                        //else if( isWhitelisted )
+                        //{
+                        //   return null;
+                        //}
                      }
                   }
                }

+ 2 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs

@@ -86,6 +86,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static TextPostProcessing RomajiPostProcessing;
       public static TextPostProcessing TranslationPostProcessing;
       public static bool EnableExperimentalHooks;
+      public static bool ForceExperimentalHooks;
 
       public static string TextureDirectory;
       public static bool EnableTextureTranslation;
@@ -158,6 +159,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          RomajiPostProcessing = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "RomajiPostProcessing", TextPostProcessing.ReplaceMacronWithCircumflex | TextPostProcessing.RemoveApostrophes );
          TranslationPostProcessing = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "TranslationPostProcessing", TextPostProcessing.ReplaceMacronWithCircumflex );
          EnableExperimentalHooks = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "EnableExperimentalHooks", false );
+         ForceExperimentalHooks = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "ForceExperimentalHooks", false );
 
          TextureDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "TextureDirectory", @"Translation\Texture" );
          EnableTextureTranslation = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureTranslation", false );

+ 1 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs

@@ -47,6 +47,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Constants
       public static readonly Type Typewriter = FindType( "Typewriter" );
 
       // Utage
+      //public static readonly Type AdvCommand = FindType( "Utage.AdvCommand" );
       public static readonly Type UguiNovelText = FindType( "Utage.UguiNovelText" );
       public static readonly Type AdvEngine = FindType( "Utage.AdvEngine" );
       public static readonly Type AdvPage = FindType( "Utage.AdvPage" );

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs

@@ -23,6 +23,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Constants
       /// <summary>
       /// Gets the version of the plugin.
       /// </summary>
-      public const string Version = "3.0.2";
+      public const string Version = "3.1.0";
    }
 }

+ 1 - 2
src/XUnity.AutoTranslator.Plugin.Core/Extensions/ExceptionExtensions.cs

@@ -12,8 +12,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          var current = e;
          while( current != null )
          {
-            if( e is TException ) return true;
-
+            if( current is TException ) return true;
             current = current.InnerException;
          }
 

+ 64 - 46
src/XUnity.AutoTranslator.Plugin.Core/Extensions/HarmonyInstanceExtensions.cs

@@ -9,6 +9,8 @@ using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Hooks;
 using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.RuntimeHooker;
+using XUnity.RuntimeHooker.Core;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
@@ -26,73 +28,82 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
       public static void PatchType( this HarmonyInstance instance, Type type )
       {
+         MethodBase original = null;
          try
          {
-            var prepare = type.GetMethod( "Prepare", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
+            var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
+            var prepare = type.GetMethod( "Prepare", flags );
             if( prepare == null || (bool)prepare.Invoke( null, new object[] { instance } ) )
             {
-               var original = (MethodBase)type.GetMethod( "TargetMethod", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public ).Invoke( null, new object[] { instance } );
+               original = (MethodBase)type.GetMethod( "TargetMethod", flags ).Invoke( null, new object[] { instance } );
                if( original != null )
                {
-                  var overrider = type.GetMethod( "Override", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
-                  var callerProperty = type.GetProperty( "Caller", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
+                  var requireRuntimeHooker = (bool?)type.GetProperty( "RequireRuntimeHooker", flags )?.GetValue( null, null ) == true;
 
-                  if( overrider != null && callerProperty != null )
+                  var priority = type.GetCustomAttributes( typeof( HarmonyPriority ), false )
+                     .OfType<HarmonyPriority>()
+                     .FirstOrDefault()
+                     ?.info.prioritiy;
+
+                  var prefix = type.GetMethod( "Prefix", flags );
+                  var postfix = type.GetMethod( "Postfix", flags );
+                  var transpiler = type.GetMethod( "Transpiler", flags );
+
+                  var harmonyPrefix = prefix != null ? new HarmonyMethod( prefix ) : null;
+                  var harmonyPostfix = postfix != null ? new HarmonyMethod( postfix ) : null;
+                  var harmonyTranspiler = transpiler != null ? new HarmonyMethod( transpiler ) : null;
+
+                  if( priority.HasValue )
                   {
-                     if( Settings.EnableExperimentalHooks )
+                     if( harmonyPrefix != null )
                      {
-                        long replacementMethodLocation = MemoryHelper.GetMethodStartLocation( overrider );
-                        long originalMethodLocation = MemoryHelper.GetMethodStartLocation( original );
-                        byte[] originalCode = null;
+                        harmonyPrefix.prioritiy = priority.Value;
+                     }
 
-                        try
-                        {
-                           originalCode = MemoryHelper.GetInstructionsAtLocationRequiredToWriteJump( originalMethodLocation );
-                           var caller = new JumpedMethodCaller( originalMethodLocation, replacementMethodLocation, originalCode );
-                           callerProperty.SetValue( null, caller, null );
+                     if( harmonyPostfix != null )
+                     {
+                        harmonyPostfix.prioritiy = priority.Value;
+                     }
+                  }
 
-                           MemoryHelper.WriteJump( true, originalMethodLocation, replacementMethodLocation );
-                        }
-                        catch
-                        {
-                           if( originalCode != null )
-                           {
-                              MemoryHelper.RestoreInstructionsAtLocation( true, originalMethodLocation, originalCode );
-                           }
+                  if( requireRuntimeHooker || Settings.ForceExperimentalHooks )
+                  {
+                     if( Settings.EnableExperimentalHooks )
+                     {
+                        XuaLogger.Current.Info( $"Hooking '{original.DeclaringType.FullName}.{original.Name}' through experimental hooks." );
 
-                           throw;
-                        }
+                        var hookPrefix = harmonyPrefix != null ? new HookMethod( harmonyPrefix.method, harmonyPrefix.prioritiy ) : null;
+                        var hookPostfix = harmonyPostfix != null ? new HookMethod( harmonyPostfix.method, harmonyPostfix.prioritiy ) : null;
+
+                        RuntimeMethodPatcher.Patch( original, hookPrefix, hookPostfix );
+                     }
+                     else
+                     {
+                        XuaLogger.Current.Info( $"Skipping hook on '{original.DeclaringType.FullName}.{original.Name}' because it requires 'EnableExperimentalHooks' enabled is config file." );
                      }
                   }
                   else
                   {
-                     var priority = type.GetCustomAttributes( typeof( HarmonyPriority ), false )
-                        .OfType<HarmonyPriority>()
-                        .FirstOrDefault()
-                        ?.info.prioritiy;
-
-                     var prefix = type.GetMethod( "Prefix", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
-                     var postfix = type.GetMethod( "Postfix", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
-                     var transpiler = type.GetMethod( "Transpiler", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public );
-
-                     var harmonyPrefix = prefix != null ? new HarmonyMethod( prefix ) : null;
-                     var harmonyPostfix = postfix != null ? new HarmonyMethod( postfix ) : null;
-                     var harmonyTranspiler = transpiler != null ? new HarmonyMethod( transpiler ) : null;
-
-                     if( priority.HasValue )
+                     try
+                     {
+                        PatchMethod.Invoke( instance, new object[] { original, harmonyPrefix, harmonyPostfix, harmonyTranspiler } );
+                     }
+                     catch( Exception e ) when( e.IsCausedBy<PlatformNotSupportedException>() )
                      {
-                        if( harmonyPrefix != null )
+                        if( Settings.EnableExperimentalHooks )
                         {
-                           harmonyPrefix.prioritiy = priority.Value;
-                        }
+                           XuaLogger.Current.Info( $"Harmony is not supported in this runtime. Hooking '{original.DeclaringType.FullName}.{original.Name}' through experimental hooks." );
 
-                        if( harmonyPostfix != null )
+                           var hookPrefix = harmonyPrefix != null ? new HookMethod( harmonyPrefix.method, harmonyPrefix.prioritiy ) : null;
+                           var hookPostfix = harmonyPostfix != null ? new HookMethod( harmonyPostfix.method, harmonyPostfix.prioritiy ) : null;
+
+                           RuntimeMethodPatcher.Patch( original, hookPrefix, hookPostfix );
+                        }
+                        else
                         {
-                           harmonyPostfix.prioritiy = priority.Value;
+                           XuaLogger.Current.Error( $"Cannot hook '{original.DeclaringType.FullName}.{original.Name}'. Harmony is not supported in this runtime. You can try to 'EnableExperimentalHooks' in the config file to enable support for this game." );
                         }
                      }
-
-                     PatchMethod.Invoke( instance, new object[] { original, harmonyPrefix, harmonyPostfix, harmonyTranspiler } );
                   }
                }
                else
@@ -103,7 +114,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          }
          catch( Exception e )
          {
-            XuaLogger.Current.Warn( e, "An error occurred while patching a property/method on a class. Failing class: " + type.Name );
+            if( original != null )
+            {
+               XuaLogger.Current.Warn( e, $"An error occurred while patching property/method '{original.DeclaringType.FullName}.{original.Name}'." );
+            }
+            else
+            {
+               XuaLogger.Current.Warn( e, $"An error occurred while patching a property/method. Failing hook: '{type.Name}'." );
+            }
          }
       }
    }

+ 35 - 11
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs

@@ -25,7 +25,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return ( Settings.EnableUGUI && ui is Text )
             || ( Settings.EnableIMGUI && ui is GUIContent )
             || ( Settings.EnableNGUI && ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type ) )
-            || ( Settings.EnableTextMeshPro && ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) );
+            || ( Settings.EnableTextMeshPro && ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) )
+            /*|| ( ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) )*/;
       }
 
       public static bool SupportsRichText( this object ui )
@@ -36,7 +37,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
          return ( ui as Text )?.supportRichText == true
             || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) && Equals( type.GetProperty( RichTextPropertyName )?.GetValue( ui, null ), true ) )
-            || ( ClrTypes.UguiNovelText != null && ClrTypes.UguiNovelText.IsAssignableFrom( type ) );
+            || ( ClrTypes.UguiNovelText != null && ClrTypes.UguiNovelText.IsAssignableFrom( type ) )
+            /*|| ( ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) )*/;
       }
 
       public static bool SupportsStabilization( this object ui )
@@ -135,21 +137,43 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
                   var textData = ClrTypes.TextData.GetConstructor( new[] { typeof( string ) } ).Invoke( new[] { text } );
                   var length = (int)textData.GetType().GetProperty( "Length", flags ).GetValue( textData, null );
 
-                  try
+                  var remakeTextData = page.GetType().GetMethod( "RemakeTextData", flags );
+                  if( remakeTextData == null )
                   {
-                     Settings.InvokeEvents = false;
-                     Settings.RemakeTextData = advPage =>
+                     try
                      {
-                        advPage.GetType().GetProperty( "TextData", flags ).SetValue( page, textData, null );
+                        Settings.InvokeEvents = false;
+
+                        page.GetType().GetProperty( "TextData", flags ).SetValue( page, textData, null );
                         page.GetType().GetProperty( "CurrentTextLengthMax", flags ).GetSetMethod( true ).Invoke( page, new object[] { length } );
-                     };
+                        page.GetType().GetProperty( "Status", flags ).GetSetMethod( true ).Invoke( page, new object[] { 1 /*SendChar*/ } );
 
-                     page.GetType().GetMethod( "RemakeText" ).Invoke( page, null );
+                        var messageWindowManager = engine.GetType().GetProperty( "MessageWindowManager", flags ).GetValue( engine, null );
+                        messageWindowManager.GetType().GetMethod( "OnPageTextChange", flags ).Invoke( messageWindowManager, new object[] { page } );
+                     }
+                     finally
+                     {
+                        Settings.InvokeEvents = true;
+                     }
                   }
-                  finally
+                  else
                   {
-                     Settings.InvokeEvents = true;
-                     Settings.RemakeTextData = null;
+                     try
+                     {
+                        Settings.InvokeEvents = false;
+                        Settings.RemakeTextData = advPage =>
+                        {
+                           advPage.GetType().GetProperty( "TextData", flags ).SetValue( page, textData, null );
+                           advPage.GetType().GetProperty( "CurrentTextLengthMax", flags ).GetSetMethod( true ).Invoke( page, new object[] { length } );
+                        };
+
+                        page.GetType().GetMethod( "RemakeText" ).Invoke( page, null );
+                     }
+                     finally
+                     {
+                        Settings.InvokeEvents = true;
+                        Settings.RemakeTextData = null;
+                     }
                   }
 
                   if( nameText )

+ 3 - 10
src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs

@@ -58,21 +58,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          return AccessTools.Property( ClrTypes.Sprite, "texture" )?.GetGetMethod();
       }
 
-      static object Override( object __instance )
+      static void Postfix( Sprite __instance )
       {
-         return Caller.Func( () => ActualOverride( __instance ) );
-      }
-
-      static object ActualOverride( object __instance )
-      {
-         var texture = ( (Sprite)__instance ).texture;
+         var texture = __instance.texture;
 
          AutoTranslationPlugin.Current.Hook_ImageChanged( texture, true );
-
-         return texture;
       }
 
-      static JumpedMethodCaller Caller { get; set; }
+      static bool RequireRuntimeHooker => true;
    }
 
    [Harmony]

+ 0 - 52
src/XUnity.AutoTranslator.Plugin.Core/Hooks/JumpedMethodCaller.cs

@@ -1,52 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Harmony.ILCopying;
-using XUnity.AutoTranslator.Plugin.Core.Extensions;
-using XUnity.AutoTranslator.Plugin.Core.Utilities;
-
-namespace XUnity.AutoTranslator.Plugin.Core.Hooks
-{
-   internal class JumpedMethodCaller
-   {
-      private readonly long _originalMethodLocation;
-      private readonly long _replacementMethodLocation;
-      private readonly byte[] _originalCode;
-
-      public JumpedMethodCaller( long originalMethodLocation, long replacementMethodLocation, byte[] originalCode )
-      {
-         _originalMethodLocation = originalMethodLocation;
-         _replacementMethodLocation = replacementMethodLocation;
-         _originalCode = originalCode;
-      }
-
-      public object Func( Func<object> call )
-      {
-         try
-         {
-            MemoryHelper.RestoreInstructionsAtLocation( false, _originalMethodLocation, _originalCode );
-
-            return call();
-         }
-         finally
-         {
-            MemoryHelper.WriteJump( false, _originalMethodLocation, _replacementMethodLocation );
-         }
-      }
-
-      public void Action( Action call )
-      {
-         try
-         {
-            MemoryHelper.RestoreInstructionsAtLocation( false, _originalMethodLocation, _originalCode );
-
-            call();
-         }
-         finally
-         {
-            MemoryHelper.WriteJump( false, _originalMethodLocation, _replacementMethodLocation );
-         }
-      }
-   }
-}

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Hooks/UtageHooks.cs

@@ -26,7 +26,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
    //{
    //   static bool Prepare( HarmonyInstance instance )
    //   {
-   //      return ClrTypes.AdvCommand != null;
+   //      return ClrTypes.AdvCommand != null && AdvPage_RemakeTextData_Hook.TargetMethod( instance ) == null;
    //   }
 
    //   static MethodBase TargetMethod( HarmonyInstance instance )

+ 0 - 31
src/XUnity.AutoTranslator.Plugin.Core/Kernel32.cs

@@ -55,36 +55,5 @@ namespace XUnity.AutoTranslator.Plugin.Core
           int cbMultiByte,
           [Out, MarshalAs( UnmanagedType.LPArray )] char[] lpWideCharStr,
           int cchWideChar );
-
-      [DllImport( "kernel32.dll" )]
-      internal static extern bool VirtualProtect( IntPtr lpAddress, UIntPtr dwSize, Protection flNewProtect, out Protection lpflOldProtect );
-   }
-
-   /// <summary>A bit-field of flags for protections</summary>
-   [Flags]
-   internal enum Protection
-   {
-      /// <summary>No access</summary>
-      PAGE_NOACCESS = 0x01,
-      /// <summary>Read only</summary>
-      PAGE_READONLY = 0x02,
-      /// <summary>Read write</summary>
-      PAGE_READWRITE = 0x04,
-      /// <summary>Write copy</summary>
-      PAGE_WRITECOPY = 0x08,
-      /// <summary>No access</summary>
-      PAGE_EXECUTE = 0x10,
-      /// <summary>Execute read</summary>
-      PAGE_EXECUTE_READ = 0x20,
-      /// <summary>Execute read write</summary>
-      PAGE_EXECUTE_READWRITE = 0x40,
-      /// <summary>Execute write copy</summary>
-      PAGE_EXECUTE_WRITECOPY = 0x80,
-      /// <summary>guard</summary>
-      PAGE_GUARD = 0x100,
-      /// <summary>No cache</summary>
-      PAGE_NOCACHE = 0x200,
-      /// <summary>Write combine</summary>
-      PAGE_WRITECOMBINE = 0x400
    }
 }

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextGetterCompatModeHelper.cs

@@ -24,7 +24,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
          {
             // 0. This method
             // 1. Postfix
-            // 2. Harmony-related method
+            // 2. Harmony-related trampoline method
             // 3. Original method
             var callingMethod = new StackFrame( 3 ).GetMethod();
 

+ 2 - 1
src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.0.2</Version>
+      <Version>3.1.0</Version>
       <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    </PropertyGroup>
 
@@ -27,6 +27,7 @@
 
    <ItemGroup>
      <ProjectReference Include="..\XUnity.AutoTranslator.Plugin.ExtProtocol\XUnity.AutoTranslator.Plugin.ExtProtocol.csproj" />
+     <ProjectReference Include="..\XUnity.RuntimeHooker\XUnity.RuntimeHooker.csproj" />
    </ItemGroup>
 
    <ItemGroup>

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.0.2</Version>
+      <Version>3.1.0</Version>
    </PropertyGroup>
 
    <ItemGroup>
@@ -28,7 +28,7 @@
       <ItemGroup>
          <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
       </ItemGroup>
-      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\IPA\Plugins\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\IPA\Plugins\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\IPA\Plugins' -DestinationPath '$(SolutionDir)dist\IPA\XUnity.AutoTranslator-IPA-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\IPA\Plugins\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.Core.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\IPA\Plugins\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\IPA\Plugins' -DestinationPath '$(SolutionDir)dist\IPA\XUnity.AutoTranslator-IPA-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
    </Target>
 
 </Project>

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.0.2</Version>
+      <Version>3.1.0</Version>
    </PropertyGroup>
 
    <ItemGroup>
@@ -28,7 +28,7 @@
       <ItemGroup>
          <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
       </ItemGroup>
-      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\Config\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\UnityInjector\UnityInjector' -DestinationPath '$(SolutionDir)dist\UnityInjector\XUnity.AutoTranslator-UnityInjector-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   for %%f in (&quot;$(SolutionDir)dist\Translators\*&quot;) do XCOPY /Y /I &quot;%%f&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\Config\Translators\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.ExtProtocol.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.Core.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.RuntimeHooker.dll&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\&quot;&#xD;&#xA;   COPY /Y &quot;$(SolutionDir)README.md&quot; &quot;$(SolutionDir)dist\UnityInjector\UnityInjector\README (AutoTranslator).md&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\UnityInjector\UnityInjector' -DestinationPath '$(SolutionDir)dist\UnityInjector\XUnity.AutoTranslator-UnityInjector-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
    </Target>
 
 </Project>

+ 2 - 1
src/XUnity.AutoTranslator.Setup/Program.cs

@@ -61,7 +61,8 @@ namespace XUnity.AutoTranslator.Setup
             AddFile( Path.Combine( managedDir, "ReiPatcher.exe" ), Resources.ReiPatcher );
             AddFile( Path.Combine( managedDir, "XUnity.AutoTranslator.Plugin.Core.dll" ), Resources.XUnity_AutoTranslator_Plugin_Core, true );
             AddFile( Path.Combine( managedDir, "XUnity.AutoTranslator.Plugin.ExtProtocol.dll" ), Resources.XUnity_AutoTranslator_Plugin_ExtProtocol, true );
-            //AddFile( Path.Combine( managedDir, "XUnity.RuntimeHooker.dll" ), Resources.XUnity_RuntimeHooker, true );
+            AddFile( Path.Combine( managedDir, "XUnity.RuntimeHooker.dll" ), Resources.XUnity_RuntimeHooker, true );
+            AddFile( Path.Combine( managedDir, "XUnity.RuntimeHooker.Core.dll" ), Resources.XUnity_RuntimeHooker_Core, true );
 
             // create an .ini file for each launcher, if it does not already exist
             var iniInfo = new FileInfo( Path.Combine( reiPath, Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".ini" ) );

+ 20 - 0
src/XUnity.AutoTranslator.Setup/Properties/Resources.Designer.cs

@@ -260,6 +260,26 @@ namespace XUnity.AutoTranslator.Setup.Properties {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized resource of type System.Byte[].
+        /// </summary>
+        internal static byte[] XUnity_RuntimeHooker {
+            get {
+                object obj = ResourceManager.GetObject("XUnity_RuntimeHooker", resourceCulture);
+                return ((byte[])(obj));
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized resource of type System.Byte[].
+        /// </summary>
+        internal static byte[] XUnity_RuntimeHooker_Core {
+            get {
+                object obj = ResourceManager.GetObject("XUnity_RuntimeHooker_Core", resourceCulture);
+                return ((byte[])(obj));
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized resource of type System.Byte[].
         /// </summary>

+ 6 - 0
src/XUnity.AutoTranslator.Setup/Properties/Resources.resx

@@ -151,6 +151,12 @@
    <data name="XUnity_AutoTranslator_Plugin_ExtProtocol" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\XUnity.AutoTranslator.Plugin.ExtProtocol.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
    </data>
+   <data name="XUnity_RuntimeHooker_Core" type="System.Resources.ResXFileRef, System.Windows.Forms">
+      <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\XUnity.RuntimeHooker.Core.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+   </data>
+   <data name="XUnity_RuntimeHooker" type="System.Resources.ResXFileRef, System.Windows.Forms">
+      <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\XUnity.RuntimeHooker.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+   </data>
    <data name="BaiduTranslate" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\BaiduTranslate.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
    </data>

+ 1 - 1
src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj

@@ -4,7 +4,7 @@
       <OutputType>Exe</OutputType>
       <TargetFramework>net40</TargetFramework>
       <AssemblyName>SetupReiPatcherAndAutoTranslator</AssemblyName>
-      <Version>3.0.2</Version>
+      <Version>3.1.0</Version>
       <ApplicationIcon>icon.ico</ApplicationIcon>
       <Win32Resource />
    </PropertyGroup>

+ 169 - 0
src/XUnity.RuntimeHooker.Core/HookCallbackInvoker.cs

@@ -0,0 +1,169 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using XUnity.RuntimeHooker.Core;
+using XUnity.RuntimeHooker.Core.Utilities;
+
+namespace XUnity.RuntimeHooker.Core
+{
+   public class HookCallbackInvoker : IComparable<HookCallbackInvoker>
+   {
+      private readonly HookMethod _hookMethod;
+      private readonly MethodBase _method;
+      private readonly Func<object, object[], object> _fastInvoke;
+      private readonly object[] _boundParameters;
+      private readonly int[] _parameterIndices;
+      private readonly int _returnValueIndex;
+      private readonly int _instanceIndex;
+
+      public HookCallbackInvoker( MethodBase originalMethod, HookMethod hookMethod )
+      {
+         var method = hookMethod.Method;
+         _hookMethod = hookMethod;
+         _method = method;
+         _returnValueIndex = -1;
+         _instanceIndex = -1;
+
+         var originalParameterNames = originalMethod.GetParameters().Select( x => x.Name ).ToArray();
+         var callbackParameters = method.GetParameters();
+         _parameterIndices = new int[ callbackParameters.Length ];
+         _boundParameters = new object[ callbackParameters.Length ];
+
+         // create 'parameter binding map' and determine if function can be fast-invoked
+         bool hasRefOrOut = false;
+         for( int i = 0 ; i < callbackParameters.Length ; i++ )
+         {
+            var callbackParameter = callbackParameters[ i ];
+            if( callbackParameter.IsOut || callbackParameter.ParameterType.IsByRef )
+            {
+               hasRefOrOut = true;
+            }
+
+            var parameterName = callbackParameter.Name;
+            if( parameterName == "__instance" )
+            {
+               if( originalMethod.IsStatic )
+               {
+                  throw new InvalidOperationException( "Cannot get instance of static method." );
+               }
+
+               _instanceIndex = i;
+            }
+            else if( parameterName == "__result" )
+            {
+               _returnValueIndex = i;
+            }
+            else
+            {
+               // find the parameter name in the originalMethod
+               var index = Array.IndexOf( originalParameterNames, parameterName );
+               if( index > -1 )
+               {
+                  _parameterIndices[ i ] = index;
+               }
+            }
+         }
+
+         if( !hasRefOrOut && _method is MethodInfo methodInfo )
+         {
+            _fastInvoke = ExpressionHelper.CreateFastInvoke( methodInfo );
+         }
+      }
+
+      public bool PrefixInvoke( object[] parameters, object __instance )
+      {
+         var parameterIndices = _parameterIndices;
+         var boundParameters = _boundParameters;
+         var len = boundParameters.Length;
+
+         try
+         {
+            // bind parameters from original method call to hook call
+            for( int i = 0 ; i < len ; i++ )
+            {
+               if( i == _instanceIndex )
+               {
+                  boundParameters[ i ] = __instance;
+               }
+               else
+               {
+                  boundParameters[ i ] = parameters[ parameterIndices[ i ] ];
+               }
+            }
+
+            object retval;
+            if( _fastInvoke != null )
+            {
+               retval = _fastInvoke( null, boundParameters );
+            }
+            else
+            {
+               retval = _method.Invoke( null, boundParameters );
+
+               // rebind parameters back to original array to support ref types
+               for( int i = 0 ; i < len ; i++ )
+               {
+                  parameters[ parameterIndices[ i ] ] = boundParameters[ i ];
+               }
+            }
+
+            return retval == null || (bool)retval == true;
+         }
+         finally
+         {
+            for( int i = 0 ; i < len ; i++ )
+            {
+               boundParameters[ i ] = null;
+            }
+         }
+      }
+
+      public void PostfixInvoke( object[] parameters, object __instance, ref object __result )
+      {
+         var parameterIndices = _parameterIndices;
+         var boundParameters = _boundParameters;
+         var len = boundParameters.Length;
+
+         try
+         {
+            // bind parameters from original method call to hook call
+            for( int i = 0 ; i < len ; i++ )
+            {
+               if( i == _returnValueIndex )
+               {
+                  boundParameters[ i ] = __result;
+               }
+               else if( i == _instanceIndex )
+               {
+                  boundParameters[ i ] = __instance;
+               }
+               else
+               {
+                  boundParameters[ i ] = parameters[ parameterIndices[ i ] ];
+               }
+            }
+
+            object retval = _fastInvoke != null
+               ? _fastInvoke( null, boundParameters )
+               : _method.Invoke( null, boundParameters );
+
+            if( _returnValueIndex > -1 )
+            {
+               __result = boundParameters[ _returnValueIndex ];
+            }
+         }
+         finally
+         {
+            for( int i = 0 ; i < len ; i++ )
+            {
+               boundParameters[ i ] = null;
+            }
+         }
+      }
+
+      public int CompareTo( HookCallbackInvoker other )
+      {
+         return other._hookMethod.Priority.CompareTo( _hookMethod.Priority );
+      }
+   }
+}

+ 25 - 0
src/XUnity.RuntimeHooker.Core/HookMethod.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Core
+{
+   public class HookMethod
+   {
+      public HookMethod( MethodBase method )
+         : this( method, HookPriority.Normal )
+      {
+      }
+
+      public HookMethod( MethodBase method, int priority )
+      {
+         Method = method;
+         Priority = priority;
+      }
+
+      public MethodBase Method { get; set; }
+
+      public int Priority { get; set; }
+   }
+}

+ 15 - 0
src/XUnity.RuntimeHooker.Core/HookPriority.cs

@@ -0,0 +1,15 @@
+namespace XUnity.RuntimeHooker.Core
+{
+   public static class HookPriority
+   {
+      public const int Last = 0;
+      public const int VeryLow = 100;
+      public const int Low = 200;
+      public const int LowerThanNormal = 300;
+      public const int Normal = 400;
+      public const int HigherThanNormal = 500;
+      public const int High = 600;
+      public const int VeryHigh = 700;
+      public const int First = 800;
+   }
+}

+ 21 - 0
src/XUnity.RuntimeHooker.Core/JumpedMethodInfo.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Core
+{
+   public class JumpedMethodInfo
+   {
+      public readonly long OriginalMethodLocation;
+      public readonly long ReplacementMethodLocation;
+      public readonly byte[] OriginalCode;
+
+      public JumpedMethodInfo( long originalMethodLocation, long replacementMethodLocation, byte[] originalCode )
+      {
+         OriginalMethodLocation = originalMethodLocation;
+         ReplacementMethodLocation = replacementMethodLocation;
+         OriginalCode = originalCode;
+      }
+   }
+
+}

+ 18 - 0
src/XUnity.RuntimeHooker.Core/TrampolineData.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Core
+{
+   public class TrampolineData
+   {
+      public JumpedMethodInfo JumpedMethodInfo;
+      public MethodBase Method;
+      public Func<object, object[], object> FastInvoke;
+      public object[] Parameters;
+      public List<HookCallbackInvoker> Postfixes;
+      public List<HookCallbackInvoker> Prefixes;
+   }
+}

+ 82 - 0
src/XUnity.RuntimeHooker.Core/TrampolineHandler.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using XUnity.RuntimeHooker.Core;
+using XUnity.RuntimeHooker.Core.Utilities;
+
+namespace XUnity.RuntimeHooker.Core
+{
+   public static class TrampolineHandler
+   {
+      private static object Null;
+
+      public static object Func( object instance, TrampolineData data )
+      {
+         var jumpedMethodInfo = data.JumpedMethodInfo;
+         var parameters = data.Parameters;
+         try
+         {
+            MemoryHelper.RestoreInstructionsAtLocation( false, jumpedMethodInfo.OriginalMethodLocation, jumpedMethodInfo.OriginalCode );
+
+            var prefixes = data.Prefixes;
+            var prefixesLen = prefixes.Count;
+            for( int i = 0 ; i < prefixesLen ; i++ )
+            {
+               if( !prefixes[ i ].PrefixInvoke( parameters, instance ) ) return null;
+            }
+
+            var fastInvoke = data.FastInvoke;
+            object result = fastInvoke != null
+               ? fastInvoke( instance, parameters )
+               : data.Method.Invoke( null, parameters );
+
+            var postfixes = data.Postfixes;
+            var postfixesLen = postfixes.Count;
+            for( int i = 0 ; i < postfixesLen ; i++ )
+            {
+               postfixes[ i ].PostfixInvoke( parameters, instance, ref result );
+            }
+
+            return result;
+         }
+         finally
+         {
+            MemoryHelper.WriteJump( false, jumpedMethodInfo.OriginalMethodLocation, jumpedMethodInfo.ReplacementMethodLocation );
+         }
+      }
+
+      public static void Action( object instance, TrampolineData data )
+      {
+         var jumpedMethodInfo = data.JumpedMethodInfo;
+         var parameters = data.Parameters;
+         try
+         {
+            MemoryHelper.RestoreInstructionsAtLocation( false, jumpedMethodInfo.OriginalMethodLocation, jumpedMethodInfo.OriginalCode );
+
+            var prefixes = data.Prefixes;
+            var prefixesLen = prefixes.Count;
+            for( int i = 0 ; i < prefixesLen ; i++ )
+            {
+               if( !prefixes[ i ].PrefixInvoke( parameters, instance ) ) return;
+            }
+
+            var fastInvoke = data.FastInvoke;
+            object result = fastInvoke != null
+               ? fastInvoke( instance, parameters )
+               : data.Method.Invoke( null, parameters );
+
+            var postfixes = data.Postfixes;
+            var postfixesLen = postfixes.Count;
+            for( int i = 0 ; i < postfixesLen ; i++ )
+            {
+               postfixes[ i ].PostfixInvoke( parameters, instance, ref Null );
+            }
+         }
+         finally
+         {
+            MemoryHelper.WriteJump( false, jumpedMethodInfo.OriginalMethodLocation, jumpedMethodInfo.ReplacementMethodLocation );
+         }
+      }
+   }
+}

+ 13 - 0
src/XUnity.RuntimeHooker.Core/Unmanaged/Kernel32.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Unmanaged
+{
+   internal static class Kernel32
+   {
+      [DllImport( "kernel32.dll" )]
+      internal static extern bool VirtualProtect( IntPtr lpAddress, UIntPtr dwSize, Protection flNewProtect, out Protection lpflOldProtect );
+   }
+}

+ 34 - 0
src/XUnity.RuntimeHooker.Core/Unmanaged/Protection.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Unmanaged
+{
+   /// <summary>A bit-field of flags for protections</summary>
+   [Flags]
+   internal enum Protection
+   {
+      /// <summary>No access</summary>
+      PAGE_NOACCESS = 0x01,
+      /// <summary>Read only</summary>
+      PAGE_READONLY = 0x02,
+      /// <summary>Read write</summary>
+      PAGE_READWRITE = 0x04,
+      /// <summary>Write copy</summary>
+      PAGE_WRITECOPY = 0x08,
+      /// <summary>No access</summary>
+      PAGE_EXECUTE = 0x10,
+      /// <summary>Execute read</summary>
+      PAGE_EXECUTE_READ = 0x20,
+      /// <summary>Execute read write</summary>
+      PAGE_EXECUTE_READWRITE = 0x40,
+      /// <summary>Execute write copy</summary>
+      PAGE_EXECUTE_WRITECOPY = 0x80,
+      /// <summary>guard</summary>
+      PAGE_GUARD = 0x100,
+      /// <summary>No cache</summary>
+      PAGE_NOCACHE = 0x200,
+      /// <summary>Write combine</summary>
+      PAGE_WRITECOMBINE = 0x400
+   }
+}

+ 81 - 0
src/XUnity.RuntimeHooker.Core/Utilities/ExpressionHelper.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Core.Utilities
+{
+   public static class ExpressionHelper
+   {
+      public static Func<object, object[], object> CreateFastInvoke( MethodInfo method )
+      {
+         var instanceParameterExpression = Expression.Parameter( typeof( object ), "instance" );
+         var argumentsParameterExpression = Expression.Parameter( typeof( object[] ), "args" );
+         
+         var index = 0;
+         var argumentExtractionExpressions =
+            method
+            .GetParameters()
+            .Select( parameter =>
+                Expression.Convert(
+                   Expression.ArrayIndex(
+                      argumentsParameterExpression,
+                      Expression.Constant( index++ )
+                   ),
+                   parameter.ParameterType
+                )
+            ).ToArray();
+
+         var callExpression = method.IsStatic
+            ? Expression.Call( method, argumentExtractionExpressions )
+            : Expression.Call(
+               Expression.Convert(
+                  instanceParameterExpression,
+                  method.DeclaringType
+               ),
+               method,
+               argumentExtractionExpressions
+            );
+
+         var isVoidReturn = method.ReturnType == typeof( void );
+
+         var finalExpression = isVoidReturn
+            ? (Expression)callExpression
+            : Expression.Convert( callExpression, typeof( object ) );
+
+         if( isVoidReturn )
+         {
+            var lambdaExpression = Expression.Lambda<Action<object, object[]>>(
+               finalExpression,
+               instanceParameterExpression,
+               argumentsParameterExpression
+            );
+
+            var compiledLambda = lambdaExpression.Compile();
+            return ConvertToFunc( compiledLambda );
+         }
+         else
+         {
+            var lambdaExpression = Expression.Lambda<Func<object, object[], object>>(
+               finalExpression,
+               instanceParameterExpression,
+               argumentsParameterExpression
+            );
+
+            var compiledLambda = lambdaExpression.Compile();
+            return compiledLambda;
+         }
+      }
+
+      private static Func<object, object[], object> ConvertToFunc( Action<object, object[]> action )
+      {
+         return ( instance, args ) =>
+         {
+            action( instance, args );
+            return null;
+         };
+      }
+   }
+}

+ 15 - 18
src/XUnity.AutoTranslator.Plugin.Core/Utilities/MemoryHelper.cs → src/XUnity.RuntimeHooker.Core/Utilities/MemoryHelper.cs

@@ -1,26 +1,24 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Reflection;
 using System.Reflection.Emit;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Text;
-using Harmony.ILCopying;
-using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.RuntimeHooker.Unmanaged;
 
-namespace XUnity.AutoTranslator.Plugin.Core.Utilities
+namespace XUnity.RuntimeHooker.Core.Utilities
 {
-   internal static class MemoryHelper
+   public static class MemoryHelper
    {
       private static readonly HashSet<PlatformID> WindowsPlatformIDSet = new HashSet<PlatformID> { PlatformID.Win32NT, PlatformID.Win32S, PlatformID.Win32Windows, PlatformID.WinCE };
       private static readonly bool IsWindows = WindowsPlatformIDSet.Contains( Environment.OSVersion.Platform );
 
       private static readonly byte[] Prefix = new byte[] { 0xe9 };
-      private static readonly byte[] Instruction_Start_64bit = new byte[] { 0x48, 0xB8 };
-      private static readonly byte[] Instruction_End_64bit = new byte[] { 0xFF, 0xE0 };
-      private const byte Instruction_Start_32bit = 0x68;
-      private const byte Instruction_End_32bit = 0xc3;
+      private static readonly byte[] Instruction_1_64bit = new byte[] { 0x48, 0xB8 };
+      private static readonly byte[] Instruction_2_64bit = new byte[] { 0xFF, 0xE0 };
+      private const byte Instruction_1_32bit = 0x68;
+      private const byte Instruction_2_32bit = 0xc3;
       private const int MemoryRequiredForJumpInstruction_32bit = 6;
       private const int MemoryRequiredForJumpInstruction_64bit = 12;
 
@@ -39,7 +37,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
       }
 
 
-      public static string WriteJump( bool unprotect, long memory, long destination )
+      public static void WriteJump( bool unprotect, long memory, long destination )
       {
          if( unprotect )
          {
@@ -54,17 +52,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
                memory += 5 + offset;
             }
 
-            memory = WriteBytes( memory, Instruction_Start_64bit );
+            memory = WriteBytes( memory, Instruction_1_64bit );
             memory = WriteLong( memory, destination );
-            memory = WriteBytes( memory, Instruction_End_64bit );
+            memory = WriteBytes( memory, Instruction_2_64bit );
          }
          else
          {
-            memory = WriteByte( memory, Instruction_Start_32bit );
+            memory = WriteByte( memory, Instruction_1_32bit );
             memory = WriteInt( memory, (int)destination );
-            memory = WriteByte( memory, Instruction_End_32bit );
+            memory = WriteByte( memory, Instruction_2_32bit );
          }
-         return null;
       }
 
       public static byte[] GetInstructionsAtLocationRequiredToWriteJump( long location )
@@ -103,7 +100,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
          {
             UnprotectMemoryPage( location );
          }
-
+         
          var flag = IntPtr.Size == sizeof( long );
          if( flag )
          {
@@ -113,11 +110,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
                location += 5 + num;
             }
          }
-
+         
          foreach( var b in array )
          {
             location = WriteByte( location, b );
-         }            
+         }
       }
 
       public static unsafe bool CompareBytes( long memory, byte[] values )

+ 8 - 0
src/XUnity.RuntimeHooker.Core/XUnity.RuntimeHooker.Core.csproj

@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+   <PropertyGroup>
+      <TargetFramework>net35</TargetFramework>
+      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+   </PropertyGroup>
+
+</Project>

+ 142 - 0
src/XUnity.RuntimeHooker.Trampolines/TrampolineInitializer.cs

@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using XUnity.RuntimeHooker.Core;
+using XUnity.RuntimeHooker.Core.Utilities;
+
+namespace XUnity.RuntimeHooker.Trampolines
+{
+   internal static class TrampolineInitializer
+   {
+      public static readonly object Sync = new object();
+      public static TrampolineData Data;
+
+      public static void SetupHook( MethodBase originalMethod )
+      {
+         // just skip if already initialized
+         if( Data != null ) return;
+
+         // determine if we have a supported return type
+         var returnType = ( originalMethod as MethodInfo )?.ReturnType;
+         if( !( returnType == typeof( void ) || !returnType.IsValueType ) )
+         {
+            throw new InvalidOperationException( "Current implementation only supports hooking method that returns reference types or void." );
+         }
+
+         // obtain parameters to method, and check they are not generic
+         var parameters = originalMethod.GetParameters();
+         if( originalMethod.IsGenericMethod || originalMethod.IsGenericMethodDefinition )
+         {
+            throw new InvalidOperationException( "Current implementation does not support generic methods." );
+         }
+
+         // check the are not ref or out parameters
+         foreach( var parameter in parameters )
+         {
+            if( parameter.ParameterType.IsByRef || parameter.IsOut )
+            {
+               throw new InvalidOperationException( "Current implementation does not support out or ref parameters." );
+            }
+         }
+
+         // determine which type to use as a trampoline
+         string type = null;
+         bool hasReturn = returnType != typeof( void );
+         bool isStatic = originalMethod.IsStatic;
+         if( hasReturn && isStatic )
+         {
+            type = "XUnity.RuntimeHooker.Trampolines.StaticTrampolineWithReturn";
+         }
+         else if( !hasReturn && isStatic )
+         {
+            type = "XUnity.RuntimeHooker.Trampolines.StaticTrampoline";
+         }
+         else if( hasReturn && !isStatic )
+         {
+            type = "XUnity.RuntimeHooker.Trampolines.InstanceTrampolineWithReturn";
+         }
+         else if( !hasReturn && !isStatic )
+         {
+            type = "XUnity.RuntimeHooker.Trampolines.InstanceTrampoline";
+         }
+
+         // determine if and how many generic arguments the trampoline type has
+         var genericArguments = new List<Type>();
+         if( !originalMethod.IsStatic )
+         {
+            genericArguments.Add( originalMethod.DeclaringType );
+         }
+         genericArguments.AddRange( parameters.Select( x => x.ParameterType ) );
+         if( genericArguments.Count > 0 )
+         {
+            // and apply those generic arguments to the type name so it can be found
+            type += "`" + genericArguments.Count;
+         }
+
+         // find the trampoline type
+         var trampolineType = typeof( TrampolineInitializer ).Assembly.GetType( type );
+         if( trampolineType == null )
+         {
+            throw new InvalidOperationException( "Current implementation only supports methods with up to 5 arguments (including the instance)." );
+         }
+
+         // genericify the trampoline type
+         if( genericArguments.Count > 0 )
+         {
+            trampolineType = trampolineType.MakeGenericType( genericArguments.ToArray() );
+         }
+
+         // obtain the method that we will jump to when the original method is called
+         var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
+         var overrideMethod = trampolineType.GetMethod( "Override", flags );
+
+         // get the location of the override method
+         long replacementMethodLocation = MemoryHelper.GetMethodStartLocation( overrideMethod );
+
+         // get the location and initial code of the original method
+         long originalMethodLocation = MemoryHelper.GetMethodStartLocation( originalMethod );
+         var originalCode = MemoryHelper.GetInstructionsAtLocationRequiredToWriteJump( originalMethodLocation );
+
+         // create an (optional) delegate that enables invoking the function MUCH faster than through reflection
+         Func<object, object[], object> fastInvoke = null;
+         if( originalMethod is MethodInfo methodInfo )
+         {
+            fastInvoke = ExpressionHelper.CreateFastInvoke( methodInfo );
+         }
+
+         // store all data required by the trampoline function
+         Data = new TrampolineData
+         {
+            Method = originalMethod,
+            FastInvoke = fastInvoke,
+            JumpedMethodInfo = new JumpedMethodInfo( originalMethodLocation, replacementMethodLocation, originalCode ),
+            Parameters = new object[ parameters.Length ],
+            Postfixes = new List<HookCallbackInvoker>(),
+            Prefixes = new List<HookCallbackInvoker>()
+         };
+
+         // create a JMP instruction at the start of the original method so we jump to the override method
+         // (create the hook)
+         MemoryHelper.WriteJump( true, originalMethodLocation, replacementMethodLocation );
+      }
+
+      public static void AddPrefix( HookMethod prefix )
+      {
+         if( Data == null ) throw new InvalidOperationException( "Please call SetupHook before adding prefixes." );
+
+         var invoker = new HookCallbackInvoker( Data.Method, prefix );
+         Data.Prefixes.Add( invoker );
+         Data.Prefixes.Sort();
+      }
+
+      public static void AddPostfix( HookMethod postfix )
+      {
+         if( Data == null ) throw new InvalidOperationException( "Please call SetupHook before adding postfixes." );
+
+         var invoker = new HookCallbackInvoker( Data.Method, postfix );
+         Data.Postfixes.Add( invoker );
+         Data.Postfixes.Sort();
+      }
+   }
+}

+ 555 - 0
src/XUnity.RuntimeHooker.Trampolines/Trampolines.cs

@@ -0,0 +1,555 @@
+using System;
+using System.Text;
+using System.Threading;
+using XUnity.RuntimeHooker.Core;
+
+namespace XUnity.RuntimeHooker.Trampolines
+{
+   #region InstanceTrampolineWithReturn
+
+   internal static class InstanceTrampolineWithReturn<T1>
+   {
+      static object Override( T1 t1 )
+      {
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            object result = TrampolineHandler.Func( t1, TrampolineInitializer.Data );
+
+            return result;
+         }
+         finally
+         {
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampolineWithReturn<T1, T2>
+   {
+      static object Override( T1 t1, T2 t2 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            object result = TrampolineHandler.Func( t1, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampolineWithReturn<T1, T2, T3>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            object result = TrampolineHandler.Func( t1, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampolineWithReturn<T1, T2, T3, T4>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3, T4 t4 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            data.Parameters[ 2 ] = t4;
+            object result = TrampolineHandler.Func( t1, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampolineWithReturn<T1, T2, T3, T4, T5>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3, T4 t4, T5 t5 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            data.Parameters[ 2 ] = t4;
+            data.Parameters[ 3 ] = t5;
+            object result = TrampolineHandler.Func( t1, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   #endregion
+
+   #region InstanceTrampoline
+
+   internal static class InstanceTrampoline<T1>
+   {
+      static void Override( T1 t1 )
+      {
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            TrampolineHandler.Action( t1, TrampolineInitializer.Data );
+         }
+         finally
+         {
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampoline<T1, T2>
+   {
+      static void Override( T1 t1, T2 t2 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            TrampolineHandler.Action( t1, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampoline<T1, T2, T3>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            TrampolineHandler.Action( t1, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampoline<T1, T2, T3, T4>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3, T4 t4 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            data.Parameters[ 2 ] = t4;
+            TrampolineHandler.Action( t1, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class InstanceTrampoline<T1, T2, T3, T4, T5>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3, T4 t4, T5 t5 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t2;
+            data.Parameters[ 1 ] = t3;
+            data.Parameters[ 2 ] = t4;
+            data.Parameters[ 3 ] = t5;
+            TrampolineHandler.Action( t1, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   #endregion
+
+   #region StaticTrampolineWithReturn
+
+   internal static class StaticTrampolineWithReturn
+   {
+      static object Override()
+      {
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            object result = TrampolineHandler.Func( null, TrampolineInitializer.Data );
+
+            return result;
+         }
+         finally
+         {
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampolineWithReturn<T1>
+   {
+      static object Override( T1 t1 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            object result = TrampolineHandler.Func( null, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampolineWithReturn<T1, T2>
+   {
+      static object Override( T1 t1, T2 t2 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            object result = TrampolineHandler.Func( null, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampolineWithReturn<T1, T2, T3>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            object result = TrampolineHandler.Func( null, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampolineWithReturn<T1, T2, T3, T4>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3, T4 t4 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            data.Parameters[ 3 ] = t4;
+            object result = TrampolineHandler.Func( null, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampolineWithReturn<T1, T2, T3, T4, T5>
+   {
+      static object Override( T1 t1, T2 t2, T3 t3, T4 t4, T5 t5 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            data.Parameters[ 3 ] = t4;
+            data.Parameters[ 4 ] = t5;
+            object result = TrampolineHandler.Func( null, data );
+
+            return result;
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+            data.Parameters[ 4 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   #endregion
+
+   #region StaticTrampoline
+
+   internal static class StaticTrampoline
+   {
+      static void Override()
+      {
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            TrampolineHandler.Action( null, TrampolineInitializer.Data );
+         }
+         finally
+         {
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampoline<T1>
+   {
+      static void Override( T1 t1 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            TrampolineHandler.Action( null, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampoline<T1, T2>
+   {
+      static void Override( T1 t1, T2 t2 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            TrampolineHandler.Action( null, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampoline<T1, T2, T3>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            TrampolineHandler.Action( null, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampoline<T1, T2, T3, T4>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3, T4 t4 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            data.Parameters[ 3 ] = t4;
+            TrampolineHandler.Action( null, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   internal static class StaticTrampoline<T1, T2, T3, T4, T5>
+   {
+      static void Override( T1 t1, T2 t2, T3 t3, T4 t4, T5 t5 )
+      {
+         var data = TrampolineInitializer.Data;
+         try
+         {
+            Monitor.Enter( TrampolineInitializer.Sync );
+
+            data.Parameters[ 0 ] = t1;
+            data.Parameters[ 1 ] = t2;
+            data.Parameters[ 2 ] = t3;
+            data.Parameters[ 3 ] = t4;
+            data.Parameters[ 4 ] = t5;
+            TrampolineHandler.Action( null, data );
+         }
+         finally
+         {
+            data.Parameters[ 0 ] = null;
+            data.Parameters[ 1 ] = null;
+            data.Parameters[ 2 ] = null;
+            data.Parameters[ 3 ] = null;
+            data.Parameters[ 4 ] = null;
+
+            Monitor.Exit( TrampolineInitializer.Sync );
+         }
+      }
+   }
+
+   #endregion
+}

+ 12 - 0
src/XUnity.RuntimeHooker.Trampolines/XUnity.RuntimeHooker.Trampolines.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+     <AssemblyName>b979b301af8b4ef48f224de6dcf2ddf6</AssemblyName>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\XUnity.RuntimeHooker.Core\XUnity.RuntimeHooker.Core.csproj" />
+  </ItemGroup>
+
+</Project>

+ 73 - 0
src/XUnity.RuntimeHooker/Properties/Resources.Designer.cs

@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace XUnity.RuntimeHooker.Properties {
+    using System;
+    
+    
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources {
+        
+        private static global::System.Resources.ResourceManager resourceMan;
+        
+        private static global::System.Globalization.CultureInfo resourceCulture;
+        
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources() {
+        }
+        
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager {
+            get {
+                if (object.ReferenceEquals(resourceMan, null)) {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XUnity.RuntimeHooker.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+        
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture {
+            get {
+                return resourceCulture;
+            }
+            set {
+                resourceCulture = value;
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized resource of type System.Byte[].
+        /// </summary>
+        internal static byte[] b979b301af8b4ef48f224de6dcf2ddf6 {
+            get {
+                object obj = ResourceManager.GetObject("b979b301af8b4ef48f224de6dcf2ddf6", resourceCulture);
+                return ((byte[])(obj));
+            }
+        }
+    }
+}

+ 124 - 0
src/XUnity.RuntimeHooker/Properties/Resources.resx

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
+  <data name="b979b301af8b4ef48f224de6dcf2ddf6" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>..\Resources\b979b301af8b4ef48f224de6dcf2ddf6.dll;System.Byte[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </data>
+</root>

+ 147 - 0
src/XUnity.RuntimeHooker/RuntimeMethodPatcher.cs

@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using XUnity.RuntimeHooker.Core;
+using XUnity.RuntimeHooker.Properties;
+
+namespace XUnity.RuntimeHooker
+{
+   public static class RuntimeMethodPatcher
+   {
+      private static readonly object Sync = new object();
+      private static readonly Dictionary<MethodBase, Type> HookedMethodToTrampolineInitializer = new Dictionary<MethodBase, Type>();
+
+      public static void Patch( MethodBase originalMethod, HookMethod prefix, HookMethod postfix )
+      {
+         // WARNING: This code is in no way guaranteed to work, as it makes some wild assumptions
+         //          about how the underlying machine code is being generated during JIT
+
+         // If the code does not work it will likely crash the application immediately with a cryptic or no error message.
+
+         // This hooking implementation works like this:
+         // 1. Create a JMP at the start of the hooked method that goes to a method of an identical signature (trampoline function)
+         // 2. When the function is entered, rewrite back the old code that was overwritten in step 1
+         // 3. Call prefixes, then call original method, then call postfixes
+         // 4. Write back in the JMP that was created in step 1
+
+         // This is all very simple. The primary issue this library solves is how to do this through a 'nice'
+         // API surface, similar to that of Harmony so that it can be used as a fallback mechanism.
+
+         // The difficult issue is how to navigate to a method of identical signature to the original one
+         // if you cannot emit any IL code (netstandard2.0 limitation) because it is difficult to
+         // generate a method with the required signature at compile time, that includes types that may
+         // be inaccessible.
+
+         // This library pre-defines a set of generic classes that each has an 'Override' method, that will
+         // be used as the trampoline function. Each version of the 'Override' method makes different assumptions
+         // about the method signature and uses generic parameters defined on the class in order to do so.
+
+         // The weakness of this approach is that any arguments (generated by the JITTER) in relation to generics
+         // will *not* be passed to the override method, as it would usually expect. (I think that with the legacy
+         // .NET JITTER in 64-bit, these are often passed as an id in the RCX register of the CPU, but this is likely
+         // different for Mono and other architectures). This means that if any code is generated in the Override
+         // method that requires this information that the application will immediately fail.
+
+         // This has an especially big impact on the JITTER used in Mono, where accessing any generic arguments
+         // in the override method will immediately cause application failure. This imposes a big limitation
+         // on this approach in that the current implementation is only able to hook methods that return reference
+         // types (or void) because it is unable to use a generic argument (TReturn) to determine the size of the variable
+         // to send back to the caller (Should it use the stack? Should it use a CPU register? It does not know!).
+
+         // Another limitation of this in Mono is that you cannot use this approach to access static fields on
+         // a per-generic class basis (as that would mean using the generic arguments). To overcome this issue
+         // a new assembly that maintains state for each hooked method is generated at runtime (by renaming an
+         // existing assembly and loading it into memory). This is essentially what this function does.
+
+         // A better approach than this would be to write the trampoline function in machine code. But that
+         // would take a whole lot more effort than this. And require knowledge about how each architecture
+         // and platform emits machine code.
+
+         // Attempting to create a StackTrace will also break this implementation.
+
+         lock( Sync )
+         {
+            var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
+
+            // check if we have already hooked the specified method
+            if( !HookedMethodToTrampolineInitializer.TryGetValue( originalMethod, out var trampolineInitializer ) )
+            {
+               // obtain data for the assembly that contains the hooking logic/trampolines
+               var assemblySize = Resources.b979b301af8b4ef48f224de6dcf2ddf6.Length;
+
+               // copy the data into a new array
+               var assemblyData = new byte[ assemblySize ];
+               Buffer.BlockCopy( Resources.b979b301af8b4ef48f224de6dcf2ddf6, 0, assemblyData, 0, assemblySize );
+
+               // rename the assembly, so it can be loaded multiple times (in Mono)
+               var newName = Guid.NewGuid().ToString( "N" );
+               FindAndReplace( "b979b301af8b4ef48f224de6dcf2ddf6", newName, assemblyData );
+               var assembly = Assembly.Load( assemblyData );
+
+               // obtain the trampoline initializer type from the dynamically named assembly
+               trampolineInitializer = assembly.GetType( "XUnity.RuntimeHooker.Trampolines.TrampolineInitializer" );
+
+               // call the setup hook method with the originalMethod to hook
+               trampolineInitializer.GetMethod( "SetupHook", flags ).Invoke( null, new object[] { originalMethod } );
+
+               // add tbe method to our hooked methods dictionary, so we do not hook it again
+               HookedMethodToTrampolineInitializer.Add( originalMethod, trampolineInitializer );
+            }
+
+            // after hooking the method, lets apply our prefixes/postfixes
+            if( prefix != null )
+            {
+               trampolineInitializer.GetMethod( "AddPrefix", flags ).Invoke( null, new object[] { prefix } );
+            }
+
+            if( postfix != null )
+            {
+               trampolineInitializer.GetMethod( "AddPostfix", flags ).Invoke( null, new object[] { postfix } );
+            }
+         }
+      }
+
+      private static void FindAndReplace( string ansiStringToSearchFor, string ansiStringToReplaceWith, byte[] data )
+      {
+         if( ansiStringToReplaceWith.Length != ansiStringToSearchFor.Length )
+            throw new ArgumentException( "The string to search for must be same size as string to replace." );
+
+         int pos = 0;
+         char c;
+         byte b;
+         int startIdx = 0;
+         int searchLen = ansiStringToReplaceWith.Length;
+
+         for( int i = 0 ; i < data.Length ; i++ )
+         {
+            b = data[ i ];
+            c = (char)b;
+
+            if( pos == searchLen )
+            {
+               for( int j = 0 ; j < searchLen ; j++ )
+               {
+                  var r = ansiStringToReplaceWith[ j ];
+                  data[ j + startIdx ] = (byte)r;
+               }
+
+               pos = 0;
+            }
+            else if( c == ansiStringToSearchFor[ pos ] )
+            {
+               if( pos == 0 )
+               {
+                  startIdx = i;
+               }
+
+               pos++;
+            }
+            else
+            {
+               pos = 0;
+            }
+         }
+      }
+   }
+}

+ 30 - 0
src/XUnity.RuntimeHooker/XUnity.RuntimeHooker.csproj

@@ -0,0 +1,30 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\XUnity.RuntimeHooker.Core\XUnity.RuntimeHooker.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Update="Properties\Resources.Designer.cs">
+      <DesignTime>True</DesignTime>
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Update="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+    </EmbeddedResource>
+  </ItemGroup>
+
+  <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
+    <Exec Command="XCOPY /Y /I &quot;$(SolutionDir)src\XUnity.RuntimeHooker.Trampolines\$(OutDir)b979b301af8b4ef48f224de6dcf2ddf6.dll&quot; &quot;$(ProjectDir)Resources\&quot;" />
+  </Target>
+
+</Project>

+ 53 - 0
test/XUnity.RuntimeHooker.Benchmark/Program.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using XUnity.RuntimeHooker.Core;
+
+namespace XUnity.RuntimeHooker.Benchmark
+{
+   class Program
+   {
+      static void Main( string[] args )
+      {
+         int a = 12;
+         int b = 23;
+
+         MyMethod( a, b );
+
+         var myMethod = typeof( Program ).GetMethod( "MyMethod" );
+         var myHook = typeof( Program ).GetMethod( "MyHook" );
+
+         int iterations = 10000;
+         var sw = Stopwatch.StartNew();
+
+         for( int i = 0 ; i < iterations ; i++ )
+         {
+            MyMethod( a, b );
+         }
+
+         Console.WriteLine( sw.ElapsedMilliseconds );
+
+         RuntimeMethodPatcher.Patch( myMethod, new HookMethod( myHook ), null );
+         MyMethod( a, b );
+
+         sw.Reset();
+         sw.Start();
+
+         for( int i = 0 ; i < iterations ; i++ )
+         {
+            MyMethod( a, b );
+         }
+
+         Console.WriteLine( sw.ElapsedMilliseconds );
+      }
+
+      [MethodImpl( MethodImplOptions.NoInlining )]
+      public static void MyMethod( int a, int b )
+      {
+      }
+
+      public static void MyHook( int b, int a )
+      {
+      }
+   }
+}

+ 12 - 0
test/XUnity.RuntimeHooker.Benchmark/XUnity.RuntimeHooker.Benchmark.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+   <ItemGroup>
+      <ProjectReference Include="..\..\src\XUnity.RuntimeHooker\XUnity.RuntimeHooker.csproj" />
+   </ItemGroup>
+
+</Project>

+ 72 - 0
test/XUnity.RuntimeHooker.ConsoleTests/Program.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text;
+using XUnity.RuntimeHooker.Core;
+
+namespace XUnity.RuntimeHooker.ConsoleTests
+{
+   class Program
+   {
+      static void Main( string[] args )
+      {
+         var sayHello = typeof( Program ).GetMethod( "SayHello" );
+         var sayGoodbye = typeof( Program ).GetMethod( "SayGoodbye" );
+         RuntimeMethodPatcher.Patch( sayHello, new HookMethod( sayGoodbye ), null );
+         SayHello();
+
+         var textComponent = new TextComponent();
+         textComponent.Text = "こんにちは";
+
+         var textGetter = typeof( TextComponent ).GetProperty( "Text" ).GetGetMethod();
+         var textGetterHook = typeof( Program ).GetMethod( "TextGetterHook" );
+         RuntimeMethodPatcher.Patch( textGetter, null, new HookMethod( textGetterHook ) );
+
+         Console.WriteLine( textComponent.Text );
+
+         var printValue = typeof( Program ).GetMethod( "PrintValue" );
+         var printValueHook = typeof( Program ).GetMethod( "PrintValueHook" );
+         RuntimeMethodPatcher.Patch( printValue, new HookMethod( printValueHook ), null );
+         PrintValue( 7331 );
+      }
+
+      [MethodImpl( MethodImplOptions.NoInlining )]
+      public static void SayHello()
+      {
+         Console.WriteLine( "Hello" );
+      }
+
+      public static bool SayGoodbye()
+      {
+         Console.WriteLine( "Goodbye" );
+
+         return false;
+      }
+
+      public static void TextGetterHook( ref string __result, TextComponent __instance )
+      {
+         Console.WriteLine( "Instance: " + __instance );
+
+         __result = "Hello there!";
+      }
+
+      [MethodImpl( MethodImplOptions.NoInlining )]
+      public static void PrintValue( int i )
+      {
+         Console.WriteLine( i );
+      }
+
+      public static void PrintValueHook( ref int i )
+      {
+         i = 1337;
+      }
+   }
+
+   public class TextComponent
+   {
+      public string Text { [MethodImpl( MethodImplOptions.NoInlining )]get; set; }
+   }
+}

+ 13 - 0
test/XUnity.RuntimeHooker.ConsoleTests/XUnity.RuntimeHooker.ConsoleTests.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+   <PropertyGroup>
+      <OutputType>Exe</OutputType>
+      <TargetFramework>net35</TargetFramework>
+      <Platforms>AnyCPU;x86</Platforms>
+   </PropertyGroup>
+
+   <ItemGroup>
+     <ProjectReference Include="..\..\src\XUnity.RuntimeHooker\XUnity.RuntimeHooker.csproj" />
+   </ItemGroup>
+
+</Project>