Browse Source

Version 3.4.0

 * FEATURE - Added capability of plugin to detect textures that shares the same resource name and identify these resources in an alternative way
 * BUG FIX - Fixed an issue with TextMeshPro that could cause text to glitch in certain situations
randoman 6 years ago
parent
commit
943b3b99c3
23 changed files with 307 additions and 100 deletions
  1. 5 1
      CHANGELOG.md
  2. 9 1
      README.md
  3. 1 1
      src/XUnity.AutoTranslator.Patcher/Patcher.cs
  4. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj
  5. 1 1
      src/XUnity.AutoTranslator.Plugin.BepIn/XUnity.AutoTranslator.Plugin.BepIn.csproj
  6. 43 16
      src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs
  7. 12 0
      src/XUnity.AutoTranslator.Plugin.Core/CallOrigin.cs
  8. 13 0
      src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs
  9. 12 0
      src/XUnity.AutoTranslator.Plugin.Core/Constants/KnownGames.cs
  10. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs
  11. 2 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextComponentExtensions.cs
  12. 10 0
      src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs
  13. 14 0
      src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs
  14. 23 3
      src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs
  15. 74 35
      src/XUnity.AutoTranslator.Plugin.Core/TextureTranslationCache.cs
  16. 70 35
      src/XUnity.AutoTranslator.Plugin.Core/TextureTranslationInfo.cs
  17. 1 0
      src/XUnity.AutoTranslator.Plugin.Core/UntranslatedText.cs
  18. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextureHelper.cs
  19. 1 1
      src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj
  20. 1 1
      src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj
  21. 1 1
      src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj
  22. 1 1
      src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj
  23. 10 0
      test/XUnity.AutoTranslator.Plugin.Core.Tests/UntranslatedTextTests.cs

+ 5 - 1
CHANGELOG.md

@@ -1,4 +1,8 @@
-### 3.3.1
+### 3.4.0
+ * FEATURE - Added capability of plugin to detect textures that shares the same resource name and identify these resources in an alternative way
+ * BUG FIX - Fixed an issue with TextMeshPro that could cause text to glitch in certain situations
+
+### 3.3.1
  * MISC - Options to cache results of regex lookups and whitespace differences
  * BUG FIX - Fixed 'WhitespaceRemovalStrategy.TrimPerNewline' which was broken to remove all non-repeating whitespace, rather than only the non-repeating whitespace surrounding newlines
  * BUG FIX - Fully clear translations before reloading (ALT+R)

+ 9 - 1
README.md

@@ -271,6 +271,8 @@ EnableTextureScanOnSceneLoad=False ;Indicates whether or not the plugin should s
 EnableSpriteRendererHooking=False ;Indicates whether or not the plugin should attempt to hook SpriteRenderer. This is a seperate option because SpriteRenderer can't actually be hooked properly and the implemented workaround could have a theoretical impact on performance in certain situations
 LoadUnmodifiedTextures=False     ;Indicates whether or not unmodified textures should be loaded. Modifications are determined based on the hash in the file name. Only enable this for debugging purposes as it is likely to cause oddities
 TextureHashGenerationStrategy=FromImageName ;Indicates how the mod identifies pictures through hashes. Can be ["FromImageName", "FromImageData", "FromImageNameAndScene"]
+DuplicateTextureNames=           ;Indicates specific texture names that are duplicated in the game. List is separated by ';'.
+DetectDuplicateTextureNames=False;Indicates if the plugin should detect duplicate texture names.
 
 [Http]
 UserAgent=                       ;Override the user agent used by APIs requiring a user agent
@@ -429,6 +431,8 @@ EnableTextureScanOnSceneLoad=False
 EnableSpriteRendererHooking=False
 LoadUnmodifiedTextures=False
 TextureHashGenerationStrategy=FromImageName
+DuplicateTextureNames=
+DetectDuplicateTextureNames=False
 ```
 
 `TextureDirectory` specifies the directory where textures are dumped to and loaded from. Loading will happen from all subdirectories of the specified directory as well, so you can move dumped images to whatever folder structure you desire.
@@ -445,6 +449,10 @@ TextureHashGenerationStrategy=FromImageName
 
 `EnableTextureToggling` enables whether the ALT+T hotkey will also toggle textures. This is by no means guaranteed to work, especially if `EnableTextureScanOnSceneLoad` is also enabled. **Never redistribute the mod with this enabled.**
 
+`DuplicateTextureNames` specifies different textures in the game that are used under the same resource name. The plugin will fallback to the 'FromImageData' for image identification for these images.
+
+`DetectDuplicateTextureNames` specifies that the plugin should identify which image names are duplicated and update the configuration with these names automatically. **Never redistribute the mod with this enabled.**
+
 `TextureHashGenerationStrategy` specifies how images are identified. When images are stored, the game will need some way of associating them with the image that it has to replace.
 This is done through a hash-value that is stored in square brackets in each image file name, like this: `file_name [0223B639A2-6E698E9272].png`. This configuration specifies how these hash-values are generated:
  * `FromImageName` means that the hash is generated from the internal resource name that the game uses for the image, which may not exist for all images or even be unique. However, it is generally fairly reliable. If an image has no resource name, it will not be dumped.
@@ -460,7 +468,7 @@ If you redistribute this mod with translated images, it is recommended you delet
 You can also change the file name to whatever you desire, as long as you keep the hash appended to the end of the file name.
 
 If you take anything away from this section, it should be these two points:
- * **Never redistribute the mod with `EnableTextureDumping=True`, `EnableTextureToggling=True` or `LoadUnmodifiedTextures=True`**
+ * **Never redistribute the mod with `EnableTextureDumping=True`, `EnableTextureToggling=True`, `LoadUnmodifiedTextures=True` or `DetectDuplicateTextureNames=true`**
  * **Only redistribute the mod with `TextureHashGenerationStrategy=FromImageData` enabled if absolutely required by the game.**
 
 ### Technical details about Hash Generation in file names

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

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

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

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>net35</TargetFramework>
-    <Version>3.3.1</Version>
+    <Version>3.4.0</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.3.1</Version>
+      <Version>3.4.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

@@ -443,27 +443,48 @@ namespace XUnity.AutoTranslator.Plugin.Core
          }
       }
 
-      internal string Hook_TextChanged_WithResult( object ui, string text )
+      internal string ExternalHook_TextChanged_WithResult( object ui, string text )
       {
          if( !ui.IsKnownTextType() ) return null;
 
-         if( _textHooksEnabled && !_temporarilyDisabled )
+         try
          {
-            var translation = TranslateOrQueueWebJob( ui, text, false );
-            return _isInTranslatedMode ? translation : null;
+            CallOrigin.ExpectsTextToBeReturned = true;
+
+            if( _textHooksEnabled && !_temporarilyDisabled )
+            {
+               return TranslateOrQueueWebJob( ui, text, true );
+            }
+            return null;
+         }
+         finally
+         {
+            CallOrigin.ExpectsTextToBeReturned = false;
          }
-         return null;
       }
 
-      internal string ExternalHook_TextChanged_WithResult( object ui, string text )
+      internal string Hook_TextChanged_WithResult( object ui, string text, bool onEnable )
       {
-         if( !ui.IsKnownTextType() ) return null;
+         try
+         {
+            CallOrigin.ExpectsTextToBeReturned = true;
 
-         if( _textHooksEnabled && !_temporarilyDisabled )
+            string result = null;
+            if( _textHooksEnabled && !_temporarilyDisabled )
+            {
+               result = TranslateOrQueueWebJob( ui, text, false );
+            }
+
+            if( onEnable )
+            {
+               CheckSpriteRenderer( ui );
+            }
+            return result;
+         }
+         finally
          {
-            return TranslateOrQueueWebJob( ui, text, true );
+            CallOrigin.ExpectsTextToBeReturned = false;
          }
-         return null;
       }
 
       internal void Hook_TextChanged( object ui, bool onEnable )
@@ -548,8 +569,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
       internal void SetTranslatedText( object ui, string translatedText, TextTranslationInfo info )
       {
          info?.SetTranslatedText( translatedText );
-
-         if( _isInTranslatedMode )
+         
+         if( _isInTranslatedMode && !CallOrigin.ExpectsTextToBeReturned )
          {
             SetText( ui, translatedText, true, info );
          }
@@ -585,7 +606,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                // NGUI only behaves if you set the text after the resize behaviour
                ui.SetText( text );
-               
+
                info?.ResetScrollIn( ui );
 
                if( TranslationAggregatorWindow != null && info != null && !ui.IsSpammingComponent() )
@@ -932,6 +953,11 @@ namespace XUnity.AutoTranslator.Plugin.Core
             && format != 63;
       }
 
+      internal void RenameTextureWithKey( string name, string key, string newKey )
+      {
+         TextureCache.RenameFileWithKey( name, key, newKey );
+      }
+
       private string TranslateImmediate( object ui, string text, TextTranslationInfo info, bool ignoreComponentState )
       {
          // Get the trimmed text
@@ -953,8 +979,9 @@ namespace XUnity.AutoTranslator.Plugin.Core
             {
                if( !string.IsNullOrEmpty( translation ) )
                {
-                  SetTranslatedText( ui, textKey.Untemplate( translation ), info );
-                  return translation;
+                  var untemplatedTranslation = textKey.Untemplate( translation );
+                  SetTranslatedText( ui, untemplatedTranslation, info );
+                  return untemplatedTranslation;
                }
             }
             else
@@ -1935,7 +1962,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                         var tti = kvp.Value as TextTranslationInfo;
                         if( tti != null && tti.IsTranslated )
                         {
-                           SetText( ui, tti.OriginalText, true, tti );
+                           SetText( ui, tti.OriginalText, false, tti );
                         }
                      }
                   }

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

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core
+{
+   internal static class CallOrigin
+   {
+      public static bool ExpectsTextToBeReturned = false;
+   }
+}

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

@@ -98,6 +98,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
       public static bool EnableTextureScanOnSceneLoad;
       public static bool EnableSpriteRendererHooking;
       public static bool LoadUnmodifiedTextures;
+      public static bool DetectDuplicateTextureNames;
+      public static HashSet<string> DuplicateTextureNames;
       public static TextureHashGenerationStrategy TextureHashGenerationStrategy;
 
       public static bool CopyToClipboard;
@@ -158,6 +160,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
             EnableTextureScanOnSceneLoad = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableTextureScanOnSceneLoad", false );
             EnableSpriteRendererHooking = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "EnableSpriteRendererHooking", false );
             LoadUnmodifiedTextures = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "LoadUnmodifiedTextures", false );
+            DetectDuplicateTextureNames = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "DetectDuplicateTextureNames", false );
+            DuplicateTextureNames = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "DuplicateTextureNames", string.Empty )
+               ?.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries ).ToHashSet() ?? new HashSet<string>();
+
             TextureHashGenerationStrategy = PluginEnvironment.Current.Preferences.GetOrDefault( "Texture", "TextureHashGenerationStrategy", TextureHashGenerationStrategy.FromImageName );
 
             if( MaxCharactersPerTranslation > MaxMaxCharactersPerTranslation )
@@ -200,6 +206,13 @@ namespace XUnity.AutoTranslator.Plugin.Core.Configuration
          }
       }
 
+      public static void AddDuplicateName( string name )
+      {
+         DuplicateTextureNames.Add( name );
+         PluginEnvironment.Current.Preferences[ "Texture" ][ "DuplicateTextureNames" ].Value = string.Join( ";", DuplicateTextureNames.ToArray() );
+         Save();
+      }
+
       public static void SetEndpoint( string id )
       {
          id = id ?? string.Empty;

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

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace XUnity.AutoTranslator.Plugin.Core.Constants
+{
+   internal static class KnownGames
+   {
+      public static readonly string EmotionCreators = "emotioncreators";
+   }
+}

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

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

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

@@ -1,4 +1,5 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
@@ -8,6 +9,7 @@ using UnityEngine;
 using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
 using XUnity.AutoTranslator.Plugin.Core.Constants;
+using XUnity.AutoTranslator.Plugin.Core.Utilities;
 
 namespace XUnity.AutoTranslator.Plugin.Core.Extensions
 {

+ 10 - 0
src/XUnity.AutoTranslator.Plugin.Core/Extensions/TextureComponentExtensions.cs

@@ -11,6 +11,16 @@ namespace XUnity.AutoTranslator.Plugin.Core.Extensions
       private static readonly string CapitalMainTexturePropertyName = "MainTexture";
       private static readonly string MarkAsChangedMethodName = "MarkAsChanged";
 
+      public static string GetPath( this object ui )
+      {
+         if( ui is Component comp )
+         {
+            var go = comp.gameObject;
+            return go.GetPath();
+         }
+         return null;
+      }
+
       public static Texture2D GetTexture( this object ui )
       {
          if( ui == null ) return null;

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

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using System.Text;
@@ -92,6 +93,19 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro
          }
          AutoTranslationPlugin.Current.Hook_HandleComponent( __instance );
       }
+
+      //static void Prefix( object __instance, ref string value )
+      //{
+      //   if( !TextMeshProHooks.HooksOverriden )
+      //   {
+      //      var result = AutoTranslationPlugin.Current.Hook_TextChanged_WithResult( __instance, value, false );
+      //      if( result != null )
+      //      {
+      //         value = result;
+      //      }
+      //   }
+      //   AutoTranslationPlugin.Current.Hook_HandleComponent( __instance );
+      //}
    }
 
    [Harmony, HarmonyPriority( Priority.Last )]

+ 23 - 3
src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs

@@ -6,6 +6,7 @@ using Harmony;
 using UnityEngine;
 using UnityEngine.UI;
 using XUnity.AutoTranslator.Plugin.Core.Configuration;
+using XUnity.AutoTranslator.Plugin.Core.Constants;
 using XUnity.AutoTranslator.Plugin.Core.Extensions;
 using XUnity.AutoTranslator.Plugin.Core.Fonts;
 
@@ -16,6 +17,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
    {
       private static readonly string MultiLinePropertyName = "multiLine";
       private static readonly string OverflowMethodPropertyName = "overflowMethod";
+      private static readonly string OverflowModePropertyName = "overflowMode";
 
       private Action<object> _unresize;
       private Action<object> _unfont;
@@ -96,7 +98,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
          if( graphic is Text )
          {
             var ui = (Text)graphic;
-            
+
             // text is likely to be longer than there is space for, simply expand out anyway then
             var componentWidth = ( (RectTransform)ui.transform ).rect.width;
             var quarterScreenSize = Screen.width / 4;
@@ -140,7 +142,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
             var type = graphic.GetType();
 
             // special handling for NGUI to better handle textbox sizing
-            if( type == Constants.ClrTypes.UILabel )
+            if( type == ClrTypes.UILabel )
             {
                var originalMultiLine = type.GetProperty( MultiLinePropertyName )?.GetGetMethod()?.Invoke( graphic, null );
                var originalOverflowMethod = type.GetProperty( OverflowMethodPropertyName )?.GetGetMethod()?.Invoke( graphic, null );
@@ -169,13 +171,31 @@ namespace XUnity.AutoTranslator.Plugin.Core
                   };
                }
             }
+            else if( type == ClrTypes.TextMeshPro || type == ClrTypes.TextMeshProUGUI )
+            {
+               var originalOverflowMode = ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName )?.GetValue( graphic, null );
+
+               // ellipsis (1) works
+               // masking (2) has a tendency to break in some versions of TMP
+               // truncate (3) works
+               if( originalOverflowMode != null && (int)originalOverflowMode == 2 )
+               {
+                  ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName ).SetValue( graphic, 3, null );
+
+                  _unresize = g =>
+                  {
+                     ClrTypes.TMP_Text.GetProperty( OverflowModePropertyName ).SetValue( g, 2, null );
+                  };
+               }
+
+            }
          }
       }
 
       public void UnresizeUI( object graphic )
       {
          if( graphic == null ) return;
-
+         
          _unresize?.Invoke( graphic );
          _unresize = null;
       }

+ 74 - 35
src/XUnity.AutoTranslator.Plugin.Core/TextureTranslationCache.cs

@@ -14,6 +14,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
    {
       private Dictionary<string, byte[]> _translatedImages = new Dictionary<string, byte[]>( StringComparer.InvariantCultureIgnoreCase );
       private HashSet<string> _untranslatedImages = new HashSet<string>();
+      private Dictionary<string, string> _keyToFileName = new Dictionary<string, string>();
 
       public TextureTranslationCache()
       {
@@ -36,6 +37,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                _translatedImages.Clear();
                _untranslatedImages.Clear();
+               _keyToFileName.Clear();
+
                Directory.CreateDirectory( Path.Combine( PluginEnvironment.Current.TranslationPath, Settings.TextureDirectory ).Parameterize() );
                foreach( var fullFileName in GetTextureFiles() )
                {
@@ -54,53 +57,87 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
       private void RegisterImageFromFile( string fullFileName )
       {
-         var fileName = Path.GetFileNameWithoutExtension( fullFileName );
-         var startHash = fileName.LastIndexOf( "[" );
-         var endHash = fileName.LastIndexOf( "]" );
-
-         if( endHash > -1 && startHash > -1 && endHash > startHash )
+         try
          {
-            var takeFrom = startHash + 1;
+            var fileName = Path.GetFileNameWithoutExtension( fullFileName );
+            var startHash = fileName.LastIndexOf( "[" );
+            var endHash = fileName.LastIndexOf( "]" );
 
-            // load based on whether or not the key is image hashed
-            var parts = fileName.Substring( takeFrom, endHash - takeFrom ).Split( '-' );
-            string key;
-            string originalHash;
-            if( parts.Length == 1 )
-            {
-               key = parts[ 0 ];
-               originalHash = parts[ 0 ];
-            }
-            else if( parts.Length == 2 )
+            if( endHash > -1 && startHash > -1 && endHash > startHash )
             {
-               key = parts[ 0 ];
-               originalHash = parts[ 1 ];
+               var takeFrom = startHash + 1;
+
+               // load based on whether or not the key is image hashed
+               var parts = fileName.Substring( takeFrom, endHash - takeFrom ).Split( '-' );
+               string key;
+               string originalHash;
+               if( parts.Length == 1 )
+               {
+                  key = parts[ 0 ];
+                  originalHash = parts[ 0 ];
+               }
+               else if( parts.Length == 2 )
+               {
+                  key = parts[ 0 ];
+                  originalHash = parts[ 1 ];
+               }
+               else
+               {
+                  XuaLogger.Current.Warn( $"Image not loaded (unknown hash): {fullFileName}." );
+                  return;
+               }
+
+               var data = File.ReadAllBytes( fullFileName );
+               var currentHash = HashHelper.Compute( data );
+               var isModified = StringComparer.InvariantCultureIgnoreCase.Compare( originalHash, currentHash ) != 0;
+
+               _keyToFileName[ key ] = fullFileName;
+
+               // only load images that someone has modified!
+               if( Settings.LoadUnmodifiedTextures || isModified )
+               {
+                  RegisterTranslatedImage( key, data );
+                  XuaLogger.Current.Debug( $"Image loaded: {fullFileName}." );
+               }
+               else
+               {
+                  RegisterUntranslatedImage( key );
+                  XuaLogger.Current.Warn( $"Image not loaded (unmodified): {fullFileName}." );
+               }
             }
             else
             {
-               XuaLogger.Current.Warn( $"Image not loaded (unknown hash): {fullFileName}." );
-               return;
+               XuaLogger.Current.Warn( $"Image not loaded (no hash): {fullFileName}." );
             }
+         }
+         catch( Exception e )
+         {
+            XuaLogger.Current.Error( e, "An error occurred while loading texture file: " + fullFileName );
+         }
+      }
 
-            var data = File.ReadAllBytes( fullFileName );
-            var currentHash = HashHelper.Compute( data );
-            var isModified = StringComparer.InvariantCultureIgnoreCase.Compare( originalHash, currentHash ) != 0;
-
-            // only load images that someone has modified!
-            if( Settings.LoadUnmodifiedTextures || isModified )
-            {
-               RegisterTranslatedImage( key, data );
-               XuaLogger.Current.Debug( $"Image loaded: {fullFileName}." );
-            }
-            else
+      public void RenameFileWithKey( string name, string key, string newKey )
+      {
+         try
+         {
+            if( _keyToFileName.TryGetValue( key, out var currentFileName ) )
             {
-               RegisterUntranslatedImage( key );
-               XuaLogger.Current.Warn( $"Image not loaded (unmodified): {fullFileName}." );
+               _keyToFileName.Remove( key );
+               var newFileName = currentFileName.Replace( key, newKey );
+
+               if( !IsImageRegistered( newKey ) )
+               {
+                  var data = File.ReadAllBytes( currentFileName );
+                  RegisterImageFromData( name, newKey, data );
+                  File.Delete( currentFileName );
+
+                  XuaLogger.Current.Warn( $"Replaced old file with name '{name}' registered with key old '{key}'." );
+               }
             }
          }
-         else
+         catch( Exception e )
          {
-            XuaLogger.Current.Warn( $"Image not loaded (no hash): {fullFileName}." );
+            XuaLogger.Current.Error( e, $"An error occurred while trying to rename file with key '{key}'." );
          }
       }
 
@@ -125,6 +162,8 @@ namespace XUnity.AutoTranslator.Plugin.Core
          File.WriteAllBytes( fullName, data );
          XuaLogger.Current.Info( "Dumped texture file: " + fileName );
 
+         _keyToFileName[ key ] = fullName;
+
          if( Settings.LoadUnmodifiedTextures )
          {
             RegisterTranslatedImage( key, data );

+ 70 - 35
src/XUnity.AutoTranslator.Plugin.Core/TextureTranslationInfo.cs

@@ -11,7 +11,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
 {
    internal class TextureTranslationInfo
    {
-      //private static readonly Dictionary<int, string> KnownHashes = new Dictionary<int, string>();
+      private static Dictionary<string, string> NameToHash = new Dictionary<string, string>();
       private static readonly Encoding UTF8 = new UTF8Encoding( false );
 
       private string _key;
@@ -54,12 +54,67 @@ namespace XUnity.AutoTranslator.Plugin.Core
          return TextureHelper.GetData( texture ).Data;
       }
 
+      private TextureDataResult SetupKeyForNameWithFallback( string name, Texture2D texture )
+      {
+         bool detectedDuplicateName = false;
+         string existingHash = null;
+         string hash = null;
+
+         TextureDataResult result = null;
+
+         if( Settings.DetectDuplicateTextureNames )
+         {
+            result = TextureHelper.GetData( texture );
+            hash = HashHelper.Compute( result.Data );
+
+            if( NameToHash.TryGetValue( name, out existingHash ) )
+            {
+               if( existingHash != hash )
+               {
+                  XuaLogger.Current.Warn( "Detected duplicate image name: " + name );
+                  detectedDuplicateName = true;
+
+                  Settings.AddDuplicateName( name );
+               }
+            }
+            else
+            {
+               NameToHash[ name ] = hash;
+            }
+         }
+
+         if( Settings.DuplicateTextureNames.Contains( name ) )
+         {
+            if( hash == null )
+            {
+               if( result == null )
+               {
+                  result = TextureHelper.GetData( texture );
+               }
+
+               hash = HashHelper.Compute( result.Data );
+            }
+
+            _key = hash;
+         }
+         else
+         {
+            _key = HashHelper.Compute( UTF8.GetBytes( name ) );
+         }
+
+         if( detectedDuplicateName )
+         {
+            var oldKey = HashHelper.Compute( UTF8.GetBytes( name ) );
+            AutoTranslationPlugin.Current.RenameTextureWithKey( name, oldKey, existingHash );
+         }
+
+         return result;
+      }
+
       private void SetupHashAndData( Texture2D texture )
       {
          if( _key == null )
          {
-            var instanceId = texture.GetInstanceID();
-
             if( Settings.TextureHashGenerationStrategy == TextureHashGenerationStrategy.FromImageData )
             {
                var result = TextureHelper.GetData( texture );
@@ -67,43 +122,20 @@ namespace XUnity.AutoTranslator.Plugin.Core
                _originalData = result.Data;
                _nonReadable = result.NonReadable;
                _key = HashHelper.Compute( _originalData );
-
-               //if( KnownHashes.TryGetValue( instanceId, out string hashValue ) )
-               //{
-               //   _key = hashValue;
-
-               //   if( Settings.EnableTextureToggling )
-               //   {
-               //      var result = TextureHelper.GetData( texture );
-
-               //      _originalData = result.Data;
-               //      _nonReadable = result.NonReadable;
-               //   }
-               //}
-               //else
-               //{
-               //   var result = TextureHelper.GetData( texture );
-
-               //   _originalData = result.Data;
-               //   _nonReadable = result.NonReadable;
-               //   _key = HashHelper.Compute( _originalData );
-
-               //   if( !string.IsNullOrEmpty( texture.name ) && result.CalculationTime > 0.6f )
-               //   {
-               //      KnownHashes[ instanceId ] = _key;
-               //   }
-               //}
             }
             else if( Settings.TextureHashGenerationStrategy == TextureHashGenerationStrategy.FromImageName )
             {
                var name = texture.name; // name may be duplicate, WILL be duplicate!
                if( string.IsNullOrEmpty( name ) ) return;
 
-               _key = HashHelper.Compute( UTF8.GetBytes( name ) );
+               var result = SetupKeyForNameWithFallback( name, texture );
 
-               if( Settings.EnableTextureToggling )
+               if( Settings.EnableTextureToggling || Settings.DetectDuplicateTextureNames )
                {
-                  var result = TextureHelper.GetData( texture );
+                  if( result == null )
+                  {
+                     result = TextureHelper.GetData( texture );
+                  }
 
                   _originalData = result.Data;
                   _nonReadable = result.NonReadable;
@@ -116,11 +148,14 @@ namespace XUnity.AutoTranslator.Plugin.Core
 
                name += "|" + SceneManagerHelper.GetActiveSceneId();
 
-               _key = HashHelper.Compute( UTF8.GetBytes( name ) );
+               var result = SetupKeyForNameWithFallback( name, texture );
 
-               if( Settings.EnableTextureToggling )
+               if( Settings.EnableTextureToggling || Settings.DetectDuplicateTextureNames )
                {
-                  var result = TextureHelper.GetData( texture );
+                  if( result == null )
+                  {
+                     result = TextureHelper.GetData( texture );
+                  }
 
                   _originalData = result.Data;
                   _nonReadable = result.NonReadable;

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

@@ -115,6 +115,7 @@ namespace XUnity.AutoTranslator.Plugin.Core
                            currentWhitespaceChar = ch;
                         }
 
+                        // FIXME: Test this...
                         if( Settings.UsesWhitespaceBetweenWords && ( ch == '\n' || ch == '\r' ) )
                         {
                            if( builder.Length > 0 && builder[ builder.Length - 1 ] != ' ' )

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

@@ -52,7 +52,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Utilities
       }
    }
 
-   internal struct TextureDataResult
+   internal class TextureDataResult
    {
       public TextureDataResult( byte[] data, bool nonReadable, float calculationTime )
       {

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.3.1</Version>
+      <Version>3.4.0</Version>
    </PropertyGroup>
 
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.3.1</Version>
+      <Version>3.4.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

@@ -2,7 +2,7 @@
 
    <PropertyGroup>
       <TargetFramework>net35</TargetFramework>
-      <Version>3.3.1</Version>
+      <Version>3.4.0</Version>
    </PropertyGroup>
 
    <ItemGroup>

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

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

+ 10 - 0
test/XUnity.AutoTranslator.Plugin.Core.Tests/UntranslatedTextTests.cs

@@ -11,6 +11,9 @@ namespace XUnity.AutoTranslator.Plugin.Core.Tests
    public class UntranslatedTextTests
    {
       [Theory( DisplayName = "Can_Trim_Surrounding_Whitespace" )]
+      [InlineData( "  Hello  ", "Hello", "  ", "  " )]
+      [InlineData( " Hello", "Hello", " ", null )]
+      [InlineData( "Hello", "Hello", null, null )]
       [InlineData( "\r\n \r\nHello", "Hello", "\r\n \r\n", null )]
       [InlineData( "\r\n \r\nHello\n", "Hello", "\r\n \r\n", "\n" )]
       [InlineData( "\r\r\r\r\n \n Hello  \r\n", "Hello", "\r\r\r\r\n \n ", "  \r\n" )]
@@ -28,6 +31,8 @@ namespace XUnity.AutoTranslator.Plugin.Core.Tests
       [InlineData( "Hel lo", "Hel lo" )]
       [InlineData( "Hel\r\n lo", "Hello" )]
       [InlineData( "Hel\n\nlo", "Hel\n\nlo" )]
+      [InlineData( "Hello\nWhat\nYou", "HelloWhatYou" )]
+      [InlineData( "Hello\n\nWhat\nYou", "Hello\n\nWhatYou" )]
       public void Can_Trim_Internal_Whitespace( string input, string expectedTrimmedText )
       {
          var untranslatedText = new UntranslatedText( input, false, true );
@@ -57,6 +62,11 @@ namespace XUnity.AutoTranslator.Plugin.Core.Tests
 
       [Theory( DisplayName = "Can_Trim_Internal_And_Surrounding_Whitespace_And_Template" )]
       [InlineData( "\r\n \r\nFPS: 60.53", "FPS: {{A}}", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nFPS:   60.53", "FPS:   {{A}}", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nFPS:\n 60.53", "FPS:{{A}}", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nFPS:\n  60.53", "FPS:  {{A}}", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nFPS:\n \n 60.53", "FPS:{{A}}", "\r\n \r\n", null )]
+      [InlineData( "\r\n \r\nFPS:\n  \n 60.53", "FPS:  {{A}}", "\r\n \r\n", null )]
       public void Can_Trim_Internal_And_Surrounding_Whitespace_And_Template( string input, string expectedTrimmedText, string expectedLeadingWhitespace, string expectedTrailingWhitespace )
       {
          var untranslatedText = new UntranslatedText( input, true, true );