From f2eb35804351d4ed50011a8513404744ea71689a Mon Sep 17 00:00:00 2001 From: randoman <738b86bb93c44695854182cc459afcbb@lonestar.no> Date: Mon, 20 Jul 2020 09:59:54 +0200 Subject: [PATCH] Version 4.12.0 * FEATURE - Specialized plugin translation support. Can now read text translation files that are only used for specific plugins * FEATURE - Proper IMGUI support in Unity 2018 and 2019+ * MISC - Changed guidance on IMGUI redistribution * MISC - Changed the way output text is determined when multiple text parsers are involved in the translation (now priority based rather than first come first serve) * MISC - Removed feature listening to text changed event from external translation plugins --- CHANGELOG.md | 9 +- README.md | 78 +++- src/XUnity.AutoTranslator.Patcher/Patcher.cs | 2 +- ...nity.AutoTranslator.Plugin.BepIn-5x.csproj | 2 +- .../AutoTranslationPlugin.cs | 332 ++++++++++---- .../AutoTranslatorSettings.cs | 4 +- .../CallOrigin.cs | 91 ++++ .../CompositeTextTranslationCache.cs | 47 ++ .../Configuration/Settings.cs | 4 +- .../Constants/PluginData.cs | 2 +- .../Extensions/GameObjectExtensions.cs | 33 ++ .../Features.cs | 2 +- .../Hooks/FairyGUIHooks.cs | 12 +- .../Hooks/HooksSetup.cs | 110 ++--- .../Hooks/IMGUIHooks.cs | 406 +++++++++++++----- .../Hooks/NGUIHooks.cs | 12 +- .../Hooks/PluginTranslationHooks.cs | 231 ++++++++++ .../Hooks/TextMeshHooks.cs | 29 +- .../Hooks/TextMeshProHooks.cs | 61 +-- .../Hooks/UGUIHooks.cs | 14 +- .../IMGUIBlocker.cs | 14 +- .../IMGUIPluginTranslationHooks.cs | 173 ++++++++ .../IReadOnlyTextTranslationCache.cs | 20 + .../KeyValuePairTranslationPackage.cs | 46 ++ .../ParserTranslationContext.cs | 34 +- .../Parsing/GameLogTextParser.cs | 11 +- .../Parsing/ITextParser.cs | 7 - .../Parsing/ParserResult.cs | 2 + .../Parsing/ParserResultOrigin.cs | 4 +- .../Parsing/RegexSplittingTextParser.cs | 11 +- .../Parsing/RichTextParser.cs | 2 +- .../Parsing/UnityTextParsers.cs | 6 +- .../SettingsTranslationsInitializer.cs | 92 ++++ .../StreamTranslationPackage.cs | 93 ++++ .../TextTranslationCache.cs | 380 +++++++++++----- .../TextTranslationInfo.cs | 2 + .../TranslationFileLoadingContext.cs | 51 ++- .../TranslationRegistry.cs | 101 +++++ .../UIResize/UIResizeCache.cs | 13 +- .../Utilities/TextHelper.cs | 191 -------- .../XUnity.AutoTranslator.Plugin.Core.csproj | 2 +- .../XUnity.AutoTranslator.Plugin.IPA.csproj | 2 +- ...AutoTranslator.Plugin.UnityInjector.csproj | 2 +- .../XUnity.AutoTranslator.Setup.csproj | 2 +- src/XUnity.Common/Constants/ClrTypes.cs | 3 + src/XUnity.Common/Utilities/HookingHelper.cs | 12 +- 46 files changed, 2024 insertions(+), 733 deletions(-) create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/CompositeTextTranslationCache.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/Hooks/PluginTranslationHooks.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/IMGUIPluginTranslationHooks.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/IReadOnlyTextTranslationCache.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/KeyValuePairTranslationPackage.cs delete mode 100644 src/XUnity.AutoTranslator.Plugin.Core/Parsing/ITextParser.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/SettingsTranslationsInitializer.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/StreamTranslationPackage.cs create mode 100644 src/XUnity.AutoTranslator.Plugin.Core/TranslationRegistry.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e41a87..01107a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -### 4.11.4 +### 4.12.0 + * FEATURE - Specialized plugin translation support. Can now read text translation files that are only used for specific plugins + * FEATURE - Proper IMGUI support in Unity 2018 and 2019+ + * MISC - Changed guidance on IMGUI redistribution + * MISC - Changed the way output text is determined when multiple text parsers are involved in the translation (now priority based rather than first come first serve) + * MISC - Removed feature listening to text changed event from external translation plugins + +### 4.11.4 * MISC - Allow using separate service endpoint for google. Use to circumvent GFWoC * BUG FIX - Fix bug with scene scan that could sometimes fail in certain versions of Unity diff --git a/README.md b/README.md index 7571b441..6450f637 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,6 @@ EnableNGUI=True ;Enable or disable NGUI translation EnableTextMeshPro=True ;Enable or disable TextMeshPro translation EnableTextMesh=False ;Enable or disable TextMesh translation EnableIMGUI=False ;Enable or disable IMGUI translation -AllowPluginHookOverride=True ;Allow other text translation plugins to override this plugin's hooks [Behaviour] MaxCharactersPerTranslation=200 ;Max characters per text to translate. Max 1000. @@ -475,7 +474,7 @@ A: For now, additional support for services that does not require some form of a ## Translating Mods Often other mods UI are implemented through IMGUI. As you can see above, this is disabled by default. By changing the "EnableIMGUI" value to "True", it will start translating IMGUI as well, which likely means that other mods UI will be translated. -This may seem like a nice feature to have enabled by default but **never redistribute the mod with this enabled**. The reason here being that IMGUI has a very spammy nature. This does not mean that IMGUI in general will spam the endpoint, just that it is more likely to cause spam (and therefore cause the plugin to shutdown). +It is also possible to provide plugin-specific translations. See next section. ## Manual Translations When you use this plugin, you can always go to the file `Translation\{Lang}\Text\_AutoGeneratedTranslations.txt` (OutputFile) to edit any auto generated translations and they will show up the next time you run the game. Or you can press (ALT+R) to reload the translation immediately. @@ -486,6 +485,62 @@ In this context, the `Translation\{Lang}\Text\_AutoGeneratedTranslations.txt` (O In some ADV engines text 'scrolls' into place slowly. Different techniques are used for this and in some instances if you want the translated text to be scrolling in instead of the untranslated text, you may need to set `GeneratePartialTranslations=True`. This should not be turned on unless required by the game. +### Plugin-specific Manual Translations +Often you may want to provide translations for other plugins that are not naturally translated. This is obviously also possible with this plugin as described in the previous section. But what if you want to provide translations that should be specific to that plugin because such translation would conflict with a different plugin/generic translation? + +In order to add plugin-specific translations, simply create a `Plugins` direcetory in the text translation `Directory`. In this directory you can create a new directory for each plugin you want to provide plugin-specific translations for. The name of the directory should be the same as the dll name without the extension (.dll). + +Within this directory you can create translations files as you normally would. In addition you can add the following directive in these files: + +``` +#enable fallback +``` + +This will allow the plugin-specific translations to fallback to the generic/automated translations provided by the plugin. It does not matter which translation file this directive is placed it and it only need to be added once. + +As a plugin author it is also possible to embed these translation files in your plugin and register them through code with the following API: + +```csharp +/// +/// Entry point for manipulating translations that have been loaded by the plugin. +/// +/// Methods on this interface should be called during plugin initialization. Preferably during the Start callback. +/// +public static class TranslationRegistry +{ + /// + /// Obtains the translations registry instance. + /// + public static ITranslationRegistry Default { get; } +} + +/// +/// Interface for manipulating translation that have been loaded by the plugin. +/// +public interface ITranslationRegistry +{ + /// + /// Registers and loads the specified translation package. + /// + /// The assembly that the behaviour should be applied to. + /// Package containing translations. + void RegisterPluginSpecificTranslations( Assembly assembly, StreamTranslationPackage package ); + + /// + /// Registers and loads the specified translation package. + /// + /// The assembly that the behaviour should be applied to. + /// Package containing translations. + void RegisterPluginSpecificTranslations( Assembly assembly, KeyValuePairTranslationPackage package ); + + /// + /// Allow plugin-specific translation to fallback to generic translations. + /// + /// The assembly that the behaviour should be applied to. + void EnablePluginTranslationFallback( Assembly assembly ); +} +``` + ### Substitutions It is also possible to add substitutions that are applied to found texts before translations are created. This is controlled through the `SubstitutionFile`, which uses the same format as normal translation text files, although things like regexes are not supported. @@ -784,25 +839,6 @@ If `TextureHashGenerationStrategy=FromImageData` is specified, only a single has ## Integrating with Auto Translator *NOTE: Everything below this point requires programming knowledge!* -### Implementing a dedicated translation component -As a mod author implementing a translation plugin, you are able to, if you cannot find a translation to a string, simply delegate it to this mod, and you can do it without taking any references to this plugin. - -Here's how it works and what is required: - * You must implement a Component (MonoBehaviour for instance) that this plugin is able to locate by simply traversing all objects during startup. - * On this component you must add an event for the text hooks you want to override from XUnity AutoTranslator. This is done on a per text framework basis. The signature of these events must be: Func. The arguments are, in order: - 1. The component that represents the text in the UI. (The one that probably has a property called 'text'). - 2. The untranslated text - 3. This is the return value and will be the translated text IF an immediate translation took place. Otherwise it will simply be null. - * The signature for each framework looks like: - 1. UGUI: public static event Func OnUnableToTranslateUGUI - 2. TextMeshPro: public static event Func OnUnableToTranslateTextMeshPro - 3. NGUI: public static event Func OnUnableToTranslateNGUI - 3. IMGUI: public static event Func OnUnableToTranslateIMGUI - 3. TextMesh: public static event Func OnUnableToTranslateTextMesh - * Also, the events can be either instance based or static. - -Be aware that if you do this, that the hooking functionality of the Auto Translator itself will be disabled. So you are entirely responsible for implementing the required hooks for the overriden text framework. - ### Implementing a plugin that can query translations As a mod author, you may want to query translations from the plugin. This easily done, take a look at the example below. diff --git a/src/XUnity.AutoTranslator.Patcher/Patcher.cs b/src/XUnity.AutoTranslator.Patcher/Patcher.cs index 304e10c4..adab8ecc 100644 --- a/src/XUnity.AutoTranslator.Patcher/Patcher.cs +++ b/src/XUnity.AutoTranslator.Patcher/Patcher.cs @@ -29,7 +29,7 @@ public override string Version { get { - return "4.11.4"; + return "4.12.0"; } } diff --git a/src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj b/src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj index 39b7b4ae..c4efeef2 100644 --- a/src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj +++ b/src/XUnity.AutoTranslator.Plugin.BepIn-5x/XUnity.AutoTranslator.Plugin.BepIn-5x.csproj @@ -2,7 +2,7 @@ net35 - 4.11.4 + 4.12.0 diff --git a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs index 84f2dbe9..954a61eb 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslationPlugin.cs @@ -41,9 +41,8 @@ namespace XUnity.AutoTranslator.Plugin.Core /// /// Main plugin class for the AutoTranslator. /// - public class AutoTranslationPlugin : MonoBehaviour, IInternalTranslator + public class AutoTranslationPlugin : MonoBehaviour, IInternalTranslator, ITranslationRegistry { - /// /// Allow the instance to be accessed statically, as only one will exist. /// @@ -54,6 +53,7 @@ public class AutoTranslationPlugin : MonoBehaviour, IInternalTranslator internal TranslationAggregatorOptionsWindow TranslationAggregatorOptionsWindow; internal TranslationManager TranslationManager; internal TextTranslationCache TextCache; + internal Dictionary PluginTextCaches = new Dictionary( StringComparer.OrdinalIgnoreCase ); internal TextureTranslationCache TextureCache; internal UIResizeCache ResizeCache; internal SpamChecker SpamChecker; @@ -108,12 +108,14 @@ public void Initialize() // Setup console, if enabled DebugConsole.Enable(); + InitializeTextTranslationCaches(); + // Setup hooks HooksSetup.InstallTextHooks(); HooksSetup.InstallImageHooks(); HooksSetup.InstallTextGetterCompatHooks(); + HooksSetup.InstallComponentBasedPluginTranslationHooks(); - TextCache = new TextTranslationCache(); TextureCache = new TextureTranslationCache(); ResizeCache = new UIResizeCache(); TranslationManager = new TranslationManager(); @@ -123,7 +125,7 @@ public void Initialize() SpamChecker = new SpamChecker( TranslationManager ); // WORKAROUND: Initialize text parsers with delegate indicating if text should be translated - UnityTextParsers.Initialize( TextCache, ( text, scope ) => TextCache.IsTranslatable( text, true, scope ) && IsBelowMaxLength( text ) ); + UnityTextParsers.Initialize(); // resource redirectors InitializeResourceRedirector(); @@ -135,7 +137,7 @@ public void Initialize() EnableSceneLoadScan(); // load all translations from files - LoadTranslations(); + LoadTranslations( false ); // initialize ui InitializeGUI(); @@ -143,6 +145,29 @@ public void Initialize() XuaLogger.AutoTranslator.Info( $"Loaded XUnity.AutoTranslator into Unity [{Application.unityVersion}] game." ); } + private void InitializeTextTranslationCaches() + { + try + { + TextCache = new TextTranslationCache(); + + var path = Path.Combine( Settings.TranslationsPath, "plugins" ); + var directory = new DirectoryInfo( path ); + if( directory.Exists ) + { + foreach( var dir in directory.GetDirectories() ) + { + var cache = new TextTranslationCache( dir ); + PluginTextCaches.Add( dir.Name, cache ); + } + } + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while initializing text translation caches." ); + } + } + private static void EnableLogAllLoadedResources() { ResourceRedirection.LogAllLoadedResources = true; @@ -393,10 +418,60 @@ private void OnLevelWasLoaded( int id ) /// /// Loads the translations found in Translation.{lang}.txt /// - private void LoadTranslations() + private void LoadTranslations( bool reload ) { ResizeCache.LoadResizeCommandsInFiles(); + + SettingsTranslationsInitializer.LoadTranslations(); TextCache.LoadTranslationFiles(); + + if( reload ) + { + var dict = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var path = Path.Combine( Settings.TranslationsPath, "plugins" ); + var directory = new DirectoryInfo( path ); + if( directory.Exists ) + { + foreach( var dir in directory.GetDirectories() ) + { + dict.Add( dir.Name, dir ); + } + } + + foreach( var pluginCache in PluginTextCaches ) + { + pluginCache.Value.LoadTranslationFiles(); + dict.Remove( pluginCache.Key ); + } + + // we need to hook any newly created folders + foreach( var kvp in dict ) + { + var assemblyName = kvp.Value.Name; + + var cache = new TextTranslationCache( kvp.Value ); + PluginTextCaches.Add( assemblyName, cache ); + cache.LoadTranslationFiles(); + + var assembly = AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault( x => x.GetName().Name.Equals( assemblyName, StringComparison.OrdinalIgnoreCase ) ); + + if( assembly != null ) + { + HooksSetup.InstallIMGUIBasedPluginTranslationHooks( assembly, true ); + } + } + HooksSetup.InstallComponentBasedPluginTranslationHooks(); + } + else + { + foreach( var pluginCache in PluginTextCaches ) + { + pluginCache.Value.LoadTranslationFiles(); + } + } + TextureCache.LoadTranslationFiles(); } @@ -483,27 +558,6 @@ private void QueueNewUntranslatedForClipboard( UntranslatedText key ) } } - internal string ExternalHook_TextChanged_WithResult( object ui, string text ) - { - if( ui == null ) return null; - - try - { - if( _textHooksEnabled && !_temporarilyDisabled ) - { - var info = ui.GetOrCreateTextTranslationInfo(); - CallOrigin.ExpectsTextToBeReturned = info.GetIsKnownTextComponent(); - - return TranslateOrQueueWebJob( ui, text, true, info ); - } - return null; - } - finally - { - CallOrigin.ExpectsTextToBeReturned = false; - } - } - internal string Hook_TextChanged_WithResult( object ui, string text, bool onEnable ) { try @@ -512,6 +566,11 @@ internal string Hook_TextChanged_WithResult( object ui, string text, bool onEnab if( _textHooksEnabled && !_temporarilyDisabled ) { var info = ui.GetOrCreateTextTranslationInfo(); + if( onEnable && info != null && CallOrigin.TextCache != null ) + { + info.TextCache = CallOrigin.TextCache; + } + CallOrigin.ExpectsTextToBeReturned = true; result = TranslateOrQueueWebJob( ui, text, false, info ); @@ -534,6 +593,11 @@ internal void Hook_TextChanged( object ui, bool onEnable ) if( _textHooksEnabled && !_temporarilyDisabled ) { var info = ui.GetOrCreateTextTranslationInfo(); + if( onEnable && info != null && CallOrigin.TextCache != null ) + { + info.TextCache = CallOrigin.TextCache; + } + TranslateOrQueueWebJob( ui, null, false, info ); } @@ -715,12 +779,26 @@ private bool IsBelowMaxLength( string str ) private string TranslateOrQueueWebJob( object ui, string text, bool ignoreComponentState, TextTranslationInfo info ) { + var tc = CallOrigin.GetTextCache( info, TextCache ); + if( info != null && info.IsStabilizingText == true ) { - return TranslateImmediate( ui, text, info, ignoreComponentState ); + return TranslateImmediate( ui, text, info, ignoreComponentState, tc ); } - return TranslateOrQueueWebJobImmediate( ui, text, TranslationScopes.None, info, info.GetSupportsStabilization(), ignoreComponentState, false, true ); + //XuaLogger.AutoTranslator.Warn( tc.GetType().Name ); + //XuaLogger.AutoTranslator.Warn( tc.AllowGeneratingNewTranslations.ToString() ); + + return TranslateOrQueueWebJobImmediate( + ui, + text, + TranslationScopes.None, + info, + info.GetSupportsStabilization(), + ignoreComponentState, + false, + tc.AllowGeneratingNewTranslations, + tc ); } private static bool IsCurrentlySetting( TextTranslationInfo info ) @@ -787,6 +865,7 @@ private void TranslateTexture( object source, ref Texture2D texture, bool isPref bool hasContext = context != null; bool forceReload = false; + bool changedImage = false; if( hasContext ) { forceReload = context.RegisterTextureInContextAndDetermineWhetherToReload( texture ); @@ -804,10 +883,12 @@ private void TranslateTexture( object source, ref Texture2D texture, bool isPref if( !Settings.EnableLegacyTextureLoading ) { texture.LoadImageEx( newData ); + changedImage = true; } else { tti.CreateTranslatedTexture( newData ); + changedImage = true; } } finally @@ -856,11 +937,13 @@ private void TranslateTexture( object source, ref Texture2D texture, bool isPref if( !Settings.EnableLegacyTextureLoading ) { texture.LoadImageEx( originalData ); + changedImage = true; } else { // we just need to ensure we set/change the reference tti.CreateOriginalTexture(); + changedImage = true; } } finally @@ -909,11 +992,13 @@ private void TranslateTexture( object source, ref Texture2D texture, bool isPref if( !Settings.EnableLegacyTextureLoading ) { texture.LoadImageEx( originalData ); + changedImage = true; } else { // we just need to ensure we set/change the reference tti.CreateOriginalTexture(); + changedImage = true; } } finally @@ -976,7 +1061,7 @@ private void TranslateTexture( object source, ref Texture2D texture, bool isPref texture = previousTextureValue; } - if( forceReload ) + if( forceReload && changedImage ) { XuaLogger.AutoTranslator.Info( $"Reloaded texture: {texture.name} ({key})." ); } @@ -1044,7 +1129,7 @@ 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 ) + private string TranslateImmediate( object ui, string text, TextTranslationInfo info, bool ignoreComponentState, IReadOnlyTextTranslationCache tc ) { text = text ?? ui.GetText(); @@ -1059,30 +1144,28 @@ private string TranslateImmediate( object ui, string text, TextTranslationInfo i info?.Reset( originalText ); - //XuaLogger.Current.Warn( "3: " + originalText + " - " + ui.GetHashCode() ); - var scope = TranslationScopeProvider.GetScope( ui ); - if( !text.IsNullOrWhiteSpace() && TextCache.IsTranslatable( text, false, scope ) && ui.ShouldTranslateTextComponent( ignoreComponentState ) && !IsCurrentlySetting( info ) ) + if( !text.IsNullOrWhiteSpace() && tc.IsTranslatable( text, false, scope ) && ui.ShouldTranslateTextComponent( ignoreComponentState ) && !IsCurrentlySetting( info ) ) { //var textKey = new TranslationKey( ui, text, !ui.SupportsStabilization(), false ); var isSpammer = ui.IsSpammingComponent(); var textKey = GetCacheKey( ui, text, isSpammer ); // potentially shortcircuit if fully templated - if( ( textKey.IsTemplated && !TextCache.IsTranslatable( textKey.TemplatedOriginal_Text, false, scope ) ) || textKey.IsOnlyTemplate ) + if( ( textKey.IsTemplated && !tc.IsTranslatable( textKey.TemplatedOriginal_Text, false, scope ) ) || textKey.IsOnlyTemplate ) { var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginal_Text ); - var isPartial = TextCache.IsPartial( textKey.TemplatedOriginal_Text, scope ); + var isPartial = tc.IsPartial( textKey.TemplatedOriginal_Text, scope ); SetTranslatedText( ui, untemplatedTranslation, !isPartial ? originalText : null, info ); return untemplatedTranslation; } // if we already have translation loaded in our cache, simply load it and set text string translation; - if( TextCache.TryGetTranslation( textKey, false, false, scope, out translation ) ) + if( tc.TryGetTranslation( textKey, false, false, scope, out translation ) ) { var untemplatedTranslation = textKey.Untemplate( translation ); - var isPartial = TextCache.IsPartial( textKey.TemplatedOriginal_Text, scope ); + var isPartial = tc.IsPartial( textKey.TemplatedOriginal_Text, scope ); SetTranslatedText( ui, untemplatedTranslation, !isPartial ? originalText : null, info ); return untemplatedTranslation; } @@ -1090,13 +1173,13 @@ private string TranslateImmediate( object ui, string text, TextTranslationInfo i { if( UnityTextParsers.GameLogTextParser.CanApply( ui ) ) { - var result = UnityTextParsers.GameLogTextParser.Parse( text, scope ); + var result = UnityTextParsers.GameLogTextParser.Parse( text, scope, tc ); if( result != null ) { - translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, null ); + translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, tc, null ); if( translation != null ) { - var isPartial = TextCache.IsPartial( textKey.TemplatedOriginal_Text, scope ); + var isPartial = tc.IsPartial( textKey.TemplatedOriginal_Text, scope ); SetTranslatedText( ui, translation, null, info ); return translation; } @@ -1137,7 +1220,7 @@ bool ITranslator.TryTranslate( string text, int scope, out string translatedText private bool TryTranslate( string text, int scope, out string translatedText ) { - if(scope == TranslationScopes.None) + if( scope == TranslationScopes.None ) { scope = TranslationScopeProvider.GetScope( null ); } @@ -1163,7 +1246,7 @@ private bool TryTranslate( string text, int scope, out string translatedText ) } else { - var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope ) ?? UnityTextParsers.RichTextParser.Parse( text, scope ); + var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope, TextCache ) ?? UnityTextParsers.RichTextParser.Parse( text, scope ); if( parserResult != null ) { translatedText = TranslateByParserResult( null, parserResult, scope, null, false, true, null ); @@ -1211,7 +1294,7 @@ private InternalTranslationResult Translate( string text, int scope, Translation { if( context.GetLevelsOfRecursion() < Settings.MaxTextParserRecursion ) { - var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope ); + var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope, TextCache ); if( parserResult != null ) { translation = TranslateByParserResult( endpoint, parserResult, scope, result, allowStartTranslateImmediate, result.IsGlobal, context ); @@ -1318,7 +1401,7 @@ private InternalTranslationResult Translate( string text, int scope, Translation { if( context.GetLevelsOfRecursion() < Settings.MaxTextParserRecursion ) { - var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, TranslationScopes.None ); + var parserResult = UnityTextParsers.RegexSplittingTextParser.Parse( text, TranslationScopes.None, TextCache ); if( parserResult != null ) { translation = TranslateByParserResult( endpoint, parserResult, TranslationScopes.None, result, allowStartTranslateImmediate, result.IsGlobal, context ); @@ -1522,6 +1605,7 @@ private string TranslateOrQueueWebJobImmediate( object ui, string text, int scope, TextTranslationInfo info, bool allowStabilizationOnTextComponent, bool ignoreComponentState, bool allowStartTranslationImmediate, bool allowStartTranslationLater, + IReadOnlyTextTranslationCache tc, ParserTranslationContext context = null ) { text = text ?? ui.GetText(); @@ -1543,7 +1627,7 @@ private string TranslateOrQueueWebJobImmediate( } // Ensure that we actually want to translate this text and its owning UI element. - if( !text.IsNullOrWhiteSpace() && TextCache.IsTranslatable( text, false, scope ) && ui.ShouldTranslateTextComponent( ignoreComponentState ) && !IsCurrentlySetting( info ) ) + if( !text.IsNullOrWhiteSpace() && tc.IsTranslatable( text, false, scope ) && ui.ShouldTranslateTextComponent( ignoreComponentState ) && !IsCurrentlySetting( info ) ) { var isSpammer = ui.IsSpammingComponent(); if( isSpammer && !IsBelowMaxLength( text ) ) return null; // avoid templating long strings every frame for IMGUI, important! @@ -1552,7 +1636,7 @@ private string TranslateOrQueueWebJobImmediate( var textKey = GetCacheKey( ui, text, isSpammer ); // potentially shortcircuit if fully templated - if( ( textKey.IsTemplated && !TextCache.IsTranslatable( textKey.TemplatedOriginal_Text, false, scope ) ) || textKey.IsOnlyTemplate ) + if( ( textKey.IsTemplated && !tc.IsTranslatable( textKey.TemplatedOriginal_Text, false, scope ) ) || textKey.IsOnlyTemplate ) { var untemplatedTranslation = textKey.Untemplate( textKey.TemplatedOriginal_Text ); if( context == null ) @@ -1564,7 +1648,7 @@ private string TranslateOrQueueWebJobImmediate( // if we already have translation loaded in our _translatios dictionary, simply load it and set text string translation; - if( TextCache.TryGetTranslation( textKey, !isSpammer, false, scope, out translation ) ) + if( tc.TryGetTranslation( textKey, !isSpammer, false, scope, out translation ) ) { if( context == null && !isSpammer ) { @@ -1574,7 +1658,7 @@ private string TranslateOrQueueWebJobImmediate( var untemplatedTranslation = textKey.Untemplate( translation ); if( context == null ) // never set text if operation is contextualized (only a part translation) { - var isPartial = TextCache.IsPartial( textKey.TemplatedOriginal_Text, scope ); + var isPartial = tc.IsPartial( textKey.TemplatedOriginal_Text, scope ); SetTranslatedText( ui, untemplatedTranslation, !isPartial ? originalText : null, info ); } return untemplatedTranslation; @@ -1589,10 +1673,10 @@ private string TranslateOrQueueWebJobImmediate( if( UnityTextParsers.GameLogTextParser.CanApply( ui ) && context == null ) // only at the first layer! { - var result = UnityTextParsers.GameLogTextParser.Parse( text, scope ); + var result = UnityTextParsers.GameLogTextParser.Parse( text, scope, tc ); if( result != null ) { - translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, context ); + translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, tc, context ); if( translation != null ) { if( context == null ) @@ -1608,12 +1692,12 @@ private string TranslateOrQueueWebJobImmediate( } } } - if( UnityTextParsers.RegexSplittingTextParser.CanApply( ui ) && isBelowMaxLength ) + if( isBelowMaxLength && UnityTextParsers.RegexSplittingTextParser.CanApply( ui ) ) { - var result = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope ); + var result = UnityTextParsers.RegexSplittingTextParser.Parse( text, scope, tc ); if( result != null ) { - translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, context ); + translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, tc, context ); if( translation != null ) { if( context == null ) @@ -1629,12 +1713,12 @@ private string TranslateOrQueueWebJobImmediate( } } } - if( UnityTextParsers.RichTextParser.CanApply( ui ) && isBelowMaxLength && !context.HasBeenParsedBy( ParserResultOrigin.RichTextParser ) ) + if( isBelowMaxLength && UnityTextParsers.RichTextParser.CanApply( ui ) && !context.HasBeenParsedBy( ParserResultOrigin.RichTextParser ) ) { var result = UnityTextParsers.RichTextParser.Parse( text, scope ); if( result != null ) { - translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, context ); + translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, allowStartTranslationImmediate, allowStartTranslationLater && !allowStabilizationOnTextComponent, tc, context ); if( translation != null ) { if( context == null ) @@ -1716,13 +1800,13 @@ private string TranslateOrQueueWebJobImmediate( info?.Reset( originalText ); - if( !stabilizedText.IsNullOrWhiteSpace() && TextCache.IsTranslatable( stabilizedText, false, scope ) ) + if( !stabilizedText.IsNullOrWhiteSpace() && tc.IsTranslatable( stabilizedText, false, scope ) ) { // potentially shortcircuit if templated is a translation var stabilizedTextKey = GetCacheKey( ui, stabilizedText, false ); // potentially shortcircuit if fully templated - if( ( stabilizedTextKey.IsTemplated && !TextCache.IsTranslatable( stabilizedTextKey.TemplatedOriginal_Text, false, scope ) ) || stabilizedTextKey.IsOnlyTemplate ) + if( ( stabilizedTextKey.IsTemplated && !tc.IsTranslatable( stabilizedTextKey.TemplatedOriginal_Text, false, scope ) ) || stabilizedTextKey.IsOnlyTemplate ) { var untemplatedTranslation = stabilizedTextKey.Untemplate( stabilizedTextKey.TemplatedOriginal_Text ); SetTranslatedText( ui, untemplatedTranslation, originalText, info ); @@ -1732,9 +1816,9 @@ private string TranslateOrQueueWebJobImmediate( QueueNewUntranslatedForClipboard( stabilizedTextKey ); // once the text has stabilized, attempt to look it up - if( TextCache.TryGetTranslation( stabilizedTextKey, true, false, scope, out translation ) ) + if( tc.TryGetTranslation( stabilizedTextKey, true, false, scope, out translation ) ) { - var isPartial = TextCache.IsPartial( stabilizedTextKey.TemplatedOriginal_Text, scope ); + var isPartial = tc.IsPartial( stabilizedTextKey.TemplatedOriginal_Text, scope ); SetTranslatedText( ui, stabilizedTextKey.Untemplate( translation ), !isPartial ? originalText : null, info ); } else @@ -1744,10 +1828,10 @@ private string TranslateOrQueueWebJobImmediate( var isBelowMaxLength = IsBelowMaxLength( stabilizedText ); if( UnityTextParsers.GameLogTextParser.CanApply( ui ) && context == null ) { - var result = UnityTextParsers.GameLogTextParser.Parse( stabilizedText, scope ); + var result = UnityTextParsers.GameLogTextParser.Parse( stabilizedText, scope, tc ); if( result != null ) { - var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, context ); + var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, tc, context ); if( translatedText != null && context == null ) { SetTranslatedText( ui, translatedText, null, info ); @@ -1755,12 +1839,12 @@ private string TranslateOrQueueWebJobImmediate( return; } } - if( UnityTextParsers.RegexSplittingTextParser.CanApply( ui ) && isBelowMaxLength ) + if( isBelowMaxLength && UnityTextParsers.RegexSplittingTextParser.CanApply( ui ) ) { - var result = UnityTextParsers.RegexSplittingTextParser.Parse( stabilizedText, scope ); + var result = UnityTextParsers.RegexSplittingTextParser.Parse( stabilizedText, scope, tc ); if( result != null ) { - var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, context ); + var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, tc, context ); if( translatedText != null && context == null ) { SetTranslatedText( ui, translatedText, originalText, info ); @@ -1768,12 +1852,12 @@ private string TranslateOrQueueWebJobImmediate( return; } } - if( UnityTextParsers.RichTextParser.CanApply( ui ) && isBelowMaxLength && !context.HasBeenParsedBy( ParserResultOrigin.RichTextParser ) ) + if( isBelowMaxLength && UnityTextParsers.RichTextParser.CanApply( ui ) && !context.HasBeenParsedBy( ParserResultOrigin.RichTextParser ) ) { var result = UnityTextParsers.RichTextParser.Parse( stabilizedText, scope ); if( result != null ) { - var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, context ); + var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, true, false, tc, context ); if( translatedText != null && context == null ) { SetTranslatedText( ui, translatedText, originalText, info ); @@ -1842,7 +1926,7 @@ private string TranslateOrQueueWebJobImmediate( { // if we already have translation loaded in our _translatios dictionary, simply load it and set text string translation; - if( TextCache.TryGetTranslation( textKey, !isSpammer, false, scope, out translation ) ) + if( tc.TryGetTranslation( textKey, !isSpammer, false, scope, out translation ) ) { // no need to do anything ! } @@ -1855,7 +1939,7 @@ private string TranslateOrQueueWebJobImmediate( // once the text has stabilized, attempt to look it up if( !Settings.IsShutdown && !endpoint.HasFailedDueToConsecutiveErrors ) { - if( !TextCache.TryGetTranslation( textKey, true, false, scope, out translation ) ) + if( !tc.TryGetTranslation( textKey, true, false, scope, out translation ) ) { CreateTranslationJobFor( endpoint, ui, textKey, null, context, true, true, true, isTranslatable ); } @@ -1871,7 +1955,7 @@ private string TranslateOrQueueWebJobImmediate( return null; } - private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserResult result, int scope, bool allowStartTranslationImmediate, bool allowStartTranslationLater, ParserTranslationContext parentContext ) + private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserResult result, int scope, bool allowStartTranslationImmediate, bool allowStartTranslationLater, IReadOnlyTextTranslationCache tc, ParserTranslationContext parentContext ) { // attempt to lookup ALL strings immediately; return result if possible; queue operations var allowPartial = TranslationManager.CurrentEndpoint == null && result.AllowPartialTranslation; @@ -1879,13 +1963,13 @@ private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserR var translation = result.GetTranslationFromParts( untranslatedTextPart => { - if( !untranslatedTextPart.IsNullOrWhiteSpace() && TextCache.IsTranslatable( untranslatedTextPart, true, scope ) && IsBelowMaxLength( untranslatedTextPart ) ) + if( !untranslatedTextPart.IsNullOrWhiteSpace() && tc.IsTranslatable( untranslatedTextPart, true, scope ) && IsBelowMaxLength( untranslatedTextPart ) ) { var textKey = new UntranslatedText( untranslatedTextPart, false, false, Settings.FromLanguageUsesWhitespaceBetweenWords ); - if( TextCache.IsTranslatable( textKey.TemplatedOriginal_Text, true, scope ) ) + if( tc.IsTranslatable( textKey.TemplatedOriginal_Text, true, scope ) ) { string partTranslation; - if( TextCache.TryGetTranslation( textKey, false, true, scope, out partTranslation ) ) + if( tc.TryGetTranslation( textKey, false, true, scope, out partTranslation ) ) { return textKey.Untemplate( partTranslation ) ?? string.Empty; } @@ -1895,7 +1979,7 @@ private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserR } else { - partTranslation = TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, scope, null, false, true, allowStartTranslationImmediate, allowStartTranslationLater, context ); + partTranslation = TranslateOrQueueWebJobImmediate( ui, untranslatedTextPart, scope, null, false, true, allowStartTranslationImmediate, allowStartTranslationLater, tc, context ); if( partTranslation != null ) { return textKey.Untemplate( partTranslation ) ?? string.Empty; @@ -2065,12 +2149,90 @@ void Awake() } } + private TextTranslationCache GetTextCacheFor( string assemblyName ) + { + if( !PluginTextCaches.TryGetValue( assemblyName, out var cache ) ) + { + cache = new TextTranslationCache( assemblyName ); + PluginTextCaches[ assemblyName ] = cache; + } + return cache; + } + + void ITranslationRegistry.RegisterPluginSpecificTranslations( Assembly assembly, StreamTranslationPackage package ) + { + var cache = GetTextCacheFor( assembly.GetName().Name ); + cache.RegisterPackage( package ); + cache.LoadTranslationFiles(); + + HooksSetup.InstallComponentBasedPluginTranslationHooks(); + HooksSetup.InstallIMGUIBasedPluginTranslationHooks( assembly, true ); + } + + void ITranslationRegistry.RegisterPluginSpecificTranslations( Assembly assembly, KeyValuePairTranslationPackage package ) + { + var cache = GetTextCacheFor( assembly.GetName().Name ); + cache.RegisterPackage( package ); + cache.LoadTranslationFiles(); + + HooksSetup.InstallComponentBasedPluginTranslationHooks(); + HooksSetup.InstallIMGUIBasedPluginTranslationHooks( assembly, true ); + } + + void ITranslationRegistry.EnablePluginTranslationFallback( Assembly assembly ) + { + var cache = GetTextCacheFor( assembly.GetName().Name ); + cache.AllowFallback = true; + cache.DefaultAllowFallback = true; + + HooksSetup.InstallComponentBasedPluginTranslationHooks(); + HooksSetup.InstallIMGUIBasedPluginTranslationHooks( assembly, true ); + } + + IEnumerator HookLoadedPlugins() + { + yield return null; + + if( PluginTextCaches.Count == 0 ) + { + XuaLogger.AutoTranslator.Info( "Skipping plugin scan because no plugin-specific translations has been registered." ); + yield break; + } + else + { + XuaLogger.AutoTranslator.Info( "Scanning for plugins to hook for translations..." ); + } + + var gameDataPath = Application.dataPath.UseCorrectDirectorySeparators(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach( var assembly in assemblies ) + { + try + { + if( assembly.FullName.StartsWith( "XUnity" ) ) + continue; + + if( assembly.ManifestModule.GetType().FullName.Contains( "Emit" ) ) + continue; + + var location = assembly.Location.UseCorrectDirectorySeparators(); + if( !location.StartsWith( gameDataPath, StringComparison.OrdinalIgnoreCase ) && PluginTextCaches.TryGetValue( assembly.GetName().Name, out _ ) ) + { + HooksSetup.InstallIMGUIBasedPluginTranslationHooks( assembly, false ); + } + } + catch( Exception e1 ) + { + XuaLogger.AutoTranslator.Warn( e1, "An error occurred while scanning assembly: " + assembly.FullName ); + } + } + } + void Start() { try { - // this is delayed to ensure other plugins has had a chance to startup - HooksSetup.InstallOverrideTextHooks(); + StartCoroutine( HookLoadedPlugins() ); } catch( Exception e ) { @@ -2432,7 +2594,7 @@ private void OnJobCompleted( TranslationJob job ) string translatedText; if( context.TranslationResult == null ) { - translatedText = TranslateOrQueueWebJobImmediateByParserResult( context.Component, result, TranslationScopes.None, false, false, null ); + translatedText = TranslateOrQueueWebJobImmediateByParserResult( context.Component, result, TranslationScopes.None, false, false, TextCache, null ); } else { @@ -2441,11 +2603,11 @@ private void OnJobCompleted( TranslationJob job ) if( !string.IsNullOrEmpty( translatedText ) ) { - if( result.CacheCombinedResult ) + if( context.CachedCombinedResult() ) { if( job.SaveResultGlobally ) { - TextCache.AddTranslationToCache( context.Result.OriginalText, translatedText, result.PersistCombinedResult, TranslationType.Full, TranslationScopes.None ); + TextCache.AddTranslationToCache( context.Result.OriginalText, translatedText, context.PersistCombinedResult(), TranslationType.Full, TranslationScopes.None ); } job.Endpoint.AddTranslationToCache( context.Result.OriginalText, translatedText ); } @@ -2495,7 +2657,7 @@ private static UntranslatedText GetCacheKey( object ui, string originalText, boo private void ReloadTranslations() { - LoadTranslations(); + LoadTranslations( true ); var context = new TextureReloadContext(); foreach( var kvp in ExtensionDataHelper.GetAllRegisteredObjects() ) @@ -2524,10 +2686,10 @@ private void ReloadTranslations() { if( UnityTextParsers.GameLogTextParser.CanApply( ui ) ) { - var result = UnityTextParsers.GameLogTextParser.Parse( originalText, scope ); + var result = UnityTextParsers.GameLogTextParser.Parse( originalText, scope, TextCache ); if( result != null ) { - var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, null ); + var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, TextCache, null ); if( translation != null ) { tti.UnresizeUI( ui ); @@ -2538,10 +2700,10 @@ private void ReloadTranslations() } if( UnityTextParsers.RegexSplittingTextParser.CanApply( ui ) && isBelowMaxLength ) { - var result = UnityTextParsers.RegexSplittingTextParser.Parse( originalText, scope ); + var result = UnityTextParsers.RegexSplittingTextParser.Parse( originalText, scope, TextCache ); if( result != null ) { - var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, null ); + var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, TextCache, null ); if( translation != null ) { tti.UnresizeUI( ui ); @@ -2555,7 +2717,7 @@ private void ReloadTranslations() var result = UnityTextParsers.RichTextParser.Parse( originalText, scope ); if( result != null ) { - var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, null ); + var translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, scope, false, false, TextCache, null ); if( translation != null ) { tti.UnresizeUI( ui ); diff --git a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs index b44e5a5d..196a68bd 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/AutoTranslatorSettings.cs @@ -1,4 +1,6 @@ -using XUnity.AutoTranslator.Plugin.Core.Configuration; +using System.Linq; +using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.Common.Extensions; namespace XUnity.AutoTranslator.Plugin.Core { diff --git a/src/XUnity.AutoTranslator.Plugin.Core/CallOrigin.cs b/src/XUnity.AutoTranslator.Plugin.Core/CallOrigin.cs index da4462ec..a08b7e4e 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/CallOrigin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/CallOrigin.cs @@ -1,12 +1,103 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Text; +using UnityEngine; +using XUnity.AutoTranslator.Plugin.Core.Extensions; +using XUnity.AutoTranslator.Plugin.Core.Utilities; +using XUnity.Common.Constants; +using XUnity.Common.Logging; namespace XUnity.AutoTranslator.Plugin.Core { internal static class CallOrigin { public static bool ExpectsTextToBeReturned = false; + public static IReadOnlyTextTranslationCache TextCache = null; + + private static readonly HashSet BreakingAssemblies; + + static CallOrigin() + { + BreakingAssemblies = new HashSet(); + try + { + BreakingAssemblies.AddRange( + AppDomain.CurrentDomain + .GetAssemblies() + .Where( x => x.GetName().Name.Equals( "Assembly-CSharp" ) || x.GetName().Equals( "Assembly-CSharp-firstpass" ) ) + ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while scanning for game assemblies." ); + } + } + + public static IReadOnlyTextTranslationCache GetTextCache( TextTranslationInfo info, TextTranslationCache generic ) + { + if( info != null ) + { + return info.TextCache ?? generic; + } + else + { + return TextCache ?? generic; + } + } + + public static void SetTextCacheForAllObjectsInHierachy( GameObject go, IReadOnlyTextTranslationCache cache ) + { + try + { + foreach( var comp in go.GetAllTextComponentsInChildren() ) + { + var info = comp.GetOrCreateTextTranslationInfo(); + info.TextCache = cache; + } + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while scanning object hierarchy for text components." ); + } + } + + public static IReadOnlyTextTranslationCache CalculateTextCacheFromStackTrace() + { + try + { + var trace = new StackTrace( 2 ); + var caches = AutoTranslationPlugin.Current.PluginTextCaches; + var frames = trace.GetFrames(); + var len = frames.Length; + for( int i = 0; i < len; i++ ) + { + var frame = frames[ i ]; + var method = frame.GetMethod(); + if( method != null ) + { + var type = method.DeclaringType; + var assembly = type.Assembly; + if( BreakingAssemblies.Contains( assembly ) ) + break; + + var name = assembly.GetName().Name; + if( caches.TryGetValue( name, out var tc ) ) + { + var translationCache = AutoTranslationPlugin.Current.TextCache.GetOrCreateCompositeCache( tc ); + return translationCache; + } + } + } + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while calculating text translation cache from stack trace." ); + } + + return null; + } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/CompositeTextTranslationCache.cs b/src/XUnity.AutoTranslator.Plugin.Core/CompositeTextTranslationCache.cs new file mode 100644 index 00000000..c4d34b75 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/CompositeTextTranslationCache.cs @@ -0,0 +1,47 @@ +using System.Text.RegularExpressions; +using XUnity.AutoTranslator.Plugin.Core.Parsing; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + internal class CompositeTextTranslationCache : IReadOnlyTextTranslationCache + { + private IReadOnlyTextTranslationCache _first; + private IReadOnlyTextTranslationCache _second; + + public CompositeTextTranslationCache( + IReadOnlyTextTranslationCache first, + IReadOnlyTextTranslationCache second ) + { + _first = first; + _second = second; + } + + public bool AllowGeneratingNewTranslations => _first.AllowFallback; + + public bool AllowFallback => _first.AllowFallback; + + public bool IsTranslatable( string text, bool isToken, int scope ) + { + return _first.IsTranslatable( text, isToken, scope ) + || ( _first.AllowFallback && _second.IsTranslatable( text, isToken, scope ) ); + } + + public bool IsPartial( string text, int scope ) + { + return _first.IsPartial( text, scope ) + || ( _first.AllowFallback && _second.IsPartial( text, scope ) ); + } + + public bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool allowToken, int scope, out string value ) + { + return _first.TryGetTranslation( key, allowRegex, allowToken, scope, out value ) + || ( _first.AllowFallback && _second.TryGetTranslation( key, allowRegex, allowToken, scope, out value ) ); + } + + public bool TryGetTranslationSplitter( string text, int scope, out Match match, out RegexTranslationSplitter splitter ) + { + return _first.TryGetTranslationSplitter( text, scope, out match, out splitter ) + || ( _first.AllowFallback && _second.TryGetTranslationSplitter( text, scope, out match, out splitter ) ); + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs index 2eeb1dba..4ed3520d 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Configuration/Settings.cs @@ -84,7 +84,6 @@ internal static class Settings public static bool EnableTextMeshPro; public static bool EnableTextMesh; public static bool EnableFairyGUI; - public static bool AllowPluginHookOverride; public static bool IgnoreWhitespaceInDialogue; public static bool IgnoreWhitespaceInNGUI; public static int MinDialogueChars; @@ -185,7 +184,6 @@ public static void Configure() EnableTextMeshPro = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableTextMeshPro", true ); EnableTextMesh = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableTextMesh", false ); EnableFairyGUI = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "EnableFairyGUI", true ); - AllowPluginHookOverride = PluginEnvironment.Current.Preferences.GetOrDefault( "TextFrameworks", "AllowPluginHookOverride", true ); MaxCharactersPerTranslation = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "MaxCharactersPerTranslation", 200 ); IgnoreWhitespaceInDialogue = PluginEnvironment.Current.Preferences.GetOrDefault( "Behaviour", "IgnoreWhitespaceInDialogue", true ); @@ -289,7 +287,7 @@ public static void Configure() SubstitutionFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, SubstitutionFile.UseCorrectDirectorySeparators() ).Parameterize(); PreprocessorsFilePath = Path.Combine( PluginEnvironment.Current.TranslationPath, PreprocessorsFile.UseCorrectDirectorySeparators() ).Parameterize(); - TranslationsPath = Path.Combine( PluginEnvironment.Current.TranslationPath, Settings.TranslationDirectory ).Parameterize(); + TranslationsPath = Path.Combine( PluginEnvironment.Current.TranslationPath, Settings.TranslationDirectory.UseCorrectDirectorySeparators() ).Parameterize(); FromLanguageUsesWhitespaceBetweenWords = LanguageHelper.RequiresWhitespaceUponLineMerging( FromLanguage ); ToLanguageUsesWhitespaceBetweenWords = LanguageHelper.RequiresWhitespaceUponLineMerging( Language ); diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs b/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs index d7594043..66d6ae63 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Constants/PluginData.cs @@ -23,6 +23,6 @@ public static class PluginData /// /// Gets the version of the plugin. /// - public const string Version = "4.11.4"; + public const string Version = "4.12.0"; } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs index 38b96a28..24585664 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Extensions/GameObjectExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using UnityEngine; +using XUnity.Common.Constants; namespace XUnity.AutoTranslator.Plugin.Core.Extensions { @@ -31,6 +32,38 @@ public static Component GetFirstComponentInSelfOrAncestor( this GameObject go, T return null; } + public static IEnumerable GetAllTextComponentsInChildren( this GameObject go ) + { + if( ClrTypes.TMP_Text != null ) + { + foreach( var comp in go.GetComponentsInChildren( ClrTypes.TMP_Text ) ) + { + yield return comp; + } + } + if( ClrTypes.Text != null ) + { + foreach( var comp in go.GetComponentsInChildren( ClrTypes.Text ) ) + { + yield return comp; + } + } + if( ClrTypes.TextMesh != null ) + { + foreach( var comp in go.GetComponentsInChildren( ClrTypes.TextMesh ) ) + { + yield return comp; + } + } + if( ClrTypes.UILabel != null ) + { + foreach( var comp in go.GetComponentsInChildren( ClrTypes.UILabel ) ) + { + yield return comp; + } + } + } + public static string[] GetPathSegments( this GameObject obj ) { int i = 0; diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Features.cs b/src/XUnity.AutoTranslator.Plugin.Core/Features.cs index 34ae9e97..9521ed13 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Features.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Features.cs @@ -40,7 +40,7 @@ public static class Features /// Gets a bool indicating if the WaitForSecondsRealtime class is supported. /// public static bool SupportsWaitForSecondsRealtime { get; } = false; - + static Features() { try diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/FairyGUIHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/FairyGUIHooks.cs index 20e0d1dd..6a27356a 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/FairyGUIHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/FairyGUIHooks.cs @@ -12,8 +12,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.FairyGUI { internal static class FairyGUIHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { typeof( TextField_text_Hook ), typeof( TextField_htmlText_Hook ), @@ -35,10 +33,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !FairyGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -72,10 +67,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !FairyGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs index 3a12019d..735fc07b 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/HooksSetup.cs @@ -21,34 +21,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks { internal static class HooksSetup { - public static void InstallOverrideTextHooks() - { - if( Settings.EnableUGUI ) - { - UGUIHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateUGUI, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - if( Settings.EnableTextMeshPro ) - { - TextMeshProHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateTextMeshPro, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - if( Settings.EnableNGUI ) - { - NGUIHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateNGUI, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - if( Settings.EnableIMGUI ) - { - IMGUIHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateIMGUI, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - if( Settings.EnableTextMesh ) - { - TextMeshProHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateTextMesh, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - if( Settings.EnableFairyGUI ) - { - TextMeshProHooks.HooksOverriden = SetupHook( KnownEvents.OnUnableToTranslateFairyGUI, AutoTranslationPlugin.Current.ExternalHook_TextChanged_WithResult ); - } - } - public static void InstallTextGetterCompatHooks() { try @@ -103,7 +75,6 @@ public static void InstallTextAssetHooks() public static void InstallTextHooks() { - try { if( Settings.EnableUGUI ) @@ -186,56 +157,61 @@ public static void InstallTextHooks() } } - public static bool SetupHook( string eventName, Func callback ) + private static bool _installedPluginTranslationHooks = false; + public static void InstallComponentBasedPluginTranslationHooks() { - if( !Settings.AllowPluginHookOverride ) return false; + if( AutoTranslationPlugin.Current.PluginTextCaches.Count > 0 ) + { + if( !_installedPluginTranslationHooks ) + { + _installedPluginTranslationHooks = true; + try + { + HookingHelper.PatchAll( PluginTranslationHooks.All, Settings.ForceMonoModHooks ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while setting up hooks for Plugin translations." ); + } + } + } + } - try + private static HashSet _installedAssemblies = new HashSet(); + public static void InstallIMGUIBasedPluginTranslationHooks( Assembly assembly, bool final ) + { + if( Settings.EnableIMGUI && !_installedAssemblies.Contains( assembly ) ) { - var objects = GameObject.FindObjectsOfType(); - foreach( var gameObject in objects ) + if( final ) + { + IMGUIPluginTranslationHooks.ResetHandledForAllInAssembly( assembly ); + } + + var types = assembly.GetTypes(); + foreach( var type in types ) { - if( gameObject != null ) + try { - var components = gameObject.GetComponents(); - foreach( var component in components ) + if( typeof( MonoBehaviour ).IsAssignableFrom( type ) && !type.IsAbstract ) { - if( component != null ) + var method = type.GetMethod( "OnGUI", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy ); + if( method != null ) { - var e = component.GetType().GetEvent( eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); - if( e != null ) - { - var addMethod = e.GetAddMethod(); - if( addMethod != null ) - { - try - { - if( addMethod.IsStatic ) - { - addMethod.Invoke( null, new object[] { callback } ); - } - else - { - addMethod.Invoke( component, new object[] { callback } ); - } - - XuaLogger.AutoTranslator.Info( eventName + " was hooked by external plugin." ); - return true; - } - catch { } - } - } + IMGUIPluginTranslationHooks.HookIfConfigured( method ); } } } + catch( Exception e2 ) + { + XuaLogger.AutoTranslator.Warn( e2, "An error occurred while hooking type: " + type.FullName ); + } } - } - catch( Exception e ) - { - XuaLogger.AutoTranslator.Error( e, $"An error occurred while setting up override hooks for '{eventName}'." ); - } - return false; + if( final ) + { + _installedAssemblies.Add( assembly ); + } + } } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs index 2a0fb2e1..f1e8f162 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/IMGUIHooks.cs @@ -17,13 +17,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.IMGUI { internal static class IMGUIHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { - //typeof( GUIContent_text_Hook ), - //typeof( GUIContent_Temp_Hook1 ), - //typeof( GUIContent_Temp_Hook2 ), - //typeof( GUIContent_Temp_Hook3 ), + //typeof( GUIStyle_Internal_Draw_Hook ), + //typeof( GUIStyle_Internal_Draw2_Hook ), + //typeof( GUIStyle_Internal_DrawCursor_Hook ), typeof( GUI_BeginGroup_Hook ), typeof( GUI_Box_Hook ), @@ -34,95 +31,120 @@ internal static class IMGUIHooks typeof( GUI_DoWindow_Hook ), typeof( GUI_DoButtonGrid_Hook ), typeof( GUI_DoToggle_Hook ), + + typeof( GUI_BeginGroup_Hook_New ), + typeof( GUI_DoLabel_Hook_New ), + typeof( GUI_DoButton_Hook_New ), + typeof( GUI_DoButtonGrid_Hook_2018 ), + typeof( GUI_DoButtonGrid_Hook_2019 ), + typeof( GUI_DoToggle_Hook_New ), }; - } + internal static bool Use2018StyleIMGUI = AccessToolsShim.Method( ClrTypes.GUIStyle, "Internal_Draw", new[] { typeof( Rect ), typeof( GUIContent ), typeof( bool ), typeof( bool ), typeof( bool ), typeof( bool ) } ) != null; + } - //[HarmonyPriority( HookPriority.Last )] - //internal static class GUIContent_text_Hook + //[HookingHelperPriority( HookPriority.Last )] + //internal static class GUIStyle_Internal_Draw_Hook //{ + // delegate void OriginalMethod( GUIStyle arg1, Rect arg2, GUIContent arg3, bool arg4, bool arg5, bool arg6, bool arg7 ); + // static bool Prepare( object instance ) // { - // return ClrTypes.GUIContent != null; + // return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUIStyle != null; // } // static MethodBase TargetMethod( object instance ) // { - // return AccessTools.Property( ClrTypes.GUIContent, "text" )?.GetSetMethod(); + // return AccessToolsShim.Method( ClrTypes.GUIStyle, "Internal_Draw", new[] { typeof( Rect ), typeof( GUIContent ), typeof( bool ), typeof( bool ), typeof( bool ), typeof( bool ) } ); // } - // static void Postfix( GUIContent __instance ) + // static void Prefix( GUIContent content ) // { - // if( !IMGUIHooks.HooksOverriden ) - // { - // AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - // } + // AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); // } - //} - //[HarmonyPriority( HookPriority.Last )] - //internal static class GUIContent_Temp_Hook1 - //{ - // static bool Prepare( object instance ) - // { - // return ClrTypes.GUIContent != null; - // } + // static OriginalMethod _original; - // static MethodBase TargetMethod( object instance ) + // static void MM_Init( object detour ) // { - // return AccessTools.Method( ClrTypes.GUIContent, "Temp", new[] { typeof( string ) } ); + // _original = detour.GenerateTrampolineEx(); // } - // static void Postfix( GUIContent __result ) + // static void MM_Detour( GUIStyle arg1, Rect arg2, GUIContent arg3, bool arg4, bool arg5, bool arg6, bool arg7 ) // { - // if( !IMGUIHooks.HooksOverriden ) - // { - // AutoTranslationPlugin.Current.Hook_TextChanged( __result, false ); - // } + // Prefix( arg3 ); + + // _original( arg1, arg2, arg3, arg4, arg5, arg6, arg7 ); // } //} - //[HarmonyPriority( HookPriority.Last )] - //internal static class GUIContent_Temp_Hook2 + //[HookingHelperPriority( HookPriority.Last )] + //internal static class GUIStyle_Internal_Draw2_Hook //{ + // delegate void OriginalMethod( GUIStyle arg1, Rect arg2, GUIContent arg3, int arg4, bool arg5 ); + // static bool Prepare( object instance ) // { - // return ClrTypes.GUIContent != null; + // return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUIStyle != null; // } // static MethodBase TargetMethod( object instance ) // { - // return AccessTools.Method( ClrTypes.GUIContent, "Temp", new[] { typeof( string ), typeof( string ) } ); + // return AccessToolsShim.Method( ClrTypes.GUIStyle, "Internal_Draw2", new[] { typeof( Rect ), typeof( GUIContent ), typeof( int ), typeof( bool ) } ); // } - // static void Postfix( GUIContent __result ) + // static void Prefix( GUIContent content ) // { - // if( !IMGUIHooks.HooksOverriden ) - // { - // AutoTranslationPlugin.Current.Hook_TextChanged( __result, false ); - // } + // AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + // } + + // static OriginalMethod _original; + + // static void MM_Init( object detour ) + // { + // _original = detour.GenerateTrampolineEx(); + // } + + // static void MM_Detour( GUIStyle arg1, Rect arg2, GUIContent arg3, int arg4, bool arg5 ) + // { + // Prefix( arg3 ); + + // _original( arg1, arg2, arg3, arg4, arg5 ); // } //} - //[HarmonyPriority( HookPriority.Last )] - //internal static class GUIContent_Temp_Hook3 + //[HookingHelperPriority( HookPriority.Last )] + //internal static class GUIStyle_Internal_DrawCursor_Hook //{ + // delegate void OriginalMethod( GUIStyle arg1, Rect arg2, GUIContent arg3, int arg4, Color arg5 ); + // static bool Prepare( object instance ) // { - // return ClrTypes.GUIContent != null; + // return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUIStyle != null; // } // static MethodBase TargetMethod( object instance ) // { - // return AccessTools.Method( ClrTypes.GUIContent, "Temp", new[] { typeof( string ), typeof( Texture ) } ); + // return AccessToolsShim.Method( ClrTypes.GUIStyle, "Internal_DrawCursor", new[] { typeof( Rect ), typeof( GUIContent ), typeof( int ), typeof( Color ) } ); // } - // static void Postfix( GUIContent __result ) + // static void Prefix( GUIContent content ) // { - // if( !IMGUIHooks.HooksOverriden ) - // { - // AutoTranslationPlugin.Current.Hook_TextChanged( __result, false ); - // } + // AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + // } + + // static OriginalMethod _original; + + // static void MM_Init( object detour ) + // { + // _original = detour.GenerateTrampolineEx(); + // } + + // static void MM_Detour( GUIStyle arg1, Rect arg2, GUIContent arg3, int arg4, Color arg5 ) + // { + // Prefix( arg3 ); + + // _original( arg1, arg2, arg3, arg4, arg5 ); // } //} @@ -137,12 +159,19 @@ internal static class IMGUIHooks + + + + + + + [HookingHelperPriority( HookPriority.Last )] internal static class GUI_BeginGroup_Hook { static bool Prepare( object instance ) { - return ClrTypes.GUI != null; + return !IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; } static MethodBase TargetMethod( object instance ) @@ -152,10 +181,7 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent content ) { - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } static Action _original; @@ -173,6 +199,39 @@ static void MM_Detour( Rect arg1, GUIContent arg2, GUIStyle arg3 ) } } + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_BeginGroup_Hook_New + { + static bool Prepare( object instance ) + { + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "BeginGroup", new[] { typeof( Rect ), typeof( GUIContent ), typeof( GUIStyle ), typeof( Vector2 ) } ); + } + + static void Prefix( GUIContent content ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + + static Action _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static void MM_Detour( Rect arg1, GUIContent arg2, GUIStyle arg3, Vector2 arg4 ) + { + Prefix( arg2 ); + + _original( arg1, arg2, arg3, arg4 ); + } + } + [HookingHelperPriority( HookPriority.Last )] internal static class GUI_Box_Hook { @@ -188,10 +247,7 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent content ) { - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } static Action _original; @@ -224,10 +280,7 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent content ) { - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } static Func _original; @@ -250,7 +303,7 @@ internal static class GUI_DoLabel_Hook { static bool Prepare( object instance ) { - return ClrTypes.GUI != null; + return !IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; } static MethodBase TargetMethod( object instance ) @@ -260,10 +313,7 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent content ) { - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } static Action _original; @@ -281,12 +331,45 @@ static void MM_Detour( Rect arg1, GUIContent arg2, IntPtr arg3 ) } } + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_DoLabel_Hook_New + { + static bool Prepare( object instance ) + { + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "DoLabel", new[] { typeof( Rect ), typeof( GUIContent ), typeof( GUIStyle ) } ); + } + + static void Prefix( GUIContent content ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + + static Action _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static void MM_Detour( Rect arg1, GUIContent arg2, GUIStyle arg3 ) + { + Prefix( arg2 ); + + _original( arg1, arg2, arg3 ); + } + } + [HookingHelperPriority( HookPriority.Last )] internal static class GUI_DoButton_Hook { static bool Prepare( object instance ) { - return ClrTypes.GUI != null; + return !IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; } static MethodBase TargetMethod( object instance ) @@ -296,10 +379,7 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent content ) { - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } static Func _original; @@ -317,6 +397,39 @@ static bool MM_Detour( Rect arg1, GUIContent arg2, IntPtr arg3 ) } } + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_DoButton_Hook_New + { + static bool Prepare( object instance ) + { + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "DoButton", new[] { typeof( Rect ), typeof( int ), typeof( GUIContent ), typeof( GUIStyle ) } ); + } + + static void Prefix( GUIContent content ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static bool MM_Detour( Rect arg1, int arg2, GUIContent arg3, GUIStyle arg4 ) + { + Prefix( arg3 ); + + return _original( arg1, arg2, arg3, arg4 ); + } + } + [HookingHelperPriority( HookPriority.Last )] internal static class GUI_DoModalWindow_Hook { @@ -332,15 +445,10 @@ static MethodBase TargetMethod( object instance ) static void Prefix( int id, WindowFunction func, GUIContent content ) { - if( Settings.BlacklistedIMGUIPlugins.Count > 0 ) - { - IMGUIBlocker.BlockIfConfigured( func.Method, id ); - } + IMGUIBlocker.BlockIfConfigured( func, id ); + IMGUIPluginTranslationHooks.HookIfConfigured( func ); - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } delegate Rect OriginalMethod( int arg1, Rect arg2, WindowFunction arg3, GUIContent arg4, GUIStyle arg5, GUISkin arg6 ); @@ -375,15 +483,10 @@ static MethodBase TargetMethod( object instance ) static void Prefix( int id, WindowFunction func, GUIContent title ) { - if( Settings.BlacklistedIMGUIPlugins.Count > 0 ) - { - IMGUIBlocker.BlockIfConfigured( func.Method, id ); - } + IMGUIBlocker.BlockIfConfigured( func, id ); + IMGUIPluginTranslationHooks.HookIfConfigured( func ); - if( !IMGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( title, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( title, false ); } delegate Rect OriginalMethod( int arg1, Rect arg2, WindowFunction arg3, GUIContent arg4, GUIStyle arg5, GUISkin arg6, bool arg7 ); @@ -408,7 +511,7 @@ internal static class GUI_DoButtonGrid_Hook { static bool Prepare( object instance ) { - return ClrTypes.GUI != null; + return !IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; } static MethodBase TargetMethod( object instance ) @@ -418,12 +521,9 @@ static MethodBase TargetMethod( object instance ) static void Prefix( GUIContent[] contents ) { - if( !IMGUIHooks.HooksOverriden ) + foreach( var content in contents ) { - foreach( var content in contents ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } } @@ -445,26 +545,99 @@ static int MM_Detour( Rect arg1, int arg2, GUIContent[] arg3, int arg4, GUIStyle } [HookingHelperPriority( HookPriority.Last )] - internal static class GUI_DoToggle_Hook + internal static class GUI_DoButtonGrid_Hook_2018 { static bool Prepare( object instance ) { - return ClrTypes.GUI != null; + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null && ClrTypes.GUI_ToolbarButtonSize != null; } static MethodBase TargetMethod( object instance ) { - return AccessToolsShim.Method( ClrTypes.GUI, "DoToggle", new[] { typeof( Rect ), typeof( int ), typeof( bool ), typeof( GUIContent ), typeof( IntPtr ) } ); + return AccessToolsShim.Method( ClrTypes.GUI, "DoButtonGrid", new[] { typeof( Rect ), typeof( int ), typeof( GUIContent[] ), typeof( string[] ), typeof( int ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ), ClrTypes.GUI_ToolbarButtonSize } ); } - static void Prefix( GUIContent content ) + static void Prefix( GUIContent[] contents ) + { + foreach( var content in contents ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + } + + delegate int OriginalMethod( Rect arg1, int arg2, GUIContent[] arg3, string[] arg4, int arg5, GUIStyle arg6, GUIStyle arg7, GUIStyle arg8, GUIStyle arg9, int arg10 ); + + static OriginalMethod _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx(); + } + + static int MM_Detour( Rect arg1, int arg2, GUIContent[] arg3, string[] arg4, int arg5, GUIStyle arg6, GUIStyle arg7, GUIStyle arg8, GUIStyle arg9, int arg10 ) + { + Prefix( arg3 ); + + return _original( arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 ); + } + } + + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_DoButtonGrid_Hook_2019 + { + static bool Prepare( object instance ) + { + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null && ClrTypes.GUI_ToolbarButtonSize != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "DoButtonGrid", new[] { typeof( Rect ), typeof( int ), typeof( GUIContent[] ), typeof( string[] ), typeof( int ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ), typeof( GUIStyle ), ClrTypes.GUI_ToolbarButtonSize, typeof( bool[] ) } ); + } + + static void Prefix( GUIContent[] contents ) { - if( !IMGUIHooks.HooksOverriden ) + foreach( var content in contents ) { AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); } } + delegate int OriginalMethod( Rect arg1, int arg2, GUIContent[] arg3, string[] arg4, int arg5, GUIStyle arg6, GUIStyle arg7, GUIStyle arg8, GUIStyle arg9, int arg10, bool[] arg11 ); + + static OriginalMethod _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx(); + } + + static int MM_Detour( Rect arg1, int arg2, GUIContent[] arg3, string[] arg4, int arg5, GUIStyle arg6, GUIStyle arg7, GUIStyle arg8, GUIStyle arg9, int arg10, bool[] arg11 ) + { + Prefix( arg3 ); + + return _original( arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 ); + } + } + + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_DoToggle_Hook + { + static bool Prepare( object instance ) + { + return !IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "DoToggle", new[] { typeof( Rect ), typeof( int ), typeof( bool ), typeof( GUIContent ), typeof( IntPtr ) } ); + } + + static void Prefix( GUIContent content ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + delegate bool OriginalMethod( Rect arg1, int arg2, bool arg3, GUIContent arg4, IntPtr arg5 ); static OriginalMethod _original; @@ -481,4 +654,39 @@ static bool MM_Detour( Rect arg1, int arg2, bool arg3, GUIContent arg4, IntPtr a return _original( arg1, arg2, arg3, arg4, arg5 ); } } + + [HookingHelperPriority( HookPriority.Last )] + internal static class GUI_DoToggle_Hook_New + { + static bool Prepare( object instance ) + { + return IMGUIHooks.Use2018StyleIMGUI && ClrTypes.GUI != null; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GUI, "DoToggle", new[] { typeof( Rect ), typeof( int ), typeof( bool ), typeof( GUIContent ), typeof( GUIStyle ) } ); + } + + static void Prefix( GUIContent content ) + { + AutoTranslationPlugin.Current.Hook_TextChanged( content, false ); + } + + delegate bool OriginalMethod( Rect arg1, int arg2, bool arg3, GUIContent arg4, GUIStyle arg5 ); + + static OriginalMethod _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx(); + } + + static bool MM_Detour( Rect arg1, int arg2, bool arg3, GUIContent arg4, GUIStyle arg5 ) + { + Prefix( arg4 ); + + return _original( arg1, arg2, arg3, arg4, arg5 ); + } + } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs index ad0b10d3..f4497e60 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/NGUIHooks.cs @@ -16,8 +16,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI { internal static class NGUIHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { typeof( UILabel_text_Hook ), typeof( UILabel_OnEnable_Hook ) @@ -39,10 +37,7 @@ static MethodBase TargetMethod( object instance ) public static void Postfix( object __instance ) { - if( !NGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -78,10 +73,7 @@ public static void Postfix( object __instance ) { if( ClrTypes.UILabel.IsAssignableFrom( __instance.GetType() ) ) { - if( !NGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/PluginTranslationHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/PluginTranslationHooks.cs new file mode 100644 index 00000000..1ac92a38 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/PluginTranslationHooks.cs @@ -0,0 +1,231 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using UnityEngine; +using XUnity.AutoTranslator.Plugin.Core.Constants; +using XUnity.AutoTranslator.Plugin.Core.Extensions; +using XUnity.AutoTranslator.Plugin.Core.Utilities; +using XUnity.Common.Constants; +using XUnity.Common.Harmony; +using XUnity.Common.Logging; +using XUnity.Common.MonoMod; + +namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextGetterCompat +{ + internal static class PluginTranslationHooks + { + public static readonly Type[] All = new[] { + typeof( GameObject_AddComponent_Hook ), + typeof( Object_InstantiateSingle_Hook ), + typeof( Object_InstantiateSingleWithParent_Hook ), + typeof( Object_CloneSingle_Hook ), + typeof( Object_CloneSingleWithParent_Hook ), + }; + } + + internal static class GameObject_AddComponent_Hook + { + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.GameObject, "Internal_AddComponentWithType", new Type[] { typeof( Type ) } ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static Component MM_Detour( GameObject __instance, Type componentType ) + { + var result = _original( __instance, componentType ); + + if( result.IsKnownTextType() ) + { + var cache = CallOrigin.CalculateTextCacheFromStackTrace(); + if( cache != null ) + { + var info = result.GetOrCreateTextTranslationInfo(); + info.TextCache = cache; + } + } + + return result; + } + } + + internal static class Object_InstantiateSingle_Hook + { + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.Object, "Internal_InstantiateSingle", new Type[] { typeof( UnityEngine.Object ), typeof( Vector3 ), typeof( Quaternion ) } ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static UnityEngine.Object MM_Detour( UnityEngine.Object data, Vector3 pos, Quaternion rot ) + { + var prev = CallOrigin.TextCache; + try + { + CallOrigin.TextCache = CallOrigin.CalculateTextCacheFromStackTrace(); + + var result = _original( data, pos, rot ); + + if( CallOrigin.TextCache != null && result is GameObject go && !go.activeInHierarchy ) + { + CallOrigin.SetTextCacheForAllObjectsInHierachy( go, CallOrigin.TextCache ); + } + + return result; + } + finally + { + CallOrigin.TextCache = prev; + } + } + } + + internal static class Object_InstantiateSingleWithParent_Hook + { + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.Object, "Internal_InstantiateSingleWithParent", new Type[] { typeof( UnityEngine.Object ), typeof( Transform ), typeof( Vector3 ), typeof( Quaternion ) } ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static UnityEngine.Object MM_Detour( UnityEngine.Object data, Transform parent, Vector3 pos, Quaternion rot ) + { + var prev = CallOrigin.TextCache; + try + { + CallOrigin.TextCache = CallOrigin.CalculateTextCacheFromStackTrace(); + + var result = _original( data, parent, pos, rot ); + + if( CallOrigin.TextCache != null && result is GameObject go && !go.activeInHierarchy ) + { + CallOrigin.SetTextCacheForAllObjectsInHierachy( go, CallOrigin.TextCache ); + } + + return result; + } + finally + { + CallOrigin.TextCache = prev; + } + } + } + + internal static class Object_CloneSingle_Hook + { + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.Object, "Internal_CloneSingle", new Type[] { typeof( UnityEngine.Object ) } ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static UnityEngine.Object MM_Detour( UnityEngine.Object data ) + { + var prev = CallOrigin.TextCache; + try + { + CallOrigin.TextCache = CallOrigin.CalculateTextCacheFromStackTrace(); + + var result = _original( data ); + + if( CallOrigin.TextCache != null && result is GameObject go && !go.activeInHierarchy ) + { + CallOrigin.SetTextCacheForAllObjectsInHierachy( go, CallOrigin.TextCache ); + } + + return result; + } + finally + { + CallOrigin.TextCache = prev; + } + } + } + + internal static class Object_CloneSingleWithParent_Hook + { + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return AccessToolsShim.Method( ClrTypes.Object, "Internal_CloneSingleWithParent", new Type[] { typeof( UnityEngine.Object ), typeof( Transform ), typeof( bool ) } ); + } + + static Func _original; + + static void MM_Init( object detour ) + { + _original = detour.GenerateTrampolineEx>(); + } + + static UnityEngine.Object MM_Detour( UnityEngine.Object data, Transform parent, bool worldPositionStays ) + { + var prev = CallOrigin.TextCache; + try + { + CallOrigin.TextCache = CallOrigin.CalculateTextCacheFromStackTrace(); + + var result = _original( data, parent, worldPositionStays ); + + if( CallOrigin.TextCache != null && result is GameObject go && !go.activeInHierarchy ) + { + CallOrigin.SetTextCacheForAllObjectsInHierachy( go, CallOrigin.TextCache ); + } + + return result; + } + finally + { + CallOrigin.TextCache = prev; + } + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshHooks.cs index 9ce9e837..b7773704 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshHooks.cs @@ -12,8 +12,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI { internal static class TextMeshHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { typeof( TextMesh_text_Hook ), typeof( GameObject_active_Hook ), @@ -36,10 +34,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); } static Action _original; @@ -72,15 +67,12 @@ static MethodBase TargetMethod( object instance ) static void Postfix( GameObject __instance, bool active ) { - if( !TextMeshHooks.HooksOverriden ) + if( active ) { - if( active ) + var tms = __instance.GetComponentsInChildren( ClrTypes.TextMesh ); + foreach( var tm in tms ) { - var tms = __instance.GetComponentsInChildren( ClrTypes.TextMesh ); - foreach( var tm in tms ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( tm, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( tm, true ); } } } @@ -115,15 +107,12 @@ static MethodBase TargetMethod( object instance ) static void Postfix( GameObject __instance, bool active ) { - if( !TextMeshHooks.HooksOverriden ) + if( active ) { - if( active ) + var tms = __instance.GetComponentsInChildren( ClrTypes.TextMesh ); + foreach( var tm in tms ) { - var tms = __instance.GetComponentsInChildren( ClrTypes.TextMesh ); - foreach( var tm in tms ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( tm, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( tm, true ); } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs index 9302e5c6..fbaeb302 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/TextMeshProHooks.cs @@ -16,8 +16,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro { internal static class TextMeshProHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { typeof( TextWindow_SetText_Hook ), typeof( TeshMeshProUGUI_OnEnable_Hook ), @@ -82,12 +80,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - // NOTE: Has function, but overridden - - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -121,10 +114,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -158,12 +148,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - // NOTE: Has function, but overridden - - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -197,10 +182,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -234,10 +216,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -271,10 +250,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -308,10 +284,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -347,10 +320,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -384,10 +354,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -421,10 +388,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -458,10 +422,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !TextMeshProHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs index 24d1d884..72597d70 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Hooks/UGUIHooks.cs @@ -15,8 +15,6 @@ namespace XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI { internal static class UGUIHooks { - public static bool HooksOverriden = false; - public static readonly Type[] All = new[] { typeof( Text_text_Hook ), typeof( Text_OnEnable_Hook ), @@ -38,10 +36,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - if( !UGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, false ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } @@ -75,12 +70,7 @@ static MethodBase TargetMethod( object instance ) static void Postfix( object __instance ) { - // NOTE: Has function, but overridden - - if( !UGUIHooks.HooksOverriden ) - { - AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); - } + AutoTranslationPlugin.Current.Hook_TextChanged( __instance, true ); AutoTranslationPlugin.Current.Hook_HandleComponent( __instance ); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/IMGUIBlocker.cs b/src/XUnity.AutoTranslator.Plugin.Core/IMGUIBlocker.cs index 9d934af1..2e7d357f 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/IMGUIBlocker.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/IMGUIBlocker.cs @@ -10,6 +10,7 @@ using XUnity.AutoTranslator.Plugin.Core.UI; using XUnity.Common.Logging; using XUnity.Common.Utilities; +using static UnityEngine.GUI; namespace XUnity.AutoTranslator.Plugin.Core { @@ -17,10 +18,11 @@ internal static class IMGUIBlocker { private static HashSet HandledMethods = new HashSet(); - public static void BlockIfConfigured( MethodInfo method, int windowId ) + public static void BlockIfConfigured( WindowFunction function, int windowId ) { - //if( Settings.BlacklistedIMGUIWindowNames.Count == 0 ) return; + if( Settings.BlacklistedIMGUIPlugins.Count == 0 ) return; + var method = function.Method; if( !HandledMethods.Contains( method ) ) { HandledMethods.Add( method ); @@ -85,8 +87,14 @@ static MethodBase TargetMethod( object instance ) return _nextMethod; } - static void MM_Init( object detour ) + static void Prefix() { + AutoTranslationPlugin.Current.DisableAutoTranslator(); + } + + static void Finalizer() + { + AutoTranslationPlugin.Current.EnableAutoTranslator(); } static MethodInfo Get_MM_Detour() diff --git a/src/XUnity.AutoTranslator.Plugin.Core/IMGUIPluginTranslationHooks.cs b/src/XUnity.AutoTranslator.Plugin.Core/IMGUIPluginTranslationHooks.cs new file mode 100644 index 00000000..3551e931 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/IMGUIPluginTranslationHooks.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using UnityEngine; +using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.AutoTranslator.Plugin.Core.Extensions; +using XUnity.AutoTranslator.Plugin.Core.Hooks; +using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro; +using XUnity.AutoTranslator.Plugin.Core.UI; +using XUnity.Common.Logging; +using XUnity.Common.Utilities; +using static UnityEngine.GUI; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + internal static class IMGUIPluginTranslationHooks + { + private static HashSet HandledMethods = new HashSet(); + private static HashSet HookedMethods = new HashSet(); + + public static void HookIfConfigured( WindowFunction function ) + { + if( AutoTranslationPlugin.Current.PluginTextCaches.Count == 0 ) return; + + HookIfConfigured( function.Method ); + } + + public static void ResetHandledForAllInAssembly( Assembly assembly ) + { + HandledMethods.RemoveWhere( x => x.DeclaringType.Assembly.Equals( assembly ) ); + } + + public static void HookIfConfigured( MethodInfo method ) + { + if( !HandledMethods.Contains( method ) ) + { + HandledMethods.Add( method ); + + if( !HookedMethods.Contains( method ) ) + { + var methodName = method.DeclaringType.FullName.ToString() + "." + method.Name; + try + { + var assembly = method.DeclaringType.Assembly; + if( !AutoTranslationPlugin.Current.PluginTextCaches.TryGetValue( assembly.GetName().Name, out var cache ) ) + { + return; + } + + XuaLogger.AutoTranslator.Info( "Attempting to hook " + methodName + " to enable plugin specific translations." ); + + var behaviour = method.DeclaringType.Assembly + .GetTypes() + .FirstOrDefault( x => typeof( MonoBehaviour ).IsAssignableFrom( x ) ); + + if( behaviour == null ) + { + XuaLogger.AutoTranslator.Warn( "Could not find any MonoBehaviours in assembly owning method the method: " + methodName ); + return; + } + var behaviourType = behaviour.GetType(); + + var translationCache = AutoTranslationPlugin.Current.TextCache.GetOrCreateCompositeCache( cache ); + + var pluginCallbackType = typeof( PluginCallbacks_Function_Hook<> ).MakeGenericType( behaviourType ); + pluginCallbackType + .GetMethod( "Register", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) + .Invoke( null, new object[] { method } ); + pluginCallbackType + .GetMethod( "SetTextCache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) + .Invoke( null, new object[] { translationCache } ); + + HookingHelper.PatchType( pluginCallbackType, Settings.ForceMonoModHooks ); + + HookedMethods.Add( method ); + + pluginCallbackType + .GetMethod( "Clean", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ) + .Invoke( null, null ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while attempting to hook " + methodName + " to disable translation in window." ); + } + } + } + } + } + + internal static class PluginCallbacks_Function_Hook + { + private static MethodInfo _nextMethod; + private static IReadOnlyTextTranslationCache _cache; + + public static void SetTextCache( IReadOnlyTextTranslationCache cache ) + { + _cache = cache; + } + + public static void Register( MethodInfo method ) + { + _nextMethod = method; + } + + public static void Clean() + { + _nextMethod = null; + } + + static bool Prepare( object instance ) + { + return true; + } + + static MethodBase TargetMethod( object instance ) + { + return _nextMethod; + } + + //static MethodInfo Get_MM_Detour() + //{ + // if( _nextMethod.IsStatic ) + // { + // return typeof( PluginCallbacks_Function_Hook ).GetMethod( "MM_Detour_Static", BindingFlags.NonPublic | BindingFlags.Static ); + // } + // else + // { + // return typeof( PluginCallbacks_Function_Hook ).GetMethod( "MM_Detour_Instance", BindingFlags.NonPublic | BindingFlags.Static ); + // } + //} + + static void Prefix() + { + CallOrigin.TextCache = _cache; + } + + static void Finalizer() + { + CallOrigin.TextCache = null; + } + + //static void MM_Detour_Instance( Action orig, object self, int id ) + //{ + // try + // { + // CallOrigin.TextCache = _cache; + + // orig( self, id ); + // } + // finally + // { + // CallOrigin.TextCache = null; + // } + //} + + //static void MM_Detour_Static( Action orig, int id ) + //{ + // try + // { + // CallOrigin.TextCache = _cache; + + // orig( id ); + // } + // finally + // { + // CallOrigin.TextCache = null; + // } + //} + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/IReadOnlyTextTranslationCache.cs b/src/XUnity.AutoTranslator.Plugin.Core/IReadOnlyTextTranslationCache.cs new file mode 100644 index 00000000..73fd25ac --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/IReadOnlyTextTranslationCache.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; +using XUnity.AutoTranslator.Plugin.Core.Parsing; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + internal interface IReadOnlyTextTranslationCache + { + bool AllowGeneratingNewTranslations { get; } + + bool AllowFallback { get; } + + bool IsTranslatable( string text, bool isToken, int scope ); + + bool IsPartial( string text, int scope ); + + bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool allowToken, int scope, out string value ); + + bool TryGetTranslationSplitter( string text, int scope, out Match match, out RegexTranslationSplitter splitter ); + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/KeyValuePairTranslationPackage.cs b/src/XUnity.AutoTranslator.Plugin.Core/KeyValuePairTranslationPackage.cs new file mode 100644 index 00000000..d258bd66 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/KeyValuePairTranslationPackage.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + /// + /// Translation package consisting of key-value pairs of strings. + /// + public class KeyValuePairTranslationPackage + { + private List> _cachedEntries; + + /// + /// Constructs the translation package. + /// + /// The name to be displayed when it is loaded. + /// The entries to be loaded. + /// A bool indicating if the enumerable can be iterated multiple times (due translation reload). + public KeyValuePairTranslationPackage( string name, IEnumerable> entries, bool allowMultipleIterations ) + { + Name = name; + Entries = entries; + AllowMultipleIterations = allowMultipleIterations; + } + + /// + /// Gets the name of the the package. + /// + public string Name { get; } + private IEnumerable> Entries { get; } + private bool AllowMultipleIterations { get; } + + internal IEnumerable> GetIterableEntries() + { + if( !AllowMultipleIterations ) + { + if( _cachedEntries == null ) + { + _cachedEntries = Entries.ToList(); + } + return _cachedEntries; + } + return Entries; + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/ParserTranslationContext.cs b/src/XUnity.AutoTranslator.Plugin.Core/ParserTranslationContext.cs index be23bf2a..1b7c116a 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/ParserTranslationContext.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/ParserTranslationContext.cs @@ -7,6 +7,8 @@ namespace XUnity.AutoTranslator.Plugin.Core { internal class ParserTranslationContext { + private ParserResult _highestPriorityResult; + public ParserTranslationContext( object component, TranslationEndpointManager endpoint, InternalTranslationResult translationResult, ParserResult result, ParserTranslationContext parentContext ) { Jobs = new HashSet(); @@ -47,14 +49,38 @@ public ParserTranslationContext( object component, TranslationEndpointManager en public int LevelsOfRecursion { get; private set; } - public bool CachedCombinedResult() + private ParserResult GetHighestPriorityResult() { - if( Result.CacheCombinedResult ) + if( _highestPriorityResult == null ) { - return ParentContext == null || !ParentContext.CachedCombinedResult(); + var highestPriorityResult = Result; + var highestPriority = highestPriorityResult.Priority; + var currentContext = this; + + while( ( currentContext = currentContext.ParentContext ) != null ) + { + var result = currentContext.Result; + var priority = result.Priority; + if( priority > highestPriority ) + { + highestPriority = priority; + highestPriorityResult = result; + } + } + + _highestPriorityResult = highestPriorityResult; } + return _highestPriorityResult; + } - return false; + public bool CachedCombinedResult() + { + return GetHighestPriorityResult().CacheCombinedResult; + } + + public bool PersistCombinedResult() + { + return GetHighestPriorityResult().PersistCombinedResult; } public bool HasAllJobsCompleted() diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs index 247510a1..c782546f 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/GameLogTextParser.cs @@ -6,13 +6,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing { - internal class GameLogTextParser : ITextParser + internal class GameLogTextParser { - private Func _isTranslatable; - - public GameLogTextParser( Func isTranslatable ) + public GameLogTextParser() { - _isTranslatable = isTranslatable; } public bool CanApply( object ui ) @@ -20,7 +17,7 @@ public bool CanApply( object ui ) return ui.SupportsLineParser(); } - public ParserResult Parse( string input, int scope ) + public ParserResult Parse( string input, int scope, IReadOnlyTextTranslationCache cache ) { var reader = new StringReader( input ); bool containsTranslatable = false; @@ -35,7 +32,7 @@ public ParserResult Parse( string input, int scope ) { if( !string.IsNullOrEmpty( line ) ) { - if( _isTranslatable( line, scope ) ) + if( cache.IsTranslatable( line, true, scope ) ) { // template it! containsTranslatable = true; diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ITextParser.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ITextParser.cs deleted file mode 100644 index 024a355e..00000000 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ITextParser.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace XUnity.AutoTranslator.Plugin.Core.Parsing -{ - internal interface ITextParser - { - ParserResult Parse( string input, int scope ); - } -} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs index 762f0544..ee36af00 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResult.cs @@ -52,6 +52,8 @@ public ParserResult( ParserResultOrigin origin, string originalText, string temp public bool PersistTokenResult { get; } + public int Priority => (int)Origin; + public string GetTranslationFromParts( Func getTranslation ) { bool ok = true; diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResultOrigin.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResultOrigin.cs index aa57e2a4..2931d23b 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResultOrigin.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/ParserResultOrigin.cs @@ -2,8 +2,8 @@ { internal enum ParserResultOrigin { - GameLogTextParser, + RichTextParser, RegexTextParser, - RichTextParser + GameLogTextParser, } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RegexSplittingTextParser.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RegexSplittingTextParser.cs index c0a2fe6e..7921c96d 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RegexSplittingTextParser.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RegexSplittingTextParser.cs @@ -5,13 +5,10 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing { - internal class RegexSplittingTextParser : ITextParser + internal class RegexSplittingTextParser { - private readonly TextTranslationCache _cache; - - public RegexSplittingTextParser( TextTranslationCache cache ) + public RegexSplittingTextParser() { - _cache = cache; } public bool CanApply( object ui ) @@ -19,9 +16,9 @@ public bool CanApply( object ui ) return !ui.IsSpammingComponent(); } - public ParserResult Parse( string input, int scope ) + public ParserResult Parse( string input, int scope, IReadOnlyTextTranslationCache cache ) { - if( _cache.TryGetTranslationSplitter( input, scope, out var match, out var splitter ) ) + if( cache.TryGetTranslationSplitter( input, scope, out var match, out var splitter ) ) { return new ParserResult( ParserResultOrigin.RegexTextParser, input, splitter.Translation, true, true, Settings.CacheRegexPatternResults, true, splitter.CompiledRegex, match ); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs index 8c903b80..f98ad389 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/RichTextParser.cs @@ -7,7 +7,7 @@ namespace XUnity.AutoTranslator.Plugin.Core.Parsing { - internal class RichTextParser : ITextParser + internal class RichTextParser { private static readonly char[] TagNameEnders = new char[] { '=', ' ' }; private static readonly Regex TagRegex = new Regex( "<.*?>" ); diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs index d2bcc49a..111ca59a 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Parsing/UnityTextParsers.cs @@ -12,11 +12,11 @@ internal static class UnityTextParsers public static RegexSplittingTextParser RegexSplittingTextParser; public static GameLogTextParser GameLogTextParser; - public static void Initialize( TextTranslationCache cache, Func isTranslatable ) + public static void Initialize() { RichTextParser = new RichTextParser(); - RegexSplittingTextParser = new RegexSplittingTextParser( cache ); - GameLogTextParser = new GameLogTextParser( isTranslatable ); + RegexSplittingTextParser = new RegexSplittingTextParser(); + GameLogTextParser = new GameLogTextParser(); } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/SettingsTranslationsInitializer.cs b/src/XUnity.AutoTranslator.Plugin.Core/SettingsTranslationsInitializer.cs new file mode 100644 index 00000000..1532b179 --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/SettingsTranslationsInitializer.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using XUnity.AutoTranslator.Plugin.Core.Configuration; +using XUnity.AutoTranslator.Plugin.Core.Utilities; +using XUnity.Common.Logging; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + internal static class SettingsTranslationsInitializer + { + private static readonly char[] TranslationSplitters = new char[] { '=' }; + + public static void LoadTranslations() + { + Settings.Replacements.Clear(); + Settings.Preprocessors.Clear(); + + Directory.CreateDirectory( Settings.TranslationsPath ); + var substitutionFile = new FileInfo( Settings.SubstitutionFilePath ).FullName; + var preprocessorsFile = new FileInfo( Settings.PreprocessorsFilePath ).FullName; + + LoadTranslationsInFile( substitutionFile, true, false ); + LoadTranslationsInFile( preprocessorsFile, false, true ); + } + + private static void LoadTranslationsInFile( string fullFileName, bool isSubstitutionFile, bool isPreprocessorFile ) + { + var fileExists = File.Exists( fullFileName ); + if( fileExists || isSubstitutionFile || isPreprocessorFile ) + { + if( fileExists ) + { + using( var stream = File.OpenRead( fullFileName ) ) + { + LoadTranslationsInStream( stream, fullFileName, isSubstitutionFile, isPreprocessorFile ); + } + } + else if( isSubstitutionFile || isPreprocessorFile ) + { + var fi = new FileInfo( fullFileName ); + Directory.CreateDirectory( fi.Directory.FullName ); + + using( var stream = File.Create( fullFileName ) ) + { + stream.Write( new byte[] { 0xEF, 0xBB, 0xBF }, 0, 3 ); // UTF-8 BOM + stream.Close(); + } + } + } + } + + private static void LoadTranslationsInStream( Stream stream, string fullFileName, bool isSubstitutionFile, bool isPreprocessorFile ) + { + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Loading texts: {fullFileName}." ); + + var reader = new StreamReader( stream, Encoding.UTF8 ); + { + var context = new TranslationFileLoadingContext(); + var set = new HashSet(); + + string[] translations = reader.ReadToEnd().Split( new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries ); + foreach( string translatioOrDirective in translations ) + { + if( context.IsExecutable( Settings.ApplicationName ) ) + { + string[] kvp = translatioOrDirective.Split( TranslationSplitters, StringSplitOptions.None ); + if( kvp.Length == 2 ) + { + string key = TextHelper.Decode( kvp[ 0 ] ); + string value = TextHelper.Decode( kvp[ 1 ] ); + + if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) ) + { + if( isSubstitutionFile ) + { + Settings.Replacements[ key ] = value; + } + else if( isPreprocessorFile ) + { + Settings.Preprocessors[ key ] = value; + } + } + } + } + } + } + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/StreamTranslationPackage.cs b/src/XUnity.AutoTranslator.Plugin.Core/StreamTranslationPackage.cs new file mode 100644 index 00000000..4cd1948d --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/StreamTranslationPackage.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using XUnity.Common.Extensions; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + /// + /// Translation package for a stream representing the standard text translation format in UTF-8. + /// + public sealed class StreamTranslationPackage : IDisposable + { + private Stream _cachedStream; + + /// + /// Constructs the translation package. + /// + /// The name to be displayed when it is loaded. + /// The stream to be loaded. The stream represents a standard text translation file in UTF-8 format. + /// A bool indicating if the enumerable can be iterated multiple times (due translation reload). + public StreamTranslationPackage( string name, Stream stream, bool allowMultipleIterations ) + { + if( allowMultipleIterations && !stream.CanSeek ) + { + throw new ArgumentException( "Cannot iterate a non-seekable stream multiple times.", nameof( allowMultipleIterations ) ); + } + Name = name; + Stream = stream; + AllowMultipleIterations = allowMultipleIterations; + } + + /// + /// Gets the name of the the package. + /// + public string Name { get; } + private Stream Stream { get; set; } + private bool AllowMultipleIterations { get; } + + internal Stream GetReadableStream() + { + if( !AllowMultipleIterations ) + { + if( _cachedStream == null ) + { + _cachedStream = new MemoryStream( Stream.ReadFully( 0 ) ); + Stream.Dispose(); + Stream = null; + } + return _cachedStream; + } + return Stream; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + private void Dispose( bool disposing ) + { + if( !disposedValue ) + { + if( disposing ) + { + Stream?.Dispose(); + } + + Stream = null; + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~StreamTranslationPackage() + // { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + /// + /// Disposes the translation package. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose( true ); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs b/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs index a2659723..bcf85c6b 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationCache.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using UnityEngine; @@ -18,28 +19,31 @@ namespace XUnity.AutoTranslator.Plugin.Core { - class TextTranslationCache + class TextTranslationCache : IReadOnlyTextTranslationCache { private static readonly char[] TranslationSplitters = new char[] { '=' }; + private Dictionary _compositeCaches = new Dictionary(); + + private static readonly List _kvpPackages = new List(); + private static readonly List _streamPackages = new List(); + /// /// All the translations are stored in this dictionary. /// private Dictionary _staticTranslations = new Dictionary(); + private Dictionary _translations = new Dictionary(); private Dictionary _reverseTranslations = new Dictionary(); private Dictionary _tokenTranslations = new Dictionary(); private Dictionary _reverseTokenTranslations = new Dictionary(); private HashSet _partialTranslations = new HashSet(); - - private Dictionary _scopedTranslations = new Dictionary(); - //private Dictionary _scopedTokenTranslations = new Dictionary(); - private List _defaultRegexes = new List(); private HashSet _registeredRegexes = new HashSet(); - - public List _splitterRegexes = new List(); - public HashSet _registeredSplitterRegexes = new HashSet(); + private HashSet _failedRegexLookups = new HashSet(); + private List _splitterRegexes = new List(); + private HashSet _registeredSplitterRegexes = new HashSet(); + private Dictionary _scopedTranslations = new Dictionary(); /// /// These are the new translations that has not yet been persisted to the file system. @@ -47,31 +51,94 @@ class TextTranslationCache private object _writeToFileSync = new object(); private Dictionary _newTranslations = new Dictionary(); + private readonly DirectoryInfo _pluginDirectory; + public TextTranslationCache() { + AllowGeneratingNewTranslations = true; + AllowFallback = false; + DefaultAllowFallback = false; + LoadStaticTranslations(); // start function to write translations to file MaintenanceHelper.AddMaintenanceFunction( SaveNewTranslationsToDisk, 1 ); } - private static IEnumerable GetTranslationFiles() + public TextTranslationCache( DirectoryInfo pluginDirectory ) { - return Directory.GetFiles( Settings.TranslationsPath, $"*", SearchOption.AllDirectories ) + AllowGeneratingNewTranslations = false; + AllowFallback = false; + DefaultAllowFallback = false; + + _pluginDirectory = pluginDirectory; + } + + public TextTranslationCache( string pluginDirectory ) + { + AllowGeneratingNewTranslations = false; + AllowFallback = false; + DefaultAllowFallback = false; + + _pluginDirectory = new DirectoryInfo( Path.Combine( Path.Combine( Settings.TranslationsPath, "plugins" ), pluginDirectory ) ); + } + + public bool DefaultAllowFallback { get; internal set; } + + public bool AllowFallback { get; internal set; } + + public bool AllowGeneratingNewTranslations { get; private set; } + + public bool HasLoadedInMemoryTranslations => _kvpPackages.Count > 0 || _streamPackages.Count > 0; + + private IEnumerable GetTranslationFiles() + { + return Directory.GetFiles( _pluginDirectory?.FullName ?? Settings.TranslationsPath, $"*", SearchOption.AllDirectories ) .Where( x => x.EndsWith( ".txt", StringComparison.OrdinalIgnoreCase ) || x.EndsWith( ".zip", StringComparison.OrdinalIgnoreCase ) ) .Where( x => !x.EndsWith( "resizer.txt", StringComparison.OrdinalIgnoreCase ) ) .Select( x => new FileInfo( x ).FullName ); } + internal CompositeTextTranslationCache GetOrCreateCompositeCache( IReadOnlyTextTranslationCache primary ) + { + if( !_compositeCaches.TryGetValue( primary, out var compo ) ) + { + compo = new CompositeTextTranslationCache( primary, this ); + _compositeCaches[ primary ] = compo; + } + return compo; + } + internal void LoadTranslationFiles() { try { + AllowFallback = DefaultAllowFallback; + + if( _pluginDirectory != null ) + { + XuaLogger.AutoTranslator.Debug( $"--- Loading Plugin Translations ({_pluginDirectory.Name}) ---" ); + } + else + { + XuaLogger.AutoTranslator.Debug( $"--- Loading Global Translations ---" ); + } + var startTime = Time.realtimeSinceStartup; lock( _writeToFileSync ) { + string pluginsDir = Path.Combine( Settings.TranslationsPath, "plugins" ); + Directory.CreateDirectory( Settings.TranslationsPath ); - Directory.CreateDirectory( Path.GetDirectoryName( Settings.AutoTranslationsFilePath ) ); + if( _pluginDirectory != null ) + { + Directory.CreateDirectory( pluginsDir ); + Directory.CreateDirectory( _pluginDirectory.FullName ); + } + else + { + Directory.CreateDirectory( Path.GetDirectoryName( Settings.AutoTranslationsFilePath ) ); + } _registeredRegexes.Clear(); _defaultRegexes.Clear(); @@ -83,21 +150,66 @@ internal void LoadTranslationFiles() _registeredSplitterRegexes.Clear(); _splitterRegexes.Clear(); _scopedTranslations.Clear(); - Settings.Replacements.Clear(); - Settings.Preprocessors.Clear(); var mainTranslationFile = new FileInfo( Settings.AutoTranslationsFilePath ).FullName; var substitutionFile = new FileInfo( Settings.SubstitutionFilePath ).FullName; var preprocessorsFile = new FileInfo( Settings.PreprocessorsFilePath ).FullName; - LoadTranslationsInFile( substitutionFile, true, false, false ); - LoadTranslationsInFile( preprocessorsFile, false, true, true ); - LoadTranslationsInFile( mainTranslationFile, false, false, true ); + + if( _pluginDirectory == null ) + { + LoadTranslationsInFile( mainTranslationFile, true ); + } foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile, substitutionFile, preprocessorsFile }, StringComparer.OrdinalIgnoreCase ) ) { - LoadTranslationsInFile( fullFileName, false, false, false ); + try + { + if( _pluginDirectory == null && fullFileName.StartsWith( pluginsDir, StringComparison.OrdinalIgnoreCase ) ) + { + continue; + } + + LoadTranslationsInFile( fullFileName, false ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while loading translations in file: " + fullFileName ); + } + } + } + + foreach( var streamPackages in _streamPackages ) + { + try + { + var stream = streamPackages.GetReadableStream(); + if( stream.CanSeek ) + { + stream.Seek( 0, SeekOrigin.Begin ); + } + + LoadTranslationsInStream( stream, streamPackages.Name, false ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while loading translations in stream translation package: " + streamPackages.Name ); + } + } + + foreach( var kvpPackage in _kvpPackages ) + { + try + { + var iterable = kvpPackage.GetIterableEntries(); + + LoadTranslationsInKeyValuePairs( iterable, kvpPackage.Name ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Error( e, "An error occurred while loading translations in KVP translation package: " + kvpPackage.Name ); } } var endTime = Time.realtimeSinceStartup; + XuaLogger.AutoTranslator.Debug( $"Loaded translation text files (took {Math.Round( endTime - startTime, 2 )} seconds)" ); // generate variations of created translations @@ -210,13 +322,13 @@ internal void LoadTranslationFiles() XuaLogger.AutoTranslator.Debug( $"Created token translations (took {Math.Round( endTime - startTime, 2 )} seconds)" ); endTime = Time.realtimeSinceStartup; - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Global translations generated: {_translations.Count}" ); - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Global regex translations generated: {_defaultRegexes.Count}" ); - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Global regex splitters generated: {_splitterRegexes.Count}" ); - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Global token translations generated: {_tokenTranslations.Count}" ); + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Translations generated: {_translations.Count}" ); + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Regex translations generated: {_defaultRegexes.Count}" ); + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Regex splitters generated: {_splitterRegexes.Count}" ); + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Token translations generated: {_tokenTranslations.Count}" ); if( Settings.GeneratePartialTranslations ) { - XuaLogger.AutoTranslator.Debug( $"Global partial translations generated: {_partialTranslations.Count}" ); + XuaLogger.AutoTranslator.Debug( $"Partial translations generated: {_partialTranslations.Count}" ); } foreach( var kvp in _scopedTranslations.OrderBy( x => x.Key ) ) { @@ -319,7 +431,7 @@ public TranslationCharacterToken( char c, bool isVariable ) public bool IsVariable { get; set; } } - private void LoadTranslationsInStream( Stream stream, string fullFileName, bool isSubstitutionFile, bool isPreprocessorFile, bool isOutputFile ) + private void LoadTranslationsInStream( Stream stream, string fullFileName, bool isOutputFile ) { if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Loading texts: {fullFileName}." ); @@ -331,7 +443,7 @@ private void LoadTranslationsInStream( Stream stream, string fullFileName, bool string[] translations = reader.ReadToEnd().Split( new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries ); foreach( string translatioOrDirective in translations ) { - if( Settings.EnableTranslationScoping && !isOutputFile ) + if( !isOutputFile ) { var directive = TranslationFileDirective.Create( translatioOrDirective ); if( directive != null ) @@ -353,91 +465,127 @@ private void LoadTranslationsInStream( Stream stream, string fullFileName, bool if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) ) { - if( isSubstitutionFile ) - { - Settings.Replacements[ key ] = value; - } - else if( isPreprocessorFile ) - { - Settings.Preprocessors[ key ] = value; - } - else + if( key.StartsWith( "sr:" ) ) { - if( key.StartsWith( "sr:" ) ) + try { - try - { - var regex = new RegexTranslationSplitter( key, value ); + var regex = new RegexTranslationSplitter( key, value ); - var levels = context.GetLevels(); - if( levels.Count == 0 ) - { - AddTranslationSplitterRegex( regex, TranslationScopes.None ); - } - else - { - foreach( var level in levels ) - { - AddTranslationSplitterRegex( regex, level ); - } - } - } - catch( Exception e ) + var levels = context.GetLevels(); + if( levels.Count == 0 ) { - XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation splitter: '{translatioOrDirective}'." ); + AddTranslationSplitterRegex( regex, TranslationScopes.None ); } - } - else if( key.StartsWith( "r:" ) ) - { - try + else { - var regex = new RegexTranslation( key, value ); - - var levels = context.GetLevels(); - if( levels.Count == 0 ) - { - AddTranslationRegex( regex, TranslationScopes.None ); - } - else + foreach( var level in levels ) { - foreach( var level in levels ) - { - AddTranslationRegex( regex, level ); - } + AddTranslationSplitterRegex( regex, level ); } } - catch( Exception e ) - { - XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation: '{translatioOrDirective}'." ); - } } - else + catch( Exception e ) + { + XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation splitter: '{translatioOrDirective}'." ); + } + } + else if( key.StartsWith( "r:" ) ) + { + try { + var regex = new RegexTranslation( key, value ); + var levels = context.GetLevels(); if( levels.Count == 0 ) { - AddTranslation( key, value, TranslationScopes.None ); + AddTranslationRegex( regex, TranslationScopes.None ); } else { foreach( var level in levels ) { - AddTranslation( key, value, level ); + AddTranslationRegex( regex, level ); } } } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation: '{translatioOrDirective}'." ); + } + } + else + { + var levels = context.GetLevels(); + if( levels.Count == 0 ) + { + AddTranslation( key, value, TranslationScopes.None ); + } + else + { + foreach( var level in levels ) + { + AddTranslation( key, value, level ); + } + } } } } } } + + AllowFallback = AllowFallback || context.IsEnabled( "fallback" ); } } - private void LoadTranslationsInFile( string fullFileName, bool isSubstitutionFile, bool isPreprocessorFile, bool isOutputFile ) + private void LoadTranslationsInKeyValuePairs( IEnumerable> pairs, string fullFileName ) + { + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( $"Loading texts: {fullFileName}." ); + + foreach( var kvp in pairs ) + { + string key = kvp.Key; + string value = kvp.Value; + + if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) ) + { + if( key.StartsWith( "sr:" ) ) + { + try + { + var regex = new RegexTranslationSplitter( key, value ); + + AddTranslationSplitterRegex( regex, TranslationScopes.None ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation splitter: '{key}={value}'." ); + } + } + else if( key.StartsWith( "r:" ) ) + { + try + { + var regex = new RegexTranslation( key, value ); + + AddTranslationRegex( regex, TranslationScopes.None ); + } + catch( Exception e ) + { + XuaLogger.AutoTranslator.Warn( e, $"An error occurred while constructing regex translation: '{key}={value}'." ); + } + } + else + { + AddTranslation( key, value, TranslationScopes.None ); + } + } + } + } + + private void LoadTranslationsInFile( string fullFileName, bool isOutputFile ) { var fileExists = File.Exists( fullFileName ); - if( fileExists || isSubstitutionFile || isPreprocessorFile ) + if( fileExists ) { if( fileExists ) { @@ -451,28 +599,17 @@ private void LoadTranslationsInFile( string fullFileName, bool isSubstitutionFil { if( entry.IsFile && entry.Name.EndsWith( ".txt", StringComparison.OrdinalIgnoreCase ) && !entry.Name.EndsWith( "resizer.txt", StringComparison.OrdinalIgnoreCase ) ) { - LoadTranslationsInStream( zipInputStream, fullFileName + Path.DirectorySeparatorChar + entry.Name, isSubstitutionFile, isPreprocessorFile, isOutputFile ); + LoadTranslationsInStream( zipInputStream, fullFileName + Path.DirectorySeparatorChar + entry.Name, isOutputFile ); } } } } else { - LoadTranslationsInStream( stream, fullFileName, isSubstitutionFile, isPreprocessorFile, isOutputFile ); + LoadTranslationsInStream( stream, fullFileName, isOutputFile ); } } } - else if( isSubstitutionFile || isPreprocessorFile ) - { - var fi = new FileInfo( fullFileName ); - Directory.CreateDirectory( fi.Directory.FullName ); - - using( var stream = File.Create( fullFileName ) ) - { - stream.Write( new byte[] { 0xEF, 0xBB, 0xBF }, 0, 3 ); // UTF-8 BOM - stream.Close(); - } - } } } @@ -499,6 +636,16 @@ private void LoadStaticTranslations() } } + internal void RegisterPackage( StreamTranslationPackage package ) + { + _streamPackages.Add( package ); + } + + internal void RegisterPackage( KeyValuePairTranslationPackage package ) + { + _kvpPackages.Add( package ); + } + private void SaveNewTranslationsToDisk() { if( _newTranslations.Count > 0 ) @@ -716,7 +863,7 @@ internal void AddTranslationToCache( string key, string value, bool persistToDis } } - internal bool TryGetTranslationSplitter( string text, int scope, out Match match, out RegexTranslationSplitter splitter ) + public bool TryGetTranslationSplitter( string text, int scope, out Match match, out RegexTranslationSplitter splitter ) { if( scope != TranslationScopes.None && _scopedTranslations.TryGetValue( scope, out var dicts ) && dicts.SplitterRegexes.Count > 0 ) { @@ -766,7 +913,7 @@ internal bool TryGetTranslationSplitter( string text, int scope, out Match match return false; } - internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool allowToken, int scope, out string value ) + public bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool allowToken, int scope, out string value ) { bool result; string untemplated; @@ -1199,7 +1346,7 @@ internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool all // regex lookups - ONLY ORIGNAL VARIATION if( allowRegex ) { - if( dicts != null && dicts.DefaultRegexes.Count > 0 ) + if( dicts != null && dicts.DefaultRegexes.Count > 0 && !dicts.FailedRegexLookups.Contains( key.TemplatedOriginal_Text ) ) { for( int i = dicts.DefaultRegexes.Count - 1; i > -1; i-- ) { @@ -1223,28 +1370,43 @@ internal bool TryGetTranslation( UntranslatedText key, bool allowRegex, bool all XuaLogger.AutoTranslator.Error( e, $"Failed while attempting to replace or match text of regex '{regex.Original}'. Removing that regex from the cache." ); } } + + var added = dicts.FailedRegexLookups.Add( key.TemplatedOriginal_Text ); + if( added && dicts.FailedRegexLookups.Count > 10000 ) + { + dicts.FailedRegexLookups = new HashSet(); + } } - for( int i = _defaultRegexes.Count - 1; i > -1; i-- ) + if( !_failedRegexLookups.Contains( key.TemplatedOriginal_Text ) ) { - var regex = _defaultRegexes[ i ]; - try + for( int i = _defaultRegexes.Count - 1; i > -1; i-- ) { - var match = regex.CompiledRegex.Match( key.TemplatedOriginal_Text ); - if( !match.Success ) continue; + var regex = _defaultRegexes[ i ]; + try + { + var match = regex.CompiledRegex.Match( key.TemplatedOriginal_Text ); + if( !match.Success ) continue; + + value = match.Result( regex.Translation ); - value = match.Result( regex.Translation ); + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Info( $"Regex lookup: '{key.TemplatedOriginal_Text}' => '{value}'" ); + AddTranslationToCache( key.TemplatedOriginal_Text, value, Settings.CacheRegexLookups, TranslationType.Full, TranslationScopes.None ); - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Info( $"Regex lookup: '{key.TemplatedOriginal_Text}' => '{value}'" ); - AddTranslationToCache( key.TemplatedOriginal_Text, value, Settings.CacheRegexLookups, TranslationType.Full, TranslationScopes.None ); + return true; + } + catch( Exception e ) + { + _defaultRegexes.RemoveAt( i ); - return true; + XuaLogger.AutoTranslator.Error( e, $"Failed while attempting to replace or match text of regex '{regex.Original}'. Removing that regex from the cache." ); + } } - catch( Exception e ) - { - _defaultRegexes.RemoveAt( i ); - XuaLogger.AutoTranslator.Error( e, $"Failed while attempting to replace or match text of regex '{regex.Original}'. Removing that regex from the cache." ); + var added = _failedRegexLookups.Add( key.TemplatedOriginal_Text ); + if( added && _failedRegexLookups.Count > 10000 ) + { + _failedRegexLookups = new HashSet(); } } } @@ -1290,7 +1452,7 @@ internal bool TryGetReverseTranslation( string value, int scope, out string key _reverseTranslations.TryGetValue( value, out key ); } - internal bool IsTranslatable( string text, bool isToken, int scope ) + public bool IsTranslatable( string text, bool isToken, int scope ) { var translatable = !IsTranslation( text, scope ); if( isToken && translatable ) @@ -1300,7 +1462,7 @@ internal bool IsTranslatable( string text, bool isToken, int scope ) return translatable; } - internal bool IsPartial( string text, int scope ) + public bool IsPartial( string text, int scope ) { return _partialTranslations.Contains( text ); } @@ -1317,6 +1479,7 @@ public TranslationDictionaries() ReverseTokenTranslations = new Dictionary(); SplitterRegexes = new List(); RegisteredSplitterRegexes = new HashSet(); + FailedRegexLookups = new HashSet(); } public Dictionary TokenTranslations { get; } @@ -1327,6 +1490,7 @@ public TranslationDictionaries() public HashSet RegisteredRegexes { get; } public List SplitterRegexes { get; } public HashSet RegisteredSplitterRegexes { get; } + public HashSet FailedRegexLookups { get; set; } } } } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs b/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs index 6ae1e27b..a7f270c1 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/TextTranslationInfo.cs @@ -53,6 +53,8 @@ internal class TextTranslationInfo public bool IsKnownTextComponent { get; set; } public bool SupportsStabilization { get; set; } + public IReadOnlyTextTranslationCache TextCache { get; set; } + public void Initialize( object ui ) { if( !_initialized ) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/TranslationFileLoadingContext.cs b/src/XUnity.AutoTranslator.Plugin.Core/TranslationFileLoadingContext.cs index c2aace56..a27a5c77 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/TranslationFileLoadingContext.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/TranslationFileLoadingContext.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Text; +using XUnity.AutoTranslator.Plugin.Core.Configuration; namespace XUnity.AutoTranslator.Plugin.Core { @@ -10,6 +11,7 @@ internal class TranslationFileLoadingContext { private HashSet _executables = new HashSet( StringComparer.OrdinalIgnoreCase ); private HashSet _levels = new HashSet(); + private HashSet _enabledTags = new HashSet( StringComparer.OrdinalIgnoreCase ); public bool IsExecutable( string executable ) { @@ -23,6 +25,11 @@ public HashSet GetLevels() return _levels; } + public bool IsEnabled( string tag ) + { + return _enabledTags.Contains( tag ); + } + public void Apply( TranslationFileDirective directive ) { directive.ModifyContext( this ); @@ -39,6 +46,8 @@ public SetLevelTranslationFileDirective( int[] levels ) public override void ModifyContext( TranslationFileLoadingContext context ) { + if( !Settings.EnableTranslationScoping ) return; + foreach( var level in Levels ) { context._levels.Add( level ); @@ -62,6 +71,8 @@ public UnsetLevelTranslationFileDirective( int[] levels ) public override void ModifyContext( TranslationFileLoadingContext context ) { + if( !Settings.EnableTranslationScoping ) return; + foreach( var level in Levels ) { context._levels.Remove( level ); @@ -85,6 +96,8 @@ public SetExeTranslationFileDirective( string[] executables ) public override void ModifyContext( TranslationFileLoadingContext context ) { + if( !Settings.EnableTranslationScoping ) return; + foreach( var executable in Executables ) { context._executables.Add( executable ); @@ -108,6 +121,8 @@ public UnsetExeTranslationFileDirective( string[] executables ) public override void ModifyContext( TranslationFileLoadingContext context ) { + if( !Settings.EnableTranslationScoping ) return; + foreach( var executable in Executables ) { context._executables.Remove( executable ); @@ -119,6 +134,29 @@ public override string ToString() return "#unset exe " + string.Join( ",", Executables ); } } + + public class EnableTranslationFileDirective : TranslationFileDirective + { + public EnableTranslationFileDirective( string tag ) + { + Tag = tag; + } + + public string Tag { get; } + + public override void ModifyContext( TranslationFileLoadingContext context ) + { + if( Tag != null ) + { + context._enabledTags.Add( Tag ); + } + } + + public override string ToString() + { + return "#enable " + Tag; + } + } } internal abstract class TranslationFileDirective @@ -137,7 +175,7 @@ public static TranslationFileDirective Create( string directive ) if( directive.Length > 0 && directive[ 0 ] == '#' ) { var parts = directive.Split( new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries ); - if( parts.Length >= 3 ) + if( parts.Length >= 2 ) { var command = parts[ 0 ].ToLowerInvariant(); @@ -150,6 +188,8 @@ public static TranslationFileDirective Create( string directive ) return CreateSetCommand( setType, argument ); case "#unset": return CreateUnsetCommand( setType, argument ); + case "#enable": + return CreateEnableCommand( setType, argument ); default: break; } @@ -187,8 +227,15 @@ private static TranslationFileDirective CreateUnsetCommand( string setType, stri return null; } + private static TranslationFileDirective CreateEnableCommand( string setType, string argument ) + { + return new TranslationFileLoadingContext.EnableTranslationFileDirective( setType ); + } + private static int[] ParseCommaSeperatedListAsIntArray( string argument ) { + if( string.IsNullOrEmpty( argument ) ) return new int[ 0 ]; + List result = new List(); var args = argument.Split( new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries ); foreach( var arg in args ) @@ -203,6 +250,8 @@ private static int[] ParseCommaSeperatedListAsIntArray( string argument ) private static string[] ParseCommaSeperatedListAsStringArray( string argument ) { + if( string.IsNullOrEmpty( argument ) ) return new string[ 0 ]; + return argument.Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ).Select( x => x.Trim() ).ToArray(); } diff --git a/src/XUnity.AutoTranslator.Plugin.Core/TranslationRegistry.cs b/src/XUnity.AutoTranslator.Plugin.Core/TranslationRegistry.cs new file mode 100644 index 00000000..b6ea620e --- /dev/null +++ b/src/XUnity.AutoTranslator.Plugin.Core/TranslationRegistry.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace XUnity.AutoTranslator.Plugin.Core +{ + /// + /// Entry point for manipulating translations that have been loaded by the plugin. + /// + /// Methods on this interface should be called during plugin initialization. Preferably during the Start callback. + /// + public static class TranslationRegistry + { + /// + /// Obtains the translations registry instance. + /// + public static ITranslationRegistry Default => AutoTranslationPlugin.Current; + } + + /// + /// Interface for manipulating translation that have been loaded by the plugin. + /// + public interface ITranslationRegistry + { + /// + /// Registers and loads the specified translation package. + /// + /// The assembly that the behaviour should be applied to. + /// Package containing translations. + void RegisterPluginSpecificTranslations( Assembly assembly, StreamTranslationPackage package ); + + /// + /// Registers and loads the specified translation package. + /// + /// The assembly that the behaviour should be applied to. + /// Package containing translations. + void RegisterPluginSpecificTranslations( Assembly assembly, KeyValuePairTranslationPackage package ); + + /// + /// Allow plugin-specific translation to fallback to generic translations. + /// + /// The assembly that the behaviour should be applied to. + void EnablePluginTranslationFallback( Assembly assembly ); + } + + /// + /// Extension methods for the ITranslationRegistry interface. + /// + public static class TranslationRegistryExtensions + { + private static Assembly GetCallingPlugin() + { + var frame = new StackFrame( 2 ); + var method = frame.GetMethod(); + if( method != null ) + { + var ass = method.DeclaringType.Assembly; + return ass; + } + throw new ArgumentException( "Could not automatically determine the calling plugin. Consider calling the overload of this method taking an assembly name." ); + } + + /// + /// Registers and loads the specified translation package. Inspects the callstack to determine + /// the calling plugin that the translations should be associated with. + /// + /// The translation registry that the package is being registered with. + /// Package containing translations. + public static void RegisterPluginSpecificTranslations( this ITranslationRegistry registry, StreamTranslationPackage package ) + { + var assembly = GetCallingPlugin(); + registry.RegisterPluginSpecificTranslations( assembly, package ); + } + + /// + /// Registers and loads the specified translation package. Inspects the callstack to determine + /// the calling plugin that the translations should be associated with. + /// + /// The translation registry that the package is being registered with. + /// Package containing translations. + public static void RegisterPluginSpecificTranslations( this ITranslationRegistry registry, KeyValuePairTranslationPackage package ) + { + var assembly = GetCallingPlugin(); + registry.RegisterPluginSpecificTranslations( assembly, package ); + } + + /// + /// Allow plugin-specific translation to fallback to generic translations. Inspects the callstack to determine + /// the calling plugin that the translations should be associated with. + /// + /// The translation registry that the package is being registered with. + public static void EnablePluginTranslationFallback( this ITranslationRegistry registry ) + { + var assembly = GetCallingPlugin(); + registry.EnablePluginTranslationFallback( assembly ); + } + } +} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/UIResize/UIResizeCache.cs b/src/XUnity.AutoTranslator.Plugin.Core/UIResize/UIResizeCache.cs index 38534cca..715d5cfc 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/UIResize/UIResizeCache.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/UIResize/UIResizeCache.cs @@ -63,16 +63,13 @@ private void LoadResizeCommandsInStream( Stream stream, string fullFileName ) string[] translations = reader.ReadToEnd().Split( new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries ); foreach( string translatioOrDirective in translations ) { - if( Settings.EnableTranslationScoping ) + var directive = TranslationFileDirective.Create( translatioOrDirective ); + if( directive != null ) { - var directive = TranslationFileDirective.Create( translatioOrDirective ); - if( directive != null ) - { - context.Apply( directive ); + context.Apply( directive ); - if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( "Directive in file: " + fullFileName + ": " + directive.ToString() ); - continue; - } + if( !Settings.EnableSilentMode ) XuaLogger.AutoTranslator.Debug( "Directive in file: " + fullFileName + ": " + directive.ToString() ); + continue; } if( context.IsExecutable( Settings.ApplicationName ) ) diff --git a/src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs b/src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs index d23e4556..fa508710 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs +++ b/src/XUnity.AutoTranslator.Plugin.Core/Utilities/TextHelper.cs @@ -111,194 +111,3 @@ internal static string EscapeNewlines( string str ) } } } - - -//using System; -//using System.Collections.Generic; -//using System.Globalization; -//using System.Linq; -//using System.Text; - -//namespace XUnity.AutoTranslator.Plugin.Core.Utilities -//{ -// internal static class TextHelper -// { -// internal static string Decode( string text ) -// { -// // WONT WORK -// var commentIndex = text.IndexOf( "//" ); -// if( commentIndex > -1 ) -// { -// text = text.Substring( 0, commentIndex ); -// } - -// return Unescape( text ) -// .Replace( "%3D", "=" ) -// .Replace( "%2F%2F", "//" ); -// } - -// internal static string Encode( string text ) -// { -// // FIXME: Wont work because we break on "=" outside - -// return Escape( text ); -// //.Replace( "=", "%3D" ) -// //.Replace( "//", "%2F%2F" ); -// } - -// public static string Unescape( string text ) -// { -// if( text == null ) return null; - -// var builder = new StringBuilder( text ); -// string translatedText = null; -// string untranslatedText = null; - -// bool escapeNext = false; -// for( int i = 0; i < builder.Length; i++ ) -// { -// var c = builder[ i ]; -// if( escapeNext ) -// { -// bool found = true; -// char escapeWith = default( char ); -// string escapeWithString = null; -// switch( c ) -// { -// case '\\': -// escapeWith = '\\'; -// break; -// case 'n': -// escapeWith = '\n'; -// break; -// case 'r': -// escapeWith = '\r'; -// break; -// case '=': -// escapeWith = '='; -// break; -// case '/': -// if( builder.Length > i + 1 ) -// { -// var nc = builder[ i + 1 ]; -// if( nc == '/' ) -// { -// escapeWithString = "//"; -// i++; // skip next char... -// } -// } -// break; -// default: -// found = false; -// break; -// } - -// // remove previous char and go one back -// if( found ) -// { -// if( escapeWithString != null ) -// { -// i -= escapeWithString.Length; - -// builder.Remove( i, 1 + escapeWithString.Length ); -// builder.Insert( i, escapeWith ); -// } -// else -// { -// builder.Remove( --i, 2 ); -// builder.Insert( i, escapeWith ); -// } -// } - -// escapeNext = false; -// } -// else if( c == '\\' ) -// { -// escapeNext = true; -// } -// else if( c == '/' ) -// { -// if( builder.Length > i + 1 ) -// { -// var nc = builder[ i + 1 ]; -// if( nc == '/' ) -// { -// builder.Remove( i, builder.Length - i ); // remove the rest of the string - -// // TODO: Could also remove any ' ' leading up to it! - -// break; -// } -// } -// } -// else if( c == '=' && untranslatedText == null ) -// { -// untranslatedText = builder.ToString( 0, i - 1 ); // exclude '=' -// builder.Remove( 0, i ); // include "=" -// } -// } - -// translatedText = builder.ToString(); - -// if( untranslatedText != null && translatedText != null ) -// { -// // fix url encoded characters in both (legacy support) -// } -// else -// { -// return null; -// } -// } - -// public static string Escape( string str ) -// { -// if( str == null || str.Length == 0 ) -// { -// return ""; -// } - -// char c; -// int len = str.Length; -// StringBuilder sb = new StringBuilder( len + 4 ); -// for( int i = 0; i < len; i++ ) -// { -// c = str[ i ]; -// switch( c ) -// { -// case '\\': -// sb.Append( '\\' ); -// sb.Append( c ); -// break; -// case '\n': -// sb.Append( "\\n" ); -// break; -// case '\r': -// sb.Append( "\\r" ); -// break; -// case '=': -// sb.Append( "\\=" ); -// break; -// case '/': -// if( len > i + 1 ) -// { -// var nc = str[ i + 1 ]; -// if( nc == '/' ) -// { -// sb.Append( "\\//" ); -// i++; // skip next char... -// } -// } -// else -// { -// sb.Append( c ); -// } -// break; -// default: -// sb.Append( c ); -// break; -// } -// } -// return sb.ToString(); -// } -// } -//} diff --git a/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj b/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj index fd9920d0..a4bc7dd9 100644 --- a/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj +++ b/src/XUnity.AutoTranslator.Plugin.Core/XUnity.AutoTranslator.Plugin.Core.csproj @@ -11,7 +11,7 @@ True True net35 - 4.11.4 + 4.12.0 latest diff --git a/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj b/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj index b5c5c186..a7c6eb13 100644 --- a/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj +++ b/src/XUnity.AutoTranslator.Plugin.IPA/XUnity.AutoTranslator.Plugin.IPA.csproj @@ -2,7 +2,7 @@ net35 - 4.11.4 + 4.12.0 diff --git a/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj b/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj index a2a25a09..7c6fb4de 100644 --- a/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj +++ b/src/XUnity.AutoTranslator.Plugin.UnityInjector/XUnity.AutoTranslator.Plugin.UnityInjector.csproj @@ -2,7 +2,7 @@ net35 - 4.11.4 + 4.12.0 diff --git a/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj b/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj index 02eee3b2..786f0729 100644 --- a/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj +++ b/src/XUnity.AutoTranslator.Setup/XUnity.AutoTranslator.Setup.csproj @@ -4,7 +4,7 @@ Exe net40 SetupReiPatcherAndAutoTranslator - 4.11.4 + 4.12.0 icon.ico diff --git a/src/XUnity.Common/Constants/ClrTypes.cs b/src/XUnity.Common/Constants/ClrTypes.cs index 3c88ae1b..78cfb92c 100644 --- a/src/XUnity.Common/Constants/ClrTypes.cs +++ b/src/XUnity.Common/Constants/ClrTypes.cs @@ -33,6 +33,7 @@ public static class ClrTypes public static readonly Type TextField = FindType( "FairyGUI.TextField" ); // Unity + public static readonly Type GameObject = FindType( "UnityEngine.GameObject" ); public static readonly Type TextMesh = FindType( "UnityEngine.TextMesh" ); public static readonly Type Text = FindType( "UnityEngine.UI.Text" ); public static readonly Type Image = FindType( "UnityEngine.UI.Image" ); @@ -43,6 +44,8 @@ public static class ClrTypes public static readonly Type WWW = FindType( "UnityEngine.WWW" ); public static readonly Type InputField = FindType( "UnityEngine.UI.InputField" ); public static readonly Type GUI = FindType( "UnityEngine.GUI" ); + public static readonly Type GUIStyle = FindType( "UnityEngine.GUIStyle" ); + public static readonly Type GUI_ToolbarButtonSize = FindType( "UnityEngine.GUI+ToolbarButtonSize" ); public static readonly Type ImageConversion = FindType( "UnityEngine.ImageConversion" ); public static readonly Type Texture = FindType( "UnityEngine.Texture" ); public static readonly Type SpriteRenderer = FindType( "UnityEngine.SpriteRenderer" ); diff --git a/src/XUnity.Common/Utilities/HookingHelper.cs b/src/XUnity.Common/Utilities/HookingHelper.cs index bf53d180..bb2e268e 100644 --- a/src/XUnity.Common/Utilities/HookingHelper.cs +++ b/src/XUnity.Common/Utilities/HookingHelper.cs @@ -75,12 +75,13 @@ public static void PatchType( Type type, bool forceMonoModHooks ) { var prefix = type.GetMethod( "Prefix", flags ); var postfix = type.GetMethod( "Postfix", flags ); + var finalizer = type.GetMethod( "Finalizer", flags ); if( Harmony == null ) { XuaLogger.Common.Warn( "Harmony is not loaded or could not be initialized. Falling back to MonoMod hooks." ); } - if( forceMonoModHooks || Harmony == null || ( prefix == null && postfix == null ) ) + if( forceMonoModHooks || Harmony == null || ( prefix == null && postfix == null && finalizer == null ) ) { if( ClrTypes.Hook == null || ClrTypes.NativeDetour == null ) { @@ -95,14 +96,15 @@ public static void PatchType( Type type, bool forceMonoModHooks ) try { hook = ClrTypes.Hook.GetConstructor( new Type[] { typeof( MethodBase ), typeof( MethodInfo ) } ).Invoke( new object[] { original, mmdetour } ); + hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); } catch( Exception e1 ) when( e1.FirstInnerExceptionOfType() != null || e1.FirstInnerExceptionOfType()?.Message?.Contains( "Body-less" ) == true ) { suffix = "(native)"; hook = ClrTypes.NativeDetour.GetConstructor( new Type[] { typeof( MethodBase ), typeof( MethodBase ) } ).Invoke( new object[] { original, mmdetour } ); + hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); } - hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); type.GetMethod( "MM_Init", flags )?.Invoke( null, new object[] { hook } ); XuaLogger.Common.Debug( $"Hooked {original.DeclaringType.FullName}.{original.Name} through forced MonoMod hooks. {suffix}" ); @@ -123,6 +125,7 @@ public static void PatchType( Type type, bool forceMonoModHooks ) var harmonyPrefix = prefix != null ? CreateHarmonyMethod( prefix, priority ) : null; var harmonyPostfix = postfix != null ? CreateHarmonyMethod( postfix, priority ) : null; + var harmonyFinalizer = finalizer != null ? CreateHarmonyMethod( finalizer, priority ) : null; if( PatchMethod12 != null ) { @@ -130,7 +133,7 @@ public static void PatchType( Type type, bool forceMonoModHooks ) } else { - PatchMethod20.Invoke( Harmony, new object[] { original, harmonyPrefix, harmonyPostfix, null, null } ); + PatchMethod20.Invoke( Harmony, new object[] { original, harmonyPrefix, harmonyPostfix, null, harmonyFinalizer } ); } XuaLogger.Common.Debug( $"Hooked {original.DeclaringType.FullName}.{original.Name} through Harmony hooks." ); @@ -147,14 +150,15 @@ public static void PatchType( Type type, bool forceMonoModHooks ) try { hook = ClrTypes.Hook.GetConstructor( new Type[] { typeof( MethodBase ), typeof( MethodInfo ) } ).Invoke( new object[] { original, mmdetour } ); + hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); } catch( Exception e1 ) when( e1.FirstInnerExceptionOfType() != null || e1.FirstInnerExceptionOfType()?.Message?.Contains( "Body-less" ) == true ) { suffix = "(native)"; hook = ClrTypes.NativeDetour.GetConstructor( new Type[] { typeof( MethodBase ), typeof( MethodBase ) } ).Invoke( new object[] { original, mmdetour } ); + hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); } - hook.GetType().GetMethod( "Apply" ).Invoke( hook, null ); type.GetMethod( "MM_Init", flags )?.Invoke( null, new object[] { hook } ); XuaLogger.Common.Debug( $"Hooked {original.DeclaringType.FullName}.{original.Name} through MonoMod hooks. {suffix}" );