瀏覽代碼

improved API, language checks, improved UI

randoman 6 年之前
父節點
當前提交
262a37f53b
共有 26 個文件被更改,包括 201 次插入133 次删除
  1. 29 12
      README.md
  2. 2 0
      src/Translators/BaiduTranslate/BaiduTranslateEndpoint.cs
  3. 2 1
      src/Translators/BingTranslate/BingTranslateEndpoint.cs
  4. 2 1
      src/Translators/BingTranslateLegitimate/BingTranslateLegitimateEndpoint.cs
  5. 2 1
      src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs
  6. 2 1
      src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs
  7. 3 2
      src/Translators/WatsonTranslate/WatsonTranslateEndpoint.cs
  8. 3 2
      src/Translators/YandexTranslate/YandexTranslateEndpoint.cs
  9. 21 16
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  10. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  11. 20 2
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ConfiguredEndpoint.cs
  12. 0 1
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ExtProtocol/ExtProtocolEndpoint.cs
  13. 10 28
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpEndpoint.cs
  14. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpTranslationContext.cs
  15. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationContext.cs
  16. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ITranslationContext.cs
  17. 49 1
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs
  18. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationContext.cs
  19. 30 55
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwEndpoint.cs
  20. 5 0
      src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwTranslationContext.cs
  21. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/UI/GUIUtil.cs
  22. 1 4
      src/XUnity.AutoTranslator.Plugin.Core/UI/XuaWindow.cs
  23. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Web/HttpSecurity.cs
  24. 7 4
      src/XUnity.AutoTranslator.Setup/Program.cs
  25. 2 0
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj
  26. 二進制
      src/XUnity.AutoTranslator.Setup/icon.ico

+ 29 - 12
README.md

@@ -24,12 +24,16 @@ Oh, and it is also capable of doing some basic image loading/dumping. Go to the
 
 If you intend on redistributing this plugin as part of a translation suite for a game, please read [this section](#regarding-redistribution).
 
+From version 3.0.0 it is possible to implement custom translators. See [this section](#implementing-a-translator) for more info.
+
 ## Translators
 The supported online translators are:
  * [GoogleTranslate](https://anonym.to/?https://translate.google.com/), based on the online Google translation service. Does not require authentication.
    * No limitations, but unstable.
  * [GoogleTranslateLegitimate](https://anonym.to/?https://cloud.google.com/translate/), based on the Google cloud translation API. Requires an API key.
    * Provides trial period of 1 year with $300 credits. Enough for 15 million characters translations.
+ * [BingTranslate](https://anonym.to/?https://www.bing.com/translator), based on the online Bing translation service. Does not require authentication.
+   * No limitations, but unstable.
  * [BingTranslateLegitimate](https://anonym.to/?https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview), based on the Azure text translation. Requires an API key.
    * Free up to 2 million characters per month.
  * [BaiduTranslate](https://anonym.to/?https://fanyi.baidu.com/), based on Baidu translation service. Requires AppId and AppSecret.
@@ -38,16 +42,15 @@ The supported online translators are:
    * Free up to 1 million characters per day, but max 10 million characters per month.
  * [WatsonTranslate](https://anonym.to/?https://cloud.ibm.com/apidocs/language-translator), based on IBM's Watson. Requires a URL, username and password.
    * Free up to 1 million characters per month.
- * [ExciteTranslate](https://anonym.to/?https://www.excite.co.jp/world/english_japanese/), based on the Excite online translation service. Does not require authentication.
-   * No limitations, but unstable. Also very slow.
+ * LecPowerTranslator15, based on LEC's Power Translator. Does not require authentication, but does require the software installed.
+   * No limitations.
  * CustomTranslate. Alternatively you can also specify any custom HTTP url that can be used as a translation endpoint (GET request). This must use the query parameters "from", "to" and "text" and return only a string with the result (try HTTP without SSL first, as unity-mono often has issues with SSL).
    * Example Configuration:
-     * Endpoint=Custom
+     * Endpoint=CustomTranslate
      * [Custom]
      * Url=http://my-custom-translation-service.net/translate
    * Example Request: GET http://my-custom-translation-service.net/translate?from=ja&to=en&text=こんにちは
    * Example Response (only body): Hello
-   * If someone is willing to implement it, this could provide support for offline translators such as ATLAS or LEC as well, since it enables the scenario where you simply run a local web server that can serve translation requests.
  
 **Do note that if you use any of the online translators that does not require some form of authentication, that this plugin may break at any time.**
 
@@ -100,7 +103,7 @@ The default configuration file, looks as such (2.6.0+):
 
 ```ini
 [Service]
-Endpoint=GoogleTranslate         ;Endpoint to use. Can be ["GoogleTranslate", "GoogleTranslateLegitimate", "BingTranslateLegitimate", "BaiduTranslate", "YandexTranslate", "WatsonTranslate", "ExciteTranslate", "*any custom http endpoint*", ""]. If empty, it simply means: Only use cached translations
+Endpoint=GoogleTranslate         ;Endpoint to use. Can be ["GoogleTranslate", "GoogleTranslateLegitimate", "BingTranslate", "BingTranslateLegitimate", "BaiduTranslate", "YandexTranslate", "WatsonTranslate", "LecPowerTranslator15", "CustomTranslate", ""]. If empty, it simply means: Only use cached translations. Additional translators can also be used if downloaded externally.
 
 [General]
 Language=en                      ;The language to translate into
@@ -137,6 +140,7 @@ ResizeUILineSpacingScale=        ;A decimal value that the default line spacing
 ForceUIResizing=True             ;Indicates whether the UI resize behavior should be applied to all UI components regardless of them being translated.
 IgnoreTextStartingWith=\u180e;   ;Indicates that the plugin should ignore any strings starting with certain characters. This is a list seperated by ';'.
 TextGetterCompatibilityMode=False ;Indicates whether or not to enable "Text Getter Compatibility Mode". Should only be enabled if required by the game. 
+GameLogTextPaths=                ;Indicates specific paths for game objects that the game uses as "log components", where it continuously appends or prepends text to. Requires expert knowledge to setup. This is a list seperated by ';'.
 
 [Texture]
 TextureDirectory=Translation\Texture ;Directory to dump textures to, and root of directories to load images from
@@ -169,6 +173,12 @@ WatsonAPIUrl=                    ;OPTIONAL, needed if WatsonTranslate is configu
 WatsonAPIUsername=               ;OPTIONAL, needed if WatsonTranslate is configured
 WatsonAPIPassword=               ;OPTIONAL, needed if WatsonTranslate is configured
 
+[Custom]
+Url=                             ;Optional, needed if CustomTranslated is configured
+
+[LecPowerTranslator15]
+InstallationPath=                ;Optional, needed if LecPowerTranslator15 is configured
+
 [Debug]
 EnablePrintHierarchy=False       ;Used for debugging
 EnableConsole=False              ;Enables the console. Do not enable if other plugins (managers) handles this
@@ -227,7 +237,7 @@ The following aims at reducing the number of requests send to the translation en
 
 ## Key Mapping
 The following key inputs are mapped:
- * ALT + 0: Enable XUnity AutoTranslator UI. (That's a zero, not an O)
+ * ALT + 0: Toggle XUnity AutoTranslator UI. (That's a zero, not an O)
  * 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 and texture files on the fly. Not guaranteed to work for all textures.
@@ -249,6 +259,7 @@ The file structure should like like this:
 {GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.dll
 {GameDirectory}/BepInEx/XUnity.AutoTranslator.Plugin.Core.BepInEx.dll
 {GameDirectory}/BepInEx/ExIni.dll
+{GameDirectory}/BepInEx/Translators/{Translator}.dll
 {GameDirectory}/BepInEx/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
 ```
 
@@ -264,6 +275,7 @@ The file structure should like like this
 {GameDirectory}/Plugins/XUnity.AutoTranslator.Plugin.Core.IPA.dll
 {GameDirectory}/Plugins/0Harmony.dll
 {GameDirectory}/Plugins/ExIni.dll
+{GameDirectory}/Plugins/Translators/{Translator}.dll
 {GameDirectory}/Plugins/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
 
@@ -279,6 +291,7 @@ The file structure should like like this
 {GameDirectory}/UnityInjector/XUnity.AutoTranslator.Plugin.Core.UnityInjector.dll
 {GameDirectory}/UnityInjector/0Harmony.dll
 {GameDirectory}/UnityInjector/ExIni.dll
+{GameDirectory}/UnityInjector/Translators/{Translator}.dll
 {GameDirectory}/UnityInjector/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
  
@@ -288,7 +301,7 @@ REQUIRES: Nothing, ReiPatcher is provided by this download.
  1. Download XUnity.AutoTranslator-ReiPatcher-{VERSION}.zip from [releases](../../releases).
  2. Extract directly into the game directory, such that "SetupReiPatcherAndAutoTranslator.exe" is placed alongside other exe files.
  3. Execute "SetupReiPatcherAndAutoTranslator.exe". This will setup up ReiPatcher correctly.
- 4. Execute the shortcut {GameExeName}.lnk that was created besides existing executables. This will patch and launch the game.
+ 4. Execute the shortcut {GameExeName} (Patch and Run).lnk that was created besides existing executables. This will patch and launch the game.
  5. From now on you can launch the game from the {GameExeName}.exe instead.
 
 The file structure should like like this
@@ -305,7 +318,8 @@ The file structure should like like this
 {GameDirectory}/{GameExeName}_Data/Managed/XUnity.AutoTranslator.Plugin.Core.dll
 {GameDirectory}/{GameExeName}_Data/Managed/0Harmony.dll
 {GameDirectory}/{GameExeName}_Data/Managed/ExIni.dll
-{GameDirectory}/AutoTranslator/AnyTranslationFile.txt (these files will be auto generated by plugin!)
+{GameDirectory}/AutoTranslator/Translators/{Translator}.dll
+{GameDirectory}/AutoTranslator/Translation/AnyTranslationFile.txt (these files will be auto generated by plugin!)
  ```
 
 ## Translating Mods
@@ -501,9 +515,9 @@ I recommend using this class, or in case that cannot be used, falling back to th
 
 ### How-To
 Follow these steps:
- * Start a new project in Visual Studio 2017 or later. (Be a good boy and choose the "Class Library (.NET Standard)" template. After choosing that, edit the generated .csproj file and change the TargetFramework element to 'net35')
+ * Start a new project in Visual Studio 2017 or later. I recommend using the same name for your assembly/project as the "Id" you are going to use in your interface implementation.
  * Add a reference to the XUnity.AutoTranslator.Plugin.Core.dll
- * Add a reference to UnityEngine.dll (Consider using an old version of this assembly (if `Translators` exists in the Managed folder, it is not an old version!))
+ * Add a reference to UnityEngine.dll (Consider using an old version of this assembly (if `UnityEngine.CoreModule.dll` exists in the Managed folder, it is not an old version!))
  * Create a new class that either:
    * Implements the `ITranslateEndpoint` interface
    * Inherits from the `HttpEndpoint` class
@@ -545,6 +559,7 @@ Let's take a look at a more advanced example that accesses the web:
 ```C#
 internal class YandexTranslateEndpoint : HttpEndpoint
 {
+   private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "az", "sq", "am", "en", "ar", "hy", "af", "eu", "ba", "be", "bn", "my", "bg", "bs", "cy", "hu", "vi", "ht", "gl", "nl", "mrj", "el", "ka", "gu", "da", "he", "yi", "id", "ga", "it", "is", "es", "kk", "kn", "ca", "ky", "zh", "ko", "xh", "km", "lo", "la", "lv", "lt", "lb", "mg", "ms", "ml", "mt", "mk", "mi", "mr", "mhr", "mn", "de", "ne", "no", "pa", "pap", "fa", "pl", "pt", "ro", "ru", "ceb", "sr", "si", "sk", "sl", "sw", "su", "tg", "th", "tl", "ta", "tt", "te", "tr", "udm", "uz", "uk", "ur", "fi", "fr", "hi", "hr", "cs", "sv", "gd", "et", "eo", "jv", "ja" };
    private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain";
 
    private string _key;
@@ -560,8 +575,8 @@ internal class YandexTranslateEndpoint : HttpEndpoint
 
       // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
       if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
-      if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
-      if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
+      if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+      if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
    }
 
    public override void OnCreateRequest( IHttpRequestCreationContext context )
@@ -619,3 +634,5 @@ After implementing the class, simply build the project and place the generated D
 As mentioned earlier, you  can also use the abstract class `WwwEndpoint` to implement roughly the same thing. However, I do not recommend doing so, unless it is an authenticated service.
 
 Another way to implement a translator is to implement the `ExtProtocolEndpoint` class. This can be used to delegate the actual translation logic to an external process. Currently there is no documentation on this, but you can take a look at the LEC implementation, which uses it.
+
+If instead, you use the interface, it is also possible to extend from MonoBehaviour to get access to all the normal lifecycle callbacks of Unity components.

+ 2 - 0
src/Translators/BaiduTranslate/BaiduTranslateEndpoint.cs

@@ -37,6 +37,8 @@ namespace BaiduTranslate
          if( string.IsNullOrEmpty( _appSecret ) ) throw new ArgumentException( "The BaiduTranslate endpoint requires an App Secret which has not been provided." );
 
          context.EnableSslFor( "api.fanyi.baidu.com" );
+
+         // frankly, I have no idea what languages this does, or does not support...
       }
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )

+ 2 - 1
src/Translators/BingTranslate/BingTranslateEndpoint.cs

@@ -68,7 +68,8 @@ namespace BingTranslate
          // Configure service points / service point manager
          context.EnableSslFor( "www.bing.com" );
 
-         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language {context.DestinationLanguage} is not supported." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
       }
 
       public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )

+ 2 - 1
src/Translators/BingTranslateLegitimate/BingTranslateLegitimateEndpoint.cs

@@ -49,7 +49,8 @@ namespace BingTranslateLegitimate
          // Configure service points / service point manager
          context.EnableSslFor( "api.cognitive.microsofttranslator.com" );
 
-         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language {context.DestinationLanguage} is not supported." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
       }
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )

+ 2 - 1
src/Translators/GoogleTranslate/GoogleTranslateEndpoint.cs

@@ -60,7 +60,8 @@ namespace GoogleTranslate
       {
          context.EnableSslFor( "translate.google.com", "translate.googleapis.com" );
 
-         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language {context.DestinationLanguage} is not supported." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
       }
 
       public override IEnumerator OnBeforeTranslate( IHttpTranslationContext context )

+ 2 - 1
src/Translators/GoogleTranslateLegitimate/GoogleTranslateLegitimateEndpoint.cs

@@ -41,7 +41,8 @@ namespace GoogleTranslateLegitimate
          // Configure service points / service point manager
          context.EnableSslFor( "translation.googleapis.com" );
 
-         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language {context.DestinationLanguage} is not supported." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
       }
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )

+ 3 - 2
src/Translators/WatsonTranslate/WatsonTranslateEndpoint.cs

@@ -18,6 +18,7 @@ namespace WatsonTranslate
 {
    internal class WatsonTranslateEndpoint : WwwEndpoint
    {
+      private static readonly HashSet<string> SupportedLanguagePairs = new HashSet<string> { "ar-en", "ca-es", "zh-en", "zh-TW-en", "cs-en", "da-en", "nl-en", "en-ar", "en-cs", "en-da", "en-de", "en-es", "en-fi", "en-fr", "en-hi", "en-it", "en-ja", "en-ko", "en-nb", "en-nl", "en-pl", "en-pt", "en-ru", "en-sv", "en-tr", "en-zh", "en-zh-TW", "fi-en", "fr-de", "fr-en", "fr-es", "de-en", "de-fr", "de-it", "hi-en", "hu-en", "it-de", "it-en", "ja-en", "ko-en", "nb-en", "pl-en", "pt-en", "ru-en", "es-ca", "es-en", "es-fr", "sv-en", "tr-en" };
       private static readonly string RequestTemplate = "{{\"text\":[\"{2}\"],\"model_id\":\"{0}-{1}\"}}";
 
       private string _fullUrl;
@@ -41,8 +42,8 @@ namespace WatsonTranslate
 
          _fullUrl = _url.TrimEnd( '/' ) + "/v3/translate?version=2018-05-01";
 
-         if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
-         if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
+         var model = context.SourceLanguage + "-" + context.DestinationLanguage;
+         if( !SupportedLanguagePairs.Contains( model ) ) throw new Exception( $"The language model '{model}' is not supported." );
       }
 
       public override void OnCreateRequest( IWwwRequestCreationContext context )

+ 3 - 2
src/Translators/YandexTranslate/YandexTranslateEndpoint.cs

@@ -18,6 +18,7 @@ namespace YandexTranslate
 {
    internal class YandexTranslateEndpoint : HttpEndpoint
    {
+      private static readonly HashSet<string> SupportedLanguages = new HashSet<string> { "az", "sq", "am", "en", "ar", "hy", "af", "eu", "ba", "be", "bn", "my", "bg", "bs", "cy", "hu", "vi", "ht", "gl", "nl", "mrj", "el", "ka", "gu", "da", "he", "yi", "id", "ga", "it", "is", "es", "kk", "kn", "ca", "ky", "zh", "ko", "xh", "km", "lo", "la", "lv", "lt", "lb", "mg", "ms", "ml", "mt", "mk", "mi", "mr", "mhr", "mn", "de", "ne", "no", "pa", "pap", "fa", "pl", "pt", "ro", "ru", "ceb", "sr", "si", "sk", "sl", "sw", "su", "tg", "th", "tl", "ta", "tt", "te", "tr", "udm", "uz", "uk", "ur", "fi", "fr", "hi", "hr", "cs", "sv", "gd", "et", "eo", "jv", "ja" };
       private static readonly string HttpsServicePointTemplateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate?key={3}&text={2}&lang={0}-{1}&format=plain";
 
       private string _key;
@@ -33,8 +34,8 @@ namespace YandexTranslate
 
          // if the plugin cannot be enabled, simply throw so the user cannot select the plugin
          if( string.IsNullOrEmpty( _key ) ) throw new Exception( "The YandexTranslate endpoint requires an API key which has not been provided." );
-         if( context.SourceLanguage != "ja" ) throw new Exception( "Current implementation only supports japanese-to-english." );
-         if( context.DestinationLanguage != "en" ) throw new Exception( "Current implementation only supports japanese-to-english." );
+         if( !SupportedLanguages.Contains( context.SourceLanguage ) ) throw new Exception( $"The source language '{context.SourceLanguage}' is not supported." );
+         if( !SupportedLanguages.Contains( context.DestinationLanguage ) ) throw new Exception( $"The destination language '{context.DestinationLanguage}' is not supported." );
       }
 
       public override void OnCreateRequest( IHttpRequestCreationContext context )

+ 21 - 16
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -98,8 +98,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
       private Dictionary<string, byte[]> _translatedImages = new Dictionary<string, byte[]>( StringComparer.InvariantCultureIgnoreCase );
       private HashSet<string> _untranslatedImages = new HashSet<string>();
 
-      private readonly List<string> _kickedOff = new List<string>();
-
       private Component _advEngine;
       private float? _nextAdvUpdate;
 
@@ -2076,7 +2074,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                {
                   var key = kvp.Key;
                   var job = kvp.Value;
-                  _kickedOff.Add( key );
+                  _unstartedJobs.Remove( key );
 
                   if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
 
@@ -2090,20 +2088,29 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                   var untranslatedText = batch.GetFullTranslationKey();
                   XuaLogger.Current.Debug( "Starting translation for: " + untranslatedText );
-                  StartCoroutine( _endpoint.Translate( untranslatedText, Settings.FromLanguage, Settings.Language, translatedText => OnBatchTranslationCompleted( batch, translatedText ),
-                  ( msg, e ) => OnTranslationFailed( batch, msg, e ) ) );
+                  StartCoroutine(
+                     _endpoint.Translate(
+                        untranslatedText,
+                        Settings.FromLanguage,
+                        Settings.Language,
+                        translatedText => OnBatchTranslationCompleted( batch, translatedText ),
+                        ( msg, e ) => OnTranslationFailed( batch, msg, e ) ) );
                }
             }
          }
          else
          {
-            foreach( var kvp in _unstartedJobs )
+            while( _unstartedJobs.Count > 0 )
             {
                if( _endpoint.IsBusy ) break;
 
+               // ERROR ERROR ERROR --- MEGA BUG MEGA BUG, MUST REMOVE FROM _unstartedJobs INSIDE LOOP!!!!
+
+               var kvp = _unstartedJobs.FirstOrDefault();
+
                var key = kvp.Key;
                var job = kvp.Value;
-               _kickedOff.Add( key );
+               _unstartedJobs.Remove( key );
 
                // lets see if the text should still be translated before kicking anything off
                if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
@@ -2112,17 +2119,15 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                var untranslatedText = job.Key.GetDictionaryLookupKey();
                XuaLogger.Current.Debug( "Starting translation for: " + untranslatedText );
-               StartCoroutine( _endpoint.Translate( untranslatedText, Settings.FromLanguage, Settings.Language, translatedText => OnSingleTranslationCompleted( job, translatedText ),
-               ( msg, e ) => OnTranslationFailed( job, msg, e ) ) );
+               StartCoroutine(
+                  _endpoint.Translate(
+                     untranslatedText,
+                     Settings.FromLanguage,
+                     Settings.Language,
+                     translatedText => OnSingleTranslationCompleted( job, translatedText ),
+                     ( msg, e ) => OnTranslationFailed( job, msg, e ) ) );
             }
          }
-
-         for( int i = 0 ; i < _kickedOff.Count ; i++ )
-         {
-            _unstartedJobs.Remove( _kickedOff[ i ] );
-         }
-
-         _kickedOff.Clear();
       }
 
       private void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch )

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

@@ -108,6 +108,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          }
          catch( Exception e )
          {
+            ApplicationName = "Unknown";
             XuaLogger.Current.Error( e, "An error occurred while getting application name." );
          }
 

+ 20 - 2
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/ConfiguredEndpoint.cs

@@ -24,14 +24,32 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       {
          var context = new TranslationContext( untranslatedText, from, to, success, failure );
          _ongoingTranslations++;
+
+         bool ok = false;
+         IEnumerator iterator = null;
          try
          {
-            var iterator = Endpoint.Translate( context );
+            iterator = Endpoint.Translate( context );
             if( iterator != null )
             {
-               while( iterator.MoveNext() )
+               TryMe: try
+               {
+                  ok = iterator.MoveNext();
+               }
+               catch( TranslationContextException )
+               {
+                  ok = false;
+               }
+               catch( Exception e )
+               {
+                  ok = false;
+                  context.FailWithoutThrowing( "Error occurred during translation.", e );
+               }
+
+               if( ok )
                {
                   yield return iterator.Current;
+                  goto TryMe;
                }
             }
          }

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

@@ -140,7 +140,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.ExtProtocol
          if( _failed )
          {
             context.Fail( "Translator failed.", null );
-            yield break;
          }
 
          var result = new StreamReaderResult();

+ 10 - 28
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/HttpEndpoint.cs

@@ -37,26 +37,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
             }
          }
 
-         XUnityWebResponse response = null;
-         try
+         // prepare request
+         OnCreateRequest( httpContext );
+         if( httpContext.Request == null )
          {
-            // prepare request
-            OnCreateRequest( httpContext );
-            if( httpContext.Request == null )
-            {
-               httpContext.Fail( "No request object was provided by the translator.", null );
-               yield break;
-            }
-
-            var client = new XUnityWebClient();
-            response = client.Send( httpContext.Request );
-         }
-         catch( Exception e )
-         {
-            httpContext.Fail( "Error occurred while setting up translation request.", e );
-            yield break;
+            httpContext.Fail( "No request object was provided by the translator.", null );
          }
 
+         // execute request
+         var client = new XUnityWebClient();
+         var response = client.Send( httpContext.Request );
+
          // wait for completion
          if( Features.SupportsCustomYieldInstruction )
          {
@@ -77,25 +68,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
          if( response.Error != null )
          {
             httpContext.Fail( "Error occurred while retrieving translation.", response.Error );
-            yield break;
          }
 
          // failure
          if( response.Data == null )
          {
             httpContext.Fail( "Error occurred while retrieving translation. Nothing was returned.", null );
-            yield break;
          }
 
-         try
-         {
-            // attempt to extract translation from data
-            OnExtractTranslation( httpContext );
-         }
-         catch( Exception e )
-         {
-            httpContext.Fail( "Error occurred while retrieving translation.", e );
-         }
+         // extract text
+         OnExtractTranslation( httpContext );
       }
    }
 }

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

@@ -26,6 +26,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
          _context.Fail( reason, exception );
       }
 
+      public void Fail( string reason )
+      {
+         _context.Fail( reason );
+      }
+
       void IHttpRequestCreationContext.Complete( XUnityWebRequest request )
       {
          Request = request;

+ 1 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Http/IHttpTranslationContext.cs

@@ -9,5 +9,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Http
       string DestinationLanguage { get; }
 
       void Fail( string reason, Exception exception );
+      void Fail( string reason );
    }
 }

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

@@ -10,5 +10,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       void Complete( string translatedText );
       void Fail( string reason, Exception exception );
+      void Fail( string reason );
    }
 }

+ 49 - 1
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/TranslationContext.cs

@@ -30,6 +30,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public void Complete( string translatedText )
       {
+         if( IsDone ) return;
+
          try
          {
             if( !string.IsNullOrEmpty( translatedText ) )
@@ -49,6 +51,40 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
 
       public void Fail( string reason, Exception exception )
       {
+         if( IsDone ) return;
+
+         try
+         {
+            _fail( reason, exception );
+
+            throw new TranslationContextException();
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      public void Fail( string reason )
+      {
+         if( IsDone ) return;
+
+         try
+         {
+            _fail( reason, null );
+
+            throw new TranslationContextException();
+         }
+         finally
+         {
+            IsDone = true;
+         }
+      }
+
+      internal void FailWithoutThrowing( string reason, Exception exception )
+      {
+         if( IsDone ) return;
+
          try
          {
             _fail( reason, exception );
@@ -63,8 +99,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
       {
          if( !IsDone )
          {
-            Fail( "The translation request was not completed before returning from translator.", null );
+            FailWithoutThrowing( "The translation request was not completed before returning from translator.", null );
          }
       }
    }
+
+
+   [Serializable]
+   internal class TranslationContextException : Exception
+   {
+      public TranslationContextException() { }
+      public TranslationContextException( string message ) : base( message ) { }
+      public TranslationContextException( string message, Exception inner ) : base( message, inner ) { }
+      protected TranslationContextException(
+       System.Runtime.Serialization.SerializationInfo info,
+       System.Runtime.Serialization.StreamingContext context ) : base( info, context ) { }
+   }
 }

+ 1 - 0
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/IWwwTranslationContext.cs

@@ -9,5 +9,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
       string DestinationLanguage { get; }
 
       void Fail( string reason, Exception exception );
+      void Fail( string reason );
    }
 }

+ 30 - 55
src/XUnity.AutoTranslator.Plugin.Core/Endpoints/Www/WwwEndpoint.cs

@@ -43,75 +43,50 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
             }
          }
 
-         object www = null;
-         try
+         // prepare request
+         OnCreateRequest( wwwContext );
+         if( wwwContext.RequestInfo == null )
          {
-            // prepare request
-            OnCreateRequest( wwwContext );
-            if( wwwContext.RequestInfo == null )
-            {
-               wwwContext.Fail( "No request object was provided by the translator.", null );
-               yield break;
-            }
+            wwwContext.Fail( "No request object was provided by the translator.", null );
+         }
 
-            var request = wwwContext.RequestInfo;
-            var url = request.Address;
-            var data = request.Data;
-            var headers = request.Headers;
+         var request = wwwContext.RequestInfo;
+         var url = request.Address;
+         var data = request.Data;
+         var headers = request.Headers;
+
+         // execute request
+         var www = WwwConstructor.Invoke( new object[] { request.Address, data != null ? Encoding.UTF8.GetBytes( data ) : null, headers } );
+
+         // wait for completion
+         yield return www;
 
-            // execute request
-            www = WwwConstructor.Invoke( new object[] { request.Address, data != null ? Encoding.UTF8.GetBytes( data ) : null, headers } );
+         // extract error
+         string error = null;
+         try
+         {
+            error = (string)AccessTools.Property( Constants.ClrTypes.WWW, "error" ).GetValue( www, null );
          }
          catch( Exception e )
          {
-            wwwContext.Fail( "Error occurred while setting up translation request.", e );
-            yield break;
+            error = e.ToString();
          }
 
-         if( www == null )
+         if( error != null )
          {
-            wwwContext.Fail( "Unexpected error occurred while retrieving translation.", null );
-            yield break;
+            wwwContext.Fail( "Error occurred while retrieving translation." + Environment.NewLine + error, null );
          }
 
-         // wait for completion
-         yield return www;
-
-         try
+         // extract text
+         var text = (string)AccessTools.Property( Constants.ClrTypes.WWW, "text" ).GetValue( www, null );
+         if( text == null )
          {
-            // extract error
-            string error = null;
-            try
-            {
-               error = (string)AccessTools.Property( Constants.ClrTypes.WWW, "error" ).GetValue( www, null );
-            }
-            catch( Exception e )
-            {
-               error = e.ToString();
-            }
-
-            if( error != null )
-            {
-               wwwContext.Fail( "Error occurred while retrieving translation." + Environment.NewLine + error, null );
-               yield break;
-            }
-
-            // extract text
-            var text = (string)AccessTools.Property( Constants.ClrTypes.WWW, "text" ).GetValue( www, null );
-            if( text == null )
-            {
-               wwwContext.Fail( "Error occurred while extracting text from response.", null );
-               yield break;
-            }
+            wwwContext.Fail( "Error occurred while extracting text from response.", null );
+         }
 
-            wwwContext.ResponseData = text;
+         wwwContext.ResponseData = text;
 
-            OnExtractTranslation( wwwContext );
-         }
-         catch( Exception e )
-         {
-            wwwContext.Fail( "Error occurred while retrieving translation.", e );
-         }
+         OnExtractTranslation( wwwContext );
       }
    }
 }

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

@@ -34,5 +34,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Endpoints.Www
       {
          _context.Fail( reason, exception );
       }
+
+      public void Fail( string reason )
+      {
+         _context.Fail( reason );
+      }
    }
 }

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

@@ -8,7 +8,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    internal static class GUIUtil
    {
-      public const int WindowTitleClearance = 15;
+      public const int WindowTitleClearance = 10;
       public const int ComponentSpacing = 10;
       public const int LabelWidth = 60;
       public const int LabelHeight = 21;

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

@@ -8,7 +8,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
 {
    internal class XuaWindow
    {
-      private const int WindowHeight = 510;
+      private const int WindowHeight = 480;
       private const int WindowWidth = 320;
 
       private const int AvailableWidth = WindowWidth - ( GUIUtil.ComponentSpacing * 2 );
@@ -133,9 +133,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.UI
             posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
          }
 
-         GUI.Label( GUIUtil.R( col1x, posy, col12, GUIUtil.LabelHeight ), "<b>_______________________________________</b>", GUIUtil.LabelCenter );
-         posy += GUIUtil.RowHeight + GUIUtil.ComponentSpacing;
-
          var endpointDropdown = _endpointDropdown ?? ( _endpointDropdown = new DropdownGUI<TranslatorDropdownOptionViewModel, ConfiguredEndpoint>( col2x, endpointDropdownPosy, col2, _endpointOptions ) );
          endpointDropdown.OnGUI();
 

+ 1 - 1
src/XUnity.AutoTranslator.Plugin.Core/Web/HttpSecurity.cs

@@ -8,7 +8,7 @@ using System.Text;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Web
 {
-   public class HttpSecurity
+   internal class HttpSecurity
    {
       public readonly HashSet<string> _hosts = new HashSet<string>();
 

+ 7 - 4
src/XUnity.AutoTranslator.Setup/Program.cs

@@ -91,12 +91,14 @@ namespace XUnity.AutoTranslator.Setup
                Console.WriteLine( iniInfo.Name + " already exists. skipping..." );
             }
 
-            var lnkInfo = new FileInfo( Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".lnk" );
+            var shortcutPath = Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + " (Patch and Run).lnk";
+            var lnkInfo = new FileInfo( shortcutPath );
             if( !lnkInfo.Exists )
             {
                // create shortcuts
                CreateShortcut(
-                  Path.GetFileNameWithoutExtension( launcher.Executable.Name ) + ".lnk",
+                  launcher.Executable.FullName,
+                  shortcutPath,
                   gamePath,
                   Path.Combine( reiPath, "ReiPatcher.exe" ) );
 
@@ -132,7 +134,7 @@ namespace XUnity.AutoTranslator.Setup
          }
       }
 
-      public static void CreateShortcut( string shortcutName, string shortcutPath, string targetFileLocation )
+      public static void CreateShortcut( string gameExePath, string shortcutName, string shortcutPath, string targetFileLocation )
       {
          string shortcutLocation = Path.Combine( shortcutPath, shortcutName );
 
@@ -147,8 +149,9 @@ namespace XUnity.AutoTranslator.Setup
 
          // Set the .lnk file properties
          lnk.Path = targetFileLocation;
-         lnk.Arguments = "-c \"" + Path.GetFileNameWithoutExtension( shortcutName ) + ".ini\"";
+         lnk.Arguments = "-c \"" + Path.GetFileNameWithoutExtension( gameExePath ) + ".ini\"";
          lnk.WorkingDirectory = Path.GetDirectoryName( targetFileLocation );
+         lnk.SetIconLocation( gameExePath.Replace( '\\', '/' ), 0 );
 
          lnk.Save( shortcutName );
       }

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

@@ -5,6 +5,8 @@
       <TargetFramework>net40</TargetFramework>
       <AssemblyName>SetupReiPatcherAndAutoTranslator</AssemblyName>
       <Version>3.0.0</Version>
+      <ApplicationIcon>icon.ico</ApplicationIcon>
+      <Win32Resource />
    </PropertyGroup>
 
    <ItemGroup>

二進制
src/XUnity.AutoTranslator.Setup/icon.ico