3 Commity 4394f3a70a ... 3c2d07353d

Autor SHA1 Wiadomość Data
  randoman 3c2d07353d Version 3.6.0 5 lat temu
  randoman 7fac73b052 Version 3.6.0 5 lat temu
  randoman b383e1dac3 removed unityengine.ui dependency 5 lat temu
56 zmienionych plików z 1221 dodań i 310 usunięć
  1. 10 1
      CHANGELOG.md
  2. 23 0
      README.md
  3. 12 1
      XUnity.AutoTranslator.sln
  4. 1 1
      src/Translators/Lec.ExtProtocol/Program.cs
  5. 48 1
      src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs
  6. 169 0
      src/Translators/PapagoTranslate/PapagoTranslate.cs
  7. 15 0
      src/Translators/PapagoTranslate/PapagoTranslate.csproj
  8. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  9. 2 0
      src/XUnity.AutoTranslator.Plugin.BepIn-5x/AutoTranslatorPlugin.cs
  10. 1 4
      src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj
  11. 2 0
      src/XUnity.AutoTranslator.Plugin.BepIn/AutoTranslatorPlugin.cs
  12. 1 4
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  13. 127 44
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  14. 41 1
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  15. 5 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/ClrTypes.cs
  16. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  17. 2 0
      src/XUnity.AutoTranslator.Plugin.Core/DefaultPluginEnvironment.cs
  18. 3 1
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs
  19. 9 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/IInitializationContext.cs
  20. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/InitializationContext.cs
  21. 22 24
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs
  22. 23 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/IniFileExtensions.cs
  23. 36 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  24. 17 24
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs
  25. 11 18
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs
  26. 3 3
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureExtensions.cs
  27. 33 0
      src/XUnity.AutoTranslator.Plugin.Core/Features.cs
  28. 8 7
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs
  29. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/IPluginEnvironment.cs
  30. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs
  31. 6 6
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs
  32. 60 52
      src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs
  33. 74 49
      src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs
  34. 49 36
      src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs
  35. 0 1
      src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs
  36. 41 1
      src/XUnity.AutoTranslator.Plugin.Core/UI/GUIUtil.cs
  37. 4 4
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorOptionsWindow.cs
  38. 36 4
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorViewModel.cs
  39. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorWindow.cs
  40. 25 3
      src/XUnity.AutoTranslator.Plugin.Core/UI/TranslatorViewModel.cs
  41. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/UI/XuaWindow.cs
  42. 24 2
      src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs
  43. 3 2
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/CoroutineHelper.cs
  44. 38 0
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/DebounceFunction.cs
  45. 0 1
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/ObjectReferenceMapper.cs
  46. 1 4
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  47. 2 0
      src/XUnity.AutoTranslator.Plugin.IPA/AutoTranslatorPlugin.cs
  48. 1 1
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  49. 2 0
      src/XUnity.AutoTranslator.Plugin.UnityInjector/AutoTranslatorPlugin.cs
  50. 1 1
      src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj
  51. 1 0
      src/XUnity.AutoTranslator.Setup.Build/XUnity.AutoTranslator.Setup.Build.csproj
  52. 1 0
      src/XUnity.AutoTranslator.Setup/Program.cs
  53. 10 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.Designer.cs
  54. 3 0
      src/XUnity.AutoTranslator.Setup/Properties/Resources.resx
  55. 1 1
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj
  56. 197 0
      src/XUnity.RuntimeHooker.Core/Utilities/ReflectionCache.cs

+ 10 - 1
CHANGELOG.md

@@ -1,4 +1,13 @@
-### 3.5.0
+### 3.6.0
+ * FEATURE - 'Translation Aggregator'-like view that enables viewing translations for displayed texts from multiple different translators (press ALT+1)
+ * FEATURE - Substitution support. Enable dictionary lookup for strings (usually proper nouns) embedded in text to replace them with a manual translation
+ * FEATURE - Papago translator support
+ * MISC - Removed hard dependency on UnityEngine.UI to support older versions of the Unity engine
+ * MISC - Automatically initialize LEC installation path if installed when creating configuration file
+ * MISC - Automatically enable experimental hooks when installed in single mod scenario (ReiPatcher) and it is required by the runtime
+ * BUG FIX - Fixed bug where LEC was not working when run in a .NET 4.x equivalent runtime
+
+### 3.5.0
  * FEATURE - Harmony 2.0-prerelease support (in order to support BepInEx 5.0.0-RC1)
  * BUG FIX - Fixed a bug where the plugin would sometimes dump textures if 'DetectDuplicateTextureNames' was turned on, even though 'EnableTextureDumping' was turned off
  * BUG FIX - Correct whitespace handling of source languages requiring whitelines between words

+ 23 - 0
README.md

@@ -148,6 +148,7 @@ The file structure should like like this
 ## Key Mapping
 The following key inputs are mapped:
  * ALT + 0: Toggle XUnity AutoTranslator UI. (That's a zero, not an O)
+ * ALT + 1: Toggle Translation Aggregator UI.
  * ALT + T: Alternate between translated and untranslated versions of all texts provided by this plugin.
  * ALT + R: Reload translation files. Useful if you change the text and texture files on the fly. Not guaranteed to work for all textures.
  * ALT + U: Manual hooking. The default hooks wont always pick up texts. This will attempt to make lookups manually.
@@ -164,6 +165,8 @@ The supported translators are:
    * No limitations, but unstable.
  * [BingTranslateLegitimate](https://anonym.to/?https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview), based on the Azure text translation. Requires an API key.
    * Free up to 2 million characters per month.
+ * [PapagoTranslate](https://anonym.to/?https://papago.naver.com/), based on the online Google translation service. Does not require authentication.
+   * No limitations, but unstable.
  * [BaiduTranslate](https://anonym.to/?https://fanyi.baidu.com/), based on Baidu translation service. Requires AppId and AppSecret.
    * Not sure on quotas on this one.
  * [YandexTranslate](https://anonym.to/?https://tech.yandex.com/translate/), based on the Yandex translation service. Requires an API key.
@@ -173,6 +176,7 @@ The supported translators are:
  * LecPowerTranslator15, based on LEC's Power Translator. Does not require authentication, but does require the software installed.
    * No limitations.
  * CustomTranslate. Alternatively you can also specify any custom HTTP url that can be used as a translation endpoint (GET request). This must use the query parameters "from", "to" and "text" and return only a string with the result (try HTTP without SSL first, as unity-mono often has issues with SSL).
+   * *NOTE: This is a developer-centric option. You cannot simply specify "CustomTranslate" and expect it to work with any arbitrary translation service you find online. See [FAQ](#frequently-asked-questions)*
    * Example Configuration:
      * Endpoint=CustomTranslate
      * [Custom]
@@ -230,6 +234,7 @@ FromLanguage=ja                  ;The original language of the game
 [Files]
 Directory=Translation                                          ;Directory to search for cached translation files. Can use placeholder: {GameExeName}
 OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt   ;File to insert generated translations into. Can use placeholders: {GameExeName}, {lang}
+SubstitutionFile=Translation\_Substitutions.{lang}.txt         ;File that contains substitution applied before translations. Can use placeholders: {GameExeName}, {lang}
 
 [TextFrameworks]
 EnableUGUI=True                  ;Enable or disable UGUI translation
@@ -278,6 +283,11 @@ DetectDuplicateTextureNames=False;Indicates if the plugin should detect duplicat
 UserAgent=                       ;Override the user agent used by APIs requiring a user agent
 DisableCertificateValidation=False ;Indiciates whether certificate validations for the .NET API should be disabled
 
+[TranslationAggregator]
+Width=400                        ;The total width of the translation aggregator window.
+Height=100                       ;The width (per translator) of the translation aggregator window.
+EnabledTranslators=              ;The id's of the translation endpoints that has been enabled in the translation aggregator window. List is separated by ';'.
+
 [GoogleLegitimate]
 GoogleAPIKey=                    ;OPTIONAL, needed if GoogleTranslateLegitimate is configured
 
@@ -407,6 +417,19 @@ It is also worth noting that this plugin will read all text files (*.txt) in the
 
 In this context, the `Translation\_AutoGeneratedTranslations.{lang}.txt` (OutputFile) will always have the lowest priority when reading translations. So if the same translation is present in two places, it will not be the one from the (OutputFile) that is used.
 
+### Substitutions
+It is also possible to add substitutions that are applied to found texts before translations are created. This is controlled through the `SubstitutionFile`, which uses the same format as normal translation text files, although things like regexes are not supported.
+
+This is useful for replacing names that are often translated incorrectly, etc.
+
+When using substitutions, the found occurrences will be parameterized in the generated translations, like so:
+
+```
+私は{{A}}=I am {{A}}
+```
+
+When creating manual translations, use this file as sparingly as you would use regexes, as it can have an effect on performance.
+
 ## Regarding Redistribution
 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.**

+ 12 - 1
XUnity.AutoTranslator.sln

@@ -85,7 +85,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.RuntimeHooker.Benchm
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.BepIn-5x", "src\XUnity.AutoTranslator.Plugin.BepIn-5x\XUnity.AutoTranslator.Plugin.BepIn-5x.csproj", "{ADCCF172-7D31-42C6-B9D4-1779EAC8B403}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Plugin.Core.Tests", "test\XUnity.AutoTranslator.Plugin.Core.Tests\XUnity.AutoTranslator.Plugin.Core.Tests.csproj", "{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.Core.Tests", "test\XUnity.AutoTranslator.Plugin.Core.Tests\XUnity.AutoTranslator.Plugin.Core.Tests.csproj", "{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PapagoTranslate", "src\Translators\PapagoTranslate\PapagoTranslate.csproj", "{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -311,6 +313,14 @@ Global
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|Any CPU.Build.0 = Release|Any CPU
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.ActiveCfg = Release|Any CPU
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD}.Release|x86.Build.0 = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Debug|x86.Build.0 = Debug|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|x86.ActiveCfg = Release|Any CPU
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -344,6 +354,7 @@ Global
 		{E2F50278-9134-4DC8-9C50-4C0A52063A89} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
 		{ADCCF172-7D31-42C6-B9D4-1779EAC8B403} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
 		{4E1A0EB3-563A-419A-BE9D-5870A1A44CAD} = {2A4A3DDF-338C-40C0-8E26-2A810BAAADD6}
+		{6CCCF86D-AA99-45B2-A301-7AE24C2E6346} = {7A01BA34-3B96-4910-AC70-462BA59417CB}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}

+ 1 - 1
src/Translators/Lec.ExtProtocol/Program.cs

@@ -44,7 +44,7 @@ namespace Lec.ExtProtocol
                      if( message == null ) return;
 
                      var translatedTexts = new string[ message.UntranslatedTexts.Length ];
-                     for( int i = 0 ; i < message.UntranslatedTexts.Length ; i++ )
+                     for( int i = 0; i < message.UntranslatedTexts.Length; i++ )
                      {
                         var untranslatedText = message.UntranslatedTexts[ i ];
                         var translatedText = translator.Translate( untranslatedText );

+ 48 - 1
src/Translators/LecPowerTranslator15/LecPowerTranslator15Endpoint.cs

@@ -21,7 +21,15 @@ namespace LecPowerTranslator15
 
       public override void Initialize( IInitializationContext context )
       {
-         var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", "" );
+         var defaultPath = GetDefaultInstallationPath();
+         var pathToLec = context.GetOrCreateSetting( "LecPowerTranslator15", "InstallationPath", defaultPath );
+
+         if( string.IsNullOrEmpty( pathToLec ) && !string.IsNullOrEmpty( defaultPath ) )
+         {
+            context.SetSetting( "LecPowerTranslator15", "InstallationPath", defaultPath );
+            pathToLec = defaultPath;
+         }
+
          if( string.IsNullOrEmpty( pathToLec ) ) throw new Exception( "The LecPowerTranslator15 requires the path to the installation folder." );
 
          var exePath = Path.Combine( context.PluginDirectory, @"Translators\Lec.ExtProtocol.exe" );
@@ -35,5 +43,44 @@ namespace LecPowerTranslator15
          if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
          if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
       }
+
+      public static string GetDefaultInstallationPath()
+      {
+         try
+         {
+            var path = GetInstallationPathFromRegistry();
+
+            if( !string.IsNullOrEmpty( path ) )
+            {
+               var di = new DirectoryInfo( path );
+               path = di.Parent.FullName;
+            }
+
+            return path ?? string.Empty;
+         }
+         catch
+         {
+            return string.Empty;
+         }
+      }
+
+      public static string GetInstallationPathFromRegistry()
+      {
+         try
+         {
+            if( IntPtr.Size == 8 ) // 64-bit
+            {
+               return (string)Microsoft.Win32.Registry.GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\LogoMedia\LEC Power Translator 15\Configuration", "ApplicationPath", null );
+            }
+            else // 32-bit
+            {
+               return (string)Microsoft.Win32.Registry.GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\LogoMedia\LEC Power Translator 15\Configuration", "ApplicationPath", null );
+            }
+         }
+         catch
+         {
+            return null;
+         }
+      }
    }
 }

+ 169 - 0
src/Translators/PapagoTranslate/PapagoTranslate.cs

@@ -0,0 +1,169 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using SimpleJSON;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints.Http;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.AutoTranslator.Plugin.Core.Web;
+
+namespace PapagoTranslate
+{
+   public class PapagoTranslate : HttpEndpoint
+   {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "en", "ko", "zh-CN", "zh-TW", "es", "fr", "ru", "vi", "th", "id", "de", "ja" };
+
+      private static readonly string Url = "https://papago.naver.com/apis/n2mt/translate";
+      private static readonly string Website = "https://papago.naver.com";
+      private static readonly string JsonTemplate = "{{\"deviceId\":\"{0}\",\"dict\":false,\"dictDisplay\":0,\"honorific\":false,\"instant\":false,\"source\":\"{1}\",\"target\":\"{2}\",\"text\":\"{3}\"}}";
+      private static readonly string FormUrlEncodedTemplate = "data={0}";
+
+      private CookieContainer _cookies;
+      private string _deviceId;
+      private int _translationCount = 0;
+
+      public override string Id => "PapagoTranslate";
+
+      public override string FriendlyName => "Papago Translator";
+
+      public override int MaxTranslationsPerRequest => 10;
+
+      public override void Initialize( IInitializationContext context )
+      {
+         context.DisableCertificateChecksFor( "papago.naver.com" );
+
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The language '{context.DestinationLanguage}' is not supported by Papago Translate." );
+      }
+
+      public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )
+      {
+         if( _translationCount % 133 == 0 )
+         {
+            _cookies = new CookieContainer();
+            _deviceId = Guid.NewGuid().ToString();
+
+            // terminate session?????
+
+            var client = new XUnityWebClient();
+            var request = new XUnityWebRequest( Website );
+            request.Cookies = _cookies;
+
+            SetupDefaultHeaders( request );
+
+            var response = client.Send( request );
+            while( response.MoveNext() ) yield return response.Current;
+
+            // dont actually cared about the response, just the cookies
+         }
+      }
+
+      public override void OnCreateRequest( IHttpRequestCreationContext context )
+      {
+         var fullTranslationText = string.Join( "\n", context.UntranslatedTexts );
+         var jsonString = string.Format( JsonTemplate, _deviceId, context.SourceLanguage, context.DestinationLanguage, JsonHelper.Escape( fullTranslationText ) );
+         var base64 = Convert.ToBase64String( Encoding.UTF8.GetBytes( jsonString ) );
+         var obfuscatedBase64 = Obfuscate( 16, base64 );
+         var data = string.Format( FormUrlEncodedTemplate, Uri.EscapeDataString( obfuscatedBase64 ) );
+
+         var request = new XUnityWebRequest( "POST", Url, data );
+         request.Cookies = _cookies;
+
+         SetupDefaultHeaders( request );
+         SetupApiRequestHeaders( request );
+
+         context.Complete( request );
+
+         _translationCount++;
+      }
+
+      public override void OnExtractTranslation( IHttpTranslationExtractionContext context )
+      {
+         var obj = JSON.Parse( context.Response.Data ).AsObject;
+         var token = obj[ "translatedText" ].ToString();
+         var fullTranslatedText = JsonHelper.Unescape( token.Substring( 1, token.Length - 2 ) );
+
+         if( context.UntranslatedTexts.Length == 1 )
+         {
+            context.Complete( fullTranslatedText );
+         }
+         else
+         {
+            var splittedTranslations = fullTranslatedText.Split( '\n' );
+            var allTranslations = new string[ context.UntranslatedTexts.Length ];
+            int idx = 0;
+            for( int i = 0; i < context.UntranslatedTexts.Length; i++ )
+            {
+               var untranslatedLines = context.UntranslatedTexts[ i ].Split( '\n' );
+
+               StringBuilder builder = new StringBuilder();
+               for( int j = 0; j < untranslatedLines.Length; j++ )
+               {
+                  var translatedLine = splittedTranslations[ idx++ ];
+                  if( untranslatedLines.Length - 1 == j )
+                  {
+                     builder.Append( translatedLine );
+                  }
+                  else
+                  {
+                     builder.AppendLine( translatedLine );
+                  }
+               }
+
+               allTranslations[ i ] = builder.ToString();
+            }
+
+            if( idx != splittedTranslations.Length ) context.Fail( "Received invalid number of translations in batch." );
+
+            context.Complete( allTranslations );
+         }
+      }
+
+      private static void SetupDefaultHeaders( XUnityWebRequest request )
+      {
+         request.Headers[ HttpRequestHeader.UserAgent ] = string.IsNullOrEmpty( AutoTranslatorSettings.UserAgent ) ? UserAgents.Chrome_Win10_Latest : AutoTranslatorSettings.UserAgent;
+         request.Headers[ "Accept-Language" ] = "en-US";
+      }
+
+      private static void SetupApiRequestHeaders( XUnityWebRequest request )
+      {
+         request.Headers[ "device-type" ] = "pc";
+         request.Headers[ "Accept" ] = "application/json";
+         request.Headers[ "x-apigw-partnerid" ] = "papago";
+         request.Headers[ "Content-Type" ] = "application/x-www-form-urlencoded; charset=UTF-8";
+         request.Headers[ "Origin" ] = "https://papago.naver.com";
+         request.Headers[ "Referer" ] = "https://papago.naver.com/";
+      }
+
+      private static string Obfuscate( int count, string str )
+      {
+         var builder = new StringBuilder();
+         for( int i = 0; i < count; i++ )
+         {
+            var c = str[ i ];
+            if( ( 'a' <= c && c <= 'm' ) || ( 'A' <= c && c <= 'M' ) )
+            {
+               c += (char)13;
+
+            }
+            else if( ( 'n' <= c && c <= 'z' ) || 'N' <= c && c <= 'Z' )
+            {
+               c -= (char)13;
+            }
+
+            builder.Append( c );
+         }
+
+         for( int i = count; i < str.Length; i++ )
+         {
+            builder.Append( str[ i ] );
+         }
+
+         return builder.ToString();
+      }
+   }
+}

+ 15 - 0
src/Translators/PapagoTranslate/PapagoTranslate.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+  </ItemGroup>
+
+  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+    <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\Translators\&quot;&#xD;&#xA;)" />
+  </Target>
+
+</Project>

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

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

+ 2 - 0
src/XUnity.AutoTranslator.Plugin.BepIn-5x/AutoTranslatorPlugin.cs

@@ -41,6 +41,8 @@ namespace XUnity.AutoTranslator.Plugin.BepIn_5x
 
       public string TranslationPath { get; }
 
+      public bool AllowRuntimeHooksByDefault => false;
+
       public IniFile ReloadConfig()
       {
          if( !File.Exists( _configPath ) )

+ 1 - 4
src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>
-    <Version>3.5.0</Version>
+    <Version>3.6.0</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -19,9 +19,6 @@
     <Reference Include="UnityEngine">
       <HintPath>..\..\libs\UnityEngine.dll</HintPath>
     </Reference>
-    <Reference Include="UnityEngine.UI">
-      <HintPath>..\..\libs\UnityEngine.UI.dll</HintPath>
-    </Reference>
   </ItemGroup>
 
   <Target Name="PostBuild" AfterTargets="PostBuildEvent">

+ 2 - 0
src/XUnity.AutoTranslator.Plugin.BepIn/AutoTranslatorPlugin.cs

@@ -39,6 +39,8 @@ namespace XUnity.AutoTranslator.Plugin.BepIn
 
       public string ConfigPath => _dataPath;
 
+      public bool AllowRuntimeHooksByDefault => false;
+
       public IniFile ReloadConfig()
       {
          if( !File.Exists( _configPath ) )

+ 1 - 4
src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>
@@ -19,9 +19,6 @@
       <Reference Include="UnityEngine">
          <HintPath>..\..\libs\UnityEngine.dll</HintPath>
       </Reference>
-      <Reference Include="UnityEngine.UI">
-         <HintPath>..\..\libs\UnityEngine.UI.dll</HintPath>
-      </Reference>
    </ItemGroup>
 
    <Target Name="PostBuild" AfterTargets="PostBuildEvent">

+ 127 - 44
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -10,10 +10,8 @@ using System.Text.RegularExpressions;
 using System.Threading;
 using ExIni;
 using UnityEngine;
-using UnityEngine.UI;
 using System.Globalization;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
-using UnityEngine.EventSystems;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Utilities;
 using XUnity.AutoTranslator.Plugin.Core.Web;
@@ -149,11 +147,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
             DisableAutoTranslator();
 
             MainWindow = new XuaWindow( CreateXuaViewModel() );
-
-            // UNRELEASED: Not included in current release
-            //var vm = CreateTranslationAggregatorViewModel();
-            //TranslationAggregatorWindow = new TranslationAggregatorWindow( vm );
-            //TranslationAggregatorOptionsWindow = new TranslationAggregatorOptionsWindow( vm );
+            
+            var vm = CreateTranslationAggregatorViewModel();
+            TranslationAggregatorWindow = new TranslationAggregatorWindow( vm );
+            TranslationAggregatorOptionsWindow = new TranslationAggregatorOptionsWindow( vm );
          }
          catch( Exception e )
          {
@@ -236,23 +233,35 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
 
          // check if font is supported
-         if( !string.IsNullOrEmpty( Settings.OverrideFont ) )
+         try
          {
-            var available = Font.GetOSInstalledFontNames();
-            if( !available.Contains( Settings.OverrideFont ) )
-            {
-               XuaLogger.Current.Error( $"The specified override font is not available. Available fonts: " + string.Join( ", ", available ) );
-               Settings.OverrideFont = null;
-            }
-            else
+            if( !string.IsNullOrEmpty( Settings.OverrideFont ) )
             {
-               _hasValidOverrideFont = true;
-            }
+               var available = GetSupportedFonts();
+               if( !available.Contains( Settings.OverrideFont ) )
+               {
+                  XuaLogger.Current.Error( $"The specified override font is not available. Available fonts: " + string.Join( ", ", available ) );
+                  Settings.OverrideFont = null;
+               }
+               else
+               {
+                  _hasValidOverrideFont = true;
+               }
 
-            _hasOverridenFont = _hasValidOverrideFont;
+               _hasOverridenFont = _hasValidOverrideFont;
+            }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Error( e, "An error occurred while checking supported fonts." );
          }
       }
 
+      internal static string[] GetSupportedFonts()
+      {
+         return Font.GetOSInstalledFontNames();
+      }
+
       private void OnEndpointSelected( TranslationEndpointManager endpoint )
       {
          if( TranslationManager.CurrentEndpoint != endpoint )
@@ -574,7 +583,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       internal void SetTranslatedText( object ui, string translatedText, TextTranslationInfo info )
       {
          info?.SetTranslatedText( translatedText );
-         
+
          if( _isInTranslatedMode && !CallOrigin.ExpectsTextToBeReturned )
          {
             SetText( ui, translatedText, true, info );
@@ -669,7 +678,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             if( !ignoreComponentState )
             {
                var behaviour = component as Behaviour;
-               if( behaviour?.isActiveAndEnabled == false )
+               if( !go.activeInHierarchy || behaviour?.enabled == false ) // legacy "isActiveAndEnabled"
                {
                   return false;
                }
@@ -978,6 +987,13 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var isSpammer = ui.IsSpammingComponent();
             var textKey = GetCacheKey( ui, text, isSpammer, false );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               return untemplatedTranslation;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( TextCache.TryGetTranslation( textKey, false, out translation ) )
@@ -1025,13 +1041,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
             var textKey = GetCacheKey( null, text, false, context != null );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !endpoint.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               result.SetCompleted( untemplatedTranslation, true );
+               return result;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( endpoint.TryGetTranslation( textKey, out translation ) )
             {
                if( !string.IsNullOrEmpty( translation ) )
                {
-                  result.SetCompleted( translation, true );
+                  result.SetCompleted( textKey.Untemplate( translation ), true );
                }
                else
                {
@@ -1093,16 +1117,25 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var untranslatedTextPart = kvp.Value;
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && endpoint.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
-               string partTranslation;
-               if( endpoint.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), out partTranslation ) )
+               var textKey = new UntranslatedText( untranslatedTextPart, false, false );
+               if( endpoint.IsTranslatable( textKey.TemplatedOriginalText ) )
                {
-                  translations.Add( variableName, partTranslation );
+                  string partTranslation;
+                  if( endpoint.TryGetTranslation( textKey, out partTranslation ) )
+                  {
+                     translations.Add( variableName, textKey.Untemplate( partTranslation ) );
+                  }
+                  else if( allowStartJob )
+                  {
+                     // incomplete, must start job
+                     var context = new ParserTranslationContext( null, endpoint, translationResult, result );
+                     Translate( untranslatedTextPart, endpoint, context );
+                  }
                }
-               else if( allowStartJob )
+               else
                {
-                  // incomplete, must start job
-                  var context = new ParserTranslationContext( null, endpoint, translationResult, result );
-                  Translate( untranslatedTextPart, endpoint, context );
+                  // the template itself does not require a translation, which means the untranslated template equals the translated text
+                  translations.Add( variableName, textKey.Untemplate( textKey.TemplatedOriginalText ) );
                }
             }
             else
@@ -1142,6 +1175,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
             var textKey = GetCacheKey( ui, text, isSpammer, context != null );
 
+            // potentially shortcircuit if fully templated
+            if( text != textKey.TemplatedOriginalText && !TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
+            {
+               var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginalText );
+               if( context == null )
+               {
+                  SetTranslatedText( ui, untemplatedTranslation, info );
+               }
+               return untemplatedTranslation;
+            }
+
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
             if( TextCache.TryGetTranslation( textKey, !isSpammer, out translation ) )
@@ -1153,11 +1197,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                if( !string.IsNullOrEmpty( translation ) )
                {
+                  var untemplatedTranslation = textKey.Untemplate( translation );
                   if( context == null ) // never set text if operation is contextualized (only a part translation)
                   {
-                     SetTranslatedText( ui, textKey.Untemplate( translation ), info );
+                     SetTranslatedText( ui, untemplatedTranslation, info );
                   }
-                  return translation;
+                  return untemplatedTranslation;
                }
             }
             else
@@ -1234,6 +1279,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
                               {
                                  var stabilizedTextKey = GetCacheKey( ui, stabilizedText, false, false );
 
+                                 // potentially shortcircuit if fully templated
+                                 if( stabilizedText != stabilizedTextKey.TemplatedOriginalText && !TextCache.IsTranslatable( stabilizedTextKey.TemplatedOriginalText ) )
+                                 {
+                                    var untemplatedTranslation = stabilizedTextKey.Untemplate( stabilizedTextKey.TemplatedOriginalText );
+                                    SetTranslatedText( ui, untemplatedTranslation, info );
+                                    return;
+                                 }
+
                                  QueueNewUntranslatedForClipboard( stabilizedTextKey );
 
                                  info?.Reset( originalText );
@@ -1243,8 +1296,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                  {
                                     if( !string.IsNullOrEmpty( translation ) )
                                     {
-                                       // stabilized, no need to untemplate
-                                       SetTranslatedText( ui, translation, info );
+                                       SetTranslatedText( ui, stabilizedTextKey.Untemplate( translation ), info );
                                     }
                                  }
                                  else
@@ -1259,7 +1311,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                              var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
                                              if( translatedText != null )
                                              {
-                                                // stabilized, no need to untemplate
                                                 SetTranslatedText( ui, translatedText, info );
                                              }
                                              return;
@@ -1273,7 +1324,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                              var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
                                              if( translatedText != null )
                                              {
-                                                // stabilized, no need to untemplate
                                                 SetTranslatedText( ui, translatedText, info );
                                              }
                                              return;
@@ -1359,16 +1409,25 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var untranslatedTextPart = kvp.Value;
             if( !string.IsNullOrEmpty( untranslatedTextPart ) && TextCache.IsTranslatable( untranslatedTextPart ) && IsBelowMaxLength( untranslatedTextPart ) )
             {
-               string partTranslation;
-               if( TextCache.TryGetTranslation( new UntranslatedText( untranslatedTextPart, false, false ), false, out partTranslation ) )
+               var textKey = new UntranslatedText( untranslatedTextPart, false, false );
+               if( TextCache.IsTranslatable( textKey.TemplatedOriginalText ) )
                {
-                  translations.Add( variableName, partTranslation );
+                  string partTranslation;
+                  if( TextCache.TryGetTranslation( textKey, false, out partTranslation ) )
+                  {
+                     translations.Add( variableName, textKey.Untemplate( partTranslation ) );
+                  }
+                  else if( allowStartJob )
+                  {
+                     // incomplete, must start job
+                     var context = new ParserTranslationContext( ui, TranslationManager.CurrentEndpoint, null, result );
+                     TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, null, false, true, context );
+                  }
                }
-               else if( allowStartJob )
+               else
                {
-                  // incomplete, must start job
-                  var context = new ParserTranslationContext( ui, TranslationManager.CurrentEndpoint, null, result );
-                  TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, null, false, true, context );
+                  // the template itself does not require a translation, which means the untranslated template equals the translated text
+                  translations.Add( variableName, textKey.Untemplate( textKey.TemplatedOriginalText ) );
                }
             }
             else
@@ -1707,7 +1766,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
             if( !string.IsNullOrEmpty( job.TranslatedText ) )
             {
-               translationResult.SetCompleted( job.TranslatedText, false );
+               translationResult.SetCompleted( job.Key.Untemplate( job.TranslatedText ), false );
             }
             else
             {
@@ -1726,7 +1785,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   var info = component.GetOrCreateTextTranslationInfo();
                   if( !string.IsNullOrEmpty( job.TranslatedText ) )
                   {
-                     SetTranslatedText( component, job.TranslatedText, info );
+                     SetTranslatedText( component, job.Key.Untemplate( job.TranslatedText ), info );
                   }
                }
             }
@@ -1837,7 +1896,31 @@ namespace XUnity.AutoTranslator.Plugin.Core
                         var key = GetCacheKey( kvp.Key, originalText, false, false );
                         if( TextCache.TryGetTranslation( key, true, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
                         {
-                           SetTranslatedText( kvp.Key, translatedText, tti ); // no need to untemplatize the translated text
+                           SetTranslatedText( kvp.Key, key.Untemplate( translatedText ), tti ); // no need to untemplatize the translated text
+                        }
+                        else if( UnityTextParsers.GameLogTextParser.CanApply( ui ) )
+                        {
+                           var result = UnityTextParsers.GameLogTextParser.Parse( originalText );
+                           if( result.Succeeded )
+                           {
+                              var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
+                              if( translation != null )
+                              {
+                                 SetTranslatedText( ui, translation, tti );
+                              }
+                           }
+                        }
+                        else if( UnityTextParsers.RichTextParser.CanApply( ui ) && IsBelowMaxLength( originalText ) )
+                        {
+                           var result = UnityTextParsers.RichTextParser.Parse( originalText );
+                           if( result.Succeeded )
+                           {
+                              var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
+                              if( translation != null )
+                              {
+                                 SetTranslatedText( ui, translation, tti );
+                              }
+                           }
                         }
                      }
                   }

+ 41 - 1
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs

@@ -35,6 +35,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static string ApplicationName;
       public static float Timeout = 150.0f;
 
+      public static Dictionary<string, string> Replacements = new Dictionary<string, string>();
+
       public static bool SimulateError = false;
       public static bool SimulateDelayedError = false;
 
@@ -54,6 +56,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static string Language;
       public static string FromLanguage;
       public static string OutputFile;
+      public static string SubstitutionFile;
       public static string TranslationDirectory;
       public static float Delay;
       public static int MaxCharactersPerTranslation;
@@ -61,6 +64,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableConsole;
       public static bool EnableDebugLogs;
       public static string AutoTranslationsFilePath;
+      public static string SubstitutionFilePath;
       public static bool EnableIMGUI;
       public static bool EnableUGUI;
       public static bool EnableNGUI;
@@ -102,6 +106,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static HashSet<string> DuplicateTextureNames;
       public static TextureHashGenerationStrategy TextureHashGenerationStrategy;
 
+      public static float Height;
+      public static float Width;
+      public static HashSet<string> EnabledTranslators;
+
       public static bool CopyToClipboard;
       public static int MaxClipboardCopyCharacters;
 
@@ -118,6 +126,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
 
             TranslationDirectory = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "Directory", "Translation" );
             OutputFile = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "OutputFile", @"Translation\_AutoGeneratedTranslations.{lang}.txt" );
+            SubstitutionFile = PluginEnvironment.Current.Preferences.GetOrDefault( "Files", "SubstitutionFile", @"Translation\_Substitutions.{lang}.txt" );
 
             EnableIMGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableIMGUI", false );
             EnableUGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableUGUI", true );
@@ -148,7 +157,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             WhitespaceRemovalStrategy = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "WhitespaceRemovalStrategy", WhitespaceHandlingStrategy.TrimPerNewline );
             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 );
+            EnableExperimentalHooks = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "EnableExperimentalHooks", PluginEnvironment.Current.AllowRuntimeHooksByDefault && !Features.SupportsReflectionEmit );
             ForceExperimentalHooks = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "ForceExperimentalHooks", false );
             CacheRegexLookups = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "CacheRegexLookups", false );
             CacheWhitespaceDifferences = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "CacheWhitespaceDifferences", true );
@@ -175,6 +184,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             UserAgent = PluginEnvironment.Current.Preferences.GetOrDefault( "Http", "UserAgent", string.Empty );
             DisableCertificateValidation = PluginEnvironment.Current.Preferences.GetOrDefault( "Http", "DisableCertificateValidation", GetInitialDisableCertificateChecks() );
 
+
+            Width = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "Width", 400.0f );
+            Height = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "Height", 100.0f );
+            EnabledTranslators = PluginEnvironment.Current.Preferences.GetOrDefault( "TranslationAggregator", "EnabledTranslators", string.Empty )
+               ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).ToHashSet() ?? new HashSet<string>();
+
+
             EnablePrintHierarchy = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnablePrintHierarchy", false );
             EnableConsole = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableConsole", false );
             EnableDebugLogs = PluginEnvironment.Current.Preferences.GetOrDefault( "Debug", "EnableLog", false );
@@ -183,6 +199,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             MigrationsTag = PluginEnvironment.Current.Preferences.GetOrDefault( "Migrations", "Tag", string.Empty );
 
             AutoTranslationsFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, OutputFile.Replace( "{lang}", Language ) ).Replace( "/", "\\" ).Parameterize();
+            SubstitutionFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, SubstitutionFile.Replace( "{lang}", Language ) ).Replace( "/", "\\" ).Parameterize();
             UsesWhitespaceBetweenWords = LanguageHelper.RequiresWhitespaceUponLineMerging( FromLanguage );
 
 
@@ -222,6 +239,29 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          Save();
       }
 
+      public static void SetTranslationAggregatorBounds( float width, float height )
+      {
+         Width = width;
+         Height = height;
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "Width" ].Value = Width.ToString( CultureInfo.InvariantCulture );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "Height" ].Value = Height.ToString( CultureInfo.InvariantCulture );
+         Save();
+      }
+
+      public static void AddTranslator( string id )
+      {
+         EnabledTranslators.Add( id );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "EnabledTranslators" ].Value = string.Join( ";", EnabledTranslators.ToArray() );
+         Save();
+      }
+
+      public static void RemoveTranslator( string id )
+      {
+         EnabledTranslators.Remove( id );
+         PluginEnvironment.Current.Preferences[ "TranslationAggregator" ][ "EnabledTranslators" ].Value = string.Join( ";", EnabledTranslators.ToArray() );
+         Save();
+      }
+
       internal static void Save()
       {
          try

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

@@ -25,10 +25,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Constants
       public static readonly Type UIInput = FindType( "UIInput" );
 
       // Unity
+      public static readonly Type Text = FindType( "UnityEngine.UI.Text" );
+      public static readonly Type Image = FindType( "UnityEngine.UI.Image" );
+      public static readonly Type RawImage = FindType( "UnityEngine.UI.RawImage" );
+      public static readonly Type MaskableGraphic = FindType( "UnityEngine.UI.MaskableGraphic" );
+      public static readonly Type Graphic = FindType( "UnityEngine.UI.Graphic" );
       public static readonly Type GUIContent = FindType( "UnityEngine.GUIContent" );
       public static readonly Type WWW = FindType( "UnityEngine.WWW" );
       public static readonly Type InputField = FindType( "UnityEngine.UI.InputField" );
-      public static readonly Type Text = FindType( "UnityEngine.UI.Text" );
       public static readonly Type GUI = FindType( "UnityEngine.GUI" );
       public static readonly Type ImageConversion = FindType( "UnityEngine.ImageConversion" );
       public static readonly Type Texture = FindType( "UnityEngine.Texture" );

+ 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.5.0";
+      public const string Version = "3.6.0";
    }
 }

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

@@ -33,6 +33,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       public string ConfigPath => _dataFolder;
 
+      public bool AllowRuntimeHooksByDefault => true;
+
       public void SaveConfig()
       {
          _file.Save( _configPath );

+ 3 - 1
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs

@@ -2,6 +2,7 @@
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading;
@@ -86,8 +87,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
             if( _process == null )
             {
                _process = new Process();
-               _process.StartInfo.FileName = ExecutablePath;
+               _process.StartInfo.FileName = Path.Combine( Environment.CurrentDirectory, ExecutablePath );
                _process.StartInfo.Arguments = Arguments;
+               _process.StartInfo.WorkingDirectory = new FileInfo( ExecutablePath ).Directory.FullName;
                _process.EnableRaisingEvents = false;
                _process.StartInfo.UseShellExecute = false;
                _process.StartInfo.CreateNoWindow = true;

+ 9 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/IInitializationContext.cs

@@ -29,6 +29,15 @@
       /// <returns></returns>
       T GetOrCreateSetting<T>( string section, string key );
 
+      /// <summary>
+      /// Sets the specified setting.
+      /// </summary>
+      /// <typeparam name="T"></typeparam>
+      /// <param name="section"></param>
+      /// <param name="key"></param>
+      /// <param name="value"></param>
+      void SetSetting<T>( string section, string key, T value );
+
       /// <summary>
       /// Disables the certificate check for the specified hostnames.
       /// </summary>

+ 5 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/InitializationContext.cs

@@ -49,5 +49,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       {
          return PluginEnvironment.Current.Preferences.GetOrDefault( section, key, default( T ) );
       }
+
+      public void SetSetting<T>( string section, string key, T value )
+      {
+         PluginEnvironment.Current.Preferences.Set( section, key, value );
+      }
    }
 }

+ 22 - 24
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationEndpointManager.cs

@@ -63,20 +63,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public bool TryGetTranslation( UntranslatedText key, out string value )
       {
-         var unmodifiedKey = key.TranslatableText;
-         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         var translatableText = key.TranslatableText;
+         var result = _translations.TryGetValue( translatableText, out value );
          if( result )
          {
             return result;
          }
 
-         var modifiedKey = key.TrimmedTranslatableText;
-         result = _translations.TryGetValue( modifiedKey, out value );
+         var trimmedTranslatableText = key.TrimmedTranslatableText;
+         result = _translations.TryGetValue( trimmedTranslatableText, out value );
          if( result )
          {
             // add an unmodifiedKey to the dictionary
             var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
-            AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+            AddTranslationToCache( translatableText, unmodifiedValue );
 
             value = unmodifiedValue;
             return result;
@@ -96,17 +96,15 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       private void QueueNewTranslationForDisk( string key, string value )
       {
-         // FIXME: Implement
       }
 
       public void AddTranslationToCache( string key, string value )
       {
-         // UNRELEASED: Not included in current release
-         //if( !HasTranslated( key ) )
-         //{
-         //   AddTranslation( key, value );
-         //   QueueNewTranslationForDisk( key, value );
-         //}
+         if( !HasTranslated( key ) )
+         {
+            AddTranslation( key, value );
+            QueueNewTranslationForDisk( key, value );
+         }
       }
 
       public bool IsTranslatable( string text )
@@ -139,17 +137,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                _unstartedJobs.Remove( key );
                Manager.UnstartedTranslations--;
 
-               var untranslatedText = job.Key.TrimmedTranslatableText;
-               if( CanTranslate( untranslatedText ) )
+               var unpreparedUntranslatedText = job.Key.TrimmedTranslatableText;
+               var untranslatedText = job.Key.PrepareUntranslatedText( unpreparedUntranslatedText );
+               if( CanTranslate( unpreparedUntranslatedText ) )
                {
                   jobs.Add( job );
                   untranslatedTexts.Add( untranslatedText );
                   _ongoingJobs[ key ] = job;
                   Manager.OngoingTranslations++;
+
+                  XuaLogger.Current.Debug( "Started: '" + unpreparedUntranslatedText + "'" );
                }
                else
                {
-                  XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
+                  XuaLogger.Current.Warn( $"Dequeued: '{unpreparedUntranslatedText}' because the current endpoint has already failed this translation 3 times." );
                   job.State = TranslationJobState.Failed;
                   job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
 
@@ -162,10 +163,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
                AvailableBatchOperations--;
                var jobsArray = jobs.ToArray();
 
-               foreach( var untranslatedText in untranslatedTexts )
-               {
-                  XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
-               }
                CoroutineHelper.Start(
                   Translate(
                      untranslatedTexts.ToArray(),
@@ -196,13 +193,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             _unstartedJobs.Remove( key );
             Manager.UnstartedTranslations--;
 
-            var untranslatedText = job.Key.TrimmedTranslatableText;
-            if( CanTranslate( untranslatedText ) )
+            var unpreparedUntranslatedText = job.Key.TrimmedTranslatableText;
+            var untranslatedText = job.Key.PrepareUntranslatedText( unpreparedUntranslatedText );
+            if( CanTranslate( unpreparedUntranslatedText ) )
             {
                _ongoingJobs[ key ] = job;
                Manager.OngoingTranslations++;
 
-               XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
+               XuaLogger.Current.Debug( "Started: '" + unpreparedUntranslatedText + "'" );
                CoroutineHelper.Start(
                   Translate(
                      new[] { untranslatedText },
@@ -213,7 +211,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
             }
             else
             {
-               XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
+               XuaLogger.Current.Warn( $"Dequeued: '{unpreparedUntranslatedText}' because the current endpoint has already failed this translation 3 times." );
                job.State = TranslationJobState.Failed;
                job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
 
@@ -293,7 +291,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
          var hasTranslation = !string.IsNullOrEmpty( translatedText );
          if( hasTranslation )
          {
-            translatedText = key.RepairTemplate( translatedText );
+            translatedText = key.FixTranslatedText( translatedText );
             translatedText = key.LeadingWhitespace + translatedText + key.TrailingWhitespace;
 
             if( Settings.Language == Settings.Romaji && Settings.RomajiPostProcessing != TextPostProcessing.None )

+ 23 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/IniFileExtensions.cs

@@ -8,6 +8,29 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
    internal static class IniFileExtensions
    {
+      public static void Set<T>( this IniFile that, string section, string key, T value )
+      {
+         var typeOfT = typeof( T ).UnwrapNullable();
+         var iniSection = that[ section ];
+         var iniKey = iniSection[ key ];
+
+         if( value == null )
+         {
+            iniKey.Value = string.Empty;
+         }
+         else
+         {
+            if( typeOfT.IsEnum )
+            {
+               iniKey.Value = EnumHelper.GetNames( typeOfT, value );
+            }
+            else
+            {
+               iniKey.Value = Convert.ToString( value, CultureInfo.InvariantCulture );
+            }
+         }
+      }
+
       public static T GetOrDefault<T>( this IniFile that, string section, string key, T defaultValue )
       {
          var typeOfT = typeof( T ).UnwrapNullable();

+ 36 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs

@@ -77,6 +77,42 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return builder.ToString();
       }
 
+      public static TemplatedString TemplatizeByReplacements( this string str )
+      {
+         if( Settings.Replacements.Count == 0 ) return null;
+
+         var dict = new Dictionary<string, string>();
+         char arg = 'A';
+
+         foreach( var kvp in Settings.Replacements )
+         {
+            var original = kvp.Key;
+            var replacement = kvp.Value;
+
+            string key = null;
+            int idx = -1;
+            while( ( idx = str.IndexOf( original ) ) != -1 )
+            {
+               if( key == null )
+               {
+                  key = "{{" + arg++ + "}}";
+                  dict.Add( key, replacement );
+               }
+
+               str = str.Remove( idx, original.Length ).Insert( idx, key );
+            }
+         }
+
+         if( dict.Count > 0 )
+         {
+            return new TemplatedString( str, dict );
+         }
+         else
+         {
+            return null;
+         }
+      }
+
       public static TemplatedString TemplatizeByNumbers( this string str )
       {
          var dict = new Dictionary<string, string>();

+ 17 - 24
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs

@@ -6,32 +6,33 @@ using System.Linq;
 using System.Reflection;
 using System.Text;
 using UnityEngine;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Utilities;
+using XUnity.RuntimeHooker.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
    internal static class TextComponentExtensions
    {
+      private static readonly string SupportRichTextPropertyName = "supportRichText";
+      private static readonly string RichTextPropertyName = "richText";
+      private static readonly string TextPropertyName = "text";
+
       //private static readonly GUIContent[] TemporaryGUIContents = ClrTypes.GUIContent
       //   .GetFields( BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic )
       //   .Where( x => x.DeclaringType == typeof( GUIContent ) && ( x.Name == "s_Text" || x.Name == "s_TextImage" ) )
       //   .Select( x => (GUIContent)x.GetValue( null ) )
       //   .ToArray();
 
-      private static readonly string RichTextPropertyName = "richText";
-      private static readonly string TextPropertyName = "text";
-
       public static bool IsKnownTextType( this object ui )
       {
          if( ui == null ) return false;
 
          var type = ui.GetType();
 
-         return ( Settings.EnableUGUI && ui is Text )
-            || ( Settings.EnableIMGUI && ui is GUIContent )
+         return ( Settings.EnableIMGUI && ui is GUIContent )
+            || ( Settings.EnableUGUI && ClrTypes.Text != null && ClrTypes.Text.IsAssignableFrom( type ) )
             || ( Settings.EnableNGUI && ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type ) )
             || ( Settings.EnableTextMeshPro && ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) )
             /*|| ( ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) )*/;
@@ -43,8 +44,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
          var type = ui.GetType();
 
-         return ( ui as Text )?.supportRichText == true
-            || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) && Equals( type.GetProperty( RichTextPropertyName )?.GetValue( ui, null ), true ) )
+         return ( ClrTypes.Text != null && ClrTypes.Text.IsAssignableFrom( type ) && Equals( type.CachedProperty( SupportRichTextPropertyName )?.Get( ui ), true ) )
+            || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) && Equals( type.CachedProperty( RichTextPropertyName )?.Get( ui ), true ) )
             || ( ClrTypes.UguiNovelText != null && ClrTypes.UguiNovelText.IsAssignableFrom( type ) )
             /*|| ( ClrTypes.AdvCommand != null && ClrTypes.AdvCommand.IsAssignableFrom( type ) )*/;
       }
@@ -71,7 +72,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
          var type = ui.GetType();
 
-         return ui is Text
+         return ( ClrTypes.Text != null && ClrTypes.Text.IsAssignableFrom( type ) )
             || ( ClrTypes.UILabel != null && ClrTypes.UILabel.IsAssignableFrom( type ) )
             || ( ClrTypes.TMP_Text != null && ClrTypes.TMP_Text.IsAssignableFrom( type ) );
       }
@@ -113,18 +114,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          string text = null;
          var type = ui.GetType();
 
-         if( ui is Text )
-         {
-            text = ( (Text)ui ).text;
-         }
-         else if( ui is GUIContent )
+         if( ui is GUIContent )
          {
             text = ( (GUIContent)ui ).text;
          }
          else
          {
             // fallback to reflective approach
-            text = (string)ui.GetType()?.GetProperty( TextPropertyName )?.GetValue( ui, null );
+            text = (string)type.CachedProperty( TextPropertyName )?.Get( ui );
          }
 
          return text ?? string.Empty;
@@ -147,10 +144,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
                if( Equals( uguiNovelText, ui ) )
                {
                   string previousNameText = null;
-                  var nameText = (Text)uguiMessageWindow.GetType().GetField( "nameText", flags ).GetValue( uguiMessageWindow );
+                  var nameText = (UnityEngine.Object)uguiMessageWindow.GetType().GetField( "nameText", flags ).GetValue( uguiMessageWindow );
                   if( nameText )
                   {
-                     previousNameText = nameText.text;
+                     previousNameText = (string)ClrTypes.Text.CachedProperty( TextPropertyName ).Get( nameText );
                   }
 
                   var engine = uguiMessageWindow.GetType().GetProperty( "Engine", flags ).GetValue( uguiMessageWindow, null );
@@ -199,7 +196,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
                   if( nameText )
                   {
-                     nameText.text = previousNameText;
+                     ClrTypes.Text.CachedProperty( TextPropertyName ).Set( nameText, previousNameText );
                   }
 
                   return;
@@ -207,18 +204,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
             }
          }
 
-         if( ui is Text )
-         {
-            ( (Text)ui ).text = text;
-         }
-         else if( ui is GUIContent )
+         if( ui is GUIContent )
          {
             ( (GUIContent)ui ).text = text;
          }
          else
          {
             // fallback to reflective approach
-            type.GetProperty( TextPropertyName )?.GetSetMethod()?.Invoke( ui, new[] { text } );
+            type.CachedProperty( TextPropertyName )?.Set( ui, text );
          }
       }
    }

+ 11 - 18
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs

@@ -1,11 +1,13 @@
 using UnityEngine;
-using UnityEngine.UI;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Hooks;
+using XUnity.RuntimeHooker.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {
    internal static class TextureComponentExtensions
    {
+      private static readonly string SetAllDirtyMethodName = "SetAllDirty";
       private static readonly string TexturePropertyName = "texture";
       private static readonly string MainTexturePropertyName = "mainTexture";
       private static readonly string CapitalMainTexturePropertyName = "MainTexture";
@@ -25,15 +27,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       {
          if( ui == null ) return null;
 
-         if( ui is Image image )
-         {
-            return image.mainTexture as Texture2D;
-         }
-         else if( ui is RawImage rawImage )
-         {
-            return rawImage.mainTexture as Texture2D;
-         }
-         else if( ui is SpriteRenderer spriteRenderer )
+         if( ui is SpriteRenderer spriteRenderer )
          {
             return spriteRenderer.sprite?.texture;
          }
@@ -41,9 +35,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          {
             // lets attempt some reflection for several known types
             var type = ui.GetType();
-            var texture = type.GetProperty( MainTexturePropertyName )?.GetValue( ui, null )
-               ?? type.GetProperty( TexturePropertyName )?.GetValue( ui, null )
-               ?? type.GetProperty( CapitalMainTexturePropertyName )?.GetValue( ui, null );
+            var texture = type.CachedProperty( MainTexturePropertyName )?.Get( ui )
+               ?? type.CachedProperty( TexturePropertyName )?.Get( ui )
+               ?? type.CachedProperty( CapitalMainTexturePropertyName )?.Get( ui );
 
             return texture as Texture2D;
          }
@@ -53,15 +47,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       {
          if( ui == null ) return;
 
-         if( ui is Graphic graphic )
+         var type = ui.GetType();
+
+         if( ClrTypes.Graphic != null && ClrTypes.Graphic.IsAssignableFrom( type ) )
          {
-            graphic.SetAllDirty();
+            ClrTypes.Graphic.CachedMethod( SetAllDirtyMethodName ).Invoke( ui );
          }
          else
          {
-            // lets attempt some reflection for several known types
-            var type = ui.GetType();
-
             AccessToolsShim.Method( type, MarkAsChangedMethodName )?.Invoke( ui, null );
          }
       }

+ 3 - 3
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureExtensions.cs

@@ -1,11 +1,9 @@
 using System;
 using System.Collections.Generic;
-using System.Drawing;
 using System.Linq;
 using System.Reflection;
 using System.Text;
 using UnityEngine;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Hooks;
 
@@ -25,7 +23,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       {
          var type = ui.GetType();
 
-         return ( ui is Material || ui is UnityEngine.UI.Image || ui is RawImage || ui is SpriteRenderer )
+         return ( ui is Material || ui is SpriteRenderer )
+            || ( ClrTypes.Image != null && ClrTypes.Image.IsAssignableFrom( type ) )
+            || ( ClrTypes.RawImage != null && ClrTypes.RawImage.IsAssignableFrom( type ) )
             || ( ClrTypes.CubismRenderer != null && ClrTypes.CubismRenderer.IsAssignableFrom( type ) )
             || ( ClrTypes.UIWidget != null && type != ClrTypes.UILabel && ClrTypes.UIWidget.IsAssignableFrom( type ) )
             || ( ClrTypes.UIAtlas != null && ClrTypes.UIAtlas.IsAssignableFrom( type ) )

+ 33 - 0
src/XUnity.AutoTranslator.Plugin.Core/Features.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Linq;
+using System.Reflection.Emit;
 using System.Text;
+using UnityEngine;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 
 namespace XUnity.AutoTranslator.Plugin.Core
@@ -10,8 +12,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
    /// </summary>
    public static class Features
    {
+      internal static bool SupportsMouseScrollDelta { get; } = false;
+
       internal static bool SupportsClipboard { get; } = false;
 
+      internal static bool SupportsReflectionEmit { get; } = false;
+
       /// <summary>
       /// Gets a bool indicating if the class CustomYieldInstruction is available.
       /// </summary>
@@ -64,6 +70,33 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
 
          }
+
+         try
+         {
+            SupportsMouseScrollDelta = typeof( Input ).GetProperty( "mouseScrollDelta" ) != null;
+         }
+         catch( Exception )
+         {
+
+         }
+
+         try
+         {
+            TestReflectionEmit();
+
+            SupportsReflectionEmit = true;
+         }
+         catch( Exception )
+         {
+            SupportsReflectionEmit = false;
+         }
+      }
+
+      private static void TestReflectionEmit()
+      {
+         MethodToken t1 = default( MethodToken );
+         MethodToken t2 = default( MethodToken );
+         var ok = t1 == t2;
       }
    }
 }

+ 8 - 7
src/XUnity.AutoTranslator.Plugin.Core/Hooks/ImageHooks.cs

@@ -5,7 +5,6 @@ using System.Linq;
 using System.Reflection;
 using System.Text;
 using UnityEngine;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
 
@@ -169,12 +168,14 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
       static MethodBase TargetMethod( object instance )
       {
-         return AccessToolsShim.Method( typeof( MaskableGraphic ), "OnEnable" );
+         return AccessToolsShim.Method( ClrTypes.MaskableGraphic, "OnEnable" );
       }
 
       public static void Postfix( object __instance )
       {
-         if( __instance is Image || __instance is RawImage )
+         var type = __instance.GetType();
+         if( ( ClrTypes.Image != null && ClrTypes.Image.IsAssignableFrom( type ) )
+            || ( ClrTypes.RawImage != null && ClrTypes.RawImage.IsAssignableFrom( type ) ) )
          {
             AutoTranslationPlugin.Current.Hook_ImageChangedOnComponent( __instance, null, false, true );
          }
@@ -190,7 +191,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
       static MethodBase TargetMethod( object instance )
       {
-         return AccessToolsShim.Property( typeof( Image ), "sprite" )?.GetSetMethod();
+         return AccessToolsShim.Property( ClrTypes.Image, "sprite" )?.GetSetMethod();
       }
 
       public static void Postfix( object __instance )
@@ -208,7 +209,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
       static MethodBase TargetMethod( object instance )
       {
-         return AccessToolsShim.Property( typeof( Image ), "overrideSprite" )?.GetSetMethod();
+         return AccessToolsShim.Property( ClrTypes.Image, "overrideSprite" )?.GetSetMethod();
       }
 
       public static void Postfix( object __instance )
@@ -226,7 +227,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
       static MethodBase TargetMethod( object instance )
       {
-         return AccessToolsShim.Property( typeof( Image ), "material" )?.GetSetMethod();
+         return AccessToolsShim.Property( ClrTypes.Image, "material" )?.GetSetMethod();
       }
 
       public static void Postfix( object __instance )
@@ -244,7 +245,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
       static MethodBase TargetMethod( object instance )
       {
-         return AccessToolsShim.Property( typeof( RawImage ), "texture" )?.GetSetMethod();
+         return AccessToolsShim.Property( ClrTypes.RawImage, "texture" )?.GetSetMethod();
       }
 
       public static void Prefix( object __instance, Texture value )

+ 5 - 0
src/XUnity.AutoTranslator.Plugin.Core/IPluginEnvironment.cs

@@ -38,5 +38,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// Saves the preferences file.
       /// </summary>
       void SaveConfig();
+
+      /// <summary>
+      /// Gets a bool indicating whether the plugin environment allows experimental hooks by default.
+      /// </summary>
+      bool AllowRuntimeHooksByDefault { get; }
    }
 }

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs

@@ -45,7 +45,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing
                   }
                   else
                   {
-                     var key = "{{" + ( arg++ ) + "}}";
+                     var key = "[[" + ( arg++ ) + "]]";
                      template.Append( key ).Append( '\n' );
                      args.Add( key, line );
                      reverseArgs[ line ] = key;

+ 6 - 6
src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs

@@ -86,7 +86,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing
 
                if( !string.IsNullOrEmpty( text ) )
                {
-                  var argument = "{{" + ( arg++ ) + "}}";
+                  var argument = "[[" + ( arg++ ) + "]]";
                   args.Add( argument, text );
                   template.Append( argument );
                }
@@ -105,7 +105,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing
          // catch any remaining text
          if( offset < input.Length )
          {
-            var argument = "{{" + ( arg++ ) + "}}";
+            var argument = "[[" + ( arg++ ) + "]]";
             var text = input.Substring( offset, input.Length - offset );
             args.Add( argument, text );
             template.Append( argument );
@@ -114,20 +114,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing
 
          var templateString = template.ToString();
          int idx = -1;
-         while( ( idx = templateString.IndexOf( "}}{{" ) ) != -1 )
+         while( ( idx = templateString.IndexOf( "]][[" ) ) != -1 )
          {
             var arg1 = templateString[ idx - 1 ];
             var arg2 = templateString[ idx + 4 ];
 
-            var key1 = "{{" + arg1 + "}}";
-            var key2 = "{{" + arg2 + "}}";
+            var key1 = "[[" + arg1 + "]]";
+            var key2 = "[[" + arg2 + "]]";
 
             var text1 = args[ key1 ];
             var text2 = args[ key2 ];
 
             var fullText = text1 + text2;
             var fullKey = key1 + key2;
-            var newKey = "{{" + ( arg1 ) + "}}";
+            var newKey = "[[" + ( arg1 ) + "]]";
 
             args.Remove( key1 );
             args.Remove( key2 );

+ 60 - 52
src/XUnity.AutoTranslator.Plugin.Core/TemplatedString.cs

@@ -23,70 +23,78 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return text;
       }
 
-      public string RepairTemplate( string translatedTemplate )
+      public string PrepareUntranslatedText( string untranslatedText )
       {
-         foreach( var argument in Arguments.Keys )
+         foreach( var kvp in Arguments )
+         {
+            var key = kvp.Key;
+            var translatorFriendlyKey = CreateTranslatorFriendlyKey( key );
+
+            untranslatedText = untranslatedText.Replace( key, translatorFriendlyKey );
+         }
+         return untranslatedText;
+      }
+
+      public string FixTranslatedText( string translatedText )
+      {
+         foreach( var kvp in Arguments )
+         {
+            var key = kvp.Key;
+            var translatorFriendlyKey = CreateTranslatorFriendlyKey( key );
+            translatedText = ReplaceApproximateMatches( translatedText, translatorFriendlyKey, key );
+         }
+         return translatedText;
+      }
+
+      public static string CreateTranslatorFriendlyKey( string key )
+      {
+         var c = key[ 2 ];
+         var translatorFriendlyKey = "ZM" + (char)( c + 2 ) + "Z";
+         return translatorFriendlyKey;
+      }
+
+      public static string ReplaceApproximateMatches( string translatedText, string translatorFriendlyKey, string key )
+      {
+         var cidx = 0;
+         var startIdx = 0;
+
+         for( int i = 0; i < translatedText.Length; i++ )
          {
-            if( !translatedTemplate.Contains( argument ) )
+            var c = translatedText[ i ];
+            if( c == ' ' || c == ' ' ) continue;
+
+            if( c == translatorFriendlyKey[ cidx ] )
             {
-               var permutations = CreatePermutations( argument );
-               foreach( var permutation in permutations )
+               if( cidx == 0 )
                {
-                  if( translatedTemplate.Contains( permutation ) )
-                  {
-                     translatedTemplate = translatedTemplate.Replace( permutation, argument );
-                     break;
-                  }
+                  startIdx = i;
                }
+
+               cidx++;
+            }
+            else
+            {
+               cidx = 0;
+               startIdx = 0;
             }
-         }
 
-         return translatedTemplate;
-      }
+            if( cidx == translatorFriendlyKey.Length )
+            {
+               int endIdx = i + 1;
 
-      public static string[] CreatePermutations( string argument )
-      {
-         var b0_1 = argument.Insert( 2, " " );   // {{ A}}
-         var b0_2 = argument.Insert( 3, " " );   // {{A }}
+               var lengthOfKey = endIdx - startIdx;
+               var diff = lengthOfKey - key.Length;
 
-         var b1 = argument.Substring( 1 ); // {A}}
-         var b1_1 = b1.Insert( 1, " " );   // { A}}
-         var b1_2 = b1.Insert( 2, " " );   // {A }}
+               translatedText = translatedText.Remove( startIdx, lengthOfKey ).Insert( startIdx, key );
 
-         var b2 = argument.Substring( 0, argument.Length - 1 ); // {{A}
-         var b2_1 = b2.Insert( 2, " " );   // {{ A}
-         var b2_2 = b2.Insert( 3, " " );   // {{A }
+               i -= diff;
 
-         var b3 = argument.Substring( 1, argument.Length - 2 ); // {A}
-         var b3_1 = b3.Insert( 1, " " );   // { A}
-         var b3_2 = b3.Insert( 2, " " );   // {A }
+               cidx = 0;
+               startIdx = 0;
+            }
+         }
 
-         return new string[]
-         {
-            b0_1,
-            b0_1,
-            b2,
-            b2_1,
-            b2_2,
-            b1,
-            b1_1,
-            b1_2,
-            b3,
-            b3_1,
-            b3_2,
-            b0_1.ToLower(),
-            b0_2.ToLower(),
-            argument.ToLower(),
-            b2.ToLower(),
-            b2_1.ToLower(),
-            b2_2.ToLower(),
-            b1.ToLower(),
-            b1_1.ToLower(),
-            b1_2.ToLower(),
-            b3.ToLower(),
-            b3_1.ToLower(),
-            b3_2.ToLower(),
-         };
+         return translatedText;
       }
    }
 }

+ 74 - 49
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs

@@ -55,12 +55,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
                _defaultRegexes.Clear();
                _translations.Clear();
                _reverseTranslations.Clear();
+               Settings.Replacements.Clear();
 
                var mainTranslationFile = Settings.AutoTranslationsFilePath;
-               LoadTranslationsInFile( mainTranslationFile );
+               var substitutionFile = Settings.SubstitutionFilePath;
+               LoadTranslationsInFile( mainTranslationFile, false );
+               LoadTranslationsInFile( substitutionFile, true );
                foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile } ) )
                {
-                  LoadTranslationsInFile( fullFileName );
+                  LoadTranslationsInFile( fullFileName, false );
                }
             }
             var endTime = Time.realtimeSinceStartup;
@@ -72,56 +75,78 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void LoadTranslationsInFile( string fullFileName )
+      private void LoadTranslationsInFile( string fullFileName, bool isSubstitutionFile )
       {
-         if( File.Exists( fullFileName ) )
+         var fileExists = File.Exists( fullFileName );
+         if( fileExists || isSubstitutionFile )
          {
-            XuaLogger.Current.Debug( $"Loading texts: {fullFileName}." );
-
-            string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
-            foreach( string translation in translations )
+            if( fileExists )
             {
-               for( int i = 0; i < TranslationSplitters.Length; i++ )
+               XuaLogger.Current.Debug( $"Loading texts: {fullFileName}." );
+
+               string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
+               foreach( string translation in translations )
                {
-                  var splitter = TranslationSplitters[ i ];
-                  string[] kvp = translation.Split( splitter, StringSplitOptions.None );
-                  if( kvp.Length == 2 )
+                  for( int i = 0; i < TranslationSplitters.Length; i++ )
                   {
-                     string key = TextHelper.Decode( kvp[ 0 ] );
-                     string value = TextHelper.Decode( kvp[ 1 ] );
-
-                     if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
+                     var splitter = TranslationSplitters[ i ];
+                     string[] kvp = translation.Split( splitter, StringSplitOptions.None );
+                     if( kvp.Length == 2 )
                      {
-                        if( key.StartsWith( "r:" ) )
-                        {
-                           try
-                           {
-                              var regex = new RegexTranslation( key, value );
+                        string key = TextHelper.Decode( kvp[ 0 ] );
+                        string value = TextHelper.Decode( kvp[ 1 ] );
 
-                              AddTranslationRegex( regex );
-                           }
-                           catch( Exception e )
+                        if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
+                        {
+                           if( isSubstitutionFile )
                            {
-                              XuaLogger.Current.Warn( e, $"An error occurred while constructing regex translation: '{translation}'." );
+                              if( key != null && value != null )
+                              {
+                                 Settings.Replacements[ key ] = value;
+                              }
                            }
-                        }
-                        else
-                        {
-                           AddTranslation( key, value );
-
-                           // also add a modified version of the translation
-                           var ukey = new UntranslatedText( key, false, false );
-                           var uvalue = new UntranslatedText( value, false, false );
-                           if( ukey.TrimmedTranslatableText != key )
+                           else
                            {
-                              AddTranslation( ukey.TrimmedTranslatableText, uvalue.TrimmedTranslatableText );
+                              if( key.StartsWith( "r:" ) )
+                              {
+                                 try
+                                 {
+                                    var regex = new RegexTranslation( key, value );
+
+                                    AddTranslationRegex( regex );
+                                 }
+                                 catch( Exception e )
+                                 {
+                                    XuaLogger.Current.Warn( e, $"An error occurred while constructing regex translation: '{translation}'." );
+                                 }
+                              }
+                              else
+                              {
+                                 AddTranslation( key, value );
+
+                                 // also add a modified version of the translation
+                                 var ukey = new UntranslatedText( key, false, false );
+                                 var uvalue = new UntranslatedText( value, false, false );
+                                 if( ukey.TrimmedTranslatableText != key )
+                                 {
+                                    AddTranslation( ukey.TrimmedTranslatableText, uvalue.TrimmedTranslatableText );
+                                 }
+                              }
+                              break;
                            }
                         }
-                        break;
                      }
                   }
                }
             }
+            else if( isSubstitutionFile )
+            {
+               using( var stream = File.Create( fullFileName ) )
+               {
+                  stream.Write( new byte[] { 0xEF, 0xBB, 0xBF }, 0, 3 ); // UTF-8 BOM
+                  stream.Close();
+               }
+            }
          }
       }
 
@@ -243,24 +268,24 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, out string value )
       {
-         var unmodifiedKey = key.TranslatableText;
-         var result = _translations.TryGetValue( unmodifiedKey, out value );
+         var translatableText = key.TranslatableText;
+         var result = _translations.TryGetValue( translatableText, out value );
          if( result )
          {
             return result;
          }
 
-         var modifiedKey = key.TrimmedTranslatableText;
-         if( modifiedKey != unmodifiedKey )
+         var trimmedTranslatableText = key.TrimmedTranslatableText;
+         if( trimmedTranslatableText != translatableText )
          {
-            result = _translations.TryGetValue( modifiedKey, out value );
+            result = _translations.TryGetValue( trimmedTranslatableText, out value );
             if( result )
             {
                // add an unmodifiedKey to the dictionary
                var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
 
                XuaLogger.Current.Info( $"Whitespace difference: '{key.TrimmedTranslatableText}' => '{value}'" );
-               AddTranslationToCache( unmodifiedKey, unmodifiedValue, Settings.CacheWhitespaceDifferences );
+               AddTranslationToCache( translatableText, unmodifiedValue, Settings.CacheWhitespaceDifferences );
 
                value = unmodifiedValue;
                return result;
@@ -276,12 +301,12 @@ namespace XUnity.AutoTranslator.Plugin.Core
                var regex = _defaultRegexes[ i ];
                try
                {
-                  var match = regex.CompiledRegex.Match( unmodifiedKey );
+                  var match = regex.CompiledRegex.Match( translatableText );
                   if( !match.Success ) continue;
 
-                  var translation = regex.CompiledRegex.Replace( unmodifiedKey, regex.Translation );
+                  var translation = regex.CompiledRegex.Replace( translatableText, regex.Translation );
 
-                  AddTranslationToCache( unmodifiedKey, translation, Settings.CacheRegexLookups ); // Would store it to file... Should we????
+                  AddTranslationToCache( translatableText, translation, Settings.CacheRegexLookups ); // Would store it to file... Should we????
 
                   value = translation;
                   found = true;
@@ -305,15 +330,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          if( _staticTranslations.Count > 0 )
          {
-            if( _staticTranslations.TryGetValue( unmodifiedKey, out value ) )
+            if( _staticTranslations.TryGetValue( translatableText, out value ) )
             {
-               AddTranslationToCache( unmodifiedKey, value );
+               AddTranslationToCache( translatableText, value );
                return true;
             }
-            else if( _staticTranslations.TryGetValue( modifiedKey, out value ) )
+            else if( _staticTranslations.TryGetValue( trimmedTranslatableText, out value ) )
             {
                var unmodifiedValue = key.LeadingWhitespace + value + key.TrailingWhitespace;
-               AddTranslationToCache( unmodifiedKey, unmodifiedValue );
+               AddTranslationToCache( translatableText, unmodifiedValue );
 
                value = unmodifiedValue;
                return true;

+ 49 - 36
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs

@@ -3,12 +3,12 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using UnityEngine;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
 using XUnity.AutoTranslator.Plugin.Core.Fonts;
 using XUnity.AutoTranslator.Plugin.Core.Hooks;
+using XUnity.RuntimeHooker.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
@@ -49,7 +49,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                var ui = graphic as Component;
                if( ui != null )
                {
-                  _typewriter = (MonoBehaviour)ui.GetComponent( Constants.ClrTypes.Typewriter );
+                  _typewriter = (MonoBehaviour)ui.GetComponent( ClrTypes.Typewriter );
                }
             }
          }
@@ -60,23 +60,26 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      public void ChangeFont( object graphic )
+      public void ChangeFont( object ui )
       {
-         if( graphic == null ) return;
+         if( ui == null ) return;
+
+         var type = ui.GetType();
 
-         if( graphic is Text )
+         if( ClrTypes.Text != null && ClrTypes.Text.IsAssignableFrom( type ) )
          {
-            var ui = graphic as Text;
+            var fontProperty = ClrTypes.Text.CachedProperty( "font" );
+            var fontSizeProperty = ClrTypes.Text.CachedProperty( "fontSize" );
 
-            var previousFont = ui.font;
-            var newFont = FontCache.GetOrCreate( previousFont?.fontSize ?? ui.fontSize );
+            var previousFont = (Font)fontProperty.Get( ui );
+            var newFont = FontCache.GetOrCreate( previousFont?.fontSize ?? (int)fontSizeProperty.Get( ui ) );
 
             if( !ReferenceEquals( newFont, previousFont ) )
             {
-               ui.font = newFont;
+               fontProperty.Set( ui, newFont );
                _unfont = obj =>
                {
-                  ( (Text)obj ).font = previousFont;
+                  fontProperty.Set( obj, previousFont );
                };
             }
          }
@@ -90,48 +93,60 @@ namespace XUnity.AutoTranslator.Plugin.Core
          _unfont = null;
       }
 
-      public void ResizeUI( object graphic )
+      public static float GetComponentWidth( Component component )
+      {
+         // this is in it's own function because if "Text" does not exist, RectTransform likely wont exist either
+         return ( (RectTransform)component.transform ).rect.width;
+      }
+
+      public void ResizeUI( object ui )
       {
          // do not resize if there is no object of ir it is already resized
-         if( graphic == null ) return;
+         if( ui == null ) return;
+
+         var type = ui.GetType();
 
-         if( graphic is Text )
+         if( ClrTypes.Text != null && ClrTypes.Text.IsAssignableFrom( type ) )
          {
-            var ui = (Text)graphic;
+            var text = (Component)ui;
 
             // text is likely to be longer than there is space for, simply expand out anyway then
-            var componentWidth = ( (RectTransform)ui.transform ).rect.width;
+            var componentWidth = GetComponentWidth( text );
             var quarterScreenSize = Screen.width / 4;
             var isComponentWide = componentWidth > quarterScreenSize;
 
+            var horizontalOverflowProperty = ClrTypes.Text.CachedProperty( "horizontalOverflow" );
+            var verticalOverflowProperty = ClrTypes.Text.CachedProperty( "verticalOverflow" );
+            var lineSpacingProperty = ClrTypes.Text.CachedProperty( "lineSpacing" );
+            var resizeTextForBestFitProperty = ClrTypes.Text.CachedProperty( "resizeTextForBestFit" );
+
             // width < quarterScreenSize is used to determine the likelihood of a text using multiple lines
             // the idea is, if the UI element is larger than the width of half the screen, there is a larger
             // likelihood that it will go into multiple lines too.
-            var originalHorizontalOverflow = ui.horizontalOverflow;
-            var originalVerticalOverflow = ui.verticalOverflow;
-            var originalLineSpacing = ui.lineSpacing;
+            var originalHorizontalOverflow = horizontalOverflowProperty.Get( text );
+            var originalVerticalOverflow = verticalOverflowProperty.Get( text );
+            var originalLineSpacing = lineSpacingProperty.Get( text );
 
-            if( isComponentWide && !ui.resizeTextForBestFit )
+            if( isComponentWide && !(bool)resizeTextForBestFitProperty.Get( text ) )
             {
-               ui.horizontalOverflow = HorizontalWrapMode.Wrap;
-               ui.verticalOverflow = VerticalWrapMode.Overflow;
+               horizontalOverflowProperty.Set( text, 0 /* HorizontalWrapMode.Wrap */ );
+               verticalOverflowProperty.Set( text, 1 /* VerticalWrapMode.Overflow */ );
                if( Settings.ResizeUILineSpacingScale.HasValue && !Equals( _alteredSpacing, originalLineSpacing ) )
                {
-                  var alteredSpacing = originalLineSpacing * Settings.ResizeUILineSpacingScale.Value;
+                  var alteredSpacing = (float)originalLineSpacing * Settings.ResizeUILineSpacingScale.Value;
                   _alteredSpacing = alteredSpacing;
-                  ui.lineSpacing = alteredSpacing;
+                  lineSpacingProperty.Set( text, alteredSpacing );
                }
 
                if( _unresize == null )
                {
                   _unresize = g =>
                   {
-                     var gui = (Text)g;
-                     gui.horizontalOverflow = originalHorizontalOverflow;
-                     gui.verticalOverflow = originalVerticalOverflow;
+                     horizontalOverflowProperty.Set( g, originalHorizontalOverflow );
+                     verticalOverflowProperty.Set( g, originalVerticalOverflow );
                      if( Settings.ResizeUILineSpacingScale.HasValue )
                      {
-                        gui.lineSpacing = originalLineSpacing;
+                        lineSpacingProperty.Set( g, originalLineSpacing );
                      }
                   };
                }
@@ -139,17 +154,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
          else
          {
-            var type = graphic.GetType();
-
             // special handling for NGUI to better handle textbox sizing
             if( type == ClrTypes.UILabel )
             {
-               var originalMultiLine = type.GetProperty( MultiLinePropertyName )?.GetGetMethod()?.Invoke( graphic, null );
-               var originalOverflowMethod = type.GetProperty( OverflowMethodPropertyName )?.GetGetMethod()?.Invoke( graphic, null );
+               var originalMultiLine = type.GetProperty( MultiLinePropertyName )?.GetGetMethod()?.Invoke( ui, null );
+               var originalOverflowMethod = type.GetProperty( OverflowMethodPropertyName )?.GetGetMethod()?.Invoke( ui, null );
                //var originalSpacingY = graphic.GetSpacingY();
 
-               type.GetProperty( MultiLinePropertyName )?.GetSetMethod()?.Invoke( graphic, new object[] { true } );
-               type.GetProperty( OverflowMethodPropertyName )?.GetSetMethod()?.Invoke( graphic, new object[] { 0 } );
+               type.GetProperty( MultiLinePropertyName )?.GetSetMethod()?.Invoke( ui, new object[] { true } );
+               type.GetProperty( OverflowMethodPropertyName )?.GetSetMethod()?.Invoke( ui, new object[] { 0 } );
                //if( Settings.ResizeUILineSpacingScale.HasValue && !Equals( _alteredSpacing, originalSpacingY ) )
                //{
                //   var alteredSpacing = originalSpacingY.Multiply( Settings.ResizeUILineSpacingScale.Value );
@@ -173,14 +186,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
             }
             else if( type == ClrTypes.TextMeshPro || type == ClrTypes.TextMeshProUGUI )
             {
-               var originalOverflowMode = ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName )?.GetValue( graphic, null );
+               var originalOverflowMode = ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName )?.GetValue( ui, null );
 
                // ellipsis (1) works
                // masking (2) has a tendency to break in some versions of TMP
                // truncate (3) works
                if( originalOverflowMode != null && (int)originalOverflowMode == 2 )
                {
-                  ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName ).SetValue( graphic, 3, null );
+                  ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName ).SetValue( ui, 3, null );
 
                   _unresize = g =>
                   {
@@ -195,7 +208,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       public void UnresizeUI( object graphic )
       {
          if( graphic == null ) return;
-         
+
          _unresize?.Invoke( graphic );
          _unresize = null;
       }

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

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Net;
 using System.Text;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Endpoints;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
 

+ 41 - 1
src/XUnity.AutoTranslator.Plugin.Core/UI/GUIUtil.cs

@@ -21,7 +21,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
       {
          richText = false,
          margin = new RectOffset( GUI.skin.label.margin.left, GUI.skin.label.margin.right, 0, 0 ),
-         padding = new RectOffset( GUI.skin.label.padding.left, GUI.skin.label.padding.right, 0, 0 )
+         padding = new RectOffset( GUI.skin.label.padding.left, GUI.skin.label.padding.right, 2, 3 )
       };
 
       public static readonly GUIStyle LabelCenter = new GUIStyle( GUI.skin.label )
@@ -94,6 +94,26 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
          return WindowBackgroundStyle;
       }
 
+      public static bool IsAnyMouseButtonOrScrollWheelDownSafe
+      {
+         get
+         {
+            return Features.SupportsMouseScrollDelta
+               ? IsAnyMouseButtonOrScrollWheelDown
+               : IsAnyMouseButtonOrScrollWheelDownLegacy;
+         }
+      }
+
+      public static bool IsAnyMouseButtonOrScrollWheelDownLegacy
+      {
+         get
+         {
+            return Input.GetMouseButtonDown( 0 )
+               || Input.GetMouseButtonDown( 1 )
+               || Input.GetMouseButtonDown( 2 );
+         }
+      }
+
       public static bool IsAnyMouseButtonOrScrollWheelDown
       {
          get
@@ -105,6 +125,26 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
          }
       }
 
+      public static bool IsAnyMouseButtonOrScrollWheelSafe
+      {
+         get
+         {
+            return Features.SupportsMouseScrollDelta
+               ? IsAnyMouseButtonOrScrollWheel
+               : IsAnyMouseButtonOrScrollWheelLegacy;
+         }
+      }
+
+      public static bool IsAnyMouseButtonOrScrollWheelLegacy
+      {
+         get
+         {
+            return Input.GetMouseButton( 0 )
+               || Input.GetMouseButton( 1 )
+               || Input.GetMouseButton( 2 );
+         }
+      }
+
       public static bool IsAnyMouseButtonOrScrollWheel
       {
          get

+ 4 - 4
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorOptionsWindow.cs

@@ -40,13 +40,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
          _windowRect = GUI.Window( WindowId, _windowRect, CreateWindowUI, "---- Translation Aggregator Options ----" );
 
-         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDown )
+         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDownSafe )
          {
             var point = new Vector2( Input.mousePosition.x, Screen.height - Input.mousePosition.y );
             _isMouseDownOnWindow = _windowRect.Contains( point );
          }
 
-         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheel )
+         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheelSafe )
             return;
 
          // make sure window is focused if scroll wheel is used to indicate we consumed that event
@@ -90,12 +90,12 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
          GUILayout.BeginHorizontal();
          GUILayout.Label( "Height" );
-         _viewModel.Height = GUILayout.HorizontalSlider( _viewModel.Height, 50, 300, GUILayout.MaxWidth( 250 ) );
+         _viewModel.Height = Mathf.Round( GUILayout.HorizontalSlider( _viewModel.Height, 50, 300, GUILayout.MaxWidth( 250 ) ) );
          GUILayout.EndHorizontal();
 
          GUILayout.BeginHorizontal();
          GUILayout.Label( "Width" );
-         _viewModel.Width = GUILayout.HorizontalSlider( _viewModel.Width, 200, 1000, GUILayout.MaxWidth( 250 ) );
+         _viewModel.Width = Mathf.Round( GUILayout.HorizontalSlider( _viewModel.Width, 200, 1000, GUILayout.MaxWidth( 250 ) ) );
          GUILayout.EndHorizontal();
 
          GUI.DragWindow();

+ 36 - 4
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorViewModel.cs

@@ -4,24 +4,29 @@ using System.Linq;
 using UnityEngine;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    class TranslationAggregatorViewModel
    {
+      private DebounceFunction _saveHeightAndWidth;
       private LinkedList<AggregatedTranslationViewModel> _translations;
       private LinkedListNode<AggregatedTranslationViewModel> _current;
       private List<Translation> _translationsToAggregate = new List<Translation>();
       private HashSet<string> _textsToAggregate = new HashSet<string>();
       private float _lastUpdate = 0.0f;
+      private float _height;
+      private float _width;
 
       public TranslationAggregatorViewModel( TranslationManager translationManager )
       {
          _translations = new LinkedList<AggregatedTranslationViewModel>();
+         _saveHeightAndWidth = new DebounceFunction( 1, SaveHeightAndWidth );
 
          Manager = translationManager;
-         Height = 100; // TODO: Get from config
-         Width = 400; // TODO: Get from config
+         Height = Settings.Height;
+         Width = Settings.Width;
 
          AllTranslators = translationManager.AllEndpoints
             .Select( x => new TranslatorViewModel( x ) )
@@ -36,9 +41,31 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
       public bool IsShowingOptions { get; set; }
 
-      public float Height { get; set; }
+      public float Height
+      {
+         get { return _height; }
+         set
+         {
+            if( _height != value )
+            {
+               _height = value;
+               _saveHeightAndWidth.Execute();
+            }
+         }
+      }
 
-      public float Width { get; set; }
+      public float Width
+      {
+         get { return _width; }
+         set
+         {
+            if( _width != value )
+            {
+               _width = value;
+               _saveHeightAndWidth.Execute();
+            }
+         }
+      }
 
       public List<TranslatorViewModel> AvailableTranslators { get; }
 
@@ -48,6 +75,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
       public AggregatedTranslationViewModel Current => _current?.Value;
 
+      private void SaveHeightAndWidth()
+      {
+         Settings.SetTranslationAggregatorBounds( Width, Height );
+      }
+
       public void OnNewTranslationAdded( TextTranslationInfo info )
       {
          if( !_textsToAggregate.Contains( info.OriginalText ) )

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslationAggregatorWindow.cs

@@ -45,13 +45,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
          _windowRect.width = _viewModel.Width;
          _windowRect = GUI.Window( WindowId, _windowRect, CreateWindowUI, "---- Translation Aggregator ----" );
 
-         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDown )
+         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDownSafe )
          {
             var point = new Vector2( Input.mousePosition.x, Screen.height - Input.mousePosition.y );
             _isMouseDownOnWindow = _windowRect.Contains( point );
          }
 
-         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheel )
+         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheelSafe )
             return;
 
          // make sure window is focused if scroll wheel is used to indicate we consumed that event

+ 25 - 3
src/XUnity.AutoTranslator.Plugin.Core/UI/TranslatorViewModel.cs

@@ -1,17 +1,39 @@
-using XUnity.AutoTranslator.Plugin.Core.Endpoints;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Endpoints;
 
 namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    class TranslatorViewModel
    {
+      private bool _isEnabled;
+
       public TranslatorViewModel( TranslationEndpointManager endpoint )
       {
          Endpoint = endpoint;
-         IsEnabled = false; // TODO: initialize from configuration...
+         IsEnabled = Settings.EnabledTranslators.Contains( endpoint.Endpoint.Id );
       }
 
       public TranslationEndpointManager Endpoint { get; set; }
 
-      public bool IsEnabled { get; set; }
+      public bool IsEnabled
+      {
+         get { return _isEnabled; }
+         set
+         {
+            if( _isEnabled != value )
+            {
+               _isEnabled = value;
+
+               if( _isEnabled )
+               {
+                  Settings.AddTranslator( Endpoint.Endpoint.Id );
+               }
+               else
+               {
+                  Settings.RemoveTranslator( Endpoint.Endpoint.Id );
+               }
+            }
+         }
+      }
    }
 }

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/UI/XuaWindow.cs

@@ -36,13 +36,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 
          _windowRect = GUI.Window( WindowId, _windowRect, CreateWindowUI, "---- XUnity.AutoTranslator UI ----" );
 
-         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDown )
+         if( GUIUtil.IsAnyMouseButtonOrScrollWheelDownSafe )
          {
             var point = new Vector2( Input.mousePosition.x, Screen.height - Input.mousePosition.y );
             _isMouseDownOnWindow = _windowRect.Contains( point );
          }
 
-         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheel )
+         if( !_isMouseDownOnWindow || !GUIUtil.IsAnyMouseButtonOrScrollWheelSafe )
             return;
 
          // make sure window is focused if scroll wheel is used to indicate we consumed that event

+ 24 - 2
src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs

@@ -21,6 +21,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
                text = TemplatedText.Template;
             }
          }
+         else
+         {
+            TemplatedText = text.TemplatizeByReplacements();
+            if( TemplatedText != null )
+            {
+               text = TemplatedText.Template;
+            }
+         }
+
+         TemplatedOriginalText = text;
 
          int i = 0;
          int firstNonWhitespace = 0;
@@ -196,6 +206,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       public string OriginalText { get; }
 
+      public string TemplatedOriginalText { get; }
+
       public TemplatedString TemplatedText { get; }
 
       public string Untemplate( string text )
@@ -208,11 +220,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return text;
       }
 
-      public string RepairTemplate( string text )
+      public string PrepareUntranslatedText( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.PrepareUntranslatedText( text );
+         }
+
+         return text;
+      }
+
+      public string FixTranslatedText( string text )
       {
          if( TemplatedText != null )
          {
-            return TemplatedText.RepairTemplate( text );
+            return TemplatedText.FixTranslatedText( text );
          }
 
          return text;

+ 3 - 2
src/XUnity.AutoTranslator.Plugin.Core/Utilities/CoroutineHelper.cs

@@ -1,5 +1,4 @@
-using System;
-using System.Collections;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -10,5 +9,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
    internal static class CoroutineHelper
    {
       public static Coroutine Start( IEnumerator coroutine ) => AutoTranslationPlugin.Current.StartCoroutine( coroutine );
+
+      public static void Stop( Coroutine coroutine ) => AutoTranslationPlugin.Current.StopCoroutine( coroutine );
    }
 }

+ 38 - 0
src/XUnity.AutoTranslator.Plugin.Core/Utilities/DebounceFunction.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections;
+using UnityEngine;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Utilities
+{
+   internal class DebounceFunction
+   {
+      private readonly float _delaySeconds;
+      private readonly Action _callback;
+      private Coroutine _current;
+
+      public DebounceFunction( float delaySeconds, Action callback )
+      {
+         _delaySeconds = delaySeconds;
+         _callback = callback;
+      }
+
+      public void Execute()
+      {
+         if( _current != null )
+         {
+            CoroutineHelper.Stop( _current );
+         }
+
+         _current = CoroutineHelper.Start( Run() );
+      }
+
+      private IEnumerator Run()
+      {
+         yield return new WaitForSeconds( _delaySeconds );
+
+         _callback();
+
+         _current = null;
+      }
+   }
+}

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

@@ -4,7 +4,6 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
-using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Utilities;
 using UnityEngine;

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -19,9 +19,6 @@
       <Reference Include="UnityEngine">
          <HintPath>..\..\libs\UnityEngine.dll</HintPath>
       </Reference>
-      <Reference Include="UnityEngine.UI">
-         <HintPath>..\..\libs\UnityEngine.UI.dll</HintPath>
-      </Reference>
    </ItemGroup>
 
    <ItemGroup>

+ 2 - 0
src/XUnity.AutoTranslator.Plugin.IPA/AutoTranslatorPlugin.cs

@@ -38,6 +38,8 @@ namespace XUnity.AutoTranslator.Plugin.IPA
 
       public string ConfigPath => _dataPath;
 
+      public bool AllowRuntimeHooksByDefault => false;
+
       public IniFile ReloadConfig()
       {
          if( !File.Exists( _configPath ) )

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

+ 2 - 0
src/XUnity.AutoTranslator.Plugin.UnityInjector/AutoTranslatorPlugin.cs

@@ -20,6 +20,8 @@ namespace XUnity.AutoTranslator.Plugin.UnityInjector
 
       public string ConfigPath => DataPath;
 
+      public bool AllowRuntimeHooksByDefault => false;
+
       void IPluginEnvironment.SaveConfig()
       {
          SaveConfig();

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.5.0</Version>
+      <Version>3.6.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

@@ -16,6 +16,7 @@
     <ProjectReference Include="..\Translators\GoogleTranslateLegitimate\GoogleTranslateLegitimate.csproj" />
     <ProjectReference Include="..\Translators\GoogleTranslate\GoogleTranslate.csproj" />
     <ProjectReference Include="..\Translators\LecPowerTranslator15\LecPowerTranslator15.csproj" />
+    <ProjectReference Include="..\Translators\PapagoTranslate\PapagoTranslate.csproj" />
     <ProjectReference Include="..\Translators\ReverseTranslator\ReverseTranslator.csproj" />
     <ProjectReference Include="..\Translators\WatsonTranslate\WatsonTranslate.csproj" />
     <ProjectReference Include="..\Translators\YandexTranslate\YandexTranslate.csproj" />

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

@@ -52,6 +52,7 @@ namespace XUnity.AutoTranslator.Setup
          AddFile( Path.Combine( translatorsPath, "Lec.ExtProtocol.exe" ), Resources.Lec_ExtProtocol, true );
          AddFile( Path.Combine( translatorsPath, "WatsonTranslate.dll" ), Resources.WatsonTranslate, true );
          AddFile( Path.Combine( translatorsPath, "YandexTranslate.dll" ), Resources.YandexTranslate, true );
+         AddFile( Path.Combine( translatorsPath, "PapagoTranslate.dll" ), Resources.PapagoTranslate, true );
 
          foreach( var launcher in launchers )
          {

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

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

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

@@ -184,6 +184,9 @@
    <data name="YandexTranslate" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\YandexTranslate.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
    </data>
+   <data name="PapagoTranslate" type="System.Resources.ResXFileRef, System.Windows.Forms">
+      <value>..\..\XUnity.AutoTranslator.Setup.Build\bin\net35\PapagoTranslate.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+   </data>
    <data name="Lec_ExtProtocol" type="System.Resources.ResXFileRef, System.Windows.Forms">
       <value>..\..\XUnity.AutoTranslator.Setup.Build-x86\bin\net35\Lec.ExtProtocol.exe;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.5.0</Version>
+      <Version>3.6.0</Version>
       <ApplicationIcon>icon.ico</ApplicationIcon>
       <Win32Resource />
    </PropertyGroup>

+ 197 - 0
src/XUnity.RuntimeHooker.Core/Utilities/ReflectionCache.cs

@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.RuntimeHooker.Core.Utilities
+{
+   public static class ReflectionCache
+   {
+      private static Dictionary<MemberLookupKey, CachedMethod> Methods = new Dictionary<MemberLookupKey, CachedMethod>();
+      private static Dictionary<MemberLookupKey, CachedProperty> Properties = new Dictionary<MemberLookupKey, CachedProperty>();
+
+      public static CachedMethod CachedMethod( this Type type, string name )
+      {
+         var key = new MemberLookupKey( type, name );
+         if( !Methods.TryGetValue( key, out var cachedMember ) )
+         {
+            var currentType = type;
+            MethodInfo method = null;
+
+            while( method == null && currentType != null )
+            {
+               method = currentType.GetMethod( name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic );
+               currentType = currentType.BaseType;
+            }
+
+            if( method != null )
+            {
+               cachedMember = new CachedMethod( method );
+            }
+
+            // also cache nulls!
+            Methods[ key ] = cachedMember;
+         }
+
+         return cachedMember;
+      }
+
+      public static CachedProperty CachedProperty( this Type type, string name )
+      {
+         var key = new MemberLookupKey( type, name );
+         if( !Properties.TryGetValue( key, out var cachedMember ) )
+         {
+            var currentType = type;
+            PropertyInfo property = null;
+
+            while( property == null && currentType != null )
+            {
+               property = currentType.GetProperty( name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic );
+               currentType = currentType.BaseType;
+            }
+
+            if( property != null )
+            {
+               cachedMember = new CachedProperty( property );
+            }
+
+            // also cache nulls!
+            Properties[ key ] = cachedMember;
+         }
+
+         return cachedMember;
+      }
+
+      public struct MemberLookupKey
+      {
+         public MemberLookupKey( Type type, string memberName )
+         {
+            Type = type;
+            MemberName = memberName;
+         }
+
+         public Type Type { get; set; }
+
+         public string MemberName { get; set; }
+
+         // override object.Equals
+         public override bool Equals( object obj )
+         {
+            if( obj is MemberLookupKey key )
+            {
+               return Type == key.Type && MemberName == key.MemberName;
+            }
+            return false;
+         }
+
+         // override object.GetHashCode
+         public override int GetHashCode()
+         {
+            return Type.GetHashCode() + MemberName.GetHashCode();
+         }
+      }
+   }
+
+   public class CachedMethod
+   {
+      private static readonly object[] Args0 = new object[ 0 ];
+      private static readonly object[] Args1 = new object[ 1 ];
+      private static readonly object[] Args2 = new object[ 2 ];
+
+      private Func<object, object[], object> _invoke;
+
+      public CachedMethod( MethodInfo method )
+      {
+         _invoke = ExpressionHelper.CreateFastInvoke( method );
+      }
+
+      public object Invoke( object instance, object[] arguments )
+      {
+         return _invoke( instance, arguments );
+      }
+
+      public object Invoke( object instance )
+      {
+         return _invoke( instance, Args0 );
+      }
+
+      public object Invoke( object instance, object arg1 )
+      {
+         try
+         {
+            Args1[ 0 ] = arg1;
+            return _invoke( instance, Args1 );
+         }
+         finally
+         {
+            Args1[ 0 ] = null;
+         }
+      }
+
+      public object Invoke( object instance, object arg1, object arg2 )
+      {
+         try
+         {
+            Args2[ 0 ] = arg1;
+            Args2[ 1 ] = arg2;
+            return _invoke( instance, Args2 );
+         }
+         finally
+         {
+            Args2[ 0 ] = null;
+            Args2[ 1 ] = null;
+         }
+      }
+   }
+
+   public class CachedProperty
+   {
+      private static readonly object[] Args0 = new object[ 0 ];
+      private static readonly object[] Args1 = new object[ 1 ];
+
+      private Func<object, object[], object> _set;
+      private Func<object, object[], object> _get;
+
+      public CachedProperty( PropertyInfo propertyInfo )
+      {
+         if( propertyInfo.CanRead )
+         {
+            _get = ExpressionHelper.CreateFastInvoke( propertyInfo.GetGetMethod() );
+         }
+
+         if( propertyInfo.CanWrite )
+         {
+            _set = ExpressionHelper.CreateFastInvoke( propertyInfo.GetSetMethod() );
+         }
+      }
+
+      public object Set( object instance, object[] arguments )
+      {
+         return _set( instance, arguments );
+      }
+
+      public object Set( object instance, object arg1 )
+      {
+         try
+         {
+            Args1[ 0 ] = arg1;
+            return _set( instance, Args1 );
+         }
+         finally
+         {
+            Args1[ 0 ] = null;
+         }
+      }
+
+      public object Get( object instance, object[] arguments )
+      {
+         return _get( instance, arguments );
+      }
+
+      public object Get( object instance )
+      {
+         return _get( instance, Args0 );
+      }
+   }
+}