Scrublord1336 6 anni fa
parent
commit
90f70889df
27 ha cambiato i file con 3150 aggiunte e 379 eliminazioni
  1. 1 1
      CHANGELOG.md
  2. 453 126
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  3. 71 0
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationBatch.cs
  4. 19 0
      src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationLineTracker.cs
  5. 15 1
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  6. 14 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/Types.cs
  7. 19 3
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/ComponentExtensions.cs
  8. 28 3
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/ObjectExtensions.cs
  9. 11 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/StringExtensions.cs
  10. 18 6
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs
  11. 60 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/UtageHooks.cs
  12. 10 4
      src/XUnity.AutoTranslator.Plugin.Core/IKnownEndpoint.cs
  13. 32 0
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs
  14. 293 0
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParserBase.cs
  15. 22 0
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs
  16. 10 0
      src/XUnity.AutoTranslator.Plugin.Core/Parsing/UtageTextParser.cs
  17. 15 42
      src/XUnity.AutoTranslator.Plugin.Core/TranslationInfo.cs
  18. 60 10
      src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs
  19. 70 0
      src/XUnity.AutoTranslator.Plugin.Core/TranslationKey.cs
  20. 0 70
      src/XUnity.AutoTranslator.Plugin.Core/TranslationKeys.cs
  21. 72 0
      src/XUnity.AutoTranslator.Plugin.Core/UtageSupport/UtageHelpers.cs
  22. 15 9
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs
  23. 7 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/GoogleTranslateEndpoint.cs
  24. 70 41
      src/XUnity.AutoTranslator.Plugin.Core/Web/KnownHttpEndpoint.cs
  25. 68 61
      src/XUnity.AutoTranslator.Plugin.Core/Web/KnownWwwEndpoint.cs
  26. 1695 0
      src/XUnity.AutoTranslator.Plugin.Core/Web/MyWebClient.cs
  27. 2 2
      src/XUnity.AutoTranslator.Plugin.Core/Web/UnityWebClient.cs

+ 1 - 1
CHANGELOG.md

@@ -1,4 +1,4 @@
-### 2.10.0
+### 2.10.0
  * FEATURE - Support Yandex translate (requires key)
  * FEATURE - Support Watson translate (requires key)
 

+ 453 - 126
src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs

@@ -25,6 +25,9 @@ using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
 using UnityEngine.SceneManagement;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Debugging;
+using XUnity.AutoTranslator.Plugin.Core.Batching;
+using Harmony;
+using XUnity.AutoTranslator.Plugin.Core.Parsing;
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
@@ -40,11 +43,13 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// </summary>
       private List<TranslationJob> _completedJobs = new List<TranslationJob>();
       private Dictionary<string, TranslationJob> _unstartedJobs = new Dictionary<string, TranslationJob>();
+      private Dictionary<string, TranslationJob> _ongoingJobs = new Dictionary<string, TranslationJob>();
 
       /// <summary>
       /// All the translations are stored in this dictionary.
       /// </summary>
       private Dictionary<string, string> _translations = new Dictionary<string, string>();
+      private Dictionary<string, string> _reverseTranslations = new Dictionary<string, string>();
 
       /// <summary>
       /// These are the new translations that has not yet been persisted to the file system.
@@ -52,7 +57,6 @@ namespace XUnity.AutoTranslator.Plugin.Core
       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>
       /// Keeps track of things to copy to clipboard.
@@ -71,20 +75,27 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// the translation plugin.
       /// </summary>
       private HashSet<object> _ongoingOperations = new HashSet<object>();
-      private HashSet<string> _startedOperationsForNonStabilizableComponents = new HashSet<string>();
 
       /// <summary>
       /// This function will check if there are symbols of a given language contained in a string.
       /// </summary>
       private Func<string, bool> _symbolCheck;
 
+      private object _advEngine;
+      private float? _nextAdvUpdate;
+
       private IKnownEndpoint _endpoint;
 
       private int[] _currentTranslationsQueuedPerSecondRollingWindow = new int[ Settings.TranslationQueueWatchWindow ];
       private float? _timeExceededThreshold;
+      private float _translationsQueuedPerSecond;
 
       private bool _isInTranslatedMode = true;
       private bool _hooksEnabled = true;
+      private bool _batchLogicHasFailed = false;
+
+      private int _availableBatchOperations = Settings.MaxAvailableBatchOperations;
+      private float _batchOperationSecondCounter = 0;
 
       public void Initialize()
       {
@@ -95,7 +106,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
          if( Settings.EnableConsole ) DebugConsole.Enable();
 
-         HooksSetup.InstallHooks( Override_TextChanged );
+         HooksSetup.InstallHooks();
 
          try
          {
@@ -181,7 +192,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
          catch( Exception e )
          {
-            Logger.Current.Error( e, "An error occurred while saving translations to disk."  );
+            Logger.Current.Error( e, "An error occurred while saving translations to disk." );
          }
       }
 
@@ -196,6 +207,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
             {
                Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ) );
                Directory.CreateDirectory( Path.GetDirectoryName( Path.Combine( Config.Current.DataPath, Settings.OutputFile ) ) );
+               var tab = new char[] { '\t' };
+               var equals = new char[] { '=' };
+               var splitters = new char[][] { tab, equals };
 
                foreach( var fullFileName in GetTranslationFiles() )
                {
@@ -204,15 +218,20 @@ namespace XUnity.AutoTranslator.Plugin.Core
                      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 )
+                        for( int i = 0 ; i < splitters.Length ; i++ )
                         {
-                           string key = TextHelper.Decode( kvp[ 0 ].Trim() );
-                           string value = TextHelper.Decode( kvp[ 1 ].Trim() );
-
-                           if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
+                           var splitter = splitters[ i ];
+                           string[] kvp = translation.Split( splitter, StringSplitOptions.None );
+                           if( kvp.Length >= 2 )
                            {
-                              AddTranslation( key, value );
+                              string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
+                              string value = TextHelper.Decode( kvp[ 1 ].TrimIfConfigured() );
+
+                              if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
+                              {
+                                 AddTranslation( key, value );
+                                 break;
+                              }
                            }
                         }
                      }
@@ -226,29 +245,45 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private TranslationJob GetOrCreateTranslationJobFor( TranslationKeys key )
+      private TranslationJob GetOrCreateTranslationJobFor( object ui, TranslationKey key, TranslationContext context )
       {
-         if( _unstartedJobs.TryGetValue( key.GetDictionaryLookupKey(), out TranslationJob job ) )
+         var lookupKey = key.GetDictionaryLookupKey();
+
+         if( _unstartedJobs.TryGetValue( lookupKey, out TranslationJob unstartedJob ) )
          {
-            return job;
+            unstartedJob.Associate( context );
+            return unstartedJob;
+         }
+
+         if( _ongoingJobs.TryGetValue( lookupKey, out TranslationJob ongoingJob ) )
+         {
+            ongoingJob.Associate( context );
+            return ongoingJob;
          }
 
          foreach( var completedJob in _completedJobs )
          {
-            if( completedJob.Keys.GetDictionaryLookupKey() == key.GetDictionaryLookupKey() )
+            if( completedJob.Key.GetDictionaryLookupKey() == lookupKey )
             {
+               completedJob.Associate( context );
                return completedJob;
             }
          }
 
-         Logger.Current.Debug( "Queued translation for: " + key.GetDictionaryLookupKey() );
+         Logger.Current.Debug( "Queued translation for: " + lookupKey );
+
+         ongoingJob = new TranslationJob( key );
+         if( ui != null )
+         {
+            ongoingJob.OriginalSources.Add( ui );
+         }
+         ongoingJob.Associate( context );
 
-         job = new TranslationJob( key );
-         _unstartedJobs.Add( key.GetDictionaryLookupKey(), job );
+         _unstartedJobs.Add( lookupKey, ongoingJob );
 
          CheckThresholds();
 
-         return job;
+         return ongoingJob;
       }
 
       private void CheckThresholds()
@@ -257,6 +292,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          {
             _unstartedJobs.Clear();
             _completedJobs.Clear();
+            _ongoingJobs.Clear();
             Settings.IsShutdown = true;
 
             Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxUnstartedJobs} queued for translations due to unknown reasons. Shutting down plugin." );
@@ -271,8 +307,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
          _currentTranslationsQueuedPerSecondRollingWindow[ newIdx ]++;
 
          var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
-         var translationsPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
-         if( translationsPerSecond > Settings.MaxTranslationsQueuedPerSecond )
+         _translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
+         if( _translationsQueuedPerSecond > Settings.MaxTranslationsQueuedPerSecond )
          {
             if( !_timeExceededThreshold.HasValue )
             {
@@ -283,6 +319,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             {
                _unstartedJobs.Clear();
                _completedJobs.Clear();
+               _ongoingJobs.Clear();
                Settings.IsShutdown = true;
 
                Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxTranslationsQueuedPerSecond} translations per seconds queued for a {Settings.MaxSecondsAboveTranslationThreshold} second period. Shutting down plugin." );
@@ -294,6 +331,21 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
+      private void IncrementBatchOperations()
+      {
+         _batchOperationSecondCounter += Time.deltaTime;
+
+         if( _batchOperationSecondCounter > Settings.IncreaseBatchOperationsEvery )
+         {
+            if( _availableBatchOperations < Settings.MaxAvailableBatchOperations )
+            {
+               _availableBatchOperations++;
+            }
+
+            _batchOperationSecondCounter = 0;
+         }
+      }
+
       private void ResetThresholdTimerIfRequired()
       {
          var previousIdx = ( (int)( Time.time - Time.deltaTime ) ) % Settings.TranslationQueueWatchWindow;
@@ -304,9 +356,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
 
          var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
-         var translationsPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
+         _translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
 
-         if( translationsPerSecond <= Settings.MaxTranslationsQueuedPerSecond )
+         if( _translationsQueuedPerSecond <= Settings.MaxTranslationsQueuedPerSecond )
          {
             _timeExceededThreshold = null;
          }
@@ -315,35 +367,35 @@ namespace XUnity.AutoTranslator.Plugin.Core
       private void AddTranslation( string key, string value )
       {
          _translations[ key ] = value;
-         _translatedTexts.Add( value );
+         _reverseTranslations[ value ] = key;
       }
 
-      private void AddTranslation( TranslationKeys key, string value )
+      private void AddTranslation( TranslationKey key, string value )
       {
          _translations[ key.GetDictionaryLookupKey() ] = value;
-         _translatedTexts.Add( value );
+         _reverseTranslations[ value ] = key.GetDictionaryLookupKey();
       }
 
-      private void QueueNewUntranslatedForClipboard( TranslationKeys key )
+      private void QueueNewUntranslatedForClipboard( TranslationKey key )
       {
          if( Settings.CopyToClipboard )
          {
-            if( !_textsToCopyToClipboard.Contains( key.RelevantKey ) )
+            if( !_textsToCopyToClipboard.Contains( key.RelevantText ) )
             {
-               _textsToCopyToClipboard.Add( key.RelevantKey );
-               _textsToCopyToClipboardOrdered.Add( key.RelevantKey );
+               _textsToCopyToClipboard.Add( key.RelevantText );
+               _textsToCopyToClipboardOrdered.Add( key.RelevantText );
 
                _clipboardUpdated = Time.realtimeSinceStartup;
             }
          }
       }
 
-      private void QueueNewUntranslatedForDisk( TranslationKeys key )
+      private void QueueNewUntranslatedForDisk( TranslationKey key )
       {
          _newUntranslated.Add( key.GetDictionaryLookupKey() );
       }
 
-      private void QueueNewTranslationForDisk( TranslationKeys key, string value )
+      private void QueueNewTranslationForDisk( TranslationKey key, string value )
       {
          lock( _writeToFileSync )
          {
@@ -351,12 +403,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private bool TryGetTranslation( TranslationKeys key, out string value )
+      private bool TryGetTranslation( TranslationKey key, out string value )
       {
          return _translations.TryGetValue( key.GetDictionaryLookupKey(), out value );
       }
 
-      private string Override_TextChanged( object ui, string text )
+      public bool TryGetReverseTranslation( string value, out string key )
+      {
+         return _reverseTranslations.TryGetValue( value, out key );
+      }
+
+      public string Hook_TextChanged_WithResult( object ui, string text )
       {
          if( _hooksEnabled )
          {
@@ -381,15 +438,13 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      private void SetTranslatedText( object ui, string translatedText, TranslationKeys key, TranslationInfo info )
+      private void SetTranslatedText( object ui, string translatedText, TranslationInfo info )
       {
-         var untemplatedTranslatedText = key.Untemplate( translatedText );
-
-         info?.SetTranslatedText( untemplatedTranslatedText );
+         info?.SetTranslatedText( translatedText );
 
          if( _isInTranslatedMode )
          {
-            SetText( ui, untemplatedTranslatedText, true, info );
+            SetText( ui, translatedText, true, info );
          }
       }
 
@@ -413,13 +468,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                ui.SetText( text );
 
-               if( isTranslated )
+               if( Settings.EnableUIResizing )
                {
-                  info?.ResizeUI( ui );
-               }
-               else
-               {
-                  info?.UnresizeUI( ui );
+                  if( isTranslated )
+                  {
+                     info?.ResizeUI( ui );
+                  }
+                  else
+                  {
+                     info?.UnresizeUI( ui );
+                  }
                }
             }
             catch( NullReferenceException )
@@ -447,7 +505,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// </summary>
       private bool IsTranslatable( string str )
       {
-         return _symbolCheck( str ) && str.Length <= Settings.MaxCharactersPerTranslation && !_translatedTexts.Contains( str );
+         return _symbolCheck( str ) && str.Length <= Settings.MaxCharactersPerTranslation && !_reverseTranslations.ContainsKey( str );
       }
 
       public bool ShouldTranslate( object ui )
@@ -483,7 +541,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             return null;
          }
 
-         var supportsStabilization = SupportsStabilization( ui );
+         var supportsStabilization = ui.SupportsStabilization();
          if( Settings.Delay == 0 || !supportsStabilization )
          {
             return TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization );
@@ -511,17 +569,17 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// 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, bool supportsStabilization )
+      private string TranslateOrQueueWebJobImmediate( object ui, string text, TranslationInfo info, bool supportsStabilization, TranslationContext context = null )
       {
          // Get the trimmed text
-         text = ( text ?? ui.GetText() ).Trim();
+         text = ( text ?? ui.GetText() ).TrimIfConfigured();
 
          // Ensure that we actually want to translate this text and its owning UI element. 
          if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslate( ui ) && !IsCurrentlySetting( info ) )
          {
             info?.Reset( text );
+            var textKey = new TranslationKey( text, context == null && !supportsStabilization, context != null );
 
-            var textKey = new TranslationKeys( text, !supportsStabilization );
 
             // if we already have translation loaded in our _translatios dictionary, simply load it and set text
             string translation;
@@ -531,13 +589,31 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                if( !string.IsNullOrEmpty( translation ) )
                {
-                  SetTranslatedText( ui, translation, textKey, info );
+                  SetTranslatedText( ui, textKey.Untemplate( translation ), info );
                   return translation;
                }
             }
             else
             {
-               if( supportsStabilization )
+               if( context == null && ui.SupportsRichText() )
+               {
+                  var parser = UnityTextParsers.GetTextParserByGameEngine();
+                  if( parser != null )
+                  {
+                     var result = parser.Parse( text );
+                     if( result.HasRichSyntax )
+                     {
+                        translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
+                        if( translation != null )
+                        {
+                           SetTranslatedText( ui, translation, info ); // get rid of textKey here!!
+                        }
+                        return translation;
+                     }
+                  }
+               }
+
+               if( supportsStabilization && context == null ) // never stabilize a text that is contextualized or that does not support stabilization
                {
                   // 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
@@ -569,7 +645,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                               if( !string.IsNullOrEmpty( stabilizedText ) && IsTranslatable( stabilizedText ) )
                               {
-                                 var stabilizedTextKey = new TranslationKeys( stabilizedText, false );
+                                 var stabilizedTextKey = new TranslationKey( stabilizedText, false );
 
                                  QueueNewUntranslatedForClipboard( stabilizedTextKey );
 
@@ -580,17 +656,37 @@ namespace XUnity.AutoTranslator.Plugin.Core
                                  {
                                     if( !string.IsNullOrEmpty( translation ) )
                                     {
-                                       SetTranslatedText( ui, translation, stabilizedTextKey, info );
+                                       // stabilized, no need to untemplate
+                                       SetTranslatedText( ui, translation, info );
                                     }
                                  }
                                  else
                                  {
+                                    if( context == null && ui.SupportsRichText() )
+                                    {
+                                       var parser = UnityTextParsers.GetTextParserByGameEngine();
+                                       if( parser != null )
+                                       {
+                                          var result = parser.Parse( stabilizedText );
+                                          if( result.HasRichSyntax )
+                                          {
+                                             var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
+                                             if( translatedText != null )
+                                             {
+                                                // stabilized, no need to untemplate
+                                                SetTranslatedText( ui, translatedText, info );
+                                             }
+                                             return;
+                                          }
+                                       }
+                                    }
+
                                     // Lets try not to spam a service that might not be there...
                                     if( _endpoint != null )
                                     {
                                        if( _consecutiveErrors < Settings.MaxErrors && !Settings.IsShutdown )
                                        {
-                                          var job = GetOrCreateTranslationJobFor( stabilizedTextKey );
+                                          var job = GetOrCreateTranslationJobFor( ui, stabilizedTextKey, context );
                                           job.Components.Add( ui );
                                        }
                                     }
@@ -610,25 +706,18 @@ namespace XUnity.AutoTranslator.Plugin.Core
                }
                else
                {
-                  if( !_startedOperationsForNonStabilizableComponents.Contains( textKey.GetDictionaryLookupKey() ) )
+                  // Lets try not to spam a service that might not be there...
+                  if( _endpoint != null )
                   {
-                     _startedOperationsForNonStabilizableComponents.Add( textKey.GetDictionaryLookupKey() );
-
-                     QueueNewUntranslatedForClipboard( textKey );
-
-                     // Lets try not to spam a service that might not be there...
-                     if( _endpoint != null )
-                     {
-                        if( _consecutiveErrors < Settings.MaxErrors && !Settings.IsShutdown )
-                        {
-                           GetOrCreateTranslationJobFor( textKey );
-                        }
-                     }
-                     else
+                     if( _consecutiveErrors < Settings.MaxErrors && !Settings.IsShutdown )
                      {
-                        QueueNewUntranslatedForDisk( textKey );
+                        var job = GetOrCreateTranslationJobFor( ui, textKey, context );
                      }
                   }
+                  else
+                  {
+                     QueueNewUntranslatedForDisk( textKey );
+                  }
                }
             }
          }
@@ -636,9 +725,45 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return null;
       }
 
-      public bool SupportsStabilization( object ui )
+      private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserResult result, bool allowStartJob )
       {
-         return !( ui is GUIContent );
+         Dictionary<string, string> translations = new Dictionary<string, string>();
+
+         // attempt to lookup ALL strings immediately; return result if possible; queue operations
+         foreach( var kvp in result.Arguments )
+         {
+            var key = kvp.Key;
+            var value = kvp.Value.TrimIfConfigured();
+            if( !string.IsNullOrEmpty( value ) && IsTranslatable( value ) )
+            {
+               var valueKey = new TranslationKey( value, false, true );
+               string partTranslation;
+               if( TryGetTranslation( valueKey, out partTranslation ) )
+               {
+                  translations.Add( key, partTranslation );
+               }
+               else if( allowStartJob )
+               {
+                  // incomplete, must start job
+                  var context = new TranslationContext( ui, result );
+                  TranslateOrQueueWebJobImmediate( null, value, null, false, context );
+               }
+            }
+            else
+            {
+               // the value will do
+               translations.Add( key, value );
+            }
+         }
+
+         if( result.Arguments.Count == translations.Count )
+         {
+            return result.Untemplate( translations );
+         }
+         else
+         {
+            return null; // could not perform complete translation
+         }
       }
 
       /// <summary>
@@ -656,7 +781,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
             if( beforeText == afterText )
             {
-               onTextStabilized( afterText.Trim() );
+               onTextStabilized( afterText.TrimIfConfigured() );
             }
             else
             {
@@ -689,9 +814,16 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
             if( !Settings.IsShutdown )
             {
+               IncrementBatchOperations();
                ResetThresholdTimerIfRequired();
                KickoffTranslations();
                FinishTranslations();
+
+               if( _nextAdvUpdate.HasValue && Time.time > _nextAdvUpdate )
+               {
+                  _nextAdvUpdate = null;
+                  UpdateUtageText();
+               }
             }
 
             if( Input.anyKey )
@@ -727,75 +859,212 @@ namespace XUnity.AutoTranslator.Plugin.Core
       {
          if( _endpoint == null ) return;
 
-         foreach( var kvp in _unstartedJobs )
+         if( Settings.EnableBatching && _endpoint.SupportsLineSplitting && !_batchLogicHasFailed && _unstartedJobs.Count > 1 && _availableBatchOperations > 0 )
          {
-            if( _endpoint.IsBusy ) break;
+            while( _unstartedJobs.Count > 0 && _availableBatchOperations > 0 )
+            {
+               if( _endpoint.IsBusy ) break;
 
-            var key = kvp.Key;
-            var job = kvp.Value;
-            _kickedOff.Add( key );
+               var kvps = _unstartedJobs.Take( Settings.BatchSize ).ToList();
+               var batch = new TranslationBatch();
 
-            // lets see if the text should still be translated before kicking anything off
-            if( !job.AnyComponentsStillHasOriginalUntranslatedText() ) continue;
+               foreach( var kvp in kvps )
+               {
+                  var key = kvp.Key;
+                  var job = kvp.Value;
+                  _kickedOff.Add( key );
+
+                  if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
+                  
+                  batch.Add( job );
+                  _ongoingJobs[ key ] = job;
+               }
 
-            StartCoroutine( _endpoint.Translate( job.Keys.GetDictionaryLookupKey(), Settings.FromLanguage, Settings.Language, translatedText =>
+               if( !batch.IsEmpty )
+               {
+                  _availableBatchOperations--;
+
+                  StartCoroutine( _endpoint.Translate( batch.GetFullTranslationKey(), Settings.FromLanguage, Settings.Language, translatedText => OnBatchTranslationCompleted( batch, translatedText ),
+                  () => OnTranslationFailed( batch ) ) );
+               }
+            }
+         }
+         else
+         {
+            foreach( var kvp in _unstartedJobs )
             {
-               Settings.TranslationCount++;
+               if( _endpoint.IsBusy ) break;
+
+               var key = kvp.Key;
+               var job = kvp.Value;
+               _kickedOff.Add( key );
+
+               // lets see if the text should still be translated before kicking anything off
+               if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
+
+               _ongoingJobs[ key ] = job;
+
+               StartCoroutine( _endpoint.Translate( job.Key.GetDictionaryLookupKey(), Settings.FromLanguage, Settings.Language, translatedText => OnSingleTranslationCompleted( job, translatedText ),
+               () => OnTranslationFailed( job ) ) );
+            }
+         }
+
+         for( int i = 0 ; i < _kickedOff.Count ; i++ )
+         {
+            _unstartedJobs.Remove( _kickedOff[ i ] );
+         }
+
+         _kickedOff.Clear();
+      }
+
+      public void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch )
+      {
+         Settings.TranslationCount++;
 
-               if( !Settings.IsShutdown )
+         if( !Settings.IsShutdown )
+         {
+            if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
+            {
+               Settings.IsShutdown = true;
+               Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
+            }
+         }
+
+         _consecutiveErrors = 0;
+
+         var succeeded = batch.MatchWithTranslations( translatedTextBatch );
+         if( succeeded )
+         {
+            foreach( var tracker in batch.Trackers )
+            {
+               var job = tracker.Job;
+               var translatedText = tracker.RawTranslatedText;
+               if( !string.IsNullOrEmpty( translatedText ) )
                {
-                  if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
+                  if( Settings.ForceSplitTextAfterCharacters > 0 )
                   {
-                     Settings.IsShutdown = true;
-                     Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
+                     translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
                   }
-               }
+                  job.TranslatedText = job.Key.RepairTemplate( translatedText );
 
-               _consecutiveErrors = 0;
+                  QueueNewTranslationForDisk( job.Key, translatedText );
+                  _completedJobs.Add( job );
+               }
 
-               if( Settings.ForceSplitTextAfterCharacters > 0 )
+               AddTranslation( job.Key, job.TranslatedText );
+               job.State = TranslationJobState.Succeeded;
+               _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
+            }
+         }
+         else
+         {
+            // might as well re-add all translation jobs, and never do this again!
+            _batchLogicHasFailed = true;
+            foreach( var tracker in batch.Trackers )
+            {
+               var key = tracker.Job.Key.GetDictionaryLookupKey();
+               if( !_unstartedJobs.ContainsKey( key ) )
                {
-                  translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
+                  _unstartedJobs[ key ] = tracker.Job;
                }
+               _ongoingJobs.Remove( key );
+            }
 
-               job.TranslatedText = job.Keys.RepairTemplate( translatedText );
+            Logger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
+         }
+      }
 
-               if( !string.IsNullOrEmpty( translatedText ) )
-               {
-                  QueueNewTranslationForDisk( job.Keys, translatedText );
+      private void OnSingleTranslationCompleted( TranslationJob job, string translatedText )
+      {
+         Settings.TranslationCount++;
 
-                  _completedJobs.Add( job );
-               }
-            },
-            () =>
+         if( !Settings.IsShutdown )
+         {
+            if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
             {
-               _consecutiveErrors++;
+               Settings.IsShutdown = true;
+               Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
+            }
+         }
+
+         _consecutiveErrors = 0;
+
+         if( !string.IsNullOrEmpty( translatedText ) )
+         {
+            if( Settings.ForceSplitTextAfterCharacters > 0 )
+            {
+               translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
+            }
+            job.TranslatedText = job.Key.RepairTemplate( translatedText );
+
+            QueueNewTranslationForDisk( job.Key, translatedText );
+            _completedJobs.Add( job );
+         }
+
+         AddTranslation( job.Key, job.TranslatedText );
+         job.State = TranslationJobState.Succeeded;
+         _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
+      }
 
-               if( !Settings.IsShutdown )
+      private void OnTranslationFailed( TranslationJob job )
+      {
+         _consecutiveErrors++;
+
+         job.State = TranslationJobState.Failed;
+         _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
+
+         if( !Settings.IsShutdown )
+         {
+            if( _consecutiveErrors > Settings.MaxErrors )
+            {
+               if( _endpoint.ShouldGetSecondChanceAfterFailure() )
                {
-                  if( _consecutiveErrors > Settings.MaxErrors )
-                  {
-                     if( _endpoint.ShouldGetSecondChanceAfterFailure() )
-                     {
-                        Logger.Current.Warn( $"More than {Settings.MaxErrors} consecutive errors occurred. Entering fallback mode." );
-                        _consecutiveErrors = 0;
-                     }
-                     else
-                     {
-                        Settings.IsShutdown = true;
-                        Logger.Current.Error( $"More than {Settings.MaxErrors} consecutive errors occurred. Shutting down plugin." );
-                     }
-                  }
+                  Logger.Current.Warn( $"More than {Settings.MaxErrors} consecutive errors occurred. Entering fallback mode." );
+                  _consecutiveErrors = 0;
                }
-            } ) );
+               else
+               {
+                  Settings.IsShutdown = true;
+                  Logger.Current.Error( $"More than {Settings.MaxErrors} consecutive errors occurred. Shutting down plugin." );
+
+                  _unstartedJobs.Clear();
+                  _completedJobs.Clear();
+                  _ongoingJobs.Clear();
+               }
+            }
          }
+      }
 
-         for( int i = 0 ; i < _kickedOff.Count ; i++ )
+      private void OnTranslationFailed( TranslationBatch batch )
+      {
+         _consecutiveErrors++;
+
+         foreach( var tracker in batch.Trackers )
          {
-            _unstartedJobs.Remove( _kickedOff[ i ] );
+            tracker.Job.State = TranslationJobState.Failed;
+            _ongoingJobs.Remove( tracker.Job.Key.GetDictionaryLookupKey() );
          }
 
-         _kickedOff.Clear();
+         if( !Settings.IsShutdown )
+         {
+            if( _consecutiveErrors > Settings.MaxErrors )
+            {
+               if( _endpoint.ShouldGetSecondChanceAfterFailure() )
+               {
+                  Logger.Current.Warn( $"More than {Settings.MaxErrors} consecutive errors occurred. Entering fallback mode." );
+                  _consecutiveErrors = 0;
+               }
+               else
+               {
+                  Settings.IsShutdown = true;
+                  Logger.Current.Error( $"More than {Settings.MaxErrors} consecutive errors occurred. Shutting down plugin." );
+
+                  _unstartedJobs.Clear();
+                  _completedJobs.Clear();
+                  _ongoingJobs.Clear();
+               }
+            }
+         }
       }
 
       private void FinishTranslations()
@@ -810,19 +1079,74 @@ namespace XUnity.AutoTranslator.Plugin.Core
                foreach( var component in job.Components )
                {
                   // 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 = component.GetText().Trim();
-                  if( text == job.Keys.OriginalText )
+                  try
                   {
-                     var info = component.GetTranslationInfo( false );
-                     SetTranslatedText( component, job.TranslatedText, job.Keys, info );
+                     var text = component.GetText().TrimIfConfigured();
+                     if( text == job.Key.OriginalText )
+                     {
+                        var info = component.GetTranslationInfo( false );
+                        SetTranslatedText( component, job.TranslatedText, info );
+                     }
+                  }
+                  catch( NullReferenceException )
+                  {
+                     // might fail if compoent is no longer associated to game
                   }
                }
 
-               AddTranslation( job.Keys, job.TranslatedText );
+               // handle each context
+               foreach( var context in job.Contexts )
+               {
+                  // are all jobs within this context completed? If so, we can set the text
+                  if( context.Jobs.All( x => x.State == TranslationJobState.Succeeded ) )
+                  {
+                     try
+                     {
+                        var text = context.Component.GetText().TrimIfConfigured();
+                        var result = context.Result;
+
+                        if( text == result.OriginalText )
+                        {
+                           Dictionary<string, string> translations = new Dictionary<string, string>();
+                           var translatedText = TranslateOrQueueWebJobImmediateByParserResult( null, result, false );
+                           if( translatedText != null )
+                           {
+                              var info = context.Component.GetTranslationInfo( false );
+                              SetTranslatedText( context.Component, translatedText, info );
+                           }
+                        }
+                     }
+                     catch( NullReferenceException )
+                     {
+
+                     }
+                  }
+               }
+
+
+               // Utage support
+               if( Constants.Types.AdvEngine != null
+                  && job.OriginalSources.Any( x => Constants.Types.AdvCommand.IsAssignableFrom( x.GetType() ) ) )
+               {
+                  _nextAdvUpdate = Time.time + 0.5f;
+               }
             }
          }
       }
 
+      private void UpdateUtageText()
+      {
+         if( _advEngine == null )
+         {
+            _advEngine = GameObject.FindObjectOfType( Constants.Types.AdvEngine );
+         }
+
+         if( _advEngine != null )
+         {
+            AccessTools.Method( Constants.Types.AdvEngine, "ChangeLanguage" )?.Invoke( _advEngine, new object[ 0 ] );
+         }
+      }
+
       private void ReloadTranslations()
       {
          LoadTranslations();
@@ -832,10 +1156,10 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var info = kvp.Value as TranslationInfo;
             if( info != null && !string.IsNullOrEmpty( info.OriginalText ) )
             {
-               var key = new TranslationKeys( info.OriginalText, false );
+               var key = new TranslationKey( info.OriginalText, false );
                if( TryGetTranslation( key, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
                {
-                  SetTranslatedText( kvp.Key, translatedText, key, info );
+                  SetTranslatedText( kvp.Key, translatedText, info ); // no need to untemplatize the translated text
                }
             }
          }
@@ -876,11 +1200,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
       private void ToggleTranslation()
       {
          _isInTranslatedMode = !_isInTranslatedMode;
+         var objects = ObjectExtensions.GetAllRegisteredObjects();
+
+         Logger.Current.Info( $"Toggling translations of {objects.Count} objects." );
 
          if( _isInTranslatedMode )
          {
             // make sure we use the translated version of all texts
-            foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
+            foreach( var kvp in objects )
             {
                var ui = kvp.Key;
                try
@@ -905,7 +1232,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          else
          {
             // make sure we use the original version of all texts
-            foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
+            foreach( var kvp in objects )
             {
                var ui = kvp.Key;
                try

+ 71 - 0
src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationBatch.cs

@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Batching
+{
+   public class TranslationBatch
+   {
+      public TranslationBatch()
+      {
+         Trackers = new List<TranslationLineTracker>();
+      }
+
+      public List<TranslationLineTracker> Trackers { get; private set; }
+
+      public bool IsEmpty => Trackers.Count == 0;
+
+      public int TotalLinesCount { get; set; }
+
+      public void Add( TranslationJob job )
+      {
+         var lines = new TranslationLineTracker( job );
+         Trackers.Add( lines );
+         TotalLinesCount += lines.LinesCount;
+      }
+
+      public bool MatchWithTranslations( string allTranslations )
+      {
+         var lines = allTranslations.Split( '\n' );
+
+         if( lines.Length != TotalLinesCount ) return false;
+
+         int current = 0;
+         foreach( var tracker in Trackers )
+         {
+            var builder = new StringBuilder( 32 );
+            for( int i = 0 ; i < tracker.LinesCount ; i++ )
+            {
+               var translation = lines[ current++ ];
+               builder.Append( translation );
+
+               // ADD NEW LINE IF NEEDED
+               if( !( i == tracker.LinesCount - 1 ) ) // if not last line
+               {
+                  builder.Append( '\n' );
+               }
+            }
+            var fullTranslation = builder.ToString();
+
+            tracker.RawTranslatedText = fullTranslation;
+         }
+
+         return true;
+      }
+
+      public string GetFullTranslationKey()
+      {
+         var builder = new StringBuilder();
+         for( int i = 0 ; i < Trackers.Count ; i++ )
+         {
+            var tracker = Trackers[ i ];
+            builder.Append( tracker.Job.Key.GetDictionaryLookupKey() );
+
+            if( !( i == Trackers.Count - 1 ) )
+            {
+               builder.Append( '\n' );
+            }
+         }
+         return builder.ToString();
+      }
+   }
+}

+ 19 - 0
src/XUnity.AutoTranslator.Plugin.Core/Batching/TranslationLineTracker.cs

@@ -0,0 +1,19 @@
+using System.Linq;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Batching
+{
+   public class TranslationLineTracker
+   {
+      public TranslationLineTracker( TranslationJob job )
+      {
+         Job = job;
+         LinesCount = job.Key.GetDictionaryLookupKey().Count( c => c == '\n' ) + 1;
+      }
+
+      public string RawTranslatedText { get; set; }
+
+      public TranslationJob Job { get; private set; }
+
+      public int LinesCount { get; private set; }
+   }
+}

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

@@ -14,14 +14,19 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static readonly float ClipboardDebounceTime = 1f;
       public static readonly int MaxTranslationsBeforeShutdown = 10000;
       public static readonly int MaxUnstartedJobs = 3500;
+      public static readonly float IncreaseBatchOperationsEvery = 30;
+      public static readonly bool EnableObjectTracking = true;
 
       public static bool IsShutdown = false;
       public static int TranslationCount = 0;
+      public static int MaxAvailableBatchOperations = 40;
 
       public static readonly float MaxTranslationsQueuedPerSecond = 5;
       public static readonly int MaxSecondsAboveTranslationThreshold = 30;
       public static readonly int TranslationQueueWatchWindow = 6;
 
+      public static readonly int BatchSize = 20;
+
       // can be changed
       public static string ServiceEndpoint;
       public static string Language;
@@ -38,6 +43,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableUGUI;
       public static bool EnableNGUI;
       public static bool EnableTextMeshPro;
+      public static bool EnableUtage;
       public static bool AllowPluginHookOverride;
       public static bool IgnoreWhitespaceInDialogue;
       public static int MinDialogueChars;
@@ -50,6 +56,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static int ForceSplitTextAfterCharacters;
       public static bool EnableMigrations;
       public static string MigrationsTag;
+      public static bool EnableBatching;
+      public static bool TrimAllText;
+      public static bool EnableUIResizing;
 
       public static bool CopyToClipboard;
       public static int MaxClipboardCopyCharacters;
@@ -84,15 +93,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          EnableUGUI = Config.Current.Preferences[ "TextFrameworks" ][ "EnableUGUI" ].GetOrDefault( true );
          EnableNGUI = Config.Current.Preferences[ "TextFrameworks" ][ "EnableNGUI" ].GetOrDefault( true );
          EnableTextMeshPro = Config.Current.Preferences[ "TextFrameworks" ][ "EnableTextMeshPro" ].GetOrDefault( true );
+         EnableUtage = Config.Current.Preferences[ "TextFrameworks" ][ "EnableUtage" ].GetOrDefault( true );
          AllowPluginHookOverride = Config.Current.Preferences[ "TextFrameworks" ][ "AllowPluginHookOverride" ].GetOrDefault( true );
 
          Delay = Config.Current.Preferences[ "Behaviour" ][ "Delay" ].GetOrDefault( 0f );
          MaxCharactersPerTranslation = Config.Current.Preferences[ "Behaviour" ][ "MaxCharactersPerTranslation" ].GetOrDefault( 150 );
-         IgnoreWhitespaceInDialogue = Config.Current.Preferences[ "Behaviour" ][ "IgnoreWhitespaceInDialogue" ].GetOrDefault( true );
+         IgnoreWhitespaceInDialogue = Config.Current.Preferences[ "Behaviour" ][ "IgnoreWhitespaceInDialogue" ].GetOrDefault( Types.AdvEngine == null );
          MinDialogueChars = Config.Current.Preferences[ "Behaviour" ][ "MinDialogueChars" ].GetOrDefault( 20 );
          ForceSplitTextAfterCharacters = Config.Current.Preferences[ "Behaviour" ][ "ForceSplitTextAfterCharacters" ].GetOrDefault( 0 );
          CopyToClipboard = Config.Current.Preferences[ "Behaviour" ][ "CopyToClipboard" ].GetOrDefault( false );
          MaxClipboardCopyCharacters = Config.Current.Preferences[ "Behaviour" ][ "MaxClipboardCopyCharacters" ].GetOrDefault( 450 );
+         EnableUIResizing = Config.Current.Preferences[ "Behaviour" ][ "EnableUIResizing" ].GetOrDefault( true );
+         EnableBatching = Config.Current.Preferences[ "Behaviour" ][ "EnableBatching" ].GetOrDefault( true );
+         TrimAllText = Config.Current.Preferences[ "Behaviour" ][ "TrimAllText" ].GetOrDefault( Types.AdvEngine == null );
+         
 
          BaiduAppId = Config.Current.Preferences[ "Baidu" ][ "BaiduAppId" ].GetOrDefault( "" );
          BaiduAppSecret = Config.Current.Preferences[ "Baidu" ][ "BaiduAppSecret" ].GetOrDefault( "" );

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

@@ -20,6 +20,20 @@ namespace XUnity.AutoTranslator.Plugin.Core.Constants
 
       public static readonly Type WWW = FindType( "UnityEngine.WWW" );
 
+      public static readonly Type UguiNovelText = FindType( "Utage.UguiNovelText" );
+
+      public static readonly Type AdvCommand = FindType( "Utage.AdvCommand" );
+
+      public static readonly Type AdvEngine = FindType( "Utage.AdvEngine" );
+
+      public static readonly Type AdvDataManager = FindType( "Utage.AdvDataManager" );
+
+      public static readonly Type AdvScenarioData = FindType( "Utage.AdvScenarioData" );
+
+      public static readonly Type AdvScenarioLabelData = FindType( "Utage.AdvScenarioLabelData" );
+
+      public static readonly Type AdvUguiSelection = FindType( "Utage.AdvUguiSelection" );
+
       private static Type FindType( string name )
       {
          return AppDomain.CurrentDomain.GetAssemblies()

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

@@ -13,9 +13,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
       public static string GetText( this object ui )
       {
+         if( ui == null ) return null;
+
          string text = null;
+         var type = ui.GetType();
 
-         if( ui is Text )
+         if( type == Constants.Types.UguiNovelText && ( (Component)ui ).gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.AdvUguiSelection ) != null )
+         {
+            // these texts are handled by AdvCommand, unless it is a selection
+            text = ( (Text)ui ).text;
+         }
+         else if( ui is Text )
          {
             text = ( (Text)ui ).text;
          }
@@ -34,7 +42,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 
       public static void SetText( this object ui, string text )
       {
-         if( ui is Text )
+         if( ui == null ) return;
+
+         var type = ui.GetType();
+
+         if( type == Constants.Types.UguiNovelText && ( ( Component ) ui ).gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.AdvUguiSelection ) != null )
+         {
+            // these texts are handled by AdvCommand, unless it is a selection
+            ( (Text)ui ).text = text;
+         }
+         else if( ui is Text )
          {
             ( (Text)ui ).text = text;
          }
@@ -45,7 +62,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          else
          {
             // fallback to reflective approach
-            var type = ui.GetType();
             type.GetProperty( TextPropertyName )?.GetSetMethod()?.Invoke( ui, new[] { text } );
          }
       }

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

@@ -3,7 +3,9 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading;
-using UnityEngine;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using UnityEngine.UI;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
@@ -12,10 +14,33 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
    {
       private static readonly object Sync = new object();
       private static readonly WeakDictionary<object, object> DynamicFields = new WeakDictionary<object, object>();
+      
+      public static bool SupportsStabilization( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ui is Text
+            || ( Types.UILabel != null && Types.UILabel.IsAssignableFrom( type ) )
+            || ( Types.TMP_Text != null && Types.TMP_Text.IsAssignableFrom( type ) );
+      }
+
+      public static bool SupportsRichText( this object ui )
+      {
+         if( ui == null ) return false;
+
+         var type = ui.GetType();
+
+         return ( ui as Text )?.supportRichText == true
+            || ( Types.AdvCommand != null && Types.AdvCommand.IsAssignableFrom( type ) );
+      }
 
       public static TranslationInfo GetTranslationInfo( this object obj, bool isAwakening )
       {
-         if( obj is GUIContent ) return null;
+         if( !Settings.EnableObjectTracking ) return null;
+
+         if( !obj.SupportsStabilization() ) return null;
 
          var info = obj.Get<TranslationInfo>();
 
@@ -50,7 +75,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          }
       }
 
-      public static IEnumerable<KeyValuePair<object, object>> GetAllRegisteredObjects()
+      public static List<KeyValuePair<object, object>> GetAllRegisteredObjects()
       {
          lock( Sync )
          {

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

@@ -182,6 +182,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
          return sb.ToString();
       }
 
+      public static string TrimIfConfigured( this string text )
+      {
+         if( text == null ) return text;
+
+         if( Settings.TrimAllText )
+         {
+            return text.Trim();
+         }
+         return text;
+      }
+
       public static string RemoveWhitespace( this string text )
       {
          // Japanese whitespace, wtf

+ 18 - 6
src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs

@@ -19,16 +19,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
 
    public static class HooksSetup
    {
-      public static void InstallHooks( Func<object, string, string> defaultHook )
+      public static void InstallHooks()
       {
          var harmony = HarmonyInstance.Create( "gravydevsupreme.xunity.autotranslator" );
 
          bool success = false;
          try
          {
-            if( Settings.EnableUGUI )
+            if( Settings.EnableUGUI || Settings.EnableUtage )
             {
-               success = SetupHook( KnownEvents.OnUnableToTranslateUGUI, defaultHook );
+               success = SetupHook( KnownEvents.OnUnableToTranslateUGUI, AutoTranslationPlugin.Current.Hook_TextChanged_WithResult );
                if( !success )
                {
                   harmony.PatchAll( UGUIHooks.All );
@@ -44,7 +44,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          {
             if( Settings.EnableTextMeshPro )
             {
-               success = SetupHook( KnownEvents.OnUnableToTranslateTextMeshPro, defaultHook );
+               success = SetupHook( KnownEvents.OnUnableToTranslateTextMeshPro, AutoTranslationPlugin.Current.Hook_TextChanged_WithResult );
                if( !success )
                {
                   harmony.PatchAll( TextMeshProHooks.All );
@@ -60,7 +60,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          {
             if( Settings.EnableNGUI )
             {
-               success = SetupHook( KnownEvents.OnUnableToTranslateNGUI, defaultHook );
+               success = SetupHook( KnownEvents.OnUnableToTranslateNGUI, AutoTranslationPlugin.Current.Hook_TextChanged_WithResult );
                if( !success )
                {
                   harmony.PatchAll( NGUIHooks.All );
@@ -76,7 +76,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          {
             if( Settings.EnableIMGUI )
             {
-               success = SetupHook( KnownEvents.OnUnableToTranslateNGUI, defaultHook );
+               success = SetupHook( KnownEvents.OnUnableToTranslateNGUI, AutoTranslationPlugin.Current.Hook_TextChanged_WithResult );
                if( !success )
                {
                   harmony.PatchAll( IMGUIHooks.All );
@@ -94,6 +94,18 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks
          {
             Logger.Current.Error( e, "An error occurred while setting up hooks for IMGUI." );
          }
+
+         try
+         {
+            if( Settings.EnableUtage )
+            {
+               harmony.PatchAll( UtageHooks.All );
+            }
+         }
+         catch( Exception e )
+         {
+            Logger.Current.Error( e, "An error occurred while setting up hooks for Utage." );
+         }
       }
 
       public static bool SetupHook( string eventName, Func<object, string, string> callback )

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

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Harmony;
+using XUnity.AutoTranslator.Plugin.Core.UtageSupport;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Hooks
+{
+   public static class UtageHooks
+   {
+      public static readonly Type[] All = new[] {
+         typeof( AdvCommand_ParseCellLocalizedTextHook ),
+         typeof( AdvEngine_JumpScenario ),
+      };
+   }
+
+   [Harmony]
+   public static class AdvCommand_ParseCellLocalizedTextHook
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.AdvCommand != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.AdvCommand, "ParseCellLocalizedText", new Type[] { } );
+      }
+
+      static void Postfix( object __instance, ref string __result )
+      {
+         var result = AutoTranslationPlugin.Current.Hook_TextChanged_WithResult( __instance, __result );
+         if( !string.IsNullOrEmpty( result ) )
+         {
+            __result = result;
+         }
+      }
+   }
+
+   [Harmony]
+   public static class AdvEngine_JumpScenario
+   {
+      static bool Prepare( HarmonyInstance instance )
+      {
+         return Constants.Types.AdvEngine != null;
+      }
+
+      static MethodBase TargetMethod( HarmonyInstance instance )
+      {
+         return AccessTools.Method( Constants.Types.AdvEngine, "JumpScenario", new Type[] { typeof( string ) } );
+      }
+
+      static void Prefix( ref string label )
+      {
+         UtageHelpers.FixLabel( ref label );
+      }
+   }
+}

+ 10 - 4
src/XUnity.AutoTranslator.Plugin.Core/IKnownEndpoint.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.IO;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
@@ -13,7 +13,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// in an async fashion.
       /// </summary>
       IEnumerator Translate( string untranslatedText, string from, string to, Action<string> success, Action failure );
-      
+
       /// <summary>
       /// Gets a boolean indicating if we are allowed to call "Translate".
       /// </summary>
@@ -30,5 +30,11 @@ namespace XUnity.AutoTranslator.Plugin.Core
       /// "Update" game loop method.
       /// </summary>
       void OnUpdate();
+
+      /// <summary>
+      /// Gets a bool indicating if the plugin is capable of distinguishing between the untranslated text
+      /// on a line per line basis.
+      /// </summary>
+      bool SupportsLineSplitting { get; }
    }
 }

+ 32 - 0
src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs

@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Parsing
+{
+   public class ParserResult
+   {
+      public ParserResult( string originalText, string template, Dictionary<string, string> args )
+      {
+         OriginalText = originalText;
+         Template = template;
+         Arguments = args;
+      }
+
+      public string OriginalText { get; set; }
+
+      public string Template { get; private set; }
+
+      public Dictionary<string, string> Arguments { get; private set; }
+
+      public bool HasRichSyntax => Template.Length > 5; // {{A}} <-- 5 chars
+
+      public string Untemplate( Dictionary<string, string> arguments )
+      {
+         string result = Template;
+         foreach( var kvp in arguments )
+         {
+            result = result.Replace( kvp.Key, kvp.Value );
+         }
+         return result;
+      }
+   }
+}

+ 293 - 0
src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParserBase.cs

@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Parsing
+{
+   public abstract class UnityTextParserBase
+   {
+      private static readonly HashSet<char> ValidTagNameChars = new HashSet<char>
+      {
+         'a', 'b', 'c', 'd','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','z',
+         'A', 'B', 'C', 'D','E','F','G','H','I','j','K','L','M','N','O','P','Q','R','S','T','U','V','X','Y','Z'
+      };
+      private HashSet<string> _ignored = new HashSet<string>();
+
+      public UnityTextParserBase()
+      {
+
+      }
+
+      protected void AddIgnoredTag( string name )
+      {
+         _ignored.Add( name );
+      }
+
+      public ParserResult Parse( string input )
+      {
+         StringBuilder textSinceLastChange = new StringBuilder();
+
+         StringBuilder template = new StringBuilder();
+         Dictionary<string, string> args = new Dictionary<string, string>();
+         bool ignoringCurrentTag = false;
+         char arg = 'A';
+         Stack<string> tags = new Stack<string>();
+
+         var state = ParsingState.Text;
+         for( int i = 0 ; i < input.Length ; i++ )
+         {
+            var c = input[ i ];
+            if( c != '<' && c != '>' )
+            {
+               textSinceLastChange.Append( c );
+            }
+
+            var previousState = state;
+            switch( previousState )
+            {
+               case ParsingState.Text:
+                  state = ParseText( input, ref i );
+                  break;
+               case ParsingState.NamingStartTag:
+                  state = ParseNamingStartTag( input, ref i );
+                  break;
+               case ParsingState.NamingEndTag:
+                  state = ParseNamingEndTag( input, ref i );
+                  break;
+               case ParsingState.FinishingStartTag:
+                  state = ParseFinishingStartTag( input, ref i );
+                  break;
+               case ParsingState.FinishingEndTag:
+                  state = ParseFinishingEndTag( input, ref i );
+                  break;
+               default:
+                  break;
+            }
+
+            bool stateChanged = state != previousState;
+            if( stateChanged )
+            {
+               // whenever the state changes, we want to add text, potentially
+               string text;
+               if( c == '<' || c == '>' )
+               {
+                  text = textSinceLastChange.ToString();
+                  textSinceLastChange = new StringBuilder();
+               }
+               else
+               {
+                  text = TakeAllButLast( textSinceLastChange );
+               }
+               switch( previousState )
+               {
+                  case ParsingState.Text:
+                     {
+                        if( !string.IsNullOrEmpty( text ) )
+                        {
+                           var key = "{{" + arg + "}}";
+                           arg++;
+
+                           args.Add( key, text );
+                           template.Append( key );
+                        }
+                     }
+                     break;
+                  case ParsingState.NamingStartTag:
+                     {
+                        ignoringCurrentTag = _ignored.Contains( text );
+                        tags.Push( text );
+
+                        if( !ignoringCurrentTag )
+                        {
+                           template.Append( "<" + text );
+                           if( state != ParsingState.FinishingStartTag )
+                           {
+                              template.Append( ">" );
+                           }
+                        }
+                     }
+                     break;
+                  case ParsingState.FinishingStartTag:
+                     {
+                        if( !ignoringCurrentTag )
+                        {
+                           template.Append( text + ">" );
+                        }
+                     }
+                     break;
+                  case ParsingState.NamingEndTag:
+                     {
+                        if( !ignoringCurrentTag )
+                        {
+                           template.Append( "<" + text );
+                        }
+
+                        if( state != ParsingState.FinishingEndTag )
+                        {
+                           if( !ignoringCurrentTag )
+                           {
+                              template.Append( ">" );
+                           }
+
+                           var tag = tags.Pop();
+                           ignoringCurrentTag = tags.Count > 0 && _ignored.Contains( tags.Peek() );
+                        }
+                     }
+                     break;
+                  case ParsingState.FinishingEndTag:
+                     {
+                        if( !ignoringCurrentTag )
+                        {
+                           template.Append( text + ">" );
+                        }
+
+                        var tag = tags.Pop();
+                        ignoringCurrentTag = tags.Count > 0 && _ignored.Contains( tags.Peek() );
+                     }
+                     break;
+               }
+            }
+         }
+
+         if( state == ParsingState.Text )
+         {
+            var text = textSinceLastChange.ToString();
+
+            if( !string.IsNullOrEmpty( text ) )
+            {
+               var key = "{{" + arg + "}}";
+               arg++;
+
+               args.Add( key, text );
+               template.Append( key );
+            }
+         }
+
+         // finally, lets merge some of the arguments together
+         var templateString = template.ToString();
+         int idx = -1;
+         while( ( idx = templateString.IndexOf( "}}{{" ) ) != -1 )
+         {
+            var arg1 = templateString[ idx - 1 ];
+            var arg2 = templateString[ idx + 4 ];
+
+            var key1 = "{{" + arg1 + "}}";
+            var key2 = "{{" + arg2 + "}}";
+
+            var text1 = args[ key1 ];
+            var text2 = args[ key2 ];
+
+            var fullText = text1 + text2;
+            var fullKey = key1 + key2;
+            var newKey = "{{" + ( ++arg ) + "}}";
+
+            args.Remove( key1 );
+            args.Remove( key2 );
+            args.Add( newKey, fullText );
+            templateString = templateString.Replace( fullKey, newKey );
+         }
+
+
+         return new ParserResult( input, templateString, args );
+      }
+
+      private string TakeAllButLast( StringBuilder builder )
+      {
+         if( builder.Length > 0 )
+         {
+            var str = builder.ToString( 0, builder.Length - 1 );
+            builder.Remove( 0, builder.Length - 1 );
+            return str;
+         }
+         return string.Empty;
+      }
+
+      private ParsingState ParseText( string s, ref int i )
+      {
+         if( s[ i ] == '<' )
+         {
+            if( i + 1 < s.Length && s[ i + 1 ] == '/' )
+            {
+               return ParsingState.NamingEndTag;
+            }
+            else
+            {
+               return ParsingState.NamingStartTag;
+            }
+         }
+         else
+         {
+            return ParsingState.Text;
+         }
+      }
+
+      private ParsingState ParseNamingStartTag( string s, ref int i )
+      {
+         if( ValidTagNameChars.Contains( s[ i ] ) )
+         {
+            return ParsingState.NamingStartTag;
+         }
+         else if( s[ i ] == '>' )
+         {
+            // we need to determine if we are inside or outside a tag after this!
+            return ParsingState.Text;
+         }
+         else
+         {
+            return ParsingState.FinishingStartTag;
+         }
+      }
+
+      private ParsingState ParseNamingEndTag( string s, ref int i )
+      {
+         if( ValidTagNameChars.Contains( s[ i ] ) )
+         {
+            return ParsingState.NamingEndTag;
+         }
+         else if( s[ i ] == '>' )
+         {
+            // we need to determine if we are inside or outside a tag after this!
+            return ParsingState.Text;
+         }
+         else
+         {
+            return ParsingState.FinishingEndTag;
+         }
+      }
+
+      private ParsingState ParseFinishingStartTag( string s, ref int i )
+      {
+         if( s[ i ] == '>' )
+         {
+            return ParsingState.Text;
+         }
+         else
+         {
+            return ParsingState.FinishingStartTag;
+         }
+      }
+
+      private ParsingState ParseFinishingEndTag( string s, ref int i )
+      {
+         if( s[ i ] == '>' )
+         {
+            return ParsingState.Text;
+         }
+         else
+         {
+            return ParsingState.FinishingEndTag;
+         }
+      }
+
+      private enum ParsingState
+      {
+         Text,
+         NamingStartTag,
+         NamingEndTag,
+         FinishingStartTag,
+         FinishingEndTag
+      }
+   }
+}

+ 22 - 0
src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Parsing
+{
+   public static class UnityTextParsers
+   {
+      private static readonly UtageTextParser UtageTextParser = new UtageTextParser();
+
+      public static UnityTextParserBase GetTextParserByGameEngine()
+      {
+         if( Types.AdvEngine != null )
+         {
+            return UtageTextParser;
+         }
+         return null;
+      }
+   }
+}

+ 10 - 0
src/XUnity.AutoTranslator.Plugin.Core/Parsing/UtageTextParser.cs

@@ -0,0 +1,10 @@
+namespace XUnity.AutoTranslator.Plugin.Core.Parsing
+{
+   public class UtageTextParser : UnityTextParserBase
+   {
+      public UtageTextParser()
+      {
+         AddIgnoredTag( "ruby" );
+      }
+   }
+}

+ 15 - 42
src/XUnity.AutoTranslator.Plugin.Core/TranslationInfo.cs

@@ -31,59 +31,32 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       public void ResizeUI( object graphic )
       {
-         if( graphic is Text )
-         {
-            var ui = (Text)graphic;
+         if( graphic == null ) return;
 
-            // 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;
+         var type = graphic.GetType();
 
-            // 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;
-            }
+         // 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 gui = (Text)g;
-               gui.horizontalOverflow = originalHorizontalOverflow;
+               var gtype = g.GetType();
+               gtype.GetProperty( MultiLinePropertyName )?.GetSetMethod()?.Invoke( g, new object[] { originalMultiLine } );
+               gtype.GetProperty( OverflowMethodPropertyName )?.GetSetMethod()?.Invoke( g, new object[] { originalOverflowMethod } );
             };
          }
-         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 )
       {
+         if( graphic == null ) return;
+
          _reset?.Invoke( graphic );
          _reset = null;
       }

+ 60 - 10
src/XUnity.AutoTranslator.Plugin.Core/TranslationJob.cs

@@ -5,38 +5,88 @@ using System.Net;
 using System.Text;
 using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
+using XUnity.AutoTranslator.Plugin.Core.Parsing;
 
 namespace XUnity.AutoTranslator.Plugin.Core
 {
    public class TranslationJob
    {
-      public TranslationJob( TranslationKeys key )
+      public TranslationJob( TranslationKey key )
       {
-         Keys = key;
+         Key = key;
 
          Components = new List<object>();
+         OriginalSources = new HashSet<object>();
+         Contexts = new HashSet<TranslationContext>();
       }
 
+      public HashSet<TranslationContext> Contexts { get; private set; }
+
       public List<object> Components { get; private set; }
-      
-      public TranslationKeys Keys { get; private set; }
+
+      public HashSet<object> OriginalSources { get; private set; }
+
+      public TranslationKey Key { get; private set; }
 
       public string TranslatedText { get; set; }
 
-      public bool AnyComponentsStillHasOriginalUntranslatedText()
+      public TranslationJobState State { get; set; }
+
+      public bool AnyComponentsStillHasOriginalUntranslatedTextOrContextual()
       {
-         if( Components.Count == 0 ) return true; // we do not know
+         if( Components.Count == 0 || Contexts.Count > 0 ) return true; // we do not know
 
-         foreach( var component in Components )
+         for( int i = Components.Count - 1 ; i >= 0 ; i-- )
          {
-            var text = component.GetText().Trim();
-            if( text == Keys.OriginalText )
+            var component = Components[ i ];
+            try
+            {
+               var text = component.GetText().TrimIfConfigured(); 
+               if( text == Key.OriginalText )
+               {
+                  return true;
+               }
+            }
+            catch( NullReferenceException )
             {
-               return true;
+               // might fail if compoent is no longer associated to game
+               Components.RemoveAt( i );
             }
          }
 
          return false;
       }
+
+      public void Associate( TranslationContext context )
+      {
+         if( context != null )
+         {
+            Contexts.Add( context );
+            context.Jobs.Add( this );
+         }
+      }
+   }
+
+   public enum TranslationJobState
+   {
+      RunningOrQueued,
+      Succeeded,
+      Failed
+   }
+
+   public class TranslationContext
+   {
+      public TranslationContext( object component, ParserResult result )
+      {
+         Jobs = new HashSet<TranslationJob>();
+         Component = component;
+         Result = result;
+      }
+
+      public ParserResult Result { get; private set; }
+
+      public HashSet<TranslationJob> Jobs { get; private set; }
+
+      public object Component { get; private set; }
    }
 }

+ 70 - 0
src/XUnity.AutoTranslator.Plugin.Core/TranslationKey.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Extensions;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   public struct TranslationKey
+   {
+      public TranslationKey( string key, bool templatizeByNumbers, bool neverRemoveWhitespace = false )
+      {
+         OriginalText = key;
+
+         if( !neverRemoveWhitespace && Settings.IgnoreWhitespaceInDialogue && key.Length > Settings.MinDialogueChars )
+         {
+            RelevantText = key.RemoveWhitespace();
+         }
+         else
+         {
+            RelevantText = key;
+         }
+
+         if( templatizeByNumbers )
+         {
+            TemplatedText = RelevantText.TemplatizeByNumbers();
+         }
+         else
+         {
+            TemplatedText = null;
+         }
+      }
+
+      public TemplatedString TemplatedText { get; }
+
+      public string RelevantText { get; }
+
+      public string OriginalText { get; set; }
+
+      public string GetDictionaryLookupKey()
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.Template;
+         }
+         return RelevantText;
+      }
+
+      public string Untemplate( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.Untemplate( text );
+         }
+
+         return text;
+      }
+
+      public string RepairTemplate( string text )
+      {
+         if( TemplatedText != null )
+         {
+            return TemplatedText.RepairTemplate( text );
+         }
+
+         return text;
+      }
+   }
+}

+ 0 - 70
src/XUnity.AutoTranslator.Plugin.Core/TranslationKeys.cs

@@ -1,70 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using XUnity.AutoTranslator.Plugin.Core.Configuration;
-using XUnity.AutoTranslator.Plugin.Core.Extensions;
-
-namespace XUnity.AutoTranslator.Plugin.Core
-{
-   public struct TranslationKeys
-   {
-      public TranslationKeys( string key, bool templatizeByNumbers )
-      {
-         OriginalText = key;
-
-         if( Settings.IgnoreWhitespaceInDialogue && key.Length > Settings.MinDialogueChars )
-         {
-            RelevantKey = key.RemoveWhitespace();
-         }
-         else
-         {
-            RelevantKey = key;
-         }
-
-         if( templatizeByNumbers )
-         {
-            TemplatedKey = RelevantKey.TemplatizeByNumbers();
-         }
-         else
-         {
-            TemplatedKey = null;
-         }
-      }
-
-      public TemplatedString TemplatedKey { get; }
-
-      public string RelevantKey { get; }
-
-      public string OriginalText { get; set; }
-
-      public string GetDictionaryLookupKey()
-      {
-         if( TemplatedKey != null )
-         {
-            return TemplatedKey.Template;
-         }
-         return RelevantKey;
-      }
-
-      public string Untemplate( string text )
-      {
-         if( TemplatedKey != null )
-         {
-            return TemplatedKey.Untemplate( text );
-         }
-
-         return text;
-      }
-
-      public string RepairTemplate( string text )
-      {
-         if( TemplatedKey != null )
-         {
-            return TemplatedKey.RepairTemplate( text );
-         }
-
-         return text;
-      }
-   }
-}

+ 72 - 0
src/XUnity.AutoTranslator.Plugin.Core/UtageSupport/UtageHelpers.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace XUnity.AutoTranslator.Plugin.Core.UtageSupport
+{
+   public static class UtageHelpers
+   {
+      private static object AdvManager;
+      private static HashSet<string> Labels = new HashSet<string>();
+
+      public static void FixLabel( ref string label )
+      {
+         var empty = new object[ 0 ];
+         if( AdvManager == null )
+         {
+            try
+            {
+               AdvManager = GameObject.FindObjectOfType( Constants.Types.AdvDataManager );
+               var ScenarioDataTblProperty = Constants.Types.AdvDataManager.GetProperty( "ScenarioDataTbl" );
+               var ScenarioDataTbl = ScenarioDataTblProperty.GetValue( AdvManager, empty );
+               foreach( object labelToAdvScenarioDataKeyValuePair in (IEnumerable)ScenarioDataTbl )
+               {
+                  var labelToAdvScenarioDataKeyValuePairType = typeof( KeyValuePair<,> )
+                     .MakeGenericType( new Type[] { typeof( string ), Constants.Types.AdvScenarioData } );
+
+                  var AdvScenarioDataKey = (string)labelToAdvScenarioDataKeyValuePairType.GetProperty( "Key" )
+                     .GetValue( labelToAdvScenarioDataKeyValuePair, empty );
+
+                  Labels.Add( AdvScenarioDataKey );
+
+                  var AdvScenarioData = labelToAdvScenarioDataKeyValuePairType.GetProperty( "Value" )
+                     .GetValue( labelToAdvScenarioDataKeyValuePair, empty );
+
+                  if( AdvScenarioData != null )
+                  {
+                     var ScenarioLabelsProperty = AdvScenarioData.GetType().GetProperty( "ScenarioLabels" );
+
+                     var labelToAdvScenarioLabelData = ScenarioLabelsProperty.GetValue( AdvScenarioData, empty );
+
+                     foreach( object labelToAdvScenarioLabelDataKeyValuePair in (IEnumerable)labelToAdvScenarioLabelData )
+                     {
+                        var labelToAdvScenarioLabelDataKeyValuePairType = typeof( KeyValuePair<,> )
+                           .MakeGenericType( new Type[] { typeof( string ), Constants.Types.AdvScenarioLabelData } );
+
+                        var AdvScenarioLabelDataKey = (string)labelToAdvScenarioLabelDataKeyValuePairType.GetProperty( "Key" )
+                           .GetValue( labelToAdvScenarioLabelDataKeyValuePair, empty );
+
+                        Labels.Add( AdvScenarioLabelDataKey );
+                     }
+                  }
+               }
+            }
+            catch( Exception e )
+            {
+               Logger.Current.Warn( e, "An error occurred while setting up scenario set." );
+            }
+         }
+
+         if( !Labels.Contains( label ) )
+         {
+            if( AutoTranslationPlugin.Current.TryGetReverseTranslation( label, out string key ) )
+            {
+               label = key;
+            }
+         }
+      }
+   }
+}

+ 15 - 9
src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs

@@ -24,10 +24,17 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
 
       public static bool ContainsJapaneseSymbols( string text )
       {
-         // Japenese regex: [\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]
+         // Unicode Kanji Table:
+         // http://www.rikai.com/library/kanjitables/kanji_codes.unicode.shtml
          foreach( var c in text )
          {
-            if( ( c >= '\u3040' && c <= '\u30ff' ) || ( c >= '\uff00' && c <= '\uff9f' ) || ( c >= '\u4e00' && c <= '\u9faf' ) || ( c >= '\u3400' && c <= '\u4dbf' ) )
+            if( ( c >= '\u3021' && c <= '\u3029' ) // kana-like symbols
+               || ( c >= '\u3031' && c <= '\u3035' ) // kana-like symbols
+               || ( c >= '\u3041' && c <= '\u3096' ) // hiragana
+               || ( c >= '\u30a1' && c <= '\u30fa' ) // katakana
+               || ( c >= '\uff66' && c <= '\uff9d' ) // half-width katakana
+               || ( c >= '\u4e00' && c <= '\u9faf' ) // CJK unifed ideographs - Common and uncommon kanji
+               || ( c >= '\u3400' && c <= '\u4db5' ) ) // CJK unified ideographs Extension A - Rare kanji ( 3400 - 4dbf)
             {
                return true;
             }
@@ -42,10 +49,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
       /// </summary>
       public static string Decode( string text )
       {
-         // Remove these in newer version
-         text = text.Replace( "\\r", "\r" );
-         text = text.Replace( "\\n", "\n" );
-         return text;
+         return text.Replace( "\\r", "\r" )
+            .Replace( "\\n", "\n" )
+            .Replace( "%3D", "=" );
       }
 
       /// <summary>
@@ -55,9 +61,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
       /// </summary>
       public static string Encode( string text )
       {
-         text = text.Replace( "\r", "\\r" );
-         text = text.Replace( "\n", "\\n" );
-         return text;
+         return text.Replace( "\r", "\\r" )
+            .Replace( "\n", "\\n" )
+            .Replace( "=", "%3D" );
       }
    }
 }

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

@@ -31,13 +31,19 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
       public GoogleTranslateEndpoint()
       {
          _cookieContainer = new CookieContainer();
+
+         // Configure service points / service point manager
          ServicePointManager.ServerCertificateValidationCallback += Security.AlwaysAllowByHosts( "translate.google.com", "translate.googleapis.com" );
+         SetupServicePoints( "https://translate.googleapis.com", "https://translate.google.com" );
       }
 
+      public override bool SupportsLineSplitting => true;
+
       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/67.0.3396.99 Safari/537.36";
          headers[ HttpRequestHeader.Accept ] = "*/*";
+         headers[ HttpRequestHeader.Referer ] = "https://translate.google.com/";
       }
 
       public override IEnumerator OnBeforeTranslate( int translationCount )
@@ -67,6 +73,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
          try
          {
             ApplyHeaders( client.Headers );
+            client.Headers.Remove( HttpRequestHeader.Referer );
             downloadResult = client.GetDownloadResult( new Uri( HttpsTranslateUserSite ) );
          }
          catch( Exception e )

+ 70 - 41
src/XUnity.AutoTranslator.Plugin.Core/Web/KnownHttpEndpoint.cs

@@ -1,34 +1,51 @@
 using System;
 using System.Collections;
 using System.Net;
+using System.Threading;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Web
 {
    public abstract class KnownHttpEndpoint : IKnownEndpoint
    {
-      private static readonly TimeSpan MaxUnusedLifespan = TimeSpan.FromSeconds( 20 );
+      private static readonly TimeSpan MaxUnusedLifespan = TimeSpan.FromSeconds( 25 );
 
-      private static int _runningTranslations = 0;
-      private static int _maxConcurrency = 1;
-      private static bool _isSettingUp = false;
+      private ServicePoint[] _servicePoints;
+      private bool _isBusy = false;
       private UnityWebClient _client;
-      private DateTime _clientLastUse = DateTime.UtcNow;
+      private DateTime? _clientLastUse = null;
 
       public KnownHttpEndpoint()
       {
       }
 
-      public bool IsBusy => _isSettingUp || _runningTranslations >= _maxConcurrency;
+      public bool IsBusy => _isBusy;
 
-      public IEnumerator Translate( string untranslatedText, string from, string to, Action<string> success, Action failure )
+      public virtual bool SupportsLineSplitting
       {
-         _clientLastUse = DateTime.UtcNow;
+         get
+         {
+            return false;
+         }
+      }
 
-         try
+      protected void SetupServicePoints( params string[] endpoints )
+      {
+         _servicePoints = new ServicePoint[ endpoints.Length ];
+         
+         for( int i = 0 ; i < endpoints.Length ; i++ )
          {
-            _isSettingUp = true;
+            var endpoint = endpoints[ i ];
+            var servicePoint = ServicePointManager.FindServicePoint( new Uri( endpoint ) );
+            _servicePoints[ i ] = servicePoint;
+         }
+      }
 
+      public IEnumerator Translate( string untranslatedText, string from, string to, Action<string> success, Action failure )
+      {
+         _isBusy = true;
+         try
+         {
             var setup = OnBeforeTranslate( Settings.TranslationCount );
             if( setup != null )
             {
@@ -37,33 +54,23 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
                   yield return setup.Current;
                }
             }
-         }
-         finally
-         {
-            _isSettingUp = false;
-         }
-
-         Logger.Current.Debug( "Starting translation for: " + untranslatedText );
-         DownloadResult result = null;
-         try
-         {
-            var client = GetClient();
-            var url = GetServiceUrl( untranslatedText, from, to );
-            ApplyHeaders( client.Headers );
-            result = client.GetDownloadResult( new Uri( url ) );
-         }
-         catch( Exception e )
-         {
-            Logger.Current.Error( e, "Error occurred while setting up translation request." );
-         }
-
-         if( result != null )
-         {
+            Logger.Current.Debug( "Starting translation for: " + untranslatedText );
+            DownloadResult result = null;
             try
             {
-               _runningTranslations++;
+               var client = GetClient();
+               var url = GetServiceUrl( untranslatedText, from, to );
+               ApplyHeaders( client.Headers );
+               result = client.GetDownloadResult( new Uri( url ) );
+            }
+            catch( Exception e )
+            {
+               Logger.Current.Error( e, "Error occurred while setting up translation request." );
+            }
+
+            if( result != null )
+            {
                yield return result;
-               _runningTranslations--;
 
                try
                {
@@ -94,21 +101,43 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
                   failure();
                }
             }
-            finally
+            else
             {
-               _clientLastUse = DateTime.UtcNow;
+               failure();
             }
          }
+         finally
+         {
+            _clientLastUse = DateTime.UtcNow;
+            _isBusy = false;
+         }
       }
 
       public virtual void OnUpdate()
       {
-         var client = _client;
-         if( client != null && DateTime.UtcNow - _clientLastUse > MaxUnusedLifespan && !client.IsBusy )
+         if( !_isBusy && _clientLastUse.HasValue && DateTime.UtcNow - _clientLastUse > MaxUnusedLifespan && !_client.IsBusy
+            && _servicePoints != null && _servicePoints.Length > 0 )
          {
-            _client = null;
-            client.Dispose();
-            Logger.Current.Debug( "Disposing WebClient because it was not used for 20 seconds." );
+            Logger.Current.Debug( "Closing service points because they were not used for 25 seconds." );
+
+            _isBusy = true;
+            _clientLastUse = null;
+
+            ThreadPool.QueueUserWorkItem( delegate ( object state )
+            {
+               // never do a job like this on the game loop thread
+               try
+               {
+                  foreach( var servicePoint in _servicePoints )
+                  {
+                     servicePoint.CloseConnectionGroup( MyWebClient.ConnectionGroupName );
+                  }
+               }
+               finally
+               {
+                  _isBusy = false;
+               }
+            } );
          }
       }
 

+ 68 - 61
src/XUnity.AutoTranslator.Plugin.Core/Web/KnownWwwEndpoint.cs

@@ -13,22 +13,27 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
    {
       protected static readonly ConstructorInfo WwwConstructor = Constants.Types.WWW.GetConstructor( new[] { typeof( string ), typeof( byte[] ), typeof( Dictionary<string, string> ) } );
 
-      private static int _runningTranslations = 0;
-      private static int _maxConcurrency = 1;
-      private static bool _isSettingUp = false;
+      private bool _isBusy = false;
 
       public KnownWwwEndpoint()
       {
       }
 
-      public bool IsBusy => _isSettingUp || _runningTranslations >= _maxConcurrency;
+      public bool IsBusy => _isBusy;
+
+      public virtual bool SupportsLineSplitting
+      {
+         get
+         {
+            return false;
+         }
+      }
 
       public IEnumerator Translate( string untranslatedText, string from, string to, Action<string> success, Action failure )
       {
+         _isBusy = true;
          try
          {
-            _isSettingUp = true;
-
             var setup = OnBeforeTranslate( Settings.TranslationCount );
             if( setup != null )
             {
@@ -37,81 +42,83 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
                   yield return setup.Current;
                }
             }
-         }
-         finally
-         {
-            _isSettingUp = false;
-         }
-
-         Logger.Current.Debug( "Starting translation for: " + untranslatedText );
-         object www = null;
-         try
-         {
-            var headers = new Dictionary<string, string>();
-            ApplyHeaders( headers );
-            var url = GetServiceUrl( untranslatedText, from, to );
-            www = WwwConstructor.Invoke( new object[] { url, null, headers } );
-         }
-         catch( Exception e )
-         {
-            Logger.Current.Error( e, "Error occurred while setting up translation request." );
-         }
-
-         if( www != null )
-         {
-            _runningTranslations++;
-            yield return www;
-            _runningTranslations--;
 
+            Logger.Current.Debug( "Starting translation for: " + untranslatedText );
+            object www = null;
             try
             {
-               string error = null;
-               try
-               {
-                  error = (string)AccessTools.Property( Constants.Types.WWW, "error" ).GetValue( www, null );
-               }
-               catch( Exception e )
-               {
-                  error = e.ToString();
-               }
+               var headers = new Dictionary<string, string>();
+               ApplyHeaders( headers );
+               var url = GetServiceUrl( untranslatedText, from, to );
+               www = WwwConstructor.Invoke( new object[] { url, null, headers } );
+            }
+            catch( Exception e )
+            {
+               Logger.Current.Error( e, "Error occurred while setting up translation request." );
+            }
 
-               if( error != null )
-               {
-                  Logger.Current.Error( "Error occurred while retrieving translation." + Environment.NewLine + error );
-                  failure();
-               }
-               else
+            if( www != null )
+            {
+               yield return www;
+
+               try
                {
-                  var text = (string)AccessTools.Property( Constants.Types.WWW, "text" ).GetValue( www, null ); ;
+                  string error = null;
+                  try
+                  {
+                     error = (string)AccessTools.Property( Constants.Types.WWW, "error" ).GetValue( www, null );
+                  }
+                  catch( Exception e )
+                  {
+                     error = e.ToString();
+                  }
 
-                  if( text != null )
+                  if( error != null )
                   {
-                     if( TryExtractTranslated( text, out var translatedText ) )
-                     {
-                        Logger.Current.Debug( $"Translation for '{untranslatedText}' succeded. Result: {translatedText}" );
+                     Logger.Current.Error( "Error occurred while retrieving translation." + Environment.NewLine + error );
+                     failure();
+                  }
+                  else
+                  {
+                     var text = (string)AccessTools.Property( Constants.Types.WWW, "text" ).GetValue( www, null ); ;
 
-                        translatedText = translatedText ?? string.Empty;
-                        success( translatedText );
+                     if( text != null )
+                     {
+                        if( TryExtractTranslated( text, out var translatedText ) )
+                        {
+                           Logger.Current.Debug( $"Translation for '{untranslatedText}' succeded. Result: {translatedText}" );
+
+                           translatedText = translatedText ?? string.Empty;
+                           success( translatedText );
+                        }
+                        else
+                        {
+                           Logger.Current.Error( "Error occurred while extracting translation." );
+                           failure();
+                        }
                      }
                      else
                      {
-                        Logger.Current.Error( "Error occurred while extracting translation." );
+                        Logger.Current.Error( "Error occurred while extracting text from response." );
                         failure();
                      }
                   }
-                  else
-                  {
-                     Logger.Current.Error( "Error occurred while extracting text from response." );
-                     failure();
-                  }
+               }
+               catch( Exception e )
+               {
+                  Logger.Current.Error( e, "Error occurred while retrieving translation." );
+                  failure();
                }
             }
-            catch( Exception e )
+            else
             {
-               Logger.Current.Error( e, "Error occurred while retrieving translation." );
                failure();
             }
          }
+         finally
+         {
+            _isBusy = false;
+         }
       }
 
       public virtual void OnUpdate()

+ 1695 - 0
src/XUnity.AutoTranslator.Plugin.Core/Web/MyWebClient.cs

@@ -0,0 +1,1695 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Cache;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Web
+{
+   public delegate void MyDownloadStringCompletedEventHandler( object sender, MyDownloadStringCompletedEventArgs e );
+
+   public class MyDownloadStringCompletedEventArgs : AsyncCompletedEventArgs
+   {
+      private string result;
+
+      internal MyDownloadStringCompletedEventArgs( string result, Exception error, bool cancelled, object userState ) : base( error, cancelled, userState )
+      {
+         this.result = result;
+      }
+
+      public string Result
+      {
+         get
+         {
+            base.RaiseExceptionIfNecessary();
+            return this.result;
+         }
+      }
+   }
+
+   public class MyAsyncCompletedEventArgs : EventArgs
+   {
+      private bool _cancelled;
+      private Exception _error;
+      private object _userState;
+
+      public MyAsyncCompletedEventArgs( Exception error, bool cancelled, object userState )
+      {
+         this._error = error;
+         this._cancelled = cancelled;
+         this._userState = userState;
+      }
+
+      protected void RaiseExceptionIfNecessary()
+      {
+         if( this._error != null )
+         {
+            throw new TargetInvocationException( this._error );
+         }
+         if( this._cancelled )
+         {
+            throw new InvalidOperationException( "The operation was cancelled" );
+         }
+      }
+
+      public bool Cancelled
+      {
+         get
+         {
+            return this._cancelled;
+         }
+      }
+
+      public Exception Error
+      {
+         get
+         {
+            return this._error;
+         }
+      }
+
+      public object UserState
+      {
+         get
+         {
+            return this._userState;
+         }
+      }
+   }
+
+   public class MyWebClient
+   {
+      public static readonly string ConnectionGroupName = Guid.NewGuid().ToString();
+
+      private bool async;
+      private Uri baseAddress;
+      private string baseString;
+      private ICredentials credentials;
+      private System.Text.Encoding encoding = System.Text.Encoding.Default;
+      private WebHeaderCollection headers;
+      private static byte[] hexBytes = new byte[ 0x10 ];
+      private bool is_busy;
+      private IWebProxy proxy;
+      private NameValueCollection queryString;
+      private WebHeaderCollection responseHeaders;
+      private static readonly string urlEncodedCType = "application/x-www-form-urlencoded";
+
+      public event MyDownloadStringCompletedEventHandler DownloadStringCompleted;
+
+      static MyWebClient()
+      {
+         int index = 0;
+         int num2 = 0x30;
+         while( num2 <= 0x39 )
+         {
+            hexBytes[ index ] = (byte)num2;
+            num2++;
+            index++;
+         }
+         int num3 = 0x61;
+         while( num3 <= 0x66 )
+         {
+            hexBytes[ index ] = (byte)num3;
+            num3++;
+            index++;
+         }
+      }
+
+      //public void CancelAsync()
+      //{
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      if( this.async_thread != null )
+      //      {
+      //         Thread thread = this.async_thread;
+      //         this.CompleteAsync();
+      //         thread.Interrupt();
+      //      }
+      //   }
+      //}
+
+      private void CheckBusy()
+      {
+         if( this.IsBusy )
+         {
+            throw new NotSupportedException( "WebClient does not support conccurent I/O operations." );
+         }
+      }
+
+      private void CompleteAsync()
+      {
+         MyWebClient client = this;
+         lock( client )
+         {
+            this.is_busy = false;
+         }
+      }
+
+      private Uri CreateUri( string address )
+      {
+         return this.MakeUri( address );
+      }
+
+      private Uri CreateUri( Uri address )
+      {
+         string query = address.Query;
+         if( string.IsNullOrEmpty( query ) )
+         {
+            query = this.GetQueryString( true );
+         }
+         if( ( this.baseAddress == null ) && ( query == null ) )
+         {
+            return address;
+         }
+         if( this.baseAddress == null )
+         {
+            return new Uri( address.ToString() + query, query != null );
+         }
+         if( query == null )
+         {
+            return new Uri( this.baseAddress, address.ToString() );
+         }
+         return new Uri( this.baseAddress, address.ToString() + query, query != null );
+      }
+
+      private string DetermineMethod( Uri address, string method, bool is_upload )
+      {
+         if( method != null )
+         {
+            return method;
+         }
+         if( address.Scheme == Uri.UriSchemeFtp )
+         {
+            return ( !is_upload ? "RETR" : "STOR" );
+         }
+         return ( !is_upload ? "GET" : "POST" );
+      }
+
+      public byte[] DownloadData( string address )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.DownloadData( this.CreateUri( address ) );
+      }
+
+      public byte[] DownloadData( Uri address )
+      {
+         byte[] buffer;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            buffer = this.DownloadDataCore( address, null );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return buffer;
+      }
+
+      //public void DownloadDataAsync( Uri address )
+      //{
+      //   this.DownloadDataAsync( address, null );
+      //}
+
+      //public void DownloadDataAsync( Uri address, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            byte[] result = this.DownloadDataCore( (Uri)objArray[ 0 ], objArray[ 1 ] );
+      //            this.OnDownloadDataCompleted( new DownloadDataCompletedEventArgs( result, null, false, objArray[ 1 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnDownloadDataCompleted( new DownloadDataCompletedEventArgs( null, null, true, objArray[ 1 ] ) );
+      //            throw;
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnDownloadDataCompleted( new DownloadDataCompletedEventArgs( null, exception, false, objArray[ 1 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      private byte[] DownloadDataCore( Uri address, object userToken )
+      {
+         WebRequest request = null;
+         byte[] buffer;
+         try
+         {
+            request = this.SetupRequest( address );
+            WebResponse webResponse = this.GetWebResponse( request );
+            Stream responseStream = webResponse.GetResponseStream();
+            buffer = this.ReadAll( responseStream, (int)webResponse.ContentLength, userToken );
+            responseStream.Close();
+            webResponse.Close();
+         }
+         catch( ThreadInterruptedException )
+         {
+            if( request != null )
+            {
+               request.Abort();
+            }
+            throw;
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception2 )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+         }
+         return buffer;
+      }
+
+      //public void DownloadFile( string address, string fileName )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   this.DownloadFile( this.CreateUri( address ), fileName );
+      //}
+
+      //public void DownloadFile( Uri address, string fileName )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( fileName == null )
+      //   {
+      //      throw new ArgumentNullException( "fileName" );
+      //   }
+      //   try
+      //   {
+      //      this.SetBusy();
+      //      this.async = false;
+      //      this.DownloadFileCore( address, fileName, null );
+      //   }
+      //   catch( WebException )
+      //   {
+      //      throw;
+      //   }
+      //   catch( Exception exception2 )
+      //   {
+      //      throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+      //   }
+      //   finally
+      //   {
+      //      this.is_busy = false;
+      //   }
+      //}
+
+      //public void DownloadFileAsync( Uri address, string fileName )
+      //{
+      //   this.DownloadFileAsync( address, fileName, null );
+      //}
+
+      //public void DownloadFileAsync( Uri address, string fileName, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( fileName == null )
+      //   {
+      //      throw new ArgumentNullException( "fileName" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, fileName, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            this.DownloadFileCore( (Uri)objArray[ 0 ], (string)objArray[ 1 ], objArray[ 2 ] );
+      //            this.OnDownloadFileCompleted( new AsyncCompletedEventArgs( null, false, objArray[ 2 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnDownloadFileCompleted( new AsyncCompletedEventArgs( null, true, objArray[ 2 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnDownloadFileCompleted( new AsyncCompletedEventArgs( exception, false, objArray[ 2 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      //private void DownloadFileCore( Uri address, string fileName, object userToken )
+      //{
+      //   WebRequest request = null;
+      //   FileStream stream = new FileStream( fileName, FileMode.Create );
+      //   try
+      //   {
+      //      request = this.SetupRequest( address );
+      //      WebResponse webResponse = this.GetWebResponse( request );
+      //      Stream responseStream = webResponse.GetResponseStream();
+      //      int contentLength = (int)webResponse.ContentLength;
+      //      int count = ( ( contentLength > -1 ) && ( contentLength <= 0x8000 ) ) ? contentLength : 0x8000;
+      //      byte[] buffer = new byte[ count ];
+      //      int num3 = 0;
+      //      long bytesReceived = 0L;
+      //      while( ( num3 = responseStream.Read( buffer, 0, count ) ) != 0 )
+      //      {
+      //         if( this.async )
+      //         {
+      //            bytesReceived += num3;
+      //            this.OnDownloadProgressChanged( new DownloadProgressChangedEventArgs( bytesReceived, webResponse.ContentLength, userToken ) );
+      //         }
+      //         stream.Write( buffer, 0, num3 );
+      //      }
+      //   }
+      //   catch( ThreadInterruptedException )
+      //   {
+      //      if( request != null )
+      //      {
+      //         request.Abort();
+      //      }
+      //      throw;
+      //   }
+      //   finally
+      //   {
+      //      if( stream != null )
+      //      {
+      //         stream.Dispose();
+      //      }
+      //   }
+      //}
+
+      public string DownloadString( string address )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.encoding.GetString( this.DownloadData( this.CreateUri( address ) ) );
+      }
+
+      public string DownloadString( Uri address )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.encoding.GetString( this.DownloadData( this.CreateUri( address ) ) );
+      }
+
+      public void DownloadStringAsync( Uri address )
+      {
+         this.DownloadStringAsync( address, null );
+      }
+
+      public void DownloadStringAsync( Uri address, object userToken )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         MyWebClient client = this;
+         lock( client )
+         {
+            this.SetBusy();
+            this.async = true;
+            object[] parameter = new object[] { address, userToken };
+            ThreadPool.QueueUserWorkItem( delegate ( object state )
+            {
+               object[] objArray = (object[])state;
+               try
+               {
+                  string result = this.encoding.GetString( this.DownloadDataCore( (Uri)objArray[ 0 ], objArray[ 1 ] ) );
+                  this.OnDownloadStringCompleted( new MyDownloadStringCompletedEventArgs( result, null, false, objArray[ 1 ] ) );
+               }
+               catch( ThreadInterruptedException )
+               {
+                  this.OnDownloadStringCompleted( new MyDownloadStringCompletedEventArgs( null, null, true, objArray[ 1 ] ) );
+               }
+               catch( Exception exception )
+               {
+                  this.OnDownloadStringCompleted( new MyDownloadStringCompletedEventArgs( null, exception, false, objArray[ 1 ] ) );
+               }
+            }, parameter );
+         }
+      }
+
+      private static Exception GetMustImplement()
+      {
+         return new NotImplementedException();
+      }
+
+      private string GetQueryString( bool add_qmark )
+      {
+         if( ( this.queryString == null ) || ( this.queryString.Count == 0 ) )
+         {
+            return null;
+         }
+         StringBuilder builder = new StringBuilder();
+         if( add_qmark )
+         {
+            builder.Append( '?' );
+         }
+         IEnumerator enumerator = this.queryString.GetEnumerator();
+         try
+         {
+            while( enumerator.MoveNext() )
+            {
+               string current = (string)enumerator.Current;
+               builder.AppendFormat( "{0}={1}&", current, this.UrlEncode( this.queryString[ current ] ) );
+            }
+         }
+         finally
+         {
+            IDisposable disposable = enumerator as IDisposable;
+            if( disposable != null )
+            {
+               disposable.Dispose();
+            }
+         }
+         if( builder.Length != 0 )
+         {
+            builder.Length--;
+         }
+         if( builder.Length == 0 )
+         {
+            return null;
+         }
+         return builder.ToString();
+      }
+
+      protected virtual WebRequest GetWebRequest( Uri address )
+      {
+         return WebRequest.Create( address );
+      }
+
+      protected virtual WebResponse GetWebResponse( WebRequest request )
+      {
+         WebResponse response = request.GetResponse();
+         this.responseHeaders = response.Headers;
+         return response;
+      }
+
+      protected virtual WebResponse GetWebResponse( WebRequest request, IAsyncResult result )
+      {
+         WebResponse response = request.EndGetResponse( result );
+         this.responseHeaders = response.Headers;
+         return response;
+      }
+
+      private Uri MakeUri( string path )
+      {
+         string queryString = this.GetQueryString( true );
+         if( ( this.baseAddress == null ) && ( queryString == null ) )
+         {
+            try
+            {
+               return new Uri( path );
+            }
+            catch( ArgumentNullException )
+            {
+               path = Path.GetFullPath( path );
+               return new Uri( "file://" + path );
+            }
+            catch( UriFormatException )
+            {
+               path = Path.GetFullPath( path );
+               return new Uri( "file://" + path );
+            }
+         }
+         if( this.baseAddress == null )
+         {
+            return new Uri( path + queryString, queryString != null );
+         }
+         if( queryString == null )
+         {
+            return new Uri( this.baseAddress, path );
+         }
+         return new Uri( this.baseAddress, path + queryString, queryString != null );
+      }
+
+      //protected virtual void OnDownloadDataCompleted( DownloadDataCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.DownloadDataCompleted != null )
+      //   {
+      //      this.DownloadDataCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnDownloadFileCompleted( AsyncCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.DownloadFileCompleted != null )
+      //   {
+      //      this.DownloadFileCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnDownloadProgressChanged( DownloadProgressChangedEventArgs e )
+      //{
+      //   if( this.DownloadProgressChanged != null )
+      //   {
+      //      this.DownloadProgressChanged( this, e );
+      //   }
+      //}
+
+      protected virtual void OnDownloadStringCompleted( MyDownloadStringCompletedEventArgs args )
+      {
+         this.CompleteAsync();
+         if( this.DownloadStringCompleted != null )
+         {
+            this.DownloadStringCompleted( this, args );
+         }
+      }
+
+      //protected virtual void OnOpenReadCompleted( OpenReadCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.OpenReadCompleted != null )
+      //   {
+      //      this.OpenReadCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnOpenWriteCompleted( OpenWriteCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.OpenWriteCompleted != null )
+      //   {
+      //      this.OpenWriteCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnUploadDataCompleted( UploadDataCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.UploadDataCompleted != null )
+      //   {
+      //      this.UploadDataCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnUploadFileCompleted( UploadFileCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.UploadFileCompleted != null )
+      //   {
+      //      this.UploadFileCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnUploadProgressChanged( UploadProgressChangedEventArgs e )
+      //{
+      //   if( this.UploadProgressChanged != null )
+      //   {
+      //      this.UploadProgressChanged( this, e );
+      //   }
+      //}
+
+      //protected virtual void OnUploadStringCompleted( UploadStringCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.UploadStringCompleted != null )
+      //   {
+      //      this.UploadStringCompleted( this, args );
+      //   }
+      //}
+
+      //protected virtual void OnUploadValuesCompleted( UploadValuesCompletedEventArgs args )
+      //{
+      //   this.CompleteAsync();
+      //   if( this.UploadValuesCompleted != null )
+      //   {
+      //      this.UploadValuesCompleted( this, args );
+      //   }
+      //}
+
+      public Stream OpenRead( string address )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.OpenRead( this.CreateUri( address ) );
+      }
+
+      public Stream OpenRead( Uri address )
+      {
+         Stream responseStream;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         WebRequest request = null;
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            request = this.SetupRequest( address );
+            responseStream = this.GetWebResponse( request ).GetResponseStream();
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception2 )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return responseStream;
+      }
+
+      //public void OpenReadAsync( Uri address )
+      //{
+      //   this.OpenReadAsync( address, null );
+      //}
+
+      //public void OpenReadAsync( Uri address, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         WebRequest request = null;
+      //         try
+      //         {
+      //            request = this.SetupRequest( (Uri)objArray[ 0 ] );
+      //            Stream result = this.GetWebResponse( request ).GetResponseStream();
+      //            this.OnOpenReadCompleted( new OpenReadCompletedEventArgs( result, null, false, objArray[ 1 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            if( request != null )
+      //            {
+      //               request.Abort();
+      //            }
+      //            this.OnOpenReadCompleted( new OpenReadCompletedEventArgs( null, null, true, objArray[ 1 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnOpenReadCompleted( new OpenReadCompletedEventArgs( null, exception, false, objArray[ 1 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      public Stream OpenWrite( string address )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.OpenWrite( this.CreateUri( address ) );
+      }
+
+      public Stream OpenWrite( Uri address )
+      {
+         return this.OpenWrite( address, null );
+      }
+
+      public Stream OpenWrite( string address, string method )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.OpenWrite( this.CreateUri( address ), method );
+      }
+
+      public Stream OpenWrite( Uri address, string method )
+      {
+         Stream requestStream;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            requestStream = this.SetupRequest( address, method, true ).GetRequestStream();
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception2 )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return requestStream;
+      }
+
+      //public void OpenWriteAsync( Uri address )
+      //{
+      //   this.OpenWriteAsync( address, null );
+      //}
+
+      //public void OpenWriteAsync( Uri address, string method )
+      //{
+      //   this.OpenWriteAsync( address, method, null );
+      //}
+
+      //public void OpenWriteAsync( Uri address, string method, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, method, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         WebRequest request = null;
+      //         try
+      //         {
+      //            request = this.SetupRequest( (Uri)objArray[ 0 ], (string)objArray[ 1 ], true );
+      //            Stream result = request.GetRequestStream();
+      //            this.OnOpenWriteCompleted( new OpenWriteCompletedEventArgs( result, null, false, objArray[ 2 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            if( request != null )
+      //            {
+      //               request.Abort();
+      //            }
+      //            this.OnOpenWriteCompleted( new OpenWriteCompletedEventArgs( null, null, true, objArray[ 2 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnOpenWriteCompleted( new OpenWriteCompletedEventArgs( null, exception, false, objArray[ 2 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      private byte[] ReadAll( Stream stream, int length, object userToken )
+      {
+         MemoryStream stream2 = null;
+         bool flag = length == -1;
+         int count = !flag ? length : 0x2000;
+         if( flag )
+         {
+            stream2 = new MemoryStream();
+         }
+         int num2 = 0;
+         int offset = 0;
+         byte[] buffer = new byte[ count ];
+         while( ( num2 = stream.Read( buffer, offset, count ) ) != 0 )
+         {
+            if( flag )
+            {
+               stream2.Write( buffer, 0, num2 );
+            }
+            else
+            {
+               offset += num2;
+               count -= num2;
+            }
+         }
+         if( flag )
+         {
+            return stream2.ToArray();
+         }
+         return buffer;
+      }
+
+      private void SetBusy()
+      {
+         MyWebClient client = this;
+         lock( client )
+         {
+            this.CheckBusy();
+            this.is_busy = true;
+         }
+      }
+
+      private WebRequest SetupRequest( Uri uri )
+      {
+         WebRequest webRequest = this.GetWebRequest( uri );
+         webRequest.ConnectionGroupName = ConnectionGroupName;
+         if( this.Proxy != null )
+         {
+            webRequest.Proxy = this.Proxy;
+         }
+         webRequest.Credentials = this.credentials;
+         if( ( ( this.headers != null ) && ( this.headers.Count != 0 ) ) && ( webRequest is HttpWebRequest ) )
+         {
+            HttpWebRequest request2 = (HttpWebRequest)webRequest;
+            string str = this.headers[ "Expect" ];
+            string str2 = this.headers[ "Content-Type" ];
+            string str3 = this.headers[ "Accept" ];
+            string str4 = this.headers[ "Connection" ];
+            string str5 = this.headers[ "User-Agent" ];
+            string str6 = this.headers[ "Referer" ];
+            this.headers.Remove( "Expect" );
+            this.headers.Remove( "Content-Type" );
+            this.headers.Remove( "Accept" );
+            this.headers.Remove( "Connection" );
+            this.headers.Remove( "Referer" );
+            this.headers.Remove( "User-Agent" );
+            webRequest.Headers = this.headers;
+            if( ( str != null ) && ( str.Length > 0 ) )
+            {
+               request2.Expect = str;
+            }
+            if( ( str3 != null ) && ( str3.Length > 0 ) )
+            {
+               request2.Accept = str3;
+            }
+            if( ( str2 != null ) && ( str2.Length > 0 ) )
+            {
+               request2.ContentType = str2;
+            }
+            if( ( str4 != null ) && ( str4.Length > 0 ) )
+            {
+               request2.Connection = str4;
+            }
+            if( ( str5 != null ) && ( str5.Length > 0 ) )
+            {
+               request2.UserAgent = str5;
+            }
+            if( ( str6 != null ) && ( str6.Length > 0 ) )
+            {
+               request2.Referer = str6;
+            }
+         }
+         this.responseHeaders = null;
+         return webRequest;
+      }
+
+      private WebRequest SetupRequest( Uri uri, string method, bool is_upload )
+      {
+         WebRequest request = this.SetupRequest( uri );
+         request.Method = this.DetermineMethod( uri, method, is_upload );
+         return request;
+      }
+
+      public byte[] UploadData( string address, byte[] data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.UploadData( this.CreateUri( address ), data );
+      }
+
+      public byte[] UploadData( Uri address, byte[] data )
+      {
+         return this.UploadData( address, null, data );
+      }
+
+      public byte[] UploadData( string address, string method, byte[] data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.UploadData( this.CreateUri( address ), method, data );
+      }
+
+      public byte[] UploadData( Uri address, string method, byte[] data )
+      {
+         byte[] buffer;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            buffer = this.UploadDataCore( address, method, data, null );
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return buffer;
+      }
+
+      //public void UploadDataAsync( Uri address, byte[] data )
+      //{
+      //   this.UploadDataAsync( address, null, data );
+      //}
+
+      //public void UploadDataAsync( Uri address, string method, byte[] data )
+      //{
+      //   this.UploadDataAsync( address, method, data, null );
+      //}
+
+      //public void UploadDataAsync( Uri address, string method, byte[] data, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( data == null )
+      //   {
+      //      throw new ArgumentNullException( "data" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, method, data, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            byte[] result = this.UploadDataCore( (Uri)objArray[ 0 ], (string)objArray[ 1 ], (byte[])objArray[ 2 ], objArray[ 3 ] );
+      //            this.OnUploadDataCompleted( new UploadDataCompletedEventArgs( result, null, false, objArray[ 3 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnUploadDataCompleted( new UploadDataCompletedEventArgs( null, null, true, objArray[ 3 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnUploadDataCompleted( new UploadDataCompletedEventArgs( null, exception, false, objArray[ 3 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      private byte[] UploadDataCore( Uri address, string method, byte[] data, object userToken )
+      {
+         byte[] buffer;
+         WebRequest request = this.SetupRequest( address, method, true );
+         try
+         {
+            int length = data.Length;
+            request.ContentLength = length;
+            using( Stream stream = request.GetRequestStream() )
+            {
+               stream.Write( data, 0, length );
+            }
+            WebResponse webResponse = this.GetWebResponse( request );
+            Stream responseStream = webResponse.GetResponseStream();
+            buffer = this.ReadAll( responseStream, (int)webResponse.ContentLength, userToken );
+         }
+         catch( ThreadInterruptedException )
+         {
+            if( request != null )
+            {
+               request.Abort();
+            }
+            throw;
+         }
+         return buffer;
+      }
+
+      public byte[] UploadFile( string address, string fileName )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.UploadFile( this.CreateUri( address ), fileName );
+      }
+
+      public byte[] UploadFile( Uri address, string fileName )
+      {
+         return this.UploadFile( address, null, fileName );
+      }
+
+      public byte[] UploadFile( string address, string method, string fileName )
+      {
+         return this.UploadFile( this.CreateUri( address ), method, fileName );
+      }
+
+      public byte[] UploadFile( Uri address, string method, string fileName )
+      {
+         byte[] buffer;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( fileName == null )
+         {
+            throw new ArgumentNullException( "fileName" );
+         }
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            buffer = this.UploadFileCore( address, method, fileName, null );
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception2 )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return buffer;
+      }
+
+      //public void UploadFileAsync( Uri address, string fileName )
+      //{
+      //   this.UploadFileAsync( address, null, fileName );
+      //}
+
+      //public void UploadFileAsync( Uri address, string method, string fileName )
+      //{
+      //   this.UploadFileAsync( address, method, fileName, null );
+      //}
+
+      //public void UploadFileAsync( Uri address, string method, string fileName, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( fileName == null )
+      //   {
+      //      throw new ArgumentNullException( "fileName" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.SetBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, method, fileName, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            byte[] result = this.UploadFileCore( (Uri)objArray[ 0 ], (string)objArray[ 1 ], (string)objArray[ 2 ], objArray[ 3 ] );
+      //            this.OnUploadFileCompleted( new UploadFileCompletedEventArgs( result, null, false, objArray[ 3 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnUploadFileCompleted( new UploadFileCompletedEventArgs( null, null, true, objArray[ 3 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnUploadFileCompleted( new UploadFileCompletedEventArgs( null, exception, false, objArray[ 3 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      private byte[] UploadFileCore( Uri address, string method, string fileName, object userToken )
+      {
+         string str = this.Headers[ "Content-Type" ];
+         if( str != null )
+         {
+            if( str.ToLower().StartsWith( "multipart/" ) )
+            {
+               throw new WebException( "Content-Type cannot be set to a multipart type for this request." );
+            }
+         }
+         else
+         {
+            str = "application/octet-stream";
+         }
+         string str3 = "------------" + DateTime.Now.Ticks.ToString( "x" );
+         this.Headers[ "Content-Type" ] = string.Format( "multipart/form-data; boundary={0}", str3 );
+         Stream requestStream = null;
+         Stream stream2 = null;
+         byte[] buffer = null;
+         fileName = Path.GetFullPath( fileName );
+         WebRequest request = null;
+         try
+         {
+            int num;
+            stream2 = System.IO.File.OpenRead( fileName );
+            request = this.SetupRequest( address, method, true );
+            requestStream = request.GetRequestStream();
+            byte[] bytes = System.Text.Encoding.ASCII.GetBytes( "--" + str3 + "\r\n" );
+            requestStream.Write( bytes, 0, bytes.Length );
+            string s = string.Format( "Content-Disposition: form-data; name=\"file\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", Path.GetFileName( fileName ), str );
+            byte[] buffer3 = System.Text.Encoding.UTF8.GetBytes( s );
+            requestStream.Write( buffer3, 0, buffer3.Length );
+            byte[] buffer4 = new byte[ 0x1000 ];
+            while( ( num = stream2.Read( buffer4, 0, 0x1000 ) ) != 0 )
+            {
+               requestStream.Write( buffer4, 0, num );
+            }
+            requestStream.WriteByte( 13 );
+            requestStream.WriteByte( 10 );
+            requestStream.Write( bytes, 0, bytes.Length );
+            requestStream.Close();
+            requestStream = null;
+            WebResponse webResponse = this.GetWebResponse( request );
+            Stream responseStream = webResponse.GetResponseStream();
+            buffer = this.ReadAll( responseStream, (int)webResponse.ContentLength, userToken );
+         }
+         catch( ThreadInterruptedException )
+         {
+            if( request != null )
+            {
+               request.Abort();
+            }
+            throw;
+         }
+         finally
+         {
+            if( stream2 != null )
+            {
+               stream2.Close();
+            }
+            if( requestStream != null )
+            {
+               requestStream.Close();
+            }
+         }
+         return buffer;
+      }
+
+      public string UploadString( string address, string data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         byte[] bytes = this.UploadData( address, this.encoding.GetBytes( data ) );
+         return this.encoding.GetString( bytes );
+      }
+
+      public string UploadString( Uri address, string data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         byte[] bytes = this.UploadData( address, this.encoding.GetBytes( data ) );
+         return this.encoding.GetString( bytes );
+      }
+
+      public string UploadString( string address, string method, string data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         byte[] bytes = this.UploadData( address, method, this.encoding.GetBytes( data ) );
+         return this.encoding.GetString( bytes );
+      }
+
+      public string UploadString( Uri address, string method, string data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         byte[] bytes = this.UploadData( address, method, this.encoding.GetBytes( data ) );
+         return this.encoding.GetString( bytes );
+      }
+
+      //public void UploadStringAsync( Uri address, string data )
+      //{
+      //   this.UploadStringAsync( address, null, data );
+      //}
+
+      //public void UploadStringAsync( Uri address, string method, string data )
+      //{
+      //   this.UploadStringAsync( address, method, data, null );
+      //}
+
+      //public void UploadStringAsync( Uri address, string method, string data, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( data == null )
+      //   {
+      //      throw new ArgumentNullException( "data" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.CheckBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, method, data, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            string result = this.UploadString( (Uri)objArray[ 0 ], (string)objArray[ 1 ], (string)objArray[ 2 ] );
+      //            this.OnUploadStringCompleted( new UploadStringCompletedEventArgs( result, null, false, objArray[ 3 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnUploadStringCompleted( new UploadStringCompletedEventArgs( null, null, true, objArray[ 3 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnUploadStringCompleted( new UploadStringCompletedEventArgs( null, exception, false, objArray[ 3 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      public byte[] UploadValues( string address, NameValueCollection data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.UploadValues( this.CreateUri( address ), data );
+      }
+
+      public byte[] UploadValues( Uri address, NameValueCollection data )
+      {
+         return this.UploadValues( address, null, data );
+      }
+
+      public byte[] UploadValues( string address, string method, NameValueCollection data )
+      {
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         return this.UploadValues( this.CreateUri( address ), method, data );
+      }
+
+      public byte[] UploadValues( Uri address, string method, NameValueCollection data )
+      {
+         byte[] buffer;
+         if( address == null )
+         {
+            throw new ArgumentNullException( "address" );
+         }
+         if( data == null )
+         {
+            throw new ArgumentNullException( "data" );
+         }
+         try
+         {
+            this.SetBusy();
+            this.async = false;
+            buffer = this.UploadValuesCore( address, method, data, null );
+         }
+         catch( WebException )
+         {
+            throw;
+         }
+         catch( Exception exception2 )
+         {
+            throw new WebException( "An error occurred performing a WebClient request.", exception2 );
+         }
+         finally
+         {
+            this.is_busy = false;
+         }
+         return buffer;
+      }
+
+      //public void UploadValuesAsync( Uri address, NameValueCollection values )
+      //{
+      //   this.UploadValuesAsync( address, null, values );
+      //}
+
+      //public void UploadValuesAsync( Uri address, string method, NameValueCollection values )
+      //{
+      //   this.UploadValuesAsync( address, method, values, null );
+      //}
+
+      //public void UploadValuesAsync( Uri address, string method, NameValueCollection values, object userToken )
+      //{
+      //   if( address == null )
+      //   {
+      //      throw new ArgumentNullException( "address" );
+      //   }
+      //   if( values == null )
+      //   {
+      //      throw new ArgumentNullException( "values" );
+      //   }
+      //   MyWebClient client = this;
+      //   lock( client )
+      //   {
+      //      this.CheckBusy();
+      //      this.async = true;
+      //      object[] parameter = new object[] { address, method, values, userToken };
+      //      ThreadPool.QueueUserWorkItem( delegate ( object state )
+      //      {
+      //         object[] objArray = (object[])state;
+      //         try
+      //         {
+      //            byte[] result = this.UploadValuesCore( (Uri)objArray[ 0 ], (string)objArray[ 1 ], (NameValueCollection)objArray[ 2 ], objArray[ 3 ] );
+      //            this.OnUploadValuesCompleted( new UploadValuesCompletedEventArgs( result, null, false, objArray[ 3 ] ) );
+      //         }
+      //         catch( ThreadInterruptedException )
+      //         {
+      //            this.OnUploadValuesCompleted( new UploadValuesCompletedEventArgs( null, null, true, objArray[ 3 ] ) );
+      //         }
+      //         catch( Exception exception )
+      //         {
+      //            this.OnUploadValuesCompleted( new UploadValuesCompletedEventArgs( null, exception, false, objArray[ 3 ] ) );
+      //         }
+      //      }, parameter );
+      //   }
+      //}
+
+      private byte[] UploadValuesCore( Uri uri, string method, NameValueCollection data, object userToken )
+      {
+         byte[] buffer3;
+         string strA = this.Headers[ "Content-Type" ];
+         if( ( strA != null ) && ( string.Compare( strA, urlEncodedCType, true ) != 0 ) )
+         {
+            throw new WebException( "Content-Type header cannot be changed from its default value for this request." );
+         }
+         this.Headers[ "Content-Type" ] = urlEncodedCType;
+         WebRequest request = this.SetupRequest( uri, method, true );
+         try
+         {
+            MemoryStream stream = new MemoryStream();
+            IEnumerator enumerator = data.GetEnumerator();
+            try
+            {
+               while( enumerator.MoveNext() )
+               {
+                  string current = (string)enumerator.Current;
+                  byte[] bytes = System.Text.Encoding.UTF8.GetBytes( current );
+                  UrlEncodeAndWrite( stream, bytes );
+                  stream.WriteByte( 0x3d );
+                  bytes = System.Text.Encoding.UTF8.GetBytes( data[ current ] );
+                  UrlEncodeAndWrite( stream, bytes );
+                  stream.WriteByte( 0x26 );
+               }
+            }
+            finally
+            {
+               IDisposable disposable = enumerator as IDisposable;
+               if( disposable != null )
+               {
+                  disposable.Dispose();
+               }
+            }
+            int length = (int)stream.Length;
+            if( length > 0 )
+            {
+               stream.SetLength( (long)( --length ) );
+            }
+            byte[] buffer2 = stream.GetBuffer();
+            request.ContentLength = length;
+            using( Stream stream2 = request.GetRequestStream() )
+            {
+               stream2.Write( buffer2, 0, length );
+            }
+            stream.Close();
+            WebResponse webResponse = this.GetWebResponse( request );
+            Stream responseStream = webResponse.GetResponseStream();
+            buffer3 = this.ReadAll( responseStream, (int)webResponse.ContentLength, userToken );
+         }
+         catch( ThreadInterruptedException )
+         {
+            request.Abort();
+            throw;
+         }
+         return buffer3;
+      }
+
+      private string UrlEncode( string str )
+      {
+         StringBuilder builder = new StringBuilder();
+         int length = str.Length;
+         for( int i = 0 ; i < length ; i++ )
+         {
+            char ch = str[ i ];
+            if( ch == ' ' )
+            {
+               builder.Append( '+' );
+            }
+            else if( ( ( ( ( ch < '0' ) && ( ch != '-' ) ) && ( ch != '.' ) ) || ( ( ch < 'A' ) && ( ch > '9' ) ) ) || ( ( ( ( ch > 'Z' ) && ( ch < 'a' ) ) && ( ch != '_' ) ) || ( ch > 'z' ) ) )
+            {
+               builder.Append( '%' );
+               int index = ch >> 4;
+               builder.Append( (char)hexBytes[ index ] );
+               index = ch & '\x000f';
+               builder.Append( (char)hexBytes[ index ] );
+            }
+            else
+            {
+               builder.Append( ch );
+            }
+         }
+         return builder.ToString();
+      }
+
+      private static void UrlEncodeAndWrite( Stream stream, byte[] bytes )
+      {
+         if( bytes != null )
+         {
+            int length = bytes.Length;
+            if( length != 0 )
+            {
+               for( int i = 0 ; i < length ; i++ )
+               {
+                  char ch = (char)bytes[ i ];
+                  if( ch == ' ' )
+                  {
+                     stream.WriteByte( 0x2b );
+                  }
+                  else if( ( ( ( ( ch < '0' ) && ( ch != '-' ) ) && ( ch != '.' ) ) || ( ( ch < 'A' ) && ( ch > '9' ) ) ) || ( ( ( ( ch > 'Z' ) && ( ch < 'a' ) ) && ( ch != '_' ) ) || ( ch > 'z' ) ) )
+                  {
+                     stream.WriteByte( 0x25 );
+                     int index = ch >> 4;
+                     stream.WriteByte( hexBytes[ index ] );
+                     index = ch & '\x000f';
+                     stream.WriteByte( hexBytes[ index ] );
+                  }
+                  else
+                  {
+                     stream.WriteByte( (byte)ch );
+                  }
+               }
+            }
+         }
+      }
+
+      public string BaseAddress
+      {
+         get
+         {
+            if( ( this.baseString == null ) && ( this.baseAddress == null ) )
+            {
+               return string.Empty;
+            }
+            this.baseString = this.baseAddress.ToString();
+            return this.baseString;
+         }
+         set
+         {
+            if( ( value == null ) || ( value.Length == 0 ) )
+            {
+               this.baseAddress = null;
+            }
+            else
+            {
+               this.baseAddress = new Uri( value );
+            }
+         }
+      }
+
+      public RequestCachePolicy CachePolicy
+      {
+         get
+         {
+            throw GetMustImplement();
+         }
+         set
+         {
+            throw GetMustImplement();
+         }
+      }
+
+      public ICredentials Credentials
+      {
+         get
+         {
+            return this.credentials;
+         }
+         set
+         {
+            this.credentials = value;
+         }
+      }
+
+      public System.Text.Encoding Encoding
+      {
+         get
+         {
+            return this.encoding;
+         }
+         set
+         {
+            if( value == null )
+            {
+               throw new ArgumentNullException( "Encoding" );
+            }
+            this.encoding = value;
+         }
+      }
+
+      public WebHeaderCollection Headers
+      {
+         get
+         {
+            if( this.headers == null )
+            {
+               this.headers = new WebHeaderCollection();
+            }
+            return this.headers;
+         }
+         set
+         {
+            this.headers = value;
+         }
+      }
+
+      public bool IsBusy
+      {
+         get
+         {
+            return this.is_busy;
+         }
+      }
+
+      public IWebProxy Proxy
+      {
+         get
+         {
+            return this.proxy;
+         }
+         set
+         {
+            this.proxy = value;
+         }
+      }
+
+      public NameValueCollection QueryString
+      {
+         get
+         {
+            if( this.queryString == null )
+            {
+               this.queryString = new NameValueCollection();
+            }
+            return this.queryString;
+         }
+         set
+         {
+            this.queryString = value;
+         }
+      }
+
+      public WebHeaderCollection ResponseHeaders
+      {
+         get
+         {
+            return this.responseHeaders;
+         }
+      }
+
+      public bool UseDefaultCredentials
+      {
+         get
+         {
+            throw GetMustImplement();
+         }
+         set
+         {
+            throw GetMustImplement();
+         }
+      }
+   }
+
+}

+ 2 - 2
src/XUnity.AutoTranslator.Plugin.Core/Web/UnityWebClient.cs

@@ -12,7 +12,7 @@ using UnityEngine;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Web
 {
-   public class UnityWebClient : WebClient
+   public class UnityWebClient : MyWebClient
    {
       private KnownHttpEndpoint _httpEndpoint;
 
@@ -23,7 +23,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Web
          DownloadStringCompleted += UnityWebClient_DownloadStringCompleted;
       }
 
-      private void UnityWebClient_DownloadStringCompleted( object sender, DownloadStringCompletedEventArgs ev )
+      private void UnityWebClient_DownloadStringCompleted( object sender, MyDownloadStringCompletedEventArgs ev )
       {
          var handle = ev.UserState as DownloadResult;