浏览代码

Initial code commit

Bepis 7 年之前
父节点
当前提交
13795de5bd
共有 58 个文件被更改,包括 5285 次插入42 次删除
  1. 92 0
      .gitattributes
  2. 7 41
      .gitignore
  3. 20 0
      CHANGELOG.md
  4. 92 1
      README.md
  5. 58 0
      XUnity.AutoTranslator.sln
  6. 二进制
      libs/0Harmony.dll
  7. 二进制
      libs/BepInEx.dll
  8. 二进制
      libs/ExIni.dll
  9. 二进制
      libs/IllusionPlugin.dll
  10. 二进制
      libs/Mono.Cecil.Inject.dll
  11. 二进制
      libs/Mono.Cecil.dll
  12. 二进制
      libs/ReiPatcher.exe
  13. 二进制
      libs/UnityEngine.UI.dll
  14. 二进制
      libs/UnityEngine.dll
  15. 84 0
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  16. 26 0
      src/XUnity.AutoTranslator.Patcher/XUnity.AutoTranslator.Patcher.csproj
  17. 68 0
      src/XUnity.AutoTranslator.Plugin.BepIn/AutoTranslatorPlugin.cs
  18. 39 0
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  19. 839 0
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  20. 13 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Config.cs
  21. 58 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/DefaultConfiguration.cs
  22. 19 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/IConfiguration.cs
  23. 42 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs
  24. 60 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  25. 12 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs
  26. 14 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEvents.cs
  27. 12 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownPlugins.cs
  28. 17 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginInfo.cs
  29. 30 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/Types.cs
  30. 38 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs
  31. 30 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/HarmonyInstanceExtensions.cs
  32. 61 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs
  33. 16 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringBuilderExtensions.cs
  34. 99 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  35. 28 0
      src/XUnity.AutoTranslator.Plugin.Core/Files/IndentedTextWriter.cs
  36. 113 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs
  37. 230 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs
  38. 47 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs
  39. 212 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs
  40. 74 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs
  41. 1284 0
      src/XUnity.AutoTranslator.Plugin.Core/Json/SimpleJson.cs
  42. 64 0
      src/XUnity.AutoTranslator.Plugin.Core/PluginLoader.cs
  43. 106 0
      src/XUnity.AutoTranslator.Plugin.Core/TranslationInfo.cs
  44. 20 0
      src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs
  45. 48 0
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs
  46. 481 0
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/WeakDictionary.cs
  47. 208 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/AutoTranslateClient.cs
  48. 48 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs
  49. 82 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateEndpoint.cs
  50. 28 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/KnownEndpoint.cs
  51. 26 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/KnownEndpoints.cs
  52. 21 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/WebClientReference.cs
  53. 25 0
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  54. 92 0
      src/XUnity.AutoTranslator.Plugin.IPA/AutoTranslatorPlugin.cs
  55. 36 0
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  56. 21 0
      src/XUnity.AutoTranslator.Setup/GameLauncher.cs
  57. 120 0
      src/XUnity.AutoTranslator.Setup/Program.cs
  58. 25 0
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj

+ 92 - 0
.gitattributes

@@ -0,0 +1,92 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs text diff=csharp
+*.vb text
+*.c text
+*.cpp text
+*.cxx text
+*.h text
+*.hxx text
+*.py text
+*.rb text
+*.java text
+*.html text
+*.htm text
+*.css text
+*.scss text
+*.sass text
+*.less text
+*.js text
+*.lisp text
+*.clj text
+*.sql text
+*.php text
+*.lua text
+*.m text
+*.asm text
+*.erl text
+*.fs text
+*.fsx text
+*.hs text
+*.ps1 text
+*.psm1 text
+*.md text
+*.cmd text
+
+# Visual Studio Project files
+*.csproj text merge=union 
+*.ccproj text merge=union 
+*.vbproj text merge=union 
+*.fsproj text merge=union 
+*.dbproj text merge=union 
+*.xproj text merge=union 
+*.sln text eol=crlf merge=union 
+
+# Standard to msysgit
+*.doc	 diff=astextplain
+*.DOC	 diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot  diff=astextplain
+*.DOT  diff=astextplain
+*.pdf  diff=astextplain
+*.PDF	 diff=astextplain
+*.rtf	 diff=astextplain
+*.RTF	 diff=astextplain
+
+# Binaries
+*.dll binary
+*.DLL binary
+*.chm binary
+*.pdb binary
+*.PDB binary
+*.exe binary
+*.EXE binary
+
+# Certificates
+*.pfx binary
+*.p12 binary
+
+# Images
+*.png binary
+*.jpeg binary
+*.jpg binary
+*.gif binary
+*.PNG binary
+*.JPEG binary
+*.JPG binary
+*.GIF binary
+
+# Fonts
+*.eot binary
+*.otf binary
+*.ttf binary
+*.woff binary
+*.woff2 binary
+*.EOT binary
+*.OTF binary
+*.TTF binary
+*.WOFF binary
+*.WOFF2 binary

+ 7 - 41
.gitignore

@@ -1,7 +1,5 @@
 ## Ignore Visual Studio temporary files, build results, and
 ## files generated by popular Visual Studio add-ons.
-##
-## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
 
 # User-specific files
 *.suo
@@ -42,11 +40,9 @@ TestResult.xml
 [Rr]eleasePS/
 dlldata.c
 
-# .NET Core
+# DNX
 project.lock.json
-project.fragment.lock.json
 artifacts/
-**/Properties/launchSettings.json
 
 *_i.c
 *_p.c
@@ -113,10 +109,6 @@ _TeamCity*
 # DotCover is a Code Coverage Tool
 *.dotCover
 
-# Visual Studio code coverage results
-*.coverage
-*.coveragexml
-
 # NCrunch
 _NCrunch_*
 .*crunch*.local.xml
@@ -150,8 +142,8 @@ publish/
 *.azurePubxml
 # TODO: Comment the next line if you want to checkin your web deploy settings
 # but database connection strings (with potential passwords) will be unencrypted
-*.pubxml
-*.publishproj
+#*.pubxml
+#*.publishproj
 
 # Microsoft Azure Web App publish settings. Comment the next line if you want to
 # checkin your Azure Web App publish settings, but sensitive information contained
@@ -166,7 +158,7 @@ PublishScripts/
 !**/packages/build/
 # Uncomment if necessary however generally it will be regenerated when needed
 #!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignorable files
+# NuGet v3's project.json files produces more ignoreable files
 *.nuget.props
 *.nuget.targets
 
@@ -196,9 +188,8 @@ ClientBin/
 *~
 *.dbmdl
 *.dbproj.schemaview
-*.jfm
-*.pfx
 *.publishsettings
+node_modules/
 orleans.codegen.cs
 
 # Since there are multiple workflows, uncomment next line to ignore bower_components
@@ -219,7 +210,6 @@ UpgradeLog*.htm
 # SQL Server files
 *.mdf
 *.ldf
-*.ndf
 
 # Business Intelligence projects
 *.rdl.data
@@ -234,10 +224,6 @@ FakesAssemblies/
 
 # Node.js Tools for Visual Studio
 .ntvs_analysis.dat
-node_modules/
-
-# Typescript v1 declaration files
-typings/
 
 # Visual Studio 6 build log
 *.plg
@@ -245,9 +231,6 @@ typings/
 # Visual Studio 6 workspace options file
 *.opt
 
-# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
-*.vbw
-
 # Visual Studio LightSwitch build output
 **/*.HTMLClient/GeneratedArtifacts
 **/*.DesktopClient/GeneratedArtifacts
@@ -267,22 +250,5 @@ paket-files/
 .idea/
 *.sln.iml
 
-# CodeRush
-.cr/
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
-
-# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
-
-# Telerik's JustMock configuration file
-*.jmconfig
-
-# BizTalk build output
-*.btp.cs
-*.btm.cs
-*.odx.cs
-*.xsd.cs
+# Distribution
+dist/

+ 20 - 0
CHANGELOG.md

@@ -0,0 +1,20 @@
+### 2.3.0
+ * Allow usage of SSL
+ * Better dialogue caching handling. Often a dialogue might get translated multiple times because of small differences in the source text in regards to whitespace.
+
+### 2.2.0
+ * Added anti-spam safeguards to web requests that are sent. What it means: The plugin will no longer be able to attempt to translate a text it already considers translated.
+ * Changed internal programmatic HTTP service provider from .NET WebClient to Unity WWW.
+
+### 2.1.0
+ * Fixed a bug that could cause a StackOverflowException in unfortunate scenarios, if other mods interfered.
+ * Added configuration options to control which text frameworks to translate
+ * Added integration feature that allows other translation plugins to use this plugin as a fallback
+ * MUCH improved dialogue handling. Translations for dialogues should be significantly better than 2.0.1
+
+### 2.0.1
+ * Changed configuration path so to not conflict with the configuration files that other mods uses, as it does not use the shared configuration system. The previous version could override configuration from other mods.
+ * General performance improvements.
+
+### 2.0.0
+ * The initial release

+ 92 - 1
README.md

@@ -1 +1,92 @@
-# XUnity.AutoTranslator
+# XUnity Auto Translator
+
+## Text Frameworks
+This is an auto translation mod that hooks into the unity game engine and attempts to provide translations for the following text frameworks for Unity:
+ * UGUI
+ * IMGUI
+ * NGUI
+ * TextMeshPro
+
+It does go to the internet, in order to provide the translation, so if you are not comfortable with that, dont use it.
+ 
+## Plugin Frameworks
+The mod can be installed into the following Plugin Managers:
+ * [BepInEx](https://github.com/bbepis/BepInEx)
+ * [IPA](https://github.com/Eusth/IPA)
+
+Installations instructions for both methods can be found below.
+
+## Configuration
+The default configuration file, looks as such:
+
+```ini
+[AutoTranslator]
+Endpoint=GoogleTranslate                                          ;the endpoint to use to translate
+Language=en                                                       ;the language to translate into
+FromLanguage=ja                                                   ;the language to translate fromm
+Delay=0                                                           ;a delay to be applied before attempting to translate a text, if the text framework supports it. Measured in seconds
+Directory=Translation                                             ;the directory that the plugin will look for cached translations in
+OutputFile=Translation\_AutoGeneratedTranslations.{lang}.txt      ;the file that the plugin will write auto translated texts to
+MaxCharactersPerTranslation=150                                   ;the max number of characters that a text may contain in order to be translated
+EnableIMGUI=True                                                  ;specify if IMGUI should be translated
+EnableUGUI=True                                                   ;specify if UGUI should be translated
+EnableNGUI=True                                                   ;specify if NGUI should be translated
+EnableTextMeshPro=True                                            ;specify if TextMeshPro should be translated
+AllowPluginHookOverride=True                                      ;specify whether to allow other plugins to override this plugins code hooks
+
+```
+
+## Key Mapping
+The following key inputs are mapped:
+ * ALT + T: Alternate between translated and untranslated versions of all texts provided by this plugin.
+ * ALT + D: Dump untranslated texts (if no endpoint is configured)
+ * ALT + R: Reload translation files. Useful if you change the text files on the fly.
+
+## Installation
+The plugin can be installed in following ways:
+
+### BepInEx Plugin
+REQUIRES: [BepInEx plugin manager](https://github.com/bbepis/BepInEx) (follow its installation instructions first!). 
+
+ 1. Download XUnity.AutoTranslator-BepIn-{VERSION}.zip from [releases](https://github.com/XUnity-Plugins/XUnity.AutoTranslator/releases).
+ 2. Extract directly into the game directory, such that the plugin dlls are placed in BepInEx folder.
+
+The file structure should likke like this:
+```
+{GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.dll
+{GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.BepInEx.dll
+{GameDirectory}/BepInEx/ExIni.dll
+{GameDirectory}/BepInEx/Translation/AnyTranslationFile.txt (this files will be auto generated by plugin!)
+```
+
+### IPA Plugin
+REQUIRES: [IPA plugin manager](https://github.com/Eusth/IPA) (follow its installation instructions first!).
+
+ 1. Download XUnity.AutoTranslator-IPA-{VERSION}.zip from [releases](https://github.com/XUnity-Plugins/XUnity.AutoTranslator/releases).
+ 2. Extract directly into the game directory, such that the plugin dlls are placed in Plugins folder.
+
+The file structure should likke like this
+```
+{GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.Core.dll
+{GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.Core.IPA.dll
+{GameDirectory}/Plugins/0Harmony.dll
+{GameDirectory}/Plugins/ExIni.dll
+{GameDirectory}/Plugins/Translation/AnyTranslationFile.txt (this files will be auto generated by plugin!)
+ ```
+
+## Integrating with Auto Translator
+I have implemented a system that allows other dedicated translation mods to integrate with XUnity AutoTranslator.
+
+Basically, as a mod author, you are able to, if you cannot find a translation to a string, simply delegate it to this mod, and you can do it without taking any references to this plugin.
+
+Here's how it works, and what is required:
+ * You must implement a Component (MonoBehaviour for instance) that this plugin is able to locate by simply traversing all objects during startup.
+ * On this component you must add an event for the text hooks you want to override from XUnity AutoTranslator. This is done on a per text framework basis. The signature of these events must be: Func<object, string, string>. The arguments are, in order: 
+    1. The component that represents the text in the UI. (The one that probably has a property called 'text').
+    2. The untranslated text
+    3. This is the return value and will be the translated text IF an immediate translation took place. Otherwise it will simply be null.
+ * The signature for each framework looks like:
+    1. UGUI: public static event Func<object, string, string> OnUnableToTranslateUGUI
+    2. TextMeshPro: public static event Func<object, string, string> OnUnableToTranslateTextMeshPro
+    3. NGUI: public static event Func<object, string, string> OnUnableToTranslateNGUI
+ * Also, the events can be either instance based or static.

+ 58 - 0
XUnity.AutoTranslator.sln

@@ -0,0 +1,58 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27428.2015
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F9B38FC-4E57-4B83-AF0B-0993B8470823}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.Core", "src\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj", "{718A3B1D-A5E5-4223-AD53-45C60C874150}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.BepIn", "src\XUnity.AutoTranslator.Plugin.BepIn\XUnity.AutoTranslator.Plugin.BepIn.csproj", "{5442ED94-2800-47A4-BBAC-C00FBA676D02}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XUnity.AutoTranslator.Plugin.IPA", "src\XUnity.AutoTranslator.Plugin.IPA\XUnity.AutoTranslator.Plugin.IPA.csproj", "{C749698C-9E49-4CC3-8B45-62AE3AD0C938}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Patcher", "src\XUnity.AutoTranslator.Patcher\XUnity.AutoTranslator.Patcher.csproj", "{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnity.AutoTranslator.Setup", "src\XUnity.AutoTranslator.Setup\XUnity.AutoTranslator.Setup.csproj", "{86BF1F46-44C1-4301-8314-6EC32F74575F}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{718A3B1D-A5E5-4223-AD53-45C60C874150}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{86BF1F46-44C1-4301-8314-6EC32F74575F}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{718A3B1D-A5E5-4223-AD53-45C60C874150} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{5442ED94-2800-47A4-BBAC-C00FBA676D02} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{C749698C-9E49-4CC3-8B45-62AE3AD0C938} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{0A2A6B66-91D4-4A4E-AC77-80C6DD748FCD} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+		{86BF1F46-44C1-4301-8314-6EC32F74575F} = {0F9B38FC-4E57-4B83-AF0B-0993B8470823}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {EE803FED-4447-4D19-B3D6-88C56E8DFCCA}
+	EndGlobalSection
+EndGlobal

二进制
libs/0Harmony.dll


二进制
libs/BepInEx.dll


二进制
libs/ExIni.dll


二进制
libs/IllusionPlugin.dll


二进制
libs/Mono.Cecil.Inject.dll


二进制
libs/Mono.Cecil.dll


二进制
libs/ReiPatcher.exe


二进制
libs/UnityEngine.UI.dll


二进制
libs/UnityEngine.dll


+ 84 - 0
src/XUnity.AutoTranslator.Patcher/Patcher.cs

@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Mono.Cecil;
+using Mono.Cecil.Inject;
+using ReiPatcher;
+using ReiPatcher.Patch;
+
+namespace XUnity.AutoTranslator.Patcher
+{
+   public class Patcher : PatchBase
+   {
+      private static readonly HashSet<string> EntryClasses = new HashSet<string> { "Display", "Input" };
+
+      private AssemblyDefinition _hookAssembly;
+
+      public override string Name
+      {
+         get
+         {
+            return "Auto Translator";
+         }
+      }
+
+      public override string Version
+      {
+         get
+         {
+            return "2.0.1";
+         }
+      }
+
+      public override void PrePatch()
+      {
+         RPConfig.RequestAssembly( "UnityEngine.dll" );
+
+         _hookAssembly = LoadAssembly( "XUnity.AutoTranslator.Plugin.Core.dll" );
+      }
+
+      public override bool CanPatch( PatcherArguments args )
+      {
+         return args.Assembly.Name.Name == "UnityEngine" && !HasAttribute( this, args.Assembly, "XUnity.AutoTranslator.Plugin.Core" );
+      }
+
+      public override void Patch( PatcherArguments args )
+      {
+         var PluginLoader = _hookAssembly.MainModule.GetType( "XUnity.AutoTranslator.Plugin.Core.PluginLoader" );
+         var LoadThroughBootstrapper = PluginLoader.GetMethod( "LoadThroughBootstrapper" );
+
+         var entryClasses = args.Assembly.MainModule.GetTypes().Where( x => EntryClasses.Contains( x.Name ) ).ToList();
+         foreach( var entryClass in entryClasses )
+         {
+            var staticCtor = entryClass.Methods.FirstOrDefault( x => x.IsStatic && x.IsConstructor );
+            if( staticCtor != null )
+            {
+               var injecctor = new InjectionDefinition( staticCtor, LoadThroughBootstrapper, InjectFlags.None );
+               injecctor.Inject(
+                  startCode: 0,
+                  direction: InjectDirection.Before );
+            }
+         }
+
+         SetPatchedAttribute( args.Assembly, "XUnity.AutoTranslator.Plugin.Core" );
+      }
+
+      public static AssemblyDefinition LoadAssembly( string name )
+      {
+         string path = Path.Combine( RPConfig.ConfigFile.GetSection( "ReiPatcher" ).GetKey( "AssembliesDir" ).Value, name );
+         if( !File.Exists( path ) ) throw new FileNotFoundException( "Missing DLL: " + path );
+         using( Stream s = File.OpenRead( path ) )
+         {
+            return AssemblyDefinition.ReadAssembly( s );
+         }
+      }
+
+      public static bool HasAttribute( PatchBase patch, AssemblyDefinition assembly, string attribute )
+      {
+         return patch.GetPatchedAttributes( assembly ).Any( a => a.Info == attribute );
+      }
+   }
+}

+ 26 - 0
src/XUnity.AutoTranslator.Patcher/XUnity.AutoTranslator.Patcher.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="ExIni">
+      <HintPath>..\..\libs\ExIni.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil">
+      <HintPath>..\..\libs\Mono.Cecil.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Inject">
+      <HintPath>..\..\libs\Mono.Cecil.Inject.dll</HintPath>
+    </Reference>
+    <Reference Include="ReiPatcher">
+      <HintPath>..\..\libs\ReiPatcher.exe</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

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

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BepInEx;
+using ExIni;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.BepIn
+{
+   [BepInPlugin( GUID: PluginInfo.Identifier, Name: PluginInfo.Name, Version: PluginInfo.Version )]
+   public class AutoTranslatorPlugin : BaseUnityPlugin, IConfiguration
+   {
+      private IniFile _file;
+      private string _configPath;
+      private string _dataFolder;
+
+      public AutoTranslatorPlugin()
+      {
+         _dataFolder = "BepInEx";
+         _configPath = Path.Combine( _dataFolder, "AutoTranslatorConfig.ini" );
+      }
+
+      public IniFile Preferences
+      {
+         get
+         {
+            return ( _file ?? ( _file = ReloadConfig() ) ); ;
+         }
+      }
+
+      public string DataPath
+      {
+         get
+         {
+            return _dataFolder;
+         }
+      }
+
+      public IniFile ReloadConfig()
+      {
+         if( !File.Exists( _configPath ) )
+         {
+            return ( _file ?? new IniFile() );
+         }
+         IniFile ini = IniFile.FromFile( _configPath );
+         if( _file == null )
+         {
+            return ( _file = ini );
+         }
+         _file.Merge( ini );
+         return _file;
+      }
+
+      public void SaveConfig()
+      {
+         _file.Save( _configPath );
+      }
+
+      void Awake()
+      {
+         PluginLoader.LoadWithConfig( this );
+      }
+   }
+}

+ 39 - 0
src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+   <PropertyGroup>
+      <TargetFramework>net35</TargetFramework>
+      <AssemblyVersion>2.3.0.0</AssemblyVersion>
+      <Version>2.3.0</Version>
+      <FileVersion>2.3.0.0</FileVersion>
+   </PropertyGroup>
+
+   <ItemGroup>
+     <ProjectReference Include="..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+   </ItemGroup>
+
+   <ItemGroup>
+     <Reference Include="BepInEx">
+       <HintPath>..\..\libs\BepInEx.dll</HintPath>
+     </Reference>
+     <Reference Include="ExIni">
+       <HintPath>..\..\libs\ExIni.dll</HintPath>
+     </Reference>
+     <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">
+      <GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
+         <Output TaskParameter="Assemblies" ItemName="Targets" />
+      </GetAssemblyIdentity>
+      <ItemGroup>
+         <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
+      </ItemGroup>
+     <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\BepIn\BepInEx\&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\BepIn\BepInEx' -DestinationPath '$(SolutionDir)dist\BepIn\XUnity.AutoTranslator-BepIn-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+   </Target>
+
+</Project>

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

@@ -0,0 +1,839 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Text;
+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;
+using XUnity.AutoTranslator.Plugin.Core.Hooks;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI;
+using XUnity.AutoTranslator.Plugin.Core.IMGUI;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
+using UnityEngine.SceneManagement;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   public class AutoTranslationPlugin : MonoBehaviour
+   {
+      private static readonly string TextPropertyName = "text";
+
+      /// <summary>
+      /// These are the currently running translation jobs (being translated by an http request).
+      /// </summary>
+      private List<TranslationJob> _completedJobs = new List<TranslationJob>();
+      private List<TranslationJob> _unstartedJobs = new List<TranslationJob>();
+
+      /// <summary>
+      /// All the translations are stored in this dictionary.
+      /// </summary>
+      private Dictionary<string, string> _translations = new Dictionary<string, string>();
+
+      /// <summary>
+      /// These are the new translations that has not yet been persisted to the file system.
+      /// </summary>
+      private object _writeToFileSync = new object();
+      private Dictionary<string, string> _newTranslations = new Dictionary<string, string>();
+      private HashSet<string> _newUntranslated = new HashSet<string>();
+      private HashSet<string> _translatedTexts = new HashSet<string>();
+
+      /// <summary>
+      /// The number of http translation errors that has occurred up until now.
+      /// </summary>
+      private int _consecutiveErrors = 0;
+
+      /// <summary>
+      /// This is a hash set that contains all Text components that is currently being worked on by
+      /// the translation plugin.
+      /// </summary>
+      private HashSet<object> _ongoingOperations = new HashSet<object>();
+      private HashSet<string> _startedOperationsForNonStabilizableComponents = new HashSet<string>();
+
+      private bool _isInTranslatedMode = true;
+
+      public void Initialize()
+      {
+         Settings.Configure();
+
+         HooksSetup.InstallHooks( Any_TextChanged );
+
+         AutoTranslateClient.Configure();
+
+         LoadTranslations();
+
+         // start a thread that will periodically removed unused references
+         var t1 = new Thread( RemovedUnusedReferences );
+         t1.IsBackground = true;
+         t1.Start();
+
+         // start a thread that will periodically save new translations
+         var t2 = new Thread( SaveTranslationsLoop );
+         t2.IsBackground = true;
+         t2.Start();
+
+
+         // subscribe to text changes
+         UGUIHooks.TextAwakened += UguiTextEvents_OnTextAwaken;
+         UGUIHooks.TextChanged += UguiTextEvents_OnTextChanged;
+         IMGUIHooks.TextChanged += IMGUITextEvents_GUIContentChanged;
+         NGUIHooks.TextChanged += NGUITextEvents_TextChanged;
+
+         TextMeshProHooks.TextAwakened += TextMeshProHooks_OnTextAwaken;
+         TextMeshProHooks.TextChanged += TextMeshProHooks_OnTextChanged;
+      }
+
+      private string[] GetTranslationFiles()
+      {
+         return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ), $"*.txt", SearchOption.AllDirectories ) // FIXME: Add $"*{Language}.txt"
+            .Union( new[] { Settings.AutoTranslationsFilePath } )
+            .Select( x => x.Replace( "/", "\\" ) )
+            .Distinct()
+            .OrderBy( x => x )
+            .ToArray();
+      }
+
+      private void RemovedUnusedReferences( object state )
+      {
+         while( true )
+         {
+            //// What a brilliant solution...
+            //try
+            //{
+            //   AutoTranslateClient.RemoveUnusedClients();
+            //}
+            //catch( Exception e )
+            //{
+            //   Console.WriteLine( "An unexpected error occurred in XUnity.AutoTranslator: " + Environment.NewLine + e );
+            //}
+            //finally
+            //{
+            //   Thread.Sleep( 1000 * 20 );
+            //}
+
+            //try
+            //{
+            //   AutoTranslateClient.RemoveUnusedClients();
+            //}
+            //catch( Exception e )
+            //{
+            //   Console.WriteLine( "An unexpected error occurred in XUnity.AutoTranslator: " + Environment.NewLine + e );
+            //}
+            //finally
+            //{
+            //   Thread.Sleep( 1000 * 20 );
+            //}
+
+            try
+            {
+               //AutoTranslateClient.RemoveUnusedClients();
+               ObjectExtensions.Cull();
+            }
+            catch( Exception e )
+            {
+               Console.WriteLine( "An unexpected error occurred in XUnity.AutoTranslator: " + Environment.NewLine + e );
+            }
+            finally
+            {
+               Thread.Sleep( 1000 * 60 );
+            }
+         }
+      }
+
+      private void SaveTranslationsLoop( object state )
+      {
+         try
+         {
+            while( true )
+            {
+               if( _newTranslations.Count > 0 )
+               {
+                  lock( _writeToFileSync )
+                  {
+                     if( _newTranslations.Count > 0 )
+                     {
+                        using( var stream = File.Open( Settings.AutoTranslationsFilePath, FileMode.Append, FileAccess.Write ) )
+                        using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
+                        {
+                           foreach( var kvp in _newTranslations )
+                           {
+                              writer.WriteLine( TextHelper.Encode( kvp.Key ) + '=' + TextHelper.Encode( kvp.Value ) );
+                           }
+                           writer.Flush();
+                        }
+                        _newTranslations.Clear();
+                     }
+                  }
+               }
+               else
+               {
+                  Thread.Sleep( 5000 );
+               }
+            }
+         }
+         catch( Exception e )
+         {
+            Console.WriteLine( e );
+         }
+      }
+
+      /// <summary>
+      /// Loads the translations found in Translation.{lang}.txt
+      /// </summary>
+      private void LoadTranslations()
+      {
+         try
+         {
+            lock( _writeToFileSync )
+            {
+               Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ) );
+               Directory.CreateDirectory( Path.GetDirectoryName( Path.Combine( Config.Current.DataPath, Settings.OutputFile ) ) );
+
+               foreach( var fullFileName in GetTranslationFiles() )
+               {
+                  if( File.Exists( fullFileName ) )
+                  {
+                     string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
+                     foreach( string translation in translations )
+                     {
+                        string[] kvp = translation.Split( new char[] { '=', '\t' }, StringSplitOptions.None );
+                        if( kvp.Length == 2 )
+                        {
+                           string key = TextHelper.Decode( kvp[ 0 ].Trim() );
+                           string value = TextHelper.Decode( kvp[ 1 ].Trim() );
+
+                           if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
+                           {
+                              AddTranslation( key, value );
+                           }
+                        }
+                     }
+                  }
+               }
+            }
+         }
+         catch( Exception e )
+         {
+            Console.WriteLine( e );
+         }
+      }
+
+      private void AddTranslation( string key, string value )
+      {
+         _translations[ key ] = value;
+         _translatedTexts.Add( value );
+
+         if( Settings.IgnoreWhitespaceInKeys )
+         {
+            var newKey = key.RemoveWhitespace();
+            _translations[ newKey ] = value;
+         }
+      }
+
+      private bool TryGetTranslation( string key, out string value )
+      {
+         return _translations.TryGetValue( key, out value ) || ( Settings.IgnoreWhitespaceInKeys && _translations.TryGetValue( key.RemoveWhitespace(), out value ) );
+      }
+
+      private string Any_TextChanged( object graphic, string text )
+      {
+         return TranslateOrQueueWebJob( graphic, text, true );
+      }
+
+      private void NGUITextEvents_TextChanged( object graphic )
+      {
+         TranslateOrQueueWebJob( graphic, null, true );
+      }
+
+      private void IMGUITextEvents_GUIContentChanged( object content )
+      {
+         TranslateOrQueueWebJob( content, null, true );
+      }
+
+      private void UguiTextEvents_OnTextChanged( object text )
+      {
+         TranslateOrQueueWebJob( text, null, false );
+      }
+
+      private void UguiTextEvents_OnTextAwaken( object text )
+      {
+         TranslateOrQueueWebJob( text, null, true );
+      }
+
+      private void TextMeshProHooks_OnTextChanged( object text )
+      {
+         TranslateOrQueueWebJob( text, null, false );
+      }
+
+      private void TextMeshProHooks_OnTextAwaken( object text )
+      {
+         TranslateOrQueueWebJob( text, null, true );
+      }
+
+      private void SetTranslatedText( object ui, string text, TranslationInfo info )
+      {
+         info?.SetTranslatedText( text );
+
+         if( _isInTranslatedMode )
+         {
+            SetText( ui, text, true, info );
+         }
+      }
+
+
+      /// <summary>
+      /// Sets the text of a UI  text, while ensuring this will not fire a text changed event.
+      /// </summary>
+      private void SetText( object ui, string text, bool isTranslated, TranslationInfo info )
+      {
+         if( !info?.IsCurrentlySettingText ?? true )
+         {
+            try
+            {
+               UGUIHooks.TextChanged -= UguiTextEvents_OnTextChanged;
+               NGUIHooks.TextChanged -= NGUITextEvents_TextChanged;
+               TextMeshProHooks.TextChanged -= TextMeshProHooks_OnTextChanged;
+               if( info != null )
+               {
+                  info.IsCurrentlySettingText = true;
+               }
+
+
+               if( ui is Text )
+               {
+                  ( (Text)ui ).text = text;
+               }
+               else if( ui is GUIContent )
+               {
+                  ( (GUIContent)ui ).text = text;
+               }
+               else
+               {
+                  // fallback to reflective approach
+                  var type = ui.GetType();
+                  type.GetProperty( TextPropertyName )?.GetSetMethod()?.Invoke( ui, new[] { text } );
+               }
+
+               if( isTranslated )
+               {
+                  info?.ResizeUI( ui );
+               }
+               else
+               {
+                  info?.UnresizeUI( ui );
+               }
+            }
+            finally
+            {
+               UGUIHooks.TextChanged += UguiTextEvents_OnTextChanged;
+               NGUIHooks.TextChanged += NGUITextEvents_TextChanged;
+               TextMeshProHooks.TextChanged += TextMeshProHooks_OnTextChanged;
+               if( info != null )
+               {
+                  info.IsCurrentlySettingText = false;
+               }
+            }
+         }
+      }
+
+      private string GetText( object ui )
+      {
+         string text = null;
+
+         if( ui is Text )
+         {
+            text = ( (Text)ui ).text;
+         }
+         else if( ui is GUIContent )
+         {
+            text = ( (GUIContent)ui ).text;
+         }
+         else
+         {
+            text = (string)ui.GetType()?.GetProperty( TextPropertyName )?.GetValue( ui, null );
+         }
+
+         return text ?? string.Empty;
+      }
+
+      /// <summary>
+      /// Determines if a text should be translated.
+      /// </summary>
+      private bool IsTranslatable( string str )
+      {
+         return TextHelper.ContainsJapaneseSymbols( str ) && str.Length <= Settings.MaxCharactersPerTranslation && !_translatedTexts.Contains( str );
+      }
+
+      public bool ShouldTranslate( object ui )
+      {
+         var cui = ui as Component;
+         if( cui != null )
+         {
+            var go = cui.gameObject;
+            var isDummy = go.IsDummy();
+            if( isDummy )
+            {
+               return false;
+            }
+
+            var inputField = cui.gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.InputField )
+               ?? cui.gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.TMP_InputField );
+
+            return inputField == null;
+         }
+
+         return true;
+      }
+
+      private string TranslateOrQueueWebJob( object ui, string text, bool isAwakening )
+      {
+         var info = ui.GetTranslationInfo( isAwakening );
+         if( !info?.IsAwake ?? false )
+         {
+            return null;
+         }
+         if( _ongoingOperations.Contains( ui ) )
+         {
+            return null;
+         }
+
+
+         if( Settings.Delay == 0 || !SupportsStabilization( ui ) )
+         {
+            return TranslateOrQueueWebJobImmediate( ui, text, info );
+         }
+         else
+         {
+            StartCoroutine(
+               DelayForSeconds( Settings.Delay, () =>
+               {
+                  TranslateOrQueueWebJobImmediate( ui, text, info );
+               } ) );
+         }
+
+         return null;
+      }
+
+      public static bool IsAlreadyTranslating( TranslationInfo info )
+      {
+         if( info == null ) return false;
+
+         return info.IsCurrentlySettingText;
+      }
+
+      /// <summary>
+      /// Translates the string of a UI  text or queues it up to be translated
+      /// by the HTTP translation service.
+      /// </summary>
+      private string TranslateOrQueueWebJobImmediate( object ui, string text, TranslationInfo info )
+      {
+         // Get the trimmed text
+         text = ( text ?? GetText( ui ) ).Trim();
+         info?.Reset( text );
+
+         // Ensure that we actually want to translate this text and its owning UI element. 
+         if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslate( ui ) && !IsAlreadyTranslating( info ) )
+         {
+            // if we already have translation loaded in our _translatios dictionary, simply load it and set text
+            string translation;
+            if( TryGetTranslation( text, out translation ) )
+            {
+               if( !string.IsNullOrEmpty( translation ) )
+               {
+                  SetTranslatedText( ui, translation, info );
+                  return translation;
+               }
+            }
+            else
+            {
+               if( SupportsStabilization( ui ) )
+               {
+                  // if we dont know what text to translate it to, we need to figure it out.
+                  // this might take a while, so add the UI text component to the ongoing operations
+                  // list, so we dont start multiple operations for it, as its text might be constantly
+                  // changing.
+                  _ongoingOperations.Add( ui );
+
+                  // start a coroutine, that will execute once the string of the UI text has stopped
+                  // changing. For all texts except 'story' texts, this will add a delay for exactly 
+                  // 0.5s to the translation. This is barely noticable.
+                  //
+                  // on the other hand, for 'story' texts, this will take the time that it takes
+                  // for the text to stop 'scrolling' in.
+                  try
+                  {
+                     StartCoroutine(
+                        WaitForTextStablization(
+                           ui: ui,
+                           delay: 0.5f,
+                           maxTries: 100, // 100 tries == 50 seconds
+                           currentTries: 0,
+                           onMaxTriesExceeded: () =>
+                           {
+                              _ongoingOperations.Remove( ui );
+                           },
+                           onTextStabilized: stabilizedText =>
+                           {
+                              _ongoingOperations.Remove( ui );
+
+                              if( !string.IsNullOrEmpty( stabilizedText ) )
+                              {
+                                 info?.Reset( stabilizedText );
+
+                                 // once the text has stabilized, attempt to look it up
+                                 if( TryGetTranslation( stabilizedText, out translation ) )
+                                 {
+                                    if( !string.IsNullOrEmpty( translation ) )
+                                    {
+                                       SetTranslatedText( ui, translation, info );
+                                    }
+                                 }
+
+                                 if( translation == null )
+                                 {
+                                    // Lets try not to spam a service that might not be there...
+                                    if( AutoTranslateClient.IsConfigured && _consecutiveErrors < Settings.MaxErrors )
+                                    {
+                                       var job = new TranslationJob { UI = ui, UntranslatedText = stabilizedText };
+                                       _unstartedJobs.Add( job );
+                                    }
+                                    else
+                                    {
+                                       _newUntranslated.Add( stabilizedText );
+                                    }
+                                 }
+                              }
+
+                           } ) );
+                  }
+                  catch( Exception )
+                  {
+                     _ongoingOperations.Remove( ui );
+                  }
+               }
+               else
+               {
+                  if( !_startedOperationsForNonStabilizableComponents.Contains( text ) )
+                  {
+                     _startedOperationsForNonStabilizableComponents.Add( text );
+
+                     // Lets try not to spam a service that might not be there...
+                     if( AutoTranslateClient.IsConfigured && _consecutiveErrors < Settings.MaxErrors )
+                     {
+                        var job = new TranslationJob { UntranslatedText = text };
+                        _unstartedJobs.Add( job );
+                     }
+                     else
+                     {
+                        _newUntranslated.Add( text );
+                     }
+                  }
+               }
+            }
+         }
+
+         return null;
+      }
+
+      public bool SupportsStabilization( object ui )
+      {
+         return !( ui is GUIContent );
+      }
+
+      /// <summary>
+      /// Utility method that allows me to wait to call an action, until
+      /// the text has stopped changing. This is important for 'story'
+      /// mode text, which 'scrolls' into place slowly.
+      /// </summary>
+      public IEnumerator WaitForTextStablization( object ui, float delay, int maxTries, int currentTries, Action<string> onTextStabilized, Action onMaxTriesExceeded )
+      {
+         if( currentTries < maxTries ) // shortcircuit
+         {
+            var beforeText = GetText( ui );
+            yield return new WaitForSeconds( delay );
+            var afterText = GetText( ui );
+
+            if( beforeText == afterText )
+            {
+               onTextStabilized( afterText.Trim() );
+            }
+            else
+            {
+               StartCoroutine( WaitForTextStablization( ui, delay, maxTries, currentTries + 1, onTextStabilized, onMaxTriesExceeded ) );
+            }
+         }
+         else
+         {
+            onMaxTriesExceeded();
+         }
+      }
+
+      public IEnumerator DelayForSeconds( float delay, Action onContinue )
+      {
+         yield return new WaitForSeconds( delay );
+
+         onContinue();
+      }
+
+      public void Update()
+      {
+         try
+         {
+            KickoffTranslations();
+            FinishTranslations();
+
+            if( Input.anyKey )
+            {
+               if( Settings.EnablePrintHierarchy && ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.Y ) )
+               {
+                  PrintObjects();
+               }
+               else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.T ) )
+               {
+                  ToggleTranslation();
+               }
+               else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.D ) )
+               {
+                  DumpUntranslated();
+               }
+               else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.R ) )
+               {
+                  ReloadTranslations();
+               }
+            }
+         }
+         catch( Exception e )
+         {
+            Console.WriteLine( e );
+         }
+      }
+
+      private void KickoffTranslations()
+      {
+         while( AutoTranslateClient.HasAvailableClients && _unstartedJobs.Count > 0 )
+         {
+            var job = _unstartedJobs[ _unstartedJobs.Count - 1 ];
+            _unstartedJobs.RemoveAt( _unstartedJobs.Count - 1 );
+
+            // lets see if the text should still be translated before kicking anything off
+            if( job.UI != null )
+            {
+               var text = GetText( job.UI ).Trim();
+               if( text != job.UntranslatedText )
+               {
+                  continue; // just ignore this UI component, as the text has already changed anyway (maybe from game, maybe from other plugin)
+               }
+            }
+
+            //StartCoroutine( AutoTranslateClient.TranslateByWWW( job.UntranslatedText.ChangeToSingleLineForDialogue(), Settings.FromLanguage, Settings.Language, translatedText =>
+            //{
+            //   _consecutiveErrors = 0;
+
+            //   job.TranslatedText = translatedText;
+
+            //   if( !string.IsNullOrEmpty( translatedText ) )
+            //   {
+            //      lock( _writeToFileSync )
+            //      {
+            //         _newTranslations[ job.UntranslatedText ] = translatedText;
+            //      }
+            //   }
+
+            //   _completedJobs.Add( job );
+            //},
+            //() =>
+            //{
+            //   _consecutiveErrors++;
+            //} ) );
+
+            StartCoroutine( AutoTranslateClient.TranslateByWWW( job.UntranslatedText.ChangeToSingleLineForDialogue(), Settings.FromLanguage, Settings.Language, translatedText =>
+            {
+               _consecutiveErrors = 0;
+
+               job.TranslatedText = translatedText;
+
+               if( !string.IsNullOrEmpty( translatedText ) )
+               {
+                  lock( _writeToFileSync )
+                  {
+                     _newTranslations[ job.UntranslatedText ] = translatedText;
+                  }
+               }
+
+               _completedJobs.Add( job );
+            },
+            () =>
+            {
+               _consecutiveErrors++;
+            } ) );
+         }
+      }
+
+      private void FinishTranslations()
+      {
+         if( _completedJobs.Count > 0 )
+         {
+            for( int i = _completedJobs.Count - 1 ; i >= 0 ; i-- )
+            {
+               var job = _completedJobs[ i ];
+               _completedJobs.RemoveAt( i );
+
+               if( !string.IsNullOrEmpty( job.TranslatedText ) )
+               {
+                  if( job.UI != null )
+                  {
+                     // update the original text, but only if it has not been chaanged already for some reason (could be other translator plugin or game itself)
+                     var text = GetText( job.UI ).Trim();
+                     if( text == job.UntranslatedText )
+                     {
+                        var info = job.UI.GetTranslationInfo( false );
+                        SetTranslatedText( job.UI, job.TranslatedText, info );
+                     }
+                  }
+
+                  AddTranslation( job.UntranslatedText, job.TranslatedText );
+               }
+            }
+         }
+      }
+
+      private void ReloadTranslations()
+      {
+         LoadTranslations();
+
+         foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
+         {
+            var info = kvp.Value as TranslationInfo;
+            if( info != null && !string.IsNullOrEmpty( info.OriginalText ) )
+            {
+               if( TryGetTranslation( info.OriginalText, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
+               {
+                  SetTranslatedText( kvp.Key, translatedText, info );
+               }
+            }
+         }
+      }
+
+      private string CalculateDumpFileName()
+      {
+         int idx = 0;
+         string fileName = null;
+         do
+         {
+            idx++;
+            fileName = $"UntranslatedDump{idx}.txt";
+         }
+         while( File.Exists( fileName ) );
+
+         return fileName;
+      }
+
+      private void DumpUntranslated()
+      {
+         if( _newUntranslated.Count > 0 )
+         {
+            using( var stream = File.Open( CalculateDumpFileName(), FileMode.Append, FileAccess.Write ) )
+            using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
+            {
+               foreach( var untranslated in _newUntranslated )
+               {
+                  writer.WriteLine( TextHelper.Encode( untranslated ) + '=' );
+               }
+               writer.Flush();
+            }
+
+            _newUntranslated.Clear();
+         }
+      }
+
+      private void ToggleTranslation()
+      {
+         _isInTranslatedMode = !_isInTranslatedMode;
+
+         if( _isInTranslatedMode )
+         {
+            // make sure we use the translated version of all texts
+            foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
+            {
+               var ui = kvp.Key;
+               var info = (TranslationInfo)kvp.Value;
+
+               if( info != null && info.IsTranslated )
+               {
+                  SetText( ui, info.TranslatedText, true, info );
+               }
+            }
+         }
+         else
+         {
+            // make sure we use the original version of all texts
+            foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
+            {
+               var ui = kvp.Key;
+               var info = (TranslationInfo)kvp.Value;
+
+               if( info != null )
+               {
+                  SetText( ui, info.OriginalText, false, info );
+               }
+            }
+         }
+      }
+
+      private void PrintObjects()
+      {
+
+         using( var stream = File.Open( Path.Combine( Environment.CurrentDirectory, "hierarchy.txt" ), FileMode.Create ) )
+         using( var writer = new StreamWriter( stream ) )
+         {
+            foreach( var root in GetAllRoots() )
+            {
+               TraverseChildren( writer, root, "" );
+            }
+
+            writer.Flush();
+         }
+      }
+
+      private IEnumerable<GameObject> GetAllRoots()
+      {
+         var objects = GameObject.FindObjectsOfType<GameObject>();
+         foreach( var obj in objects )
+         {
+            if( obj.transform.parent == null )
+            {
+               yield return obj;
+            }
+         }
+      }
+
+      private void TraverseChildren( StreamWriter writer, GameObject obj, string identation )
+      {
+         var layer = LayerMask.LayerToName( obj.gameObject.layer );
+         var components = string.Join( ", ", obj.GetComponents<Component>().Select( x => x.GetType().Name ).ToArray() );
+         var line = string.Format( "{0,-50} {1,100}",
+            identation + obj.gameObject.name + " [" + layer + "]",
+            components );
+
+         writer.WriteLine( line );
+
+         for( int i = 0 ; i < obj.transform.childCount ; i++ )
+         {
+            var child = obj.transform.GetChild( i );
+            TraverseChildren( writer, child.gameObject, identation + " " );
+         }
+      }
+   }
+}

+ 13 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/Config.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+{
+   public static class Config
+   {
+      public static IConfiguration Current;
+   }
+}

+ 58 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/DefaultConfiguration.cs

@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using ExIni;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+{
+   public class DefaultConfiguration : IConfiguration
+   {
+      private IniFile _file;
+      private string _configPath;
+      private string _dataFolder;
+
+      public DefaultConfiguration()
+      {
+         _dataFolder = "AutoTranslator";
+         _configPath = Path.Combine( _dataFolder, "Config.ini" );
+      }
+
+      public IniFile Preferences
+      {
+         get
+         {
+            return ( _file ?? ( _file = ReloadConfig() ) ); ;
+         }
+      }
+
+      public string DataPath
+      {
+         get
+         {
+            return _dataFolder;
+         }
+      }
+
+      public void SaveConfig()
+      {
+         _file.Save( _configPath );
+      }
+
+      public IniFile ReloadConfig()
+      {
+         if( !File.Exists( _configPath ) )
+         {
+            return ( _file ?? new IniFile() );
+         }
+         IniFile ini = IniFile.FromFile( _configPath );
+         if( _file == null )
+         {
+            return ( _file = ini );
+         }
+         _file.Merge( ini );
+         return _file;
+      }
+   }
+}

+ 19 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/IConfiguration.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using ExIni;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+{
+   public interface IConfiguration
+   {
+      string DataPath { get; }
+
+      IniFile Preferences { get; }
+
+      void SaveConfig();
+
+      IniFile ReloadConfig();
+   }
+}

+ 42 - 0
src/XUnity.AutoTranslator.Plugin.Core/Configuration/IniKeyExtensions.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using ExIni;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+{
+   public static class IniKeyExtensions
+   {
+      public static T GetOrDefault<T>( this IniKey that, T defaultValue, bool allowEmpty = false )
+      {
+         if( !allowEmpty )
+         {
+            var value = that.Value;
+            if( string.IsNullOrEmpty( value ) )
+            {
+               that.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
+               return defaultValue;
+            }
+            else
+            {
+               return (T)Convert.ChangeType( that.Value, typeof( T ), CultureInfo.InvariantCulture );
+            }
+         }
+         else
+         {
+            var value = that.Value;
+            if( value == null )
+            {
+               that.Value = Convert.ToString( defaultValue, CultureInfo.InvariantCulture );
+               return defaultValue;
+            }
+            else
+            {
+               return (T)Convert.ChangeType( that.Value, typeof( T ), CultureInfo.InvariantCulture );
+            }
+         }
+      }
+   }
+}

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

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Configuration
+{
+   public static class Settings
+   {
+      // cannot be changed
+      public static readonly int MaxErrors = 5;
+      public static readonly int MaxConcurrentTranslations = 5;
+      public static readonly TimeSpan WebClientLifetime = TimeSpan.FromSeconds( 20 );
+      
+      // can be changed
+      public static string ServiceEndpoint;
+      public static string Language;
+      public static string FromLanguage;
+      public static string OutputFile;
+      public static string TranslationDirectory;
+      public static float Delay;
+      public static int MaxCharactersPerTranslation;
+      public static bool EnablePrintHierarchy;
+      public static string AutoTranslationsFilePath;
+      public static bool EnableIMGUI;
+      public static bool EnableUGUI;
+      public static bool EnableNGUI;
+      public static bool EnableTextMeshPro;
+      public static bool AllowPluginHookOverride;
+      public static bool IgnoreWhitespaceInKeys;
+      public static bool EnableSSL;
+
+      public static void Configure()
+      {
+         ServiceEndpoint = Config.Current.Preferences[ "AutoTranslator" ][ "Endpoint" ].GetOrDefault( KnownEndpointNames.GoogleTranslate );
+         Language = Config.Current.Preferences[ "AutoTranslator" ][ "Language" ].GetOrDefault( "en" );
+         FromLanguage = Config.Current.Preferences[ "AutoTranslator" ][ "FromLanguage" ].GetOrDefault( "ja", true );
+         Delay = Config.Current.Preferences[ "AutoTranslator" ][ "Delay" ].GetOrDefault( 0f );
+         TranslationDirectory = Config.Current.Preferences[ "AutoTranslator" ][ "Directory" ].GetOrDefault( @"Translation" );
+         OutputFile = Config.Current.Preferences[ "AutoTranslator" ][ "OutputFile" ].GetOrDefault( @"Translation\_AutoGeneratedTranslations.{lang}.txt" );
+         MaxCharactersPerTranslation = Config.Current.Preferences[ "AutoTranslator" ][ "MaxCharactersPerTranslation" ].GetOrDefault( 150 );
+         EnablePrintHierarchy = Config.Current.Preferences[ "AutoTranslator" ][ "EnablePrintHierarchy" ].GetOrDefault( false );
+         IgnoreWhitespaceInKeys = Config.Current.Preferences[ "AutoTranslator" ][ "IgnoreWhitespaceInKeys" ].GetOrDefault( true );
+
+         EnableIMGUI = Config.Current.Preferences[ "AutoTranslator" ][ "EnableIMGUI" ].GetOrDefault( true );
+         EnableUGUI = Config.Current.Preferences[ "AutoTranslator" ][ "EnableUGUI" ].GetOrDefault( true );
+         EnableNGUI = Config.Current.Preferences[ "AutoTranslator" ][ "EnableNGUI" ].GetOrDefault( true );
+         EnableTextMeshPro = Config.Current.Preferences[ "AutoTranslator" ][ "EnableTextMeshPro" ].GetOrDefault( true );
+         AllowPluginHookOverride = Config.Current.Preferences[ "AutoTranslator" ][ "AllowPluginHookOverride" ].GetOrDefault( true );
+
+         EnableSSL = Config.Current.Preferences[ "AutoTranslator" ][ "EnableSSL" ].GetOrDefault( false );
+
+         AutoTranslationsFilePath = Path.Combine( Config.Current.DataPath, OutputFile.Replace( "{lang}", Language ) );
+
+         Config.Current.SaveConfig();
+      }
+   }
+}

+ 12 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEndpointNames.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   public static class KnownEndpointNames
+   {
+      public const string GoogleTranslate = "GoogleTranslate";
+   }
+}

+ 14 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownEvents.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   public static class KnownEvents
+   {
+      public static string OnUnableToTranslateUGUI = "OnUnableToTranslateUGUI";
+      public static string OnUnableToTranslateTextMeshPro = "OnUnableToTranslateTextMeshPro";
+      public static string OnUnableToTranslateNGUI = "OnUnableToTranslateNGUI";
+   }
+}

+ 12 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownPlugins.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   public static class KnownPlugins
+   {
+      public const string DynamicTranslationLoader = "com.bepis.bepinex.dynamictranslationloader";
+   }
+}

+ 17 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginInfo.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   public static class PluginInfo
+   {
+      public const string Identifier = "com.leakim1336.autotranslator";
+
+      public const string Name = "XUnity Auto Translator";
+
+      public const string Version = "2.3.0";
+   }
+}

+ 30 - 0
src/XUnity.AutoTranslator.Plugin.Core/Constants/Types.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   public static class Types
+   {
+      public static readonly Type TMP_InputField = FindType( "TMPro.TMP_InputField" );
+      public static readonly Type TMP_Text = FindType( "TMPro.TMP_Text" );
+      public static readonly Type TextMeshProUGUI = FindType( "TMPro.TextMeshProUGUI" );
+      public static readonly Type TextMeshPro = FindType( "TMPro.TextMeshPro" );
+
+      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 UILabel = FindType( "UILabel" );
+
+
+      private static Type FindType( string name )
+      {
+         return AppDomain.CurrentDomain.GetAssemblies()
+            .Select( x => x.GetType( name, false ) )
+            .Where( x => x != null )
+            .FirstOrDefault();
+      }
+   }
+}

+ 38 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   public static class GameObjectExtensions
+   {
+      private static readonly string DummyName = "Dummy";
+
+      public static Component GetFirstComponentInSelfOrAncestor( this GameObject go, Type type )
+      {
+         if( type == null ) return null;
+
+         var current = go;
+
+         while( current != null )
+         {
+            var foundComponent = current.GetComponent( type );
+            if( foundComponent != null )
+            {
+               return foundComponent;
+            }
+            
+            current = current.transform?.parent?.gameObject;
+         }
+
+         return null;
+      }
+
+      public static bool IsDummy( this GameObject go )
+      {
+         return go.name.EndsWith( DummyName ) || go?.transform?.parent?.name.EndsWith( DummyName ) == true;
+      }
+   }
+}

+ 30 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/HarmonyInstanceExtensions.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Harmony;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   public static class HarmonyInstanceExtensions
+   {
+      public static void PatchAll( this HarmonyInstance instance, IEnumerable<Type> types )
+      {
+         foreach( var type in types )
+         {
+            instance.PatchType( type );
+         }
+      }
+
+      public static void PatchType( this HarmonyInstance instance, Type type )
+      {
+         var parentMethodInfos = type.GetHarmonyMethods();
+         if( parentMethodInfos != null && parentMethodInfos.Count() > 0 )
+         {
+            var info = HarmonyMethod.Merge( parentMethodInfos );
+            var processor = new PatchProcessor( instance, type, info );
+            processor.Patch();
+         }
+      }
+   }
+}

+ 61 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   public static class ObjectExtensions
+   {
+      private static readonly object Sync = new object();
+      private static readonly WeakDictionary<object, object> DynamicFields = new WeakDictionary<object, object>();
+
+      public static TranslationInfo GetTranslationInfo( this object obj, bool isAwakening )
+      {
+         if( obj is GUIContent ) return null;
+
+         var info = obj.Get<TranslationInfo>();
+
+         info.IsAwake = info.IsAwake || isAwakening;
+
+         return info;
+      }
+
+      public static T Get<T>( this object obj )
+         where T : new()
+      {
+         lock( Sync )
+         {
+            if( DynamicFields.TryGetValue( obj, out object value ) )
+            {
+               return (T)value;
+            }
+            else
+            {
+               var t = new T();
+               DynamicFields[ obj ] = t;
+               return t;
+            }
+         }
+      }
+
+      public static void Cull()
+      {
+         lock( Sync )
+         {
+            DynamicFields.RemoveCollectedEntries();
+         }
+      }
+
+      public static IEnumerable<KeyValuePair<object, object>> GetAllRegisteredObjects()
+      {
+         lock( Sync )
+         {
+            return DynamicFields.ToList();
+         }
+      }
+   }
+}

+ 16 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringBuilderExtensions.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   public static class StringBuilderExtensions
+   {
+      public static bool EndsWithWhitespaceOrNewline( this StringBuilder builder )
+      {
+         if( builder.Length == 0 ) return true;
+
+         var lastChar = builder[ builder.Length - 1 ];
+         return Char.IsWhiteSpace( lastChar ) || lastChar == '\n' || lastChar == '\r';
+      }
+   }
+}

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

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Extensions
+{
+   public static class StringExtensions
+   {
+      public static string ChangeToSingleLineForDialogue( this string that )
+      {
+         if( that.Length > 18 ) // long strings often indicate dialog
+         {
+            // Always change dialogue into one line. Otherwise translation services gets confused.
+            return that.RemoveNewlines();
+         }
+         else
+         {
+            return that;
+         }
+      }
+
+      public static string RemoveNewlines( this string text )
+      {
+         return text.Replace( "\n", "" ).Replace( "\r", "" );
+      }
+
+      public static string RemoveWhitespace( this string text )
+      {
+         // Japanese whitespace, wtf
+         return text.RemoveNewlines().Replace( " ", "" ).Replace( " ", "" );
+      }
+
+      public static string UnescapeJson( this string str )
+      {
+         if( str == null ) return null;
+
+         var builder = new StringBuilder( str );
+
+         bool escapeNext = false;
+         for( int i = 0 ; i < builder.Length ; i++ )
+         {
+            var c = builder[ i ];
+            if( escapeNext )
+            {
+               bool found = true;
+               char escapeWith = default( char );
+               switch( c )
+               {
+                  case 'b':
+                     escapeWith = '\b';
+                     break;
+                  case 'f':
+                     escapeWith = '\f';
+                     break;
+                  case 'n':
+                     escapeWith = '\n';
+                     break;
+                  case 'r':
+                     escapeWith = '\r';
+                     break;
+                  case 't':
+                     escapeWith = '\t';
+                     break;
+                  case '"':
+                     escapeWith = '\"';
+                     break;
+                  case '\\':
+                     escapeWith = '\\';
+                     break;
+                  default:
+                     found = false;
+                     break;
+               }
+
+               // remove previous char and go one back
+               if( found )
+               {
+                  // found proper escaping
+                  builder.Remove( --i, 2 );
+                  builder.Insert( i, escapeWith );
+               }
+               else
+               {
+                  // dont do anything
+               }
+
+               escapeNext = false;
+            }
+            else if( c == '\\' )
+            {
+               escapeNext = true;
+            }
+         }
+
+         return builder.ToString();
+      }
+   }
+}

+ 28 - 0
src/XUnity.AutoTranslator.Plugin.Core/Files/IndentedTextWriter.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Files
+{
+   public class IndentedTextWriter
+   {
+      private readonly TextWriter _writer;
+      private readonly char _indent;
+
+      public IndentedTextWriter( TextWriter writer, char indent )
+      {
+         _writer = writer;
+         _indent = indent;
+      }
+
+      public int Indent { get; set; }
+
+      public void WriteLine( string line )
+      {
+         _writer.Write( new string( _indent, Indent ) );
+         _writer.WriteLine( line );
+      }
+   }
+}

+ 113 - 0
src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs

@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro;
+using XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI;
+using XUnity.AutoTranslator.Plugin.Core.IMGUI;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks
+{
+
+   public static class HooksSetup
+   {
+      public static void InstallHooks( Func<object, string, string> defaultHook )
+      {
+         try
+         {
+            var harmony = HarmonyInstance.Create( "com.leakim1336.autotranslator" );
+
+            bool success = false;
+            if( Settings.EnableUGUI )
+            {
+               success = SetupHook( KnownEvents.OnUnableToTranslateUGUI, defaultHook );
+               if( !success )
+               {
+                  harmony.PatchAll( UGUIHooks.All );
+               }
+            }
+
+            if( Settings.EnableTextMeshPro )
+            {
+               success = SetupHook( KnownEvents.OnUnableToTranslateTextMeshPro, defaultHook );
+               if( !success )
+               {
+                  harmony.PatchAll( TextMeshProHooks.All );
+               }
+            }
+
+            if( Settings.EnableNGUI )
+            {
+               success = SetupHook( KnownEvents.OnUnableToTranslateNGUI, defaultHook );
+               if( !success )
+               {
+                  harmony.PatchAll( NGUIHooks.All );
+               }
+            }
+
+            if( Settings.EnableIMGUI )
+            {
+               harmony.PatchAll( IMGUIHooks.All );
+            }
+         }
+         catch( Exception e )
+         {
+            Console.WriteLine( "ERROR WHILE INITIALIZING AUTO TRANSLATOR: " + Environment.NewLine + e );
+         }
+      }
+
+      public static bool SetupHook( string eventName, Func<object, string, string> callback )
+      {
+         if( !Settings.AllowPluginHookOverride ) return false;
+
+         var objects = GameObject.FindObjectsOfType<GameObject>();
+         foreach( var gameObject in objects )
+         {
+            if( gameObject != null )
+            {
+               var components = gameObject.GetComponents<Component>();
+               foreach( var component in components )
+               {
+                  if( component != null )
+                  {
+                     var e = component.GetType().GetEvent( eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic );
+                     if( e != null )
+                     {
+                        var addMethod = e.GetAddMethod();
+                        if( addMethod != null )
+                        {
+                           try
+                           {
+                              if( addMethod.IsStatic )
+                              {
+                                 addMethod.Invoke( null, new object[] { callback } );
+                              }
+                              else
+                              {
+                                 addMethod.Invoke( component, new object[] { callback } );
+                              }
+
+                              Application.Quit();
+
+                              return true;
+                           }
+                           catch { }
+                        }
+                     }
+                  }
+               }
+            }
+         }
+
+         return false;
+      }
+   }
+}

+ 230 - 0
src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs

@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using UnityEngine;
+using static UnityEngine.GUI;
+
+namespace XUnity.AutoTranslator.Plugin.Core.IMGUI
+{
+   public delegate void IMGUITextChanged( object text );
+
+   public static class IMGUIHooks
+   {
+      public static readonly Type[] All = new[] {
+         typeof( BeginGroupHook ),
+         typeof( BoxHook ),
+         typeof( DoRepeatButtonHook ),
+         typeof( DoLabelHook ),
+         typeof( DoButtonHook ),
+         typeof( DoModalWindowHook ),
+         typeof( DoWindowHook ),
+         typeof( DoButtonGridHook ),
+         typeof( DoTextFieldHook ),
+         typeof( DoToggleHook ),
+      };
+
+      public static event IMGUITextChanged TextChanged;
+
+      public static void FireTextChanged( object graphic )
+      {
+         TextChanged?.Invoke( graphic );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class BeginGroupHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "BeginGroup", new[] { typeof( Rect ), typeof( GUIContent ), typeof( GUIStyle ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class BoxHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "Box", new[] { typeof( Rect ), typeof( GUIContent ), typeof( GUIStyle ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoRepeatButtonHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoRepeatButton", new[] { typeof( Rect ), typeof( GUIContent ), typeof( GUIStyle ), typeof( FocusType ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoLabelHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoLabel", new[] { typeof( Rect ), typeof( GUIContent ), typeof( IntPtr ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoButtonHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoButton", new[] { typeof( Rect ), typeof( GUIContent ), typeof( IntPtr ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoModalWindowHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoModalWindow", new[] { typeof( int ), typeof( Rect ), typeof( WindowFunction ), typeof( GUIContent ), typeof( GUIStyle ), typeof( GUISkin ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoWindowHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoWindow", new[] { typeof( int ), typeof( Rect ), typeof( WindowFunction ), typeof( GUIContent ), typeof( GUIStyle ), typeof( GUISkin ), typeof( bool ) } );
+      }
+
+      static void Prefix( GUIContent title )
+      {
+         IMGUIHooks.FireTextChanged( title );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoButtonGridHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoButtonGrid", new[] { typeof( Rect ), typeof( int ), typeof( GUIContent[] ), typeof( int ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ) } );
+      }
+
+      static void Prefix( GUIContent[] contents )
+      {
+         foreach( var content in contents )
+         {
+            IMGUIHooks.FireTextChanged( content );
+         }
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoTextFieldHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoTextField", new[] { typeof( Rect ), typeof( int ), typeof( GUIContent ), typeof( bool ), typeof( int ), typeof( GUIStyle ), typeof( string ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class DoToggleHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.GUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.GUI, "DoToggle", new[] { typeof( Rect ), typeof( int ), typeof( bool ), typeof( GUIContent ), typeof( IntPtr ) } );
+      }
+
+      static void Prefix( GUIContent content )
+      {
+         IMGUIHooks.FireTextChanged( content );
+      }
+   }
+}

+ 47 - 0
src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Text;
+using Harmony;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI
+{
+   public delegate void NGUITextChanged( object graphic );
+
+   public static class NGUIHooks
+   {
+      public static readonly Type[] All = new[] {
+         typeof( TextPropertyHook )
+      };
+
+      public static event NGUITextChanged TextChanged;
+
+      public static void FireTextChanged( object graphic )
+      {
+         TextChanged?.Invoke( graphic );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class TextPropertyHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.UILabel != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Property( Constants.Types.UILabel, "text" ).GetSetMethod();
+      }
+
+      public static void Postfix( object __instance )
+      {
+         NGUIHooks.FireTextChanged( __instance );
+      }
+   }
+}

+ 212 - 0
src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs

@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
+{
+   public delegate void TextMeshProChanged( object graphic );
+   public delegate void TextMeshProAwakened( object graphic );
+
+   public static class TextMeshProHooks
+   {
+      public static readonly Type[] All = new[] {
+         typeof( TeshMeshProUGUIAwakeHook ),
+         typeof( TeshMeshProAwakeHook ),
+         typeof( TextPropertyHook ),
+         typeof( SetTextHook1 ),
+         typeof( SetTextHook2 ),
+         typeof( SetTextHook3 ),
+         typeof( SetCharArrayHook1 ),
+         typeof( SetCharArrayHook2 ),
+         typeof( SetCharArrayHook3 ),
+      };
+
+      public static event TextMeshProChanged TextChanged;
+      public static event TextMeshProAwakened TextAwakened;
+
+      public static void FireTextAwakened( object graphic )
+      {
+         TextAwakened?.Invoke( graphic );
+      }
+
+      public static void FireTextChanged( object graphic )
+      {
+         TextChanged?.Invoke( graphic );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class TeshMeshProUGUIAwakeHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TextMeshProUGUI != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TextMeshProUGUI, "Awake" );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextAwakened( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class TeshMeshProAwakeHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TextMeshPro != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TextMeshPro, "Awake" );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextAwakened( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class TextPropertyHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Property( Types.TMP_Text, "text" ).GetSetMethod();
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetTextHook1
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetText", new[] { typeof( StringBuilder ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetTextHook2
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetText", new[] { typeof( string ), typeof( bool ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetTextHook3
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetText", new[] { typeof( string ), typeof( float ), typeof( float ), typeof( float ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetCharArrayHook1
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetCharArray", new[] { typeof( char[] ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetCharArrayHook2
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetCharArray", new[] { typeof( char[] ), typeof( int ), typeof( int ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class SetCharArrayHook3
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.TMP_Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Types.TMP_Text, "SetCharArray", new[] { typeof( int[] ), typeof( int ), typeof( int ) } );
+      }
+
+      static void Postfix( object __instance )
+      {
+         TextMeshProHooks.FireTextChanged( __instance );
+      }
+   }
+}

+ 74 - 0
src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI
+{
+   public delegate void UGUITextChanged( object graphic );
+   public delegate void UGUITextAwakened( object graphic );
+
+   public static class UGUIHooks
+   {
+      public static readonly Type[] All = new[] {
+         typeof( TextPropertyHook ),
+         typeof( OnEnableHook ),
+      };
+
+      public static event UGUITextChanged TextChanged;
+      public static event UGUITextAwakened TextAwakened;
+
+      public static void FireTextAwakened( object graphic )
+      {
+         TextAwakened?.Invoke( graphic );
+      }
+
+      public static void FireTextChanged( object graphic )
+      {
+         TextChanged?.Invoke( graphic );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class TextPropertyHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         var text = AccessTools.Property( Types.Text, "text" );
+         return text.GetSetMethod();
+      }
+
+      static void Postfix( object __instance )
+      {
+         UGUIHooks.FireTextChanged( __instance );
+      }
+   }
+
+   [Harmony, HarmonyAfter( Constants.KnownPlugins.DynamicTranslationLoader )]
+   public static class OnEnableHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Types.Text != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         var OnEnable = AccessTools.Method( Types.Text, "OnEnable" );
+         return OnEnable;
+      }
+
+      static void Postfix( object __instance )
+      {
+         UGUIHooks.FireTextAwakened( __instance );
+      }
+   }
+}

+ 1284 - 0
src/XUnity.AutoTranslator.Plugin.Core/Json/SimpleJson.cs

@@ -0,0 +1,1284 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+/*
+* The MIT License( MIT)
+*
+* Copyright( c) 2012-2017 Markus Göbel( Bunny83)
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+* 
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+* 
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+* 
+* * * * */
+namespace SimpleJSON
+{
+   public enum JSONNodeType
+   {
+      Array = 1,
+      Object = 2,
+      String = 3,
+      Number = 4,
+      NullValue = 5,
+      Boolean = 6,
+      None = 7,
+      Custom = 0xFF,
+   }
+   public enum JSONTextMode
+   {
+      Compact,
+      Indent
+   }
+
+   public abstract partial class JSONNode
+   {
+      #region Enumerators
+      public struct Enumerator
+      {
+         private enum Type { None, Array, Object }
+         private Type type;
+         private Dictionary<string, JSONNode>.Enumerator m_Object;
+         private List<JSONNode>.Enumerator m_Array;
+         public bool IsValid { get { return type != Type.None; } }
+         public Enumerator( List<JSONNode>.Enumerator aArrayEnum )
+         {
+            type = Type.Array;
+            m_Object = default( Dictionary<string, JSONNode>.Enumerator );
+            m_Array = aArrayEnum;
+         }
+         public Enumerator( Dictionary<string, JSONNode>.Enumerator aDictEnum )
+         {
+            type = Type.Object;
+            m_Object = aDictEnum;
+            m_Array = default( List<JSONNode>.Enumerator );
+         }
+         public KeyValuePair<string, JSONNode> Current
+         {
+            get
+            {
+               if( type == Type.Array )
+                  return new KeyValuePair<string, JSONNode>( string.Empty, m_Array.Current );
+               else if( type == Type.Object )
+                  return m_Object.Current;
+               return new KeyValuePair<string, JSONNode>( string.Empty, null );
+            }
+         }
+         public bool MoveNext()
+         {
+            if( type == Type.Array )
+               return m_Array.MoveNext();
+            else if( type == Type.Object )
+               return m_Object.MoveNext();
+            return false;
+         }
+      }
+      public struct ValueEnumerator
+      {
+         private Enumerator m_Enumerator;
+         public ValueEnumerator( List<JSONNode>.Enumerator aArrayEnum ) : this( new Enumerator( aArrayEnum ) ) { }
+         public ValueEnumerator( Dictionary<string, JSONNode>.Enumerator aDictEnum ) : this( new Enumerator( aDictEnum ) ) { }
+         public ValueEnumerator( Enumerator aEnumerator ) { m_Enumerator = aEnumerator; }
+         public JSONNode Current { get { return m_Enumerator.Current.Value; } }
+         public bool MoveNext() { return m_Enumerator.MoveNext(); }
+         public ValueEnumerator GetEnumerator() { return this; }
+      }
+      public struct KeyEnumerator
+      {
+         private Enumerator m_Enumerator;
+         public KeyEnumerator( List<JSONNode>.Enumerator aArrayEnum ) : this( new Enumerator( aArrayEnum ) ) { }
+         public KeyEnumerator( Dictionary<string, JSONNode>.Enumerator aDictEnum ) : this( new Enumerator( aDictEnum ) ) { }
+         public KeyEnumerator( Enumerator aEnumerator ) { m_Enumerator = aEnumerator; }
+         public JSONNode Current { get { return m_Enumerator.Current.Key; } }
+         public bool MoveNext() { return m_Enumerator.MoveNext(); }
+         public KeyEnumerator GetEnumerator() { return this; }
+      }
+
+      public class LinqEnumerator : IEnumerator<KeyValuePair<string, JSONNode>>, IEnumerable<KeyValuePair<string, JSONNode>>
+      {
+         private JSONNode m_Node;
+         private Enumerator m_Enumerator;
+         internal LinqEnumerator( JSONNode aNode )
+         {
+            m_Node = aNode;
+            if( m_Node != null )
+               m_Enumerator = m_Node.GetEnumerator();
+         }
+         public KeyValuePair<string, JSONNode> Current { get { return m_Enumerator.Current; } }
+         object IEnumerator.Current { get { return m_Enumerator.Current; } }
+         public bool MoveNext() { return m_Enumerator.MoveNext(); }
+
+         public void Dispose()
+         {
+            m_Node = null;
+            m_Enumerator = new Enumerator();
+         }
+
+         public IEnumerator<KeyValuePair<string, JSONNode>> GetEnumerator()
+         {
+            return new LinqEnumerator( m_Node );
+         }
+
+         public void Reset()
+         {
+            if( m_Node != null )
+               m_Enumerator = m_Node.GetEnumerator();
+         }
+
+         IEnumerator IEnumerable.GetEnumerator()
+         {
+            return new LinqEnumerator( m_Node );
+         }
+      }
+
+      #endregion Enumerators
+
+      #region common interface
+
+      public static bool forceASCII = false; // Use Unicode by default
+
+      public abstract JSONNodeType Tag { get; }
+
+      public virtual JSONNode this[ int aIndex ] { get { return null; } set { } }
+
+      public virtual JSONNode this[ string aKey ] { get { return null; } set { } }
+
+      public virtual string Value { get { return ""; } set { } }
+
+      public virtual int Count { get { return 0; } }
+
+      public virtual bool IsNumber { get { return false; } }
+      public virtual bool IsString { get { return false; } }
+      public virtual bool IsBoolean { get { return false; } }
+      public virtual bool IsNull { get { return false; } }
+      public virtual bool IsArray { get { return false; } }
+      public virtual bool IsObject { get { return false; } }
+
+      public virtual bool Inline { get { return false; } set { } }
+
+      public virtual void Add( string aKey, JSONNode aItem )
+      {
+      }
+      public virtual void Add( JSONNode aItem )
+      {
+         Add( "", aItem );
+      }
+
+      public virtual JSONNode Remove( string aKey )
+      {
+         return null;
+      }
+
+      public virtual JSONNode Remove( int aIndex )
+      {
+         return null;
+      }
+
+      public virtual JSONNode Remove( JSONNode aNode )
+      {
+         return aNode;
+      }
+
+      public virtual IEnumerable<JSONNode> Children
+      {
+         get
+         {
+            yield break;
+         }
+      }
+
+      public IEnumerable<JSONNode> DeepChildren
+      {
+         get
+         {
+            foreach( var C in Children )
+               foreach( var D in C.DeepChildren )
+                  yield return D;
+         }
+      }
+
+      public override string ToString()
+      {
+         StringBuilder sb = new StringBuilder();
+         WriteToStringBuilder( sb, 0, 0, JSONTextMode.Compact );
+         return sb.ToString();
+      }
+
+      public virtual string ToString( int aIndent )
+      {
+         StringBuilder sb = new StringBuilder();
+         WriteToStringBuilder( sb, 0, aIndent, JSONTextMode.Indent );
+         return sb.ToString();
+      }
+      internal abstract void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode );
+
+      public abstract Enumerator GetEnumerator();
+      public IEnumerable<KeyValuePair<string, JSONNode>> Linq { get { return new LinqEnumerator( this ); } }
+      public KeyEnumerator Keys { get { return new KeyEnumerator( GetEnumerator() ); } }
+      public ValueEnumerator Values { get { return new ValueEnumerator( GetEnumerator() ); } }
+
+      #endregion common interface
+
+      #region typecasting properties
+
+
+      public virtual double AsDouble
+      {
+         get
+         {
+            double v = 0.0;
+            if( double.TryParse( Value, out v ) )
+               return v;
+            return 0.0;
+         }
+         set
+         {
+            Value = value.ToString();
+         }
+      }
+
+      public virtual int AsInt
+      {
+         get { return (int)AsDouble; }
+         set { AsDouble = value; }
+      }
+
+      public virtual float AsFloat
+      {
+         get { return (float)AsDouble; }
+         set { AsDouble = value; }
+      }
+
+      public virtual bool AsBool
+      {
+         get
+         {
+            bool v = false;
+            if( bool.TryParse( Value, out v ) )
+               return v;
+            return !string.IsNullOrEmpty( Value );
+         }
+         set
+         {
+            Value = ( value ) ? "true" : "false";
+         }
+      }
+
+      public virtual JSONArray AsArray
+      {
+         get
+         {
+            return this as JSONArray;
+         }
+      }
+
+      public virtual JSONObject AsObject
+      {
+         get
+         {
+            return this as JSONObject;
+         }
+      }
+
+
+      #endregion typecasting properties
+
+      #region operators
+
+      public static implicit operator JSONNode( string s )
+      {
+         return new JSONString( s );
+      }
+      public static implicit operator string( JSONNode d )
+      {
+         return ( d == null ) ? null : d.Value;
+      }
+
+      public static implicit operator JSONNode( double n )
+      {
+         return new JSONNumber( n );
+      }
+      public static implicit operator double( JSONNode d )
+      {
+         return ( d == null ) ? 0 : d.AsDouble;
+      }
+
+      public static implicit operator JSONNode( float n )
+      {
+         return new JSONNumber( n );
+      }
+      public static implicit operator float( JSONNode d )
+      {
+         return ( d == null ) ? 0 : d.AsFloat;
+      }
+
+      public static implicit operator JSONNode( int n )
+      {
+         return new JSONNumber( n );
+      }
+      public static implicit operator int( JSONNode d )
+      {
+         return ( d == null ) ? 0 : d.AsInt;
+      }
+
+      public static implicit operator JSONNode( bool b )
+      {
+         return new JSONBool( b );
+      }
+      public static implicit operator bool( JSONNode d )
+      {
+         return ( d == null ) ? false : d.AsBool;
+      }
+
+      public static implicit operator JSONNode( KeyValuePair<string, JSONNode> aKeyValue )
+      {
+         return aKeyValue.Value;
+      }
+
+      public static bool operator ==( JSONNode a, object b )
+      {
+         if( ReferenceEquals( a, b ) )
+            return true;
+         bool aIsNull = a is JSONNull || ReferenceEquals( a, null ) || a is JSONLazyCreator;
+         bool bIsNull = b is JSONNull || ReferenceEquals( b, null ) || b is JSONLazyCreator;
+         if( aIsNull && bIsNull )
+            return true;
+         return !aIsNull && a.Equals( b );
+      }
+
+      public static bool operator !=( JSONNode a, object b )
+      {
+         return !( a == b );
+      }
+
+      public override bool Equals( object obj )
+      {
+         return ReferenceEquals( this, obj );
+      }
+
+      public override int GetHashCode()
+      {
+         return base.GetHashCode();
+      }
+
+      #endregion operators
+
+      [ThreadStatic]
+      private static StringBuilder m_EscapeBuilder;
+      internal static StringBuilder EscapeBuilder
+      {
+         get
+         {
+            if( m_EscapeBuilder == null )
+               m_EscapeBuilder = new StringBuilder();
+            return m_EscapeBuilder;
+         }
+      }
+      internal static string Escape( string aText )
+      {
+         var sb = EscapeBuilder;
+         sb.Length = 0;
+         if( sb.Capacity < aText.Length + aText.Length / 10 )
+            sb.Capacity = aText.Length + aText.Length / 10;
+         foreach( char c in aText )
+         {
+            switch( c )
+            {
+               case '\\':
+                  sb.Append( "\\\\" );
+                  break;
+               case '\"':
+                  sb.Append( "\\\"" );
+                  break;
+               case '\n':
+                  sb.Append( "\\n" );
+                  break;
+               case '\r':
+                  sb.Append( "\\r" );
+                  break;
+               case '\t':
+                  sb.Append( "\\t" );
+                  break;
+               case '\b':
+                  sb.Append( "\\b" );
+                  break;
+               case '\f':
+                  sb.Append( "\\f" );
+                  break;
+               default:
+                  if( c < ' ' || ( forceASCII && c > 127 ) )
+                  {
+                     ushort val = c;
+                     sb.Append( "\\u" ).Append( val.ToString( "X4" ) );
+                  }
+                  else
+                     sb.Append( c );
+                  break;
+            }
+         }
+         string result = sb.ToString();
+         sb.Length = 0;
+         return result;
+      }
+
+      static void ParseElement( JSONNode ctx, string token, string tokenName, bool quoted )
+      {
+         if( quoted )
+         {
+            ctx.Add( tokenName, token );
+            return;
+         }
+         string tmp = token.ToLower();
+         if( tmp == "false" || tmp == "true" )
+            ctx.Add( tokenName, tmp == "true" );
+         else if( tmp == "null" )
+            ctx.Add( tokenName, null );
+         else
+         {
+            double val;
+            if( double.TryParse( token, out val ) )
+               ctx.Add( tokenName, val );
+            else
+               ctx.Add( tokenName, token );
+         }
+      }
+
+      public static JSONNode Parse( string aJSON )
+      {
+         Stack<JSONNode> stack = new Stack<JSONNode>();
+         JSONNode ctx = null;
+         int i = 0;
+         StringBuilder Token = new StringBuilder();
+         string TokenName = "";
+         bool QuoteMode = false;
+         bool TokenIsQuoted = false;
+         while( i < aJSON.Length )
+         {
+            switch( aJSON[ i ] )
+            {
+               case '{':
+                  if( QuoteMode )
+                  {
+                     Token.Append( aJSON[ i ] );
+                     break;
+                  }
+                  stack.Push( new JSONObject() );
+                  if( ctx != null )
+                  {
+                     ctx.Add( TokenName, stack.Peek() );
+                  }
+                  TokenName = "";
+                  Token.Length = 0;
+                  ctx = stack.Peek();
+                  break;
+
+               case '[':
+                  if( QuoteMode )
+                  {
+                     Token.Append( aJSON[ i ] );
+                     break;
+                  }
+
+                  stack.Push( new JSONArray() );
+                  if( ctx != null )
+                  {
+                     ctx.Add( TokenName, stack.Peek() );
+                  }
+                  TokenName = "";
+                  Token.Length = 0;
+                  ctx = stack.Peek();
+                  break;
+
+               case '}':
+               case ']':
+                  if( QuoteMode )
+                  {
+
+                     Token.Append( aJSON[ i ] );
+                     break;
+                  }
+                  if( stack.Count == 0 )
+                     throw new Exception( "JSON Parse: Too many closing brackets" );
+
+                  stack.Pop();
+                  if( Token.Length > 0 || TokenIsQuoted )
+                  {
+                     ParseElement( ctx, Token.ToString(), TokenName, TokenIsQuoted );
+                     TokenIsQuoted = false;
+                  }
+                  TokenName = "";
+                  Token.Length = 0;
+                  if( stack.Count > 0 )
+                     ctx = stack.Peek();
+                  break;
+
+               case ':':
+                  if( QuoteMode )
+                  {
+                     Token.Append( aJSON[ i ] );
+                     break;
+                  }
+                  TokenName = Token.ToString();
+                  Token.Length = 0;
+                  TokenIsQuoted = false;
+                  break;
+
+               case '"':
+                  QuoteMode ^= true;
+                  TokenIsQuoted |= QuoteMode;
+                  break;
+
+               case ',':
+                  if( QuoteMode )
+                  {
+                     Token.Append( aJSON[ i ] );
+                     break;
+                  }
+                  if( Token.Length > 0 || TokenIsQuoted )
+                  {
+                     ParseElement( ctx, Token.ToString(), TokenName, TokenIsQuoted );
+                     TokenIsQuoted = false;
+                  }
+                  TokenName = "";
+                  Token.Length = 0;
+                  TokenIsQuoted = false;
+                  break;
+
+               case '\r':
+               case '\n':
+                  break;
+
+               case ' ':
+               case '\t':
+                  if( QuoteMode )
+                     Token.Append( aJSON[ i ] );
+                  break;
+
+               case '\\':
+                  ++i;
+                  if( QuoteMode )
+                  {
+                     char C = aJSON[ i ];
+                     switch( C )
+                     {
+                        case 't':
+                           Token.Append( '\t' );
+                           break;
+                        case 'r':
+                           Token.Append( '\r' );
+                           break;
+                        case 'n':
+                           Token.Append( '\n' );
+                           break;
+                        case 'b':
+                           Token.Append( '\b' );
+                           break;
+                        case 'f':
+                           Token.Append( '\f' );
+                           break;
+                        case 'u':
+                           {
+                              string s = aJSON.Substring( i + 1, 4 );
+                              Token.Append( (char)int.Parse(
+                                  s,
+                                  System.Globalization.NumberStyles.AllowHexSpecifier ) );
+                              i += 4;
+                              break;
+                           }
+                        default:
+                           Token.Append( C );
+                           break;
+                     }
+                  }
+                  break;
+
+               default:
+                  Token.Append( aJSON[ i ] );
+                  break;
+            }
+            ++i;
+         }
+         if( QuoteMode )
+         {
+            throw new Exception( "JSON Parse: Quotation marks seems to be messed up." );
+         }
+         return ctx;
+      }
+
+   }
+   // End of JSONNode
+
+   public partial class JSONArray : JSONNode
+   {
+      private List<JSONNode> m_List = new List<JSONNode>();
+      private bool inline = false;
+      public override bool Inline
+      {
+         get { return inline; }
+         set { inline = value; }
+      }
+
+      public override JSONNodeType Tag { get { return JSONNodeType.Array; } }
+      public override bool IsArray { get { return true; } }
+      public override Enumerator GetEnumerator() { return new Enumerator( m_List.GetEnumerator() ); }
+
+      public override JSONNode this[ int aIndex ]
+      {
+         get
+         {
+            if( aIndex < 0 || aIndex >= m_List.Count )
+               return new JSONLazyCreator( this );
+            return m_List[ aIndex ];
+         }
+         set
+         {
+            if( value == null )
+               value = JSONNull.CreateOrGet();
+            if( aIndex < 0 || aIndex >= m_List.Count )
+               m_List.Add( value );
+            else
+               m_List[ aIndex ] = value;
+         }
+      }
+
+      public override JSONNode this[ string aKey ]
+      {
+         get { return new JSONLazyCreator( this ); }
+         set
+         {
+            if( value == null )
+               value = JSONNull.CreateOrGet();
+            m_List.Add( value );
+         }
+      }
+
+      public override int Count
+      {
+         get { return m_List.Count; }
+      }
+
+      public override void Add( string aKey, JSONNode aItem )
+      {
+         if( aItem == null )
+            aItem = JSONNull.CreateOrGet();
+         m_List.Add( aItem );
+      }
+
+      public override JSONNode Remove( int aIndex )
+      {
+         if( aIndex < 0 || aIndex >= m_List.Count )
+            return null;
+         JSONNode tmp = m_List[ aIndex ];
+         m_List.RemoveAt( aIndex );
+         return tmp;
+      }
+
+      public override JSONNode Remove( JSONNode aNode )
+      {
+         m_List.Remove( aNode );
+         return aNode;
+      }
+
+      public override IEnumerable<JSONNode> Children
+      {
+         get
+         {
+            foreach( JSONNode N in m_List )
+               yield return N;
+         }
+      }
+
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( '[' );
+         int count = m_List.Count;
+         if( inline )
+            aMode = JSONTextMode.Compact;
+         for( int i = 0 ; i < count ; i++ )
+         {
+            if( i > 0 )
+               aSB.Append( ',' );
+            if( aMode == JSONTextMode.Indent )
+               aSB.AppendLine();
+
+            if( aMode == JSONTextMode.Indent )
+               aSB.Append( ' ', aIndent + aIndentInc );
+            m_List[ i ].WriteToStringBuilder( aSB, aIndent + aIndentInc, aIndentInc, aMode );
+         }
+         if( aMode == JSONTextMode.Indent )
+            aSB.AppendLine().Append( ' ', aIndent );
+         aSB.Append( ']' );
+      }
+   }
+   // End of JSONArray
+
+   public partial class JSONObject : JSONNode
+   {
+      private Dictionary<string, JSONNode> m_Dict = new Dictionary<string, JSONNode>();
+
+      private bool inline = false;
+      public override bool Inline
+      {
+         get { return inline; }
+         set { inline = value; }
+      }
+
+      public override JSONNodeType Tag { get { return JSONNodeType.Object; } }
+      public override bool IsObject { get { return true; } }
+
+      public override Enumerator GetEnumerator() { return new Enumerator( m_Dict.GetEnumerator() ); }
+
+
+      public override JSONNode this[ string aKey ]
+      {
+         get
+         {
+            if( m_Dict.ContainsKey( aKey ) )
+               return m_Dict[ aKey ];
+            else
+               return new JSONLazyCreator( this, aKey );
+         }
+         set
+         {
+            if( value == null )
+               value = JSONNull.CreateOrGet();
+            if( m_Dict.ContainsKey( aKey ) )
+               m_Dict[ aKey ] = value;
+            else
+               m_Dict.Add( aKey, value );
+         }
+      }
+
+      public override JSONNode this[ int aIndex ]
+      {
+         get
+         {
+            if( aIndex < 0 || aIndex >= m_Dict.Count )
+               return null;
+            return m_Dict.ElementAt( aIndex ).Value;
+         }
+         set
+         {
+            if( value == null )
+               value = JSONNull.CreateOrGet();
+            if( aIndex < 0 || aIndex >= m_Dict.Count )
+               return;
+            string key = m_Dict.ElementAt( aIndex ).Key;
+            m_Dict[ key ] = value;
+         }
+      }
+
+      public override int Count
+      {
+         get { return m_Dict.Count; }
+      }
+
+      public override void Add( string aKey, JSONNode aItem )
+      {
+         if( aItem == null )
+            aItem = JSONNull.CreateOrGet();
+
+         if( !string.IsNullOrEmpty( aKey ) )
+         {
+            if( m_Dict.ContainsKey( aKey ) )
+               m_Dict[ aKey ] = aItem;
+            else
+               m_Dict.Add( aKey, aItem );
+         }
+         else
+            m_Dict.Add( Guid.NewGuid().ToString(), aItem );
+      }
+
+      public override JSONNode Remove( string aKey )
+      {
+         if( !m_Dict.ContainsKey( aKey ) )
+            return null;
+         JSONNode tmp = m_Dict[ aKey ];
+         m_Dict.Remove( aKey );
+         return tmp;
+      }
+
+      public override JSONNode Remove( int aIndex )
+      {
+         if( aIndex < 0 || aIndex >= m_Dict.Count )
+            return null;
+         var item = m_Dict.ElementAt( aIndex );
+         m_Dict.Remove( item.Key );
+         return item.Value;
+      }
+
+      public override JSONNode Remove( JSONNode aNode )
+      {
+         try
+         {
+            var item = m_Dict.Where( k => k.Value == aNode ).First();
+            m_Dict.Remove( item.Key );
+            return aNode;
+         }
+         catch
+         {
+            return null;
+         }
+      }
+
+      public override IEnumerable<JSONNode> Children
+      {
+         get
+         {
+            foreach( KeyValuePair<string, JSONNode> N in m_Dict )
+               yield return N.Value;
+         }
+      }
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( '{' );
+         bool first = true;
+         if( inline )
+            aMode = JSONTextMode.Compact;
+         foreach( var k in m_Dict )
+         {
+            if( !first )
+               aSB.Append( ',' );
+            first = false;
+            if( aMode == JSONTextMode.Indent )
+               aSB.AppendLine();
+            if( aMode == JSONTextMode.Indent )
+               aSB.Append( ' ', aIndent + aIndentInc );
+            aSB.Append( '\"' ).Append( Escape( k.Key ) ).Append( '\"' );
+            if( aMode == JSONTextMode.Compact )
+               aSB.Append( ':' );
+            else
+               aSB.Append( " : " );
+            k.Value.WriteToStringBuilder( aSB, aIndent + aIndentInc, aIndentInc, aMode );
+         }
+         if( aMode == JSONTextMode.Indent )
+            aSB.AppendLine().Append( ' ', aIndent );
+         aSB.Append( '}' );
+      }
+
+   }
+   // End of JSONObject
+
+   public partial class JSONString : JSONNode
+   {
+      private string m_Data;
+
+      public override JSONNodeType Tag { get { return JSONNodeType.String; } }
+      public override bool IsString { get { return true; } }
+
+      public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+
+      public override string Value
+      {
+         get { return m_Data; }
+         set
+         {
+            m_Data = value;
+         }
+      }
+
+      public JSONString( string aData )
+      {
+         m_Data = aData;
+      }
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( '\"' ).Append( Escape( m_Data ) ).Append( '\"' );
+      }
+      public override bool Equals( object obj )
+      {
+         if( base.Equals( obj ) )
+            return true;
+         string s = obj as string;
+         if( s != null )
+            return m_Data == s;
+         JSONString s2 = obj as JSONString;
+         if( s2 != null )
+            return m_Data == s2.m_Data;
+         return false;
+      }
+      public override int GetHashCode()
+      {
+         return m_Data.GetHashCode();
+      }
+   }
+   // End of JSONString
+
+   public partial class JSONNumber : JSONNode
+   {
+      private double m_Data;
+
+      public override JSONNodeType Tag { get { return JSONNodeType.Number; } }
+      public override bool IsNumber { get { return true; } }
+      public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+      public override string Value
+      {
+         get { return m_Data.ToString(); }
+         set
+         {
+            double v;
+            if( double.TryParse( value, out v ) )
+               m_Data = v;
+         }
+      }
+
+      public override double AsDouble
+      {
+         get { return m_Data; }
+         set { m_Data = value; }
+      }
+
+      public JSONNumber( double aData )
+      {
+         m_Data = aData;
+      }
+
+      public JSONNumber( string aData )
+      {
+         Value = aData;
+      }
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( m_Data );
+      }
+      private static bool IsNumeric( object value )
+      {
+         return value is int || value is uint
+             || value is float || value is double
+             || value is decimal
+             || value is long || value is ulong
+             || value is short || value is ushort
+             || value is sbyte || value is byte;
+      }
+      public override bool Equals( object obj )
+      {
+         if( obj == null )
+            return false;
+         if( base.Equals( obj ) )
+            return true;
+         JSONNumber s2 = obj as JSONNumber;
+         if( s2 != null )
+            return m_Data == s2.m_Data;
+         if( IsNumeric( obj ) )
+            return Convert.ToDouble( obj ) == m_Data;
+         return false;
+      }
+      public override int GetHashCode()
+      {
+         return m_Data.GetHashCode();
+      }
+   }
+   // End of JSONNumber
+
+   public partial class JSONBool : JSONNode
+   {
+      private bool m_Data;
+
+      public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } }
+      public override bool IsBoolean { get { return true; } }
+      public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+      public override string Value
+      {
+         get { return m_Data.ToString(); }
+         set
+         {
+            bool v;
+            if( bool.TryParse( value, out v ) )
+               m_Data = v;
+         }
+      }
+      public override bool AsBool
+      {
+         get { return m_Data; }
+         set { m_Data = value; }
+      }
+
+      public JSONBool( bool aData )
+      {
+         m_Data = aData;
+      }
+
+      public JSONBool( string aData )
+      {
+         Value = aData;
+      }
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( ( m_Data ) ? "true" : "false" );
+      }
+      public override bool Equals( object obj )
+      {
+         if( obj == null )
+            return false;
+         if( obj is bool )
+            return m_Data == (bool)obj;
+         return false;
+      }
+      public override int GetHashCode()
+      {
+         return m_Data.GetHashCode();
+      }
+   }
+   // End of JSONBool
+
+   public partial class JSONNull : JSONNode
+   {
+      static JSONNull m_StaticInstance = new JSONNull();
+      public static bool reuseSameInstance = true;
+      public static JSONNull CreateOrGet()
+      {
+         if( reuseSameInstance )
+            return m_StaticInstance;
+         return new JSONNull();
+      }
+      private JSONNull() { }
+
+      public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } }
+      public override bool IsNull { get { return true; } }
+      public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+      public override string Value
+      {
+         get { return "null"; }
+         set { }
+      }
+      public override bool AsBool
+      {
+         get { return false; }
+         set { }
+      }
+
+      public override bool Equals( object obj )
+      {
+         if( object.ReferenceEquals( this, obj ) )
+            return true;
+         return ( obj is JSONNull );
+      }
+      public override int GetHashCode()
+      {
+         return 0;
+      }
+
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( "null" );
+      }
+   }
+   // End of JSONNull
+
+   internal partial class JSONLazyCreator : JSONNode
+   {
+      private JSONNode m_Node = null;
+      private string m_Key = null;
+      public override JSONNodeType Tag { get { return JSONNodeType.None; } }
+      public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+      public JSONLazyCreator( JSONNode aNode )
+      {
+         m_Node = aNode;
+         m_Key = null;
+      }
+
+      public JSONLazyCreator( JSONNode aNode, string aKey )
+      {
+         m_Node = aNode;
+         m_Key = aKey;
+      }
+
+      private void Set( JSONNode aVal )
+      {
+         if( m_Key == null )
+         {
+            m_Node.Add( aVal );
+         }
+         else
+         {
+            m_Node.Add( m_Key, aVal );
+         }
+         m_Node = null; // Be GC friendly.
+      }
+
+      public override JSONNode this[ int aIndex ]
+      {
+         get
+         {
+            return new JSONLazyCreator( this );
+         }
+         set
+         {
+            var tmp = new JSONArray();
+            tmp.Add( value );
+            Set( tmp );
+         }
+      }
+
+      public override JSONNode this[ string aKey ]
+      {
+         get
+         {
+            return new JSONLazyCreator( this, aKey );
+         }
+         set
+         {
+            var tmp = new JSONObject();
+            tmp.Add( aKey, value );
+            Set( tmp );
+         }
+      }
+
+      public override void Add( JSONNode aItem )
+      {
+         var tmp = new JSONArray();
+         tmp.Add( aItem );
+         Set( tmp );
+      }
+
+      public override void Add( string aKey, JSONNode aItem )
+      {
+         var tmp = new JSONObject();
+         tmp.Add( aKey, aItem );
+         Set( tmp );
+      }
+
+      public static bool operator ==( JSONLazyCreator a, object b )
+      {
+         if( b == null )
+            return true;
+         return System.Object.ReferenceEquals( a, b );
+      }
+
+      public static bool operator !=( JSONLazyCreator a, object b )
+      {
+         return !( a == b );
+      }
+
+      public override bool Equals( object obj )
+      {
+         if( obj == null )
+            return true;
+         return System.Object.ReferenceEquals( this, obj );
+      }
+
+      public override int GetHashCode()
+      {
+         return 0;
+      }
+
+      public override int AsInt
+      {
+         get
+         {
+            JSONNumber tmp = new JSONNumber( 0 );
+            Set( tmp );
+            return 0;
+         }
+         set
+         {
+            JSONNumber tmp = new JSONNumber( value );
+            Set( tmp );
+         }
+      }
+
+      public override float AsFloat
+      {
+         get
+         {
+            JSONNumber tmp = new JSONNumber( 0.0f );
+            Set( tmp );
+            return 0.0f;
+         }
+         set
+         {
+            JSONNumber tmp = new JSONNumber( value );
+            Set( tmp );
+         }
+      }
+
+      public override double AsDouble
+      {
+         get
+         {
+            JSONNumber tmp = new JSONNumber( 0.0 );
+            Set( tmp );
+            return 0.0;
+         }
+         set
+         {
+            JSONNumber tmp = new JSONNumber( value );
+            Set( tmp );
+         }
+      }
+
+      public override bool AsBool
+      {
+         get
+         {
+            JSONBool tmp = new JSONBool( false );
+            Set( tmp );
+            return false;
+         }
+         set
+         {
+            JSONBool tmp = new JSONBool( value );
+            Set( tmp );
+         }
+      }
+
+      public override JSONArray AsArray
+      {
+         get
+         {
+            JSONArray tmp = new JSONArray();
+            Set( tmp );
+            return tmp;
+         }
+      }
+
+      public override JSONObject AsObject
+      {
+         get
+         {
+            JSONObject tmp = new JSONObject();
+            Set( tmp );
+            return tmp;
+         }
+      }
+      internal override void WriteToStringBuilder( StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode )
+      {
+         aSB.Append( "null" );
+      }
+   }
+   // End of JSONLazyCreator
+
+   public static class JSON
+   {
+      public static JSONNode Parse( string aJSON )
+      {
+         return JSONNode.Parse( aJSON );
+      }
+   }
+}

+ 64 - 0
src/XUnity.AutoTranslator.Plugin.Core/PluginLoader.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   public static class PluginLoader
+   {
+      private static bool _loaded;
+      private static bool _bootstrapped;
+
+      public static void LoadWithConfig( IConfiguration config )
+      {
+         if( !_loaded )
+         {
+            _loaded = true;
+            Config.Current = config;
+
+            var obj = new GameObject( "Auto Translator" );
+            var instance = obj.AddComponent<AutoTranslationPlugin>();
+            GameObject.DontDestroyOnLoad( obj );
+            instance.Initialize();
+         }
+      }
+
+      public static void Load()
+      {
+         LoadWithConfig( new DefaultConfiguration() );
+      }
+
+      public static void LoadThroughBootstrapper()
+      {
+         if( !_bootstrapped )
+         {
+            _bootstrapped = true;
+            var bootstrapper = new GameObject( "Bootstrapper" ).AddComponent<Bootstrapper>();
+            bootstrapper.Destroyed += Bootstrapper_Destroyed;
+         }
+      }
+
+      private static void Bootstrapper_Destroyed()
+      {
+         Load();
+      }
+
+      class Bootstrapper : MonoBehaviour
+      {
+         public event Action Destroyed = delegate { };
+
+         void Start()
+         {
+            Destroy( gameObject );
+         }
+         void OnDestroy()
+         {
+            Destroyed?.Invoke();
+         }
+      }
+   }
+}

+ 106 - 0
src/XUnity.AutoTranslator.Plugin.Core/TranslationInfo.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   public class TranslationInfo
+   {
+      private static readonly string MultiLinePropertyName = "multiLine";
+      private static readonly string OverflowMethodPropertyName = "overflowMethod";
+      private static readonly string UILabelClassName = "UILabel";
+
+      private Action<object> _reset;
+
+      public TranslationInfo()
+      {
+      }
+
+      public string OriginalText { get; set; }
+
+      public string TranslatedText { get; set; }
+
+      public bool IsTranslated { get; set; }
+
+      public bool IsAwake { get; set; }
+
+      public bool IsCurrentlySettingText { get; set; }
+
+      public void ResizeUI( object graphic )
+      {
+         if( graphic is Text )
+         {
+            var ui = (Text)graphic;
+
+            // text is likely to be longer than there is space for, simply expand out anyway then
+            var width = ( (RectTransform)ui.transform ).rect.width;
+            var quarterScreenSize = Screen.width / 5;
+
+            // 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;
+            if( ui.verticalOverflow == VerticalWrapMode.Truncate && width < quarterScreenSize && !ui.resizeTextForBestFit )
+            {
+               // will prevent the text from going into multiple lines and from "dispearing" if there is not enough room on a single line
+               ui.horizontalOverflow = HorizontalWrapMode.Overflow;
+            }
+            else
+            {
+               ui.horizontalOverflow = HorizontalWrapMode.Wrap;
+            }
+
+            _reset = g =>
+            {
+               var gui = (Text)g;
+               gui.horizontalOverflow = originalHorizontalOverflow;
+            };
+         }
+         else
+         {
+            var type = graphic.GetType();
+
+            // special handling for NGUI to better handle textbox sizing
+            if( type.Name == UILabelClassName )
+            {
+               var originalMultiLine = type.GetProperty( MultiLinePropertyName )?.GetGetMethod()?.Invoke( graphic, null );
+               var originalOverflowMethod = type.GetProperty( OverflowMethodPropertyName )?.GetGetMethod()?.Invoke( graphic, null );
+
+               type.GetProperty( MultiLinePropertyName )?.GetSetMethod()?.Invoke( graphic, new object[] { true } );
+               type.GetProperty( OverflowMethodPropertyName )?.GetSetMethod()?.Invoke( graphic, new object[] { 0 } );
+
+               _reset = g =>
+               {
+                  var gtype = g.GetType();
+                  gtype.GetProperty( MultiLinePropertyName )?.GetSetMethod()?.Invoke( g, new object[] { originalMultiLine } );
+                  gtype.GetProperty( OverflowMethodPropertyName )?.GetSetMethod()?.Invoke( g, new object[] { originalOverflowMethod } );
+               };
+            }
+         }
+      }
+
+      public void UnresizeUI( object graphic )
+      {
+         _reset?.Invoke( graphic );
+         _reset = null;
+      }
+
+      public TranslationInfo Reset( string newText )
+      {
+         IsTranslated = false;
+         TranslatedText = null;
+         OriginalText = newText;
+
+         return this;
+      }
+
+      public void SetTranslatedText( string translatedText )
+      {
+         IsTranslated = true;
+         TranslatedText = translatedText;
+      }
+   }
+}

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

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using UnityEngine.UI;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   public class TranslationJob
+   {
+      public object UI { get; set; }
+
+      public string UntranslatedText { get; set; }
+
+      public string TranslatedText { get; set; }
+
+      public int Failures { get; set; }
+   }
+}

+ 48 - 0
src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Utilities
+{
+   public static class TextHelper
+   {
+      public static bool ContainsJapaneseSymbols( string text )
+      {
+         // Japenese regex: [\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]
+         foreach( var c in text )
+         {
+            if( ( c >= '\u3040' && c <= '\u30ff' ) || ( c >= '\uff00' && c <= '\uff9f' ) || ( c >= '\u4e00' && c <= '\u9faf' ) || ( c >= '\u3400' && c <= '\u4dbf' ) )
+            {
+               return true;
+            }
+         }
+         return false;
+      }
+
+      /// <summary>
+      /// Decodes a text from a single-line serializable format.
+      /// 
+      /// Shamelessly stolen from original translation plugin.
+      /// </summary>
+      public static string Decode( string text )
+      {
+         // Remove these in newer version
+         text = text.Replace( "0D", "\r" ).Replace( "\\r", "\r" );
+         text = text.Replace( "0A", "\n" ).Replace( "\\n", "\n" );
+         return text;
+      }
+
+      /// <summary>
+      /// Encodes a text into a single-line serializable format.
+      /// 
+      /// Shamelessly stolen from original translation plugin.
+      /// </summary>
+      public static string Encode( string text )
+      {
+         text = text.Replace( "\r", "\\r" );
+         text = text.Replace( "\n", "\\n" );
+         return text;
+      }
+   }
+}

+ 481 - 0
src/XUnity.AutoTranslator.Plugin.Core/Utilities/WeakDictionary.cs

@@ -0,0 +1,481 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Utilities
+{
+   // Adds strong typing to WeakReference.Target using generics. Also,
+   // the Create factory method is used in place of a constructor
+   // to handle the case where target is null, but we want the 
+   // reference to still appear to be alive.
+   internal class WeakReference<T> : WeakReference where T : class
+   {
+      public static WeakReference<T> Create( T target )
+      {
+         if( target == null )
+            return WeakNullReference<T>.Singleton;
+
+         return new WeakReference<T>( target );
+      }
+
+      protected WeakReference( T target )
+          : base( target, false ) { }
+
+      public new T Target
+      {
+         get { return (T)base.Target; }
+      }
+   }
+
+   // Provides a weak reference to a null target object, which, unlike
+   // other weak references, is always considered to be alive. This 
+   // facilitates handling null dictionary values, which are perfectly
+   // legal.
+   internal class WeakNullReference<T> : WeakReference<T> where T : class
+   {
+      public static readonly WeakNullReference<T> Singleton = new WeakNullReference<T>();
+
+      private WeakNullReference() : base( null ) { }
+
+      public override bool IsAlive
+      {
+         get { return true; }
+      }
+   }
+
+   // Provides a weak reference to an object of the given type to be used in
+   // a WeakDictionary along with the given comparer.
+   internal sealed class WeakKeyReference<T> : WeakReference<T> where T : class
+   {
+      public readonly int HashCode;
+
+      public WeakKeyReference( T key, WeakKeyComparer<T> comparer )
+          : base( key )
+      {
+         // retain the object's hash code immediately so that even
+         // if the target is GC'ed we will be able to find and
+         // remove the dead weak reference.
+         this.HashCode = comparer.GetHashCode( key );
+      }
+   }
+
+   // Compares objects of the given type or WeakKeyReferences to them
+   // for equality based on the given comparer. Note that we can only
+   // implement IEqualityComparer<T> for T = object as there is no 
+   // other common base between T and WeakKeyReference<T>. We need a
+   // single comparer to handle both types because we don't want to
+   // allocate a new weak reference for every lookup.
+   internal sealed class WeakKeyComparer<T> : IEqualityComparer<object>
+       where T : class
+   {
+
+      private IEqualityComparer<T> comparer;
+
+      internal WeakKeyComparer( IEqualityComparer<T> comparer )
+      {
+         if( comparer == null )
+            comparer = EqualityComparer<T>.Default;
+
+         this.comparer = comparer;
+      }
+
+      public int GetHashCode( object obj )
+      {
+         WeakKeyReference<T> weakKey = obj as WeakKeyReference<T>;
+         if( weakKey != null ) return weakKey.HashCode;
+         return this.comparer.GetHashCode( (T)obj );
+      }
+
+      // Note: There are actually 9 cases to handle here.
+      //
+      //  Let Wa = Alive Weak Reference
+      //  Let Wd = Dead Weak Reference
+      //  Let S  = Strong Reference
+      //  
+      //  x  | y  | Equals(x,y)
+      // -------------------------------------------------
+      //  Wa | Wa | comparer.Equals(x.Target, y.Target) 
+      //  Wa | Wd | false
+      //  Wa | S  | comparer.Equals(x.Target, y)
+      //  Wd | Wa | false
+      //  Wd | Wd | x == y
+      //  Wd | S  | false
+      //  S  | Wa | comparer.Equals(x, y.Target)
+      //  S  | Wd | false
+      //  S  | S  | comparer.Equals(x, y)
+      // -------------------------------------------------
+      public new bool Equals( object x, object y )
+      {
+         bool xIsDead, yIsDead;
+         T first = GetTarget( x, out xIsDead );
+         T second = GetTarget( y, out yIsDead );
+
+         if( xIsDead )
+            return yIsDead ? x == y : false;
+
+         if( yIsDead )
+            return false;
+
+         return this.comparer.Equals( first, second );
+      }
+
+      private static T GetTarget( object obj, out bool isDead )
+      {
+         WeakKeyReference<T> wref = obj as WeakKeyReference<T>;
+         T target;
+         if( wref != null )
+         {
+            target = wref.Target;
+            isDead = !wref.IsAlive;
+         }
+         else
+         {
+            target = (T)obj;
+            isDead = false;
+         }
+         return target;
+      }
+   }
+   /// <summary>
+   /// A generic dictionary, which allows both its keys and values 
+   /// to be garbage collected if there are no other references
+   /// to them than from the dictionary itself.
+   /// </summary>
+   /// 
+   /// <remarks>
+   /// If either the key or value of a particular entry in the dictionary
+   /// has been collected, then both the key and value become effectively
+   /// unreachable. However, left-over WeakReference objects for the key
+   /// and value will physically remain in the dictionary until
+   /// RemoveCollectedEntries is called. This will lead to a discrepancy
+   /// between the Count property and the number of iterations required
+   /// to visit all of the elements of the dictionary using its
+   /// enumerator or those of the Keys and Values collections. Similarly,
+   /// CopyTo will copy fewer than Count elements in this situation.
+   /// </remarks>
+   public sealed class WeakDictionary<TKey, TValue> : BaseDictionary<TKey, TValue>
+       where TKey : class
+       where TValue : class
+   {
+
+      private Dictionary<object, TValue> dictionary;
+      private WeakKeyComparer<TKey> comparer;
+
+      public WeakDictionary()
+          : this( 0, null ) { }
+
+      public WeakDictionary( int capacity )
+          : this( capacity, null ) { }
+
+      public WeakDictionary( IEqualityComparer<TKey> comparer )
+          : this( 0, comparer ) { }
+
+      public WeakDictionary( int capacity, IEqualityComparer<TKey> comparer )
+      {
+         this.comparer = new WeakKeyComparer<TKey>( comparer );
+         this.dictionary = new Dictionary<object, TValue>( capacity, this.comparer );
+      }
+
+      // WARNING: The count returned here may include entries for which
+      // either the key or value objects have already been garbage
+      // collected. Call RemoveCollectedEntries to weed out collected
+      // entries and update the count accordingly.
+      public override int Count
+      {
+         get { return this.dictionary.Count; }
+      }
+
+      public override void Add( TKey key, TValue value )
+      {
+
+
+         if( key == null ) throw new ArgumentNullException( "key" );
+         WeakReference<TKey> weakKey = new WeakKeyReference<TKey>( key, this.comparer );
+         this.dictionary.Add( weakKey, value );
+      }
+
+      public override bool ContainsKey( TKey key )
+      {
+         return this.dictionary.ContainsKey( key );
+      }
+
+      public override bool Remove( TKey key )
+      {
+         return this.dictionary.Remove( key );
+      }
+
+      public override bool TryGetValue( TKey key, out TValue value )
+      {
+         if( this.dictionary.TryGetValue( key, out value ) )
+         {
+            return true;
+         }
+         value = null;
+         return false;
+      }
+
+      protected override void SetValue( TKey key, TValue value )
+      {
+         WeakReference<TKey> weakKey = new WeakKeyReference<TKey>( key, this.comparer );
+         this.dictionary[ weakKey ] = value;
+      }
+
+      public override void Clear()
+      {
+         this.dictionary.Clear();
+      }
+
+      public override IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
+      {
+         foreach( KeyValuePair<object, TValue> kvp in this.dictionary )
+         {
+            WeakReference<TKey> weakKey = (WeakReference<TKey>)( kvp.Key );
+            TValue weakValue = kvp.Value;
+            TKey key = weakKey.Target;
+            if( weakKey.IsAlive )
+               yield return new KeyValuePair<TKey, TValue>( key, weakValue );
+         }
+      }
+
+      // Removes the left-over weak references for entries in the dictionary
+      // whose key or value has already been reclaimed by the garbage
+      // collector. This will reduce the dictionary's Count by the number
+      // of dead key-value pairs that were eliminated.
+      public void RemoveCollectedEntries()
+      {
+         List<object> toRemove = null;
+         foreach( KeyValuePair<object, TValue> pair in this.dictionary )
+         {
+            WeakReference<TKey> weakKey = (WeakReference<TKey>)( pair.Key );
+
+            if( !weakKey.IsAlive )
+            {
+               if( toRemove == null )
+                  toRemove = new List<object>();
+               toRemove.Add( weakKey );
+            }
+         }
+
+         if( toRemove != null )
+         {
+            foreach( object key in toRemove )
+               this.dictionary.Remove( key );
+         }
+      }
+   }
+
+   /// <summary>
+   /// Represents a dictionary mapping keys to values.
+   /// </summary>
+   /// 
+   /// <remarks>
+   /// Provides the plumbing for the portions of IDictionary<TKey,
+   /// TValue> which can reasonably be implemented without any
+   /// dependency on the underlying representation of the dictionary.
+   /// </remarks>
+   [DebuggerDisplay( "Count = {Count}" )]
+   [DebuggerTypeProxy( PREFIX + "DictionaryDebugView`2" + SUFFIX )]
+   public abstract class BaseDictionary<TKey, TValue> : IDictionary<TKey, TValue>
+   {
+      private const string PREFIX = "System.Collections.Generic.Mscorlib_";
+      private const string SUFFIX = ",mscorlib,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089";
+
+      private KeyCollection keys;
+      private ValueCollection values;
+
+      protected BaseDictionary() { }
+
+      public abstract int Count { get; }
+      public abstract void Clear();
+      public abstract void Add( TKey key, TValue value );
+      public abstract bool ContainsKey( TKey key );
+      public abstract bool Remove( TKey key );
+      public abstract bool TryGetValue( TKey key, out TValue value );
+      public abstract IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
+      protected abstract void SetValue( TKey key, TValue value );
+
+      public bool IsReadOnly
+      {
+         get { return false; }
+      }
+
+      public ICollection<TKey> Keys
+      {
+         get
+         {
+            if( this.keys == null )
+               this.keys = new KeyCollection( this );
+
+            return this.keys;
+         }
+      }
+
+      public ICollection<TValue> Values
+      {
+         get
+         {
+            if( this.values == null )
+               this.values = new ValueCollection( this );
+
+            return this.values;
+         }
+      }
+
+      public TValue this[ TKey key ]
+      {
+         get
+         {
+            TValue value;
+            if( !this.TryGetValue( key, out value ) )
+               throw new KeyNotFoundException();
+
+            return value;
+         }
+         set
+         {
+            SetValue( key, value );
+         }
+      }
+
+      public void Add( KeyValuePair<TKey, TValue> item )
+      {
+         this.Add( item.Key, item.Value );
+      }
+
+      public bool Contains( KeyValuePair<TKey, TValue> item )
+      {
+         TValue value;
+         if( !this.TryGetValue( item.Key, out value ) )
+            return false;
+
+         return EqualityComparer<TValue>.Default.Equals( value, item.Value );
+      }
+
+      public void CopyTo( KeyValuePair<TKey, TValue>[] array, int arrayIndex )
+      {
+         Copy( this, array, arrayIndex );
+      }
+
+      public bool Remove( KeyValuePair<TKey, TValue> item )
+      {
+         if( !this.Contains( item ) )
+            return false;
+
+
+         return this.Remove( item.Key );
+      }
+
+      System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+      {
+         return this.GetEnumerator();
+      }
+
+      private abstract class Collection<T> : ICollection<T>
+      {
+         protected readonly IDictionary<TKey, TValue> dictionary;
+
+         protected Collection( IDictionary<TKey, TValue> dictionary )
+         {
+            this.dictionary = dictionary;
+         }
+
+         public int Count
+         {
+            get { return this.dictionary.Count; }
+         }
+
+         public bool IsReadOnly
+         {
+            get { return true; }
+         }
+
+         public void CopyTo( T[] array, int arrayIndex )
+         {
+            Copy( this, array, arrayIndex );
+         }
+
+         public virtual bool Contains( T item )
+         {
+            foreach( T element in this )
+               if( EqualityComparer<T>.Default.Equals( element, item ) )
+                  return true;
+            return false;
+         }
+
+         public IEnumerator<T> GetEnumerator()
+         {
+            foreach( KeyValuePair<TKey, TValue> pair in this.dictionary )
+               yield return GetItem( pair );
+         }
+
+         protected abstract T GetItem( KeyValuePair<TKey, TValue> pair );
+
+         public bool Remove( T item )
+         {
+            throw new NotSupportedException( "Collection is read-only." );
+         }
+
+         public void Add( T item )
+         {
+            throw new NotSupportedException( "Collection is read-only." );
+         }
+
+         public void Clear()
+         {
+            throw new NotSupportedException( "Collection is read-only." );
+         }
+
+         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+         {
+            return this.GetEnumerator();
+         }
+      }
+
+      [DebuggerDisplay( "Count = {Count}" )]
+      [DebuggerTypeProxy( PREFIX + "DictionaryKeyCollectionDebugView`2" + SUFFIX )]
+      private class KeyCollection : Collection<TKey>
+      {
+         public KeyCollection( IDictionary<TKey, TValue> dictionary )
+             : base( dictionary ) { }
+
+         protected override TKey GetItem( KeyValuePair<TKey, TValue> pair )
+         {
+            return pair.Key;
+         }
+         public override bool Contains( TKey item )
+         {
+            return this.dictionary.ContainsKey( item );
+         }
+      }
+
+      [DebuggerDisplay( "Count = {Count}" )]
+      [DebuggerTypeProxy( PREFIX + "DictionaryValueCollectionDebugView`2" + SUFFIX )]
+      private class ValueCollection : Collection<TValue>
+      {
+         public ValueCollection( IDictionary<TKey, TValue> dictionary )
+             : base( dictionary ) { }
+
+         protected override TValue GetItem( KeyValuePair<TKey, TValue> pair )
+         {
+            return pair.Value;
+         }
+      }
+
+      private static void Copy<T>( ICollection<T> source, T[] array, int arrayIndex )
+      {
+         if( array == null )
+            throw new ArgumentNullException( "array" );
+
+         if( arrayIndex < 0 || arrayIndex > array.Length )
+            throw new ArgumentOutOfRangeException( "arrayIndex" );
+
+         if( ( array.Length - arrayIndex ) < source.Count )
+            throw new ArgumentException( "Destination array is not large enough. Check array.Length and arrayIndex." );
+
+         foreach( T item in source )
+            array[ arrayIndex++ ] = item;
+      }
+   }
+}

+ 208 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/AutoTranslateClient.cs

@@ -0,0 +1,208 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+using UnityEngine.Networking;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public static class AutoTranslateClient
+   {
+      //private static readonly object Sync = new object();
+      //private static readonly HashSet<WebClientReference> AvailableClients = new HashSet<WebClientReference>();
+      //private static readonly HashSet<WebClientReference> WorkingClients = new HashSet<WebClientReference>();
+
+      private static KnownEndpoint _endpoint;
+      private static int _runningTranslations = 0;
+
+      public static void Configure()
+      {
+         _endpoint = KnownEndpoints.FindEndpoint( Settings.ServiceEndpoint );
+
+         if( _endpoint != null )
+         {
+            _endpoint.ConfigureServicePointManager();
+         }
+      }
+
+      public static bool IsConfigured
+      {
+         get
+         {
+            return _endpoint != null;
+         }
+      }
+
+      //private static int CurrentClientCount => AvailableClients.Count + WorkingClients.Count;
+
+      public static bool HasAvailableClients => _runningTranslations < Settings.MaxConcurrentTranslations;
+
+      public static IEnumerator TranslateByWWW( string untranslated, string from, string to, Action<string> success, Action failure )
+      {
+         var url = _endpoint.GetServiceUrl( WWW.EscapeURL( untranslated ), from, to );
+         var headers = new Dictionary<string, string>();
+         _endpoint.ApplyHeaders( headers );
+         using( var www = new WWW( url, null, headers ) )
+         {
+            _runningTranslations++;
+            yield return www;
+            _runningTranslations--;
+
+            string error = null;
+            try
+            {
+               error = www.error;
+            }
+            catch( Exception e )
+            {
+               error = e.ToString();
+            }
+
+            if( error != null )
+            {
+               failure();
+            }
+            else
+            {
+               string translatedText = null;
+               var text = www.text;
+               if( text != null )
+               {
+                  try
+                  {
+                     translatedText = _endpoint.ExtractTranslated( text ) ?? string.Empty;
+                  }
+                  catch { }
+               }
+
+               success( translatedText );
+            }
+         }
+      }
+
+      //public static bool TranslateByWebClient( string untranslated, string from, string to, Action<string> success, Action failure )
+      //{
+      //   var url = _endpoint.GetServiceUrl( untranslated, from, to );
+      //   var reference = GetOrCreateAvailableClient();
+      //   if( reference == null )
+      //   {
+      //      return false;
+      //   }
+      //   var client = reference.Client;
+
+      //   _endpoint.ApplyHeaders( client.Headers );
+      //   client.Encoding = Encoding.UTF8;
+
+      //   Interlocked.Increment( ref _runningTranslations );
+      //   DownloadStringCompletedEventHandler callback = null;
+      //   callback = ( s, e ) =>
+      //   {
+      //      try
+      //      {
+      //         client.DownloadStringCompleted -= callback;
+
+      //         string translatedText = null;
+      //         bool failed = false;
+
+      //         if( e.Error == null )
+      //         {
+      //            if( e.Result != null )
+      //            {
+      //               try
+      //               {
+      //                  translatedText = _endpoint.ExtractTranslated( e.Result ) ?? string.Empty;
+      //               }
+      //               catch { }
+      //            }
+      //         }
+      //         else
+      //         {
+      //            failed = true;
+      //         }
+
+      //         if( failed )
+      //         {
+      //            failure();
+      //         }
+      //         else
+      //         {
+      //            success( translatedText );
+      //         }
+      //      }
+      //      finally
+      //      {
+      //         ReleaseClientAfterUse( reference );
+      //         Interlocked.Decrement( ref _runningTranslations );
+      //      }
+      //   };
+      //   client.DownloadStringCompleted += callback;
+
+      //   client.DownloadStringAsync( new Uri( url ) );
+
+      //   return true;
+      //}
+
+      //public static void ReleaseClientAfterUse( WebClientReference client )
+      //{
+      //   lock( Sync )
+      //   {
+      //      var removed = WorkingClients.Remove( client );
+      //      if( removed )
+      //      {
+      //         client.LastTimestamp = DateTime.UtcNow;
+      //         AvailableClients.Add( client );
+      //      }
+      //   }
+      //}
+
+      //public static WebClientReference GetOrCreateAvailableClient()
+      //{
+      //   lock( Sync )
+      //   {
+      //      if( AvailableClients.Count > 0 )
+      //      {
+      //         // take a already configured client...
+      //         var client = AvailableClients.First();
+      //         AvailableClients.Remove( client );
+      //         WorkingClients.Add( client );
+      //         return client;
+      //      }
+      //      else if( CurrentClientCount < Settings.MaxConcurrentTranslations )
+      //      {
+      //         var client = new WebClient();
+      //         var reference = new WebClientReference( client );
+      //         WorkingClients.Add( reference );
+      //         return reference;
+      //      }
+      //      else
+      //      {
+      //         return null;
+      //      }
+      //   }
+      //}
+
+      //public static void RemoveUnusedClients()
+      //{
+      //   lock( Sync )
+      //   {
+      //      var now = DateTime.UtcNow;
+      //      var references = AvailableClients.ToList();
+      //      foreach( var reference in references )
+      //      {
+      //         var livedFor = now - reference.LastTimestamp;
+      //         if( livedFor > Settings.WebClientLifetime )
+      //         {
+      //            AvailableClients.Remove( reference );
+      //            reference.Client.Dispose();
+      //         }
+      //      }
+      //   }
+      //}
+   }
+}

+ 48 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/DefaultEndpoint.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using UnityEngine;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public class DefaultEndpoint : KnownEndpoint
+   {
+      private static ServicePoint ServicePoint;
+      private static readonly string ServicePointTemplateUrl = "{0}?from={1}&to={2}&text={3}";
+
+      public DefaultEndpoint( string endpoint )
+         : base( endpoint )
+      {
+      }
+
+      public override void ApplyHeaders( Dictionary<string, string> headers )
+      {
+      }
+
+      public override void ApplyHeaders( WebHeaderCollection headers )
+      {
+      }
+
+      public override void ConfigureServicePointManager()
+      {
+         try
+         {
+            ServicePoint = ServicePointManager.FindServicePoint( new Uri( Identifier ) );
+            ServicePoint.ConnectionLimit = 100;
+         }
+         catch
+         {
+         }
+      }
+
+      public override string ExtractTranslated( string result )
+      {
+         return result;
+      }
+
+      public override string GetServiceUrl( string untranslatedText, string from, string to )
+      {
+         return string.Format( ServicePointTemplateUrl, Identifier, from, to, untranslatedText );
+      }
+   }
+}

+ 82 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateEndpoint.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using SimpleJSON;
+using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public class GoogleTranslateEndpoint : KnownEndpoint
+   {
+      //private static readonly string CertificateIssuer = "CN=Google Internet Authority G3, O=Google Trust Services, C=US";
+      private static ServicePoint ServicePoint;
+      private static readonly string HttpServicePointTemplateUrl = "http://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}";
+      private static readonly string HttpsServicePointTemplateUrl = "https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}";
+
+      public GoogleTranslateEndpoint()
+         : base( KnownEndpointNames.GoogleTranslate )
+      {
+
+      }
+
+      public override void ApplyHeaders( Dictionary<string, string> headers )
+      {
+         headers[ "User-Agent" ] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36";
+         headers[ "Accept" ] = "*/*";
+         headers[ "Accept-Charset" ] = "UTF-8";
+      }
+
+      public override void ApplyHeaders( WebHeaderCollection headers )
+      {
+         headers[ HttpRequestHeader.UserAgent ] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36";
+         headers[ HttpRequestHeader.Accept ] = "*/*";
+         headers[ HttpRequestHeader.AcceptCharset ] = "UTF-8";
+      }
+
+      public override void ConfigureServicePointManager()
+      {
+         try
+         {
+            //ServicePointManager.ServerCertificateValidationCallback += ( sender, certificate, chain, sslPolicyErrors ) =>
+            //{
+            //   return certificate.Issuer == CertificateIssuer;
+            //};
+
+            ServicePoint = ServicePointManager.FindServicePoint( new Uri( "http://translate.googleapis.com" ) );
+            ServicePoint.ConnectionLimit = 5;
+            
+         }
+         catch
+         {
+         }
+      }
+
+      public override string ExtractTranslated( string result )
+      {
+         var arr = JSON.Parse( result );
+         var lineBuilder = new StringBuilder( result.Length );
+
+         foreach( JSONNode entry in arr.AsArray[ 0 ].AsArray )
+         {
+            var token = entry.AsArray[ 0 ].ToString();
+            token = token.Substring( 1, token.Length - 2 ).UnescapeJson();
+
+            if( !lineBuilder.EndsWithWhitespaceOrNewline() ) lineBuilder.Append( " " );
+
+            lineBuilder.Append( token );
+         }
+
+         return lineBuilder.ToString();
+      }
+
+      public override string GetServiceUrl( string untranslatedText, string from, string to )
+      {
+         return string.Format( Settings.EnableSSL ? HttpsServicePointTemplateUrl : HttpServicePointTemplateUrl, from, to, untranslatedText );
+      }
+   }
+}

+ 28 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/KnownEndpoint.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public abstract class KnownEndpoint
+   {
+      public KnownEndpoint( string identifier )
+      {
+         Identifier = identifier;
+      }
+
+      public string Identifier { get; }
+
+      public abstract void ConfigureServicePointManager();
+
+      public abstract string GetServiceUrl( string untranslatedText, string from, string to );
+
+      public abstract void ApplyHeaders( Dictionary<string, string> headers );
+
+      public abstract void ApplyHeaders( WebHeaderCollection headers );
+
+      public abstract string ExtractTranslated( string result );
+   }
+}

+ 26 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/KnownEndpoints.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public static class KnownEndpoints
+   {
+      public static readonly KnownEndpoint GoogleTranslate = new GoogleTranslateEndpoint();
+
+      public static KnownEndpoint FindEndpoint( string identifier )
+      {
+         if( string.IsNullOrEmpty( identifier ) ) return null;
+
+         switch( identifier )
+         {
+            case KnownEndpointNames.GoogleTranslate:
+               return GoogleTranslate;
+            default:
+               return new DefaultEndpoint( identifier );
+         }
+      }
+   }
+}

+ 21 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/WebClientReference.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public class WebClientReference
+   {
+      public WebClientReference( WebClient client )
+      {
+         Client = client;
+         LastTimestamp = DateTime.UtcNow;
+      }
+
+      public WebClient Client { get; }
+
+      public DateTime LastTimestamp { get; set; }
+   }
+}

+ 25 - 0
src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net35</TargetFramework>
+    <AssemblyVersion>2.3.0.0</AssemblyVersion>
+    <FileVersion>2.3.0.0</FileVersion>
+    <Version>2.3.0</Version>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="0Harmony">
+      <HintPath>..\..\libs\0Harmony.dll</HintPath>
+    </Reference>
+    <Reference Include="ExIni">
+      <HintPath>..\..\libs\ExIni.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\..\libs\UnityEngine.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UI">
+      <HintPath>..\..\libs\UnityEngine.UI.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

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

@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using ExIni;
+using IllusionPlugin;
+using XUnity.AutoTranslator.Plugin.Core;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.IPA
+{
+   public class AutoTranslatorPlugin : IPlugin, IConfiguration
+   {
+      private IniFile _file;
+      private string _configPath;
+      private string _dataFolder;
+
+      public AutoTranslatorPlugin()
+      {
+         _dataFolder = "Plugins";
+         _configPath = Path.Combine( _dataFolder, "AutoTranslatorConfig.ini" );
+      }
+
+      public IniFile Preferences
+      {
+         get
+         {
+            return ( _file ?? ( _file = ReloadConfig() ) ); ;
+         }
+      }
+
+      public string DataPath
+      {
+         get
+         {
+            return _dataFolder;
+         }
+      }
+
+      public IniFile ReloadConfig()
+      {
+         if( !File.Exists( _configPath ) )
+         {
+            return ( _file ?? new IniFile() );
+         }
+         IniFile ini = IniFile.FromFile( _configPath );
+         if( _file == null )
+         {
+            return ( _file = ini );
+         }
+         _file.Merge( ini );
+         return _file;
+      }
+
+      public void SaveConfig()
+      {
+         _file.Save( _configPath );
+      }
+
+      public string Name => PluginInfo.Name;
+
+      public string Version => PluginInfo.Version;
+
+      public void OnApplicationQuit()
+      {
+      }
+
+      public void OnApplicationStart()
+      {
+         PluginLoader.LoadWithConfig( this );
+      }
+
+      public void OnFixedUpdate()
+      {
+      }
+
+      public void OnLevelWasInitialized( int level )
+      {
+      }
+
+      public void OnLevelWasLoaded( int level )
+      {
+      }
+
+      public void OnUpdate()
+      {
+      }
+   }
+}

+ 36 - 0
src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj

@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+   <PropertyGroup>
+      <TargetFramework>net35</TargetFramework>
+      <AssemblyVersion>2.3.0.0</AssemblyVersion>
+      <FileVersion>2.3.0.0</FileVersion>
+      <Version>2.3.0</Version>
+   </PropertyGroup>
+
+   <ItemGroup>
+      <ProjectReference Include="..\XUnity.AutoTranslator.Plugin.Core\XUnity.AutoTranslator.Plugin.Core.csproj" />
+   </ItemGroup>
+
+   <ItemGroup>
+      <Reference Include="ExIni">
+         <HintPath>..\..\libs\ExIni.dll</HintPath>
+      </Reference>
+      <Reference Include="IllusionPlugin">
+         <HintPath>..\..\libs\IllusionPlugin.dll</HintPath>
+      </Reference>
+      <Reference Include="UnityEngine">
+         <HintPath>..\..\libs\UnityEngine.dll</HintPath>
+      </Reference>
+   </ItemGroup>
+
+   <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+      <GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
+         <Output TaskParameter="Assemblies" ItemName="Targets" />
+      </GetAssemblyIdentity>
+      <ItemGroup>
+         <VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace(&quot;%(Targets.Version)&quot;, &quot;^(.+?)(\.0+)$&quot;, &quot;$1&quot;))" />
+      </ItemGroup>
+      <Exec Command="if $(ConfigurationName) == Release (&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)ExIni.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)0Harmony.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)XUnity.AutoTranslator.Plugin.Core.dll&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   XCOPY /Y /I &quot;$(TargetDir)$(TargetName)$(TargetExt)&quot; &quot;$(SolutionDir)dist\IPA\Plugins\&quot;&#xD;&#xA;   powershell Compress-Archive -Path '$(SolutionDir)dist\IPA\Plugins' -DestinationPath '$(SolutionDir)dist\IPA\XUnity.AutoTranslator-IPA-@(VersionNumber).zip' -Force)&#xD;&#xA;)" />
+   </Target>
+
+</Project>

+ 21 - 0
src/XUnity.AutoTranslator.Setup/GameLauncher.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Setup
+{
+   public class GameLauncher
+   {
+      public GameLauncher( FileInfo executable, DirectoryInfo data )
+      {
+         Executable = executable;
+         Data = data;
+      }
+
+      public FileInfo Executable { get; private set; }
+
+      public DirectoryInfo Data { get; private set; }
+   }
+}

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

@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using IWshRuntimeLibrary;
+
+namespace XUnity.AutoTranslator.Setup
+{
+   class Program
+   {
+      static void Main( string[] args )
+      {
+         var gamePath = Environment.CurrentDirectory;
+
+
+         List<GameLauncher> launchers = new List<GameLauncher>();
+
+         // find all .exe files
+         var executables = Directory.GetFiles( gamePath, "*.exe", SearchOption.TopDirectoryOnly );
+         foreach( var executable in executables )
+         {
+            var dataFolder = new DirectoryInfo( Path.Combine( gamePath, Path.GetFileNameWithoutExtension( executable ) + "_Data" ) );
+            if( dataFolder.Exists )
+            {
+               launchers.Add( new GameLauncher( new FileInfo( executable ), dataFolder ) );
+            }
+         }
+
+         var reiPath = Path.Combine( gamePath, "ReiPatcher" );
+         var reiInfo = new DirectoryInfo( reiPath );
+         if( !reiInfo.Exists )
+         {
+            Console.WriteLine( "ReiPatcher directory missing!" );
+            Console.WriteLine( "Press any key to exit..." );
+            return;
+         }
+
+         foreach( var launcher in launchers )
+         {
+            var setupPath = Path.Combine( gamePath, "AutoTranslatorSetupFiles" );
+            var setupInfo = new DirectoryInfo( setupPath );
+            if( setupInfo.Exists )
+            {
+               var setupFiles = setupInfo.GetFiles( "*.dll", SearchOption.TopDirectoryOnly ).Concat( setupInfo.GetFiles( "*.exe", SearchOption.TopDirectoryOnly ) );
+               foreach( var setupFile in setupFiles )
+               {
+                  var copyToPath = Path.Combine( gamePath, launcher.Data.Name, "Managed", setupFile.Name );
+                  var copyToFile = new FileInfo( copyToPath );
+                  setupFile.CopyTo( copyToPath, true );
+                  Console.WriteLine( "Copied " + setupFile.Name + " to " + launcher.Data.FullName );
+               }
+            }
+            else
+            {
+               Console.WriteLine( "AutoTranslatorSetupFiles directory missing. Skipping copying files to managed directory..." );
+            }
+
+            // create an .ini file for each launcher, if it does not already exist
+            var iniInfo = new FileInfo( Path.Combine( reiPath, Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".ini" ) );
+            if( !iniInfo.Exists )
+            {
+               using( var file = new FileStream( iniInfo.FullName, FileMode.CreateNew ) )
+               using( var writer = new StreamWriter( file ) )
+               {
+                  writer.WriteLine( ";" + launcher.Executable.Name + " - ReiPatcher Configuration File" );
+                  writer.WriteLine( ";" );
+                  writer.WriteLine( "[ReiPatcher]" );
+                  writer.WriteLine( ";Directory to search for Patches" );
+                  writer.WriteLine( "PatchesDir=Patches" );
+                  writer.WriteLine( ";Directory to Look for Assemblies to Patch" );
+                  writer.WriteLine( @"AssembliesDir=..\" + launcher.Data.Name + @"\Managed" );
+                  writer.WriteLine( "" );
+                  writer.WriteLine( "[Launch]" );
+                  writer.WriteLine( @"Executable=..\" + launcher.Executable.Name );
+                  writer.WriteLine( "Arguments=" );
+                  writer.WriteLine( @"Directory=..\" );
+               }
+
+               Console.WriteLine( "Created " + iniInfo.Name );
+
+            }
+            else
+            {
+               Console.WriteLine( iniInfo.Name + " already exists. skipping..." );
+            }
+
+            var lnkInfo = new FileInfo( Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".lnk" );
+            if( !lnkInfo.Exists )
+            {
+               // create shortcuts
+               CreateShortcut(
+                  Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".lnk",
+                  gamePath,
+                  Path.Combine( reiPath, "ReiPatcher.exe" ) );
+
+               Console.WriteLine( "Created shortcut for " + launcher.Executable.Name );
+            }
+            else
+            {
+               Console.WriteLine( lnkInfo.Name + " already exists. skipping..." );
+            }
+         }
+
+         Console.WriteLine( "Setup completed. Press any key to exit." );
+         Console.ReadKey();
+      }
+
+      public static void CreateShortcut( string shortcutName, string shortcutPath, string targetFileLocation )
+      {
+         string shortcutLocation = Path.Combine( shortcutPath, shortcutName );
+         WshShell shell = new WshShell();
+         IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut( shortcutLocation );
+
+         shortcut.WorkingDirectory = Path.GetDirectoryName( targetFileLocation );
+         shortcut.TargetPath = targetFileLocation;
+         shortcut.Arguments = "-c " + Path.GetFileNameWithoutExtension( shortcutName ) + ".ini";
+         shortcut.Save();
+      }
+   }
+}

+ 25 - 0
src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net40</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <COMReference Include="IWshRuntimeLibrary.dll">
+      <Guid>f935dc20-1cf0-11d0-adb9-00c04fd58a0b</Guid>
+      <VersionMajor>1</VersionMajor>
+      <VersionMinor>0</VersionMinor>
+      <WrapperTool>tlbimp</WrapperTool>
+      <Lcid>0</Lcid>
+      <Isolated>false</Isolated>
+      <Private>false</Private>
+      <EmbedInteropTypes>true</EmbedInteropTypes>
+    </COMReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.CSharp" />
+  </ItemGroup>
+
+</Project>