using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using ExIni;
using UnityEngine;
using UnityEngine.UI;
using System.Globalization;
using XUnity.AutoTranslator.Plugin.Core.Extensions;
using UnityEngine.EventSystems;
using XUnity.AutoTranslator.Plugin.Core.Configuration;
using XUnity.AutoTranslator.Plugin.Core.Utilities;
using XUnity.AutoTranslator.Plugin.Core.Web;
using XUnity.AutoTranslator.Plugin.Core.Hooks;
using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro;
using XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI;
using XUnity.AutoTranslator.Plugin.Core.IMGUI;
using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
using UnityEngine.SceneManagement;
using XUnity.AutoTranslator.Plugin.Core.Constants;
using XUnity.AutoTranslator.Plugin.Core.Debugging;
using XUnity.AutoTranslator.Plugin.Core.Batching;
using Harmony;
using XUnity.AutoTranslator.Plugin.Core.Parsing;
namespace XUnity.AutoTranslator.Plugin.Core
{
public class AutoTranslationPlugin : MonoBehaviour
{
private static readonly char[][] TranslationSplitters = new char[][] { new char[] { '\t' }, new char[] { '=' } };
///
/// Allow the instance to be accessed statically, as only one will exist.
///
public static AutoTranslationPlugin Current;
///
/// These are the currently running translation jobs (being translated by an http request).
///
private List _completedJobs = new List();
private Dictionary _unstartedJobs = new Dictionary();
private Dictionary _ongoingJobs = new Dictionary();
///
/// All the translations are stored in this dictionary.
///
private Dictionary _staticTranslations = new Dictionary();
private Dictionary _translations = new Dictionary();
private Dictionary _reverseTranslations = new Dictionary();
///
/// These are the new translations that has not yet been persisted to the file system.
///
private object _writeToFileSync = new object();
private Dictionary _newTranslations = new Dictionary();
private HashSet _newUntranslated = new HashSet();
///
/// Keeps track of things to copy to clipboard.
///
private List _textsToCopyToClipboardOrdered = new List();
private HashSet _textsToCopyToClipboard = new HashSet();
private float _clipboardUpdated = 0.0f;
///
/// The number of http translation errors that has occurred up until now.
///
private int _consecutiveErrors = 0;
///
/// This is a hash set that contains all Text components that is currently being worked on by
/// the translation plugin.
///
private HashSet _ongoingOperations = new HashSet();
///
/// This function will check if there are symbols of a given language contained in a string.
///
private Func _symbolCheck;
///
/// Texts currently being scheduled for translation by 'immediate' components.
///
private HashSet _immediatelyTranslating = new HashSet();
private Dictionary _translatedImages = new Dictionary( StringComparer.InvariantCultureIgnoreCase );
private HashSet _untranslatedImages = new HashSet();
private Component _advEngine;
private float? _nextAdvUpdate;
private IKnownEndpoint _endpoint;
private int[] _currentTranslationsQueuedPerSecondRollingWindow = new int[ Settings.TranslationQueueWatchWindow ];
private float? _timeExceededThreshold;
private float _translationsQueuedPerSecond;
private bool _isInTranslatedMode = true;
private bool _textHooksEnabled = true;
private bool _imageHooksEnabled = true;
private bool _batchLogicHasFailed = false;
private int _availableBatchOperations = Settings.MaxAvailableBatchOperations;
private float _batchOperationSecondCounter = 0;
private string[] _previouslyQueuedText = new string[ Settings.PreviousTextStaggerCount ];
private int _staggerTextCursor = 0;
private int _concurrentStaggers = 0;
private int _frameForLastQueuedTranslation = -1;
private int _consecutiveFramesTranslated = 0;
private int _secondForQueuedTranslation = -1;
private int _consecutiveSecondsTranslated = 0;
private bool _hasOverrideFont = false;
private bool _overrideFont = false;
private bool _initialized = false;
private bool _temporarilyDisabled = false;
public void Initialize()
{
Current = this;
if( Logger.Current == null )
{
Logger.Current = new ConsoleLogger();
}
try
{
Settings.Configure();
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred during configuration. Shutting plugin down." );
_endpoint = null;
Settings.IsShutdown = true;
return;
}
if( Settings.EnableConsole ) DebugConsole.Enable();
HooksSetup.InstallTextHooks();
HooksSetup.InstallImageHooks();
try
{
_endpoint = KnownEndpoints.FindEndpoint( Settings.ServiceEndpoint );
}
catch( Exception e )
{
Logger.Current.Error( e, "An unexpected error occurred during initialization of endpoint." );
}
if( !TextHelper.IsFromLanguageSupported( Settings.FromLanguage ) )
{
Logger.Current.Error( $"The plugin has been configured to use the 'FromLanguage={Settings.FromLanguage}'. This language is not supported. Shutting plugin down." );
_endpoint = null;
Settings.IsShutdown = true;
}
_symbolCheck = TextHelper.GetSymbolCheck( Settings.FromLanguage );
if( !string.IsNullOrEmpty( Settings.OverrideFont ) )
{
var available = Font.GetOSInstalledFontNames();
if( !available.Contains( Settings.OverrideFont ) )
{
Logger.Current.Error( $"The specified override font is not available. Available fonts: " + string.Join( ", ", available ) );
Settings.OverrideFont = null;
}
else
{
_hasOverrideFont = true;
}
_overrideFont = _hasOverrideFont;
}
if( Features.SupportsScenes && Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureDumping || Settings.EnableTextureTranslation ) )
{
try
{
EnableSceneLoadScan();
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while settings up texture scene-load scans." );
}
}
LoadTranslations();
LoadStaticTranslations();
// start a thread that will periodically removed unused references
var t1 = new Thread( MaintenanceLoop );
t1.IsBackground = true;
t1.Start();
// start a thread that will periodically save new translations
var t2 = new Thread( SaveTranslationsLoop );
t2.IsBackground = true;
t2.Start();
}
private IEnumerable GetTranslationFiles()
{
return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ), $"*.txt", SearchOption.AllDirectories )
.Select( x => x.Replace( "/", "\\" ) );
}
private IEnumerable GetTextureFiles()
{
return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TextureDirectory ), $"*.png", SearchOption.AllDirectories )
.Select( x => x.Replace( "/", "\\" ) );
}
private void MaintenanceLoop( object state )
{
while( true )
{
try
{
ObjectExtensions.Cull();
}
catch( Exception e )
{
Logger.Current.Error( e, "An unexpected error occurred while removing GC'ed resources." );
}
Thread.Sleep( 1000 * 60 );
}
}
private void SaveTranslationsLoop( object state )
{
try
{
while( true )
{
if( _newTranslations.Count > 0 )
{
lock( _writeToFileSync )
{
if( _newTranslations.Count > 0 )
{
using( var stream = File.Open( Settings.AutoTranslationsFilePath, FileMode.Append, FileAccess.Write ) )
using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
{
foreach( var kvp in _newTranslations )
{
writer.WriteLine( TextHelper.Encode( kvp.Key ) + '=' + TextHelper.Encode( kvp.Value ) );
}
writer.Flush();
}
_newTranslations.Clear();
}
}
}
else
{
Thread.Sleep( 5000 );
}
}
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while saving translations to disk." );
}
}
public void EnableSceneLoadScan()
{
EnableSceneLoadScanInternal();
}
public void EnableSceneLoadScanInternal()
{
// specified in own method, because of chance that this has changed through Unity lifetime
SceneManager.sceneLoaded += ( arg1, arg2 ) => SceneManager_SceneLoaded();
}
private void SceneManager_SceneLoaded()
{
Logger.Current.Info( "SceneLoading..." );
var startTime = Time.realtimeSinceStartup;
ManualHookForTextures();
var endTime = Time.realtimeSinceStartup;
Logger.Current.Info( $"SceneLoaded (took {Math.Round( endTime - startTime, 2 )} seconds)" );
}
///
/// Loads the translations found in Translation.{lang}.txt
///
private void LoadTranslations()
{
try
{
lock( _writeToFileSync )
{
Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ) );
Directory.CreateDirectory( Path.GetDirectoryName( Path.Combine( Config.Current.DataPath, Settings.OutputFile ) ) );
var mainTranslationFile = Settings.AutoTranslationsFilePath.Replace( "/", "\\" );
LoadTranslationsInFile( mainTranslationFile );
foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile } ) )
{
LoadTranslationsInFile( fullFileName );
}
}
if( Settings.EnableTextureTranslation || Settings.EnableTextureDumping )
{
_translatedImages.Clear();
_untranslatedImages.Clear();
Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TextureDirectory ) );
foreach( var fullFileName in GetTextureFiles() )
{
RegisterImageFromFile( fullFileName );
}
}
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while loading translations." );
}
}
private void RegisterImageFromFile( string fullFileName )
{
var fileName = Path.GetFileNameWithoutExtension( fullFileName );
var startHash = fileName.LastIndexOf( "[" );
var endHash = fileName.LastIndexOf( "]" );
if( endHash > -1 && startHash > -1 && endHash > startHash )
{
var takeFrom = startHash + 1;
// load based on whether or not the key is image hashed
var parts = fileName.Substring( takeFrom, endHash - takeFrom ).Split( '-' );
string key;
string originalHash;
if( parts.Length == 1 )
{
key = parts[ 0 ];
originalHash = parts[ 0 ];
}
else if( parts.Length == 2 )
{
key = parts[ 0 ];
originalHash = parts[ 1 ];
}
else
{
Logger.Current.Warn( $"Image not loaded (unknown hash): {fullFileName}." );
return;
}
var data = File.ReadAllBytes( fullFileName );
var currentHash = HashHelper.Compute( data );
var isModified = StringComparer.InvariantCultureIgnoreCase.Compare( originalHash, currentHash ) != 0;
// only load images that someone has modified!
if( Settings.LoadUnmodifiedTextures || isModified )
{
RegisterTranslatedImage( key, data );
Logger.Current.Debug( $"Image loaded: {fullFileName}." );
}
else
{
RegisterUntranslatedImage( key );
Logger.Current.Warn( $"Image not loaded (unmodified): {fullFileName}." );
}
//if( Settings.DeleteUnmodifiedTextures && !isModified )
//{
// try
// {
// File.Delete( fullFileName );
// Logger.Current.Warn( $"Image deleted (unmodified): {fullFileName}." );
// }
// catch( Exception e )
// {
// Logger.Current.Warn( e, $"An error occurred while trying to delete unmodified image: {fullFileName}." );
// }
//}
}
else
{
Logger.Current.Warn( $"Image not loaded (no hash): {fullFileName}." );
}
}
private void RegisterImageFromData( string textureName, string key, byte[] data )
{
var name = textureName.SanitizeForFileSystem();
var root = Path.Combine( Config.Current.DataPath, Settings.TextureDirectory );
var originalHash = HashHelper.Compute( data );
// allow hash and key to be the same; only store one of them then!
string fileName;
if( key == originalHash )
{
fileName = name + " [" + key + "].png";
}
else
{
fileName = name + " [" + key + "-" + originalHash + "].png";
}
var fullName = Path.Combine( root, fileName );
File.WriteAllBytes( fullName, data );
Logger.Current.Info( "Dumped texture file: " + fileName );
if( Settings.LoadUnmodifiedTextures )
{
RegisterTranslatedImage( key, data );
}
else
{
RegisterUntranslatedImage( key );
}
}
private void RegisterTranslatedImage( string key, byte[] data )
{
_translatedImages[ key ] = data;
}
private void RegisterUntranslatedImage( string key )
{
_untranslatedImages.Add( key );
}
private void LoadTranslationsInFile( string fullFileName )
{
if( File.Exists( fullFileName ) )
{
Logger.Current.Debug( $"Loading texts: {fullFileName}." );
string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
foreach( string translation in translations )
{
for( int i = 0 ; i < TranslationSplitters.Length ; i++ )
{
var splitter = TranslationSplitters[ i ];
string[] kvp = translation.Split( splitter, StringSplitOptions.None );
if( kvp.Length == 2 )
{
string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
string value = TextHelper.Decode( kvp[ 1 ].TrimIfConfigured() );
if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
{
AddTranslation( key, value );
break;
}
}
}
}
}
}
private void LoadStaticTranslations()
{
if( Settings.UseStaticTranslations && Settings.FromLanguage == Settings.DefaultFromLanguage && Settings.Language == Settings.DefaultLanguage )
{
var tab = new char[] { '\t' };
var equals = new char[] { '=' };
var splitters = new char[][] { tab, equals };
// load static translations from previous titles
string[] translations = Properties.Resources.StaticTranslations.Split( new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries );
foreach( string translation in translations )
{
for( int i = 0 ; i < splitters.Length ; i++ )
{
var splitter = splitters[ i ];
string[] kvp = translation.Split( splitter, StringSplitOptions.None );
if( kvp.Length >= 2 )
{
string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
string value = TextHelper.Decode( kvp[ 1 ].TrimIfConfigured() );
if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
{
_staticTranslations[ key ] = value;
break;
}
}
}
}
}
}
private TranslationJob GetOrCreateTranslationJobFor( object ui, TranslationKey key, TranslationContext context )
{
var lookupKey = key.GetDictionaryLookupKey();
if( _unstartedJobs.TryGetValue( lookupKey, out TranslationJob unstartedJob ) )
{
unstartedJob.Associate( context );
return unstartedJob;
}
if( _ongoingJobs.TryGetValue( lookupKey, out TranslationJob ongoingJob ) )
{
ongoingJob.Associate( context );
return ongoingJob;
}
foreach( var completedJob in _completedJobs )
{
if( completedJob.Key.GetDictionaryLookupKey() == lookupKey )
{
completedJob.Associate( context );
return completedJob;
}
}
Logger.Current.Debug( "Queued translation for: " + lookupKey );
ongoingJob = new TranslationJob( key );
if( ui != null )
{
ongoingJob.OriginalSources.Add( ui );
}
ongoingJob.Associate( context );
_unstartedJobs.Add( lookupKey, ongoingJob );
CheckStaggerText( lookupKey );
CheckConsecutiveFrames();
CheckConsecutiveSeconds();
CheckThresholds();
return ongoingJob;
}
private void CheckConsecutiveSeconds()
{
var currentSecond = (int)Time.time;
var lastSecond = currentSecond - 1;
if( lastSecond == _secondForQueuedTranslation )
{
// we also queued something last frame, lets increment our counter
_consecutiveSecondsTranslated++;
if( _consecutiveSecondsTranslated > Settings.MaximumConsecutiveSecondsTranslated )
{
// Shutdown, this wont be tolerated!!!
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
Settings.IsShutdown = true;
Logger.Current.Error( $"SPAM DETECTED: Translations were queued every second for more than {Settings.MaximumConsecutiveSecondsTranslated} consecutive seconds. Shutting down plugin." );
}
}
else if( currentSecond == _secondForQueuedTranslation )
{
// do nothing, there may be multiple translations per frame, that wont increase this counter
}
else
{
// but if multiple Update frames has passed, we will reset the counter
_consecutiveSecondsTranslated = 0;
}
_secondForQueuedTranslation = currentSecond;
}
private void CheckConsecutiveFrames()
{
var currentFrame = Time.frameCount;
var lastFrame = currentFrame - 1;
if( lastFrame == _frameForLastQueuedTranslation )
{
// we also queued something last frame, lets increment our counter
_consecutiveFramesTranslated++;
if( _consecutiveFramesTranslated > Settings.MaximumConsecutiveFramesTranslated )
{
// Shutdown, this wont be tolerated!!!
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
Settings.IsShutdown = true;
Logger.Current.Error( $"SPAM DETECTED: Translations were queued every frame for more than {Settings.MaximumConsecutiveFramesTranslated} consecutive frames. Shutting down plugin." );
}
}
else if( currentFrame == _frameForLastQueuedTranslation )
{
// do nothing, there may be multiple translations per frame, that wont increase this counter
}
else if( _consecutiveFramesTranslated > 0 )
{
// but if multiple Update frames has passed, we will reset the counter
_consecutiveFramesTranslated--;
}
_frameForLastQueuedTranslation = currentFrame;
}
public void PeriodicResetFrameCheck()
{
var currentSecond = (int)Time.time;
if( currentSecond % 100 == 0 )
{
_consecutiveFramesTranslated = 0;
}
}
private void CheckStaggerText( string untranslatedText )
{
bool wasProblematic = false;
for( int i = 0 ; i < _previouslyQueuedText.Length ; i++ )
{
var previouslyQueuedText = _previouslyQueuedText[ i ];
if( previouslyQueuedText != null )
{
if( untranslatedText.RemindsOf( previouslyQueuedText ) )
{
wasProblematic = true;
break;
}
}
}
if( wasProblematic )
{
_concurrentStaggers++;
if( _concurrentStaggers > Settings.MaximumStaggers )
{
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
Settings.IsShutdown = true;
Logger.Current.Error( $"SPAM DETECTED: Text that is 'scrolling in' is being translated. Disable that feature. Shutting down plugin." );
}
}
else
{
_concurrentStaggers = 0;
}
_previouslyQueuedText[ _staggerTextCursor % _previouslyQueuedText.Length ] = untranslatedText;
_staggerTextCursor++;
}
private void CheckThresholds()
{
if( _unstartedJobs.Count > Settings.MaxUnstartedJobs )
{
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
Settings.IsShutdown = true;
Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxUnstartedJobs} queued for translations due to unknown reasons. Shutting down plugin." );
}
var previousIdx = ( (int)( Time.time - Time.deltaTime ) ) % Settings.TranslationQueueWatchWindow;
var newIdx = ( (int)Time.time ) % Settings.TranslationQueueWatchWindow;
if( previousIdx != newIdx )
{
_currentTranslationsQueuedPerSecondRollingWindow[ newIdx ] = 0;
}
_currentTranslationsQueuedPerSecondRollingWindow[ newIdx ]++;
var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
_translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
if( _translationsQueuedPerSecond > Settings.MaxTranslationsQueuedPerSecond )
{
if( !_timeExceededThreshold.HasValue )
{
_timeExceededThreshold = Time.time;
}
if( Time.time - _timeExceededThreshold.Value > Settings.MaxSecondsAboveTranslationThreshold )
{
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
Settings.IsShutdown = true;
Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxTranslationsQueuedPerSecond} translations per seconds queued for a {Settings.MaxSecondsAboveTranslationThreshold} second period. Shutting down plugin." );
}
}
else
{
_timeExceededThreshold = null;
}
}
private void IncrementBatchOperations()
{
_batchOperationSecondCounter += Time.deltaTime;
if( _batchOperationSecondCounter > Settings.IncreaseBatchOperationsEvery )
{
if( _availableBatchOperations < Settings.MaxAvailableBatchOperations )
{
_availableBatchOperations++;
}
_batchOperationSecondCounter = 0;
}
}
private void ResetThresholdTimerIfRequired()
{
var previousIdx = ( (int)( Time.time - Time.deltaTime ) ) % Settings.TranslationQueueWatchWindow;
var newIdx = ( (int)Time.time ) % Settings.TranslationQueueWatchWindow;
if( previousIdx != newIdx )
{
_currentTranslationsQueuedPerSecondRollingWindow[ newIdx ] = 0;
}
var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
_translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
if( _translationsQueuedPerSecond <= Settings.MaxTranslationsQueuedPerSecond )
{
_timeExceededThreshold = null;
}
}
private bool IsImageRegistered( string key )
{
return _translatedImages.ContainsKey( key ) || _untranslatedImages.Contains( key );
}
private bool TryGetTranslatedImage( string key, out byte[] data )
{
return _translatedImages.TryGetValue( key, out data );
}
private void AddTranslation( string key, string value )
{
_translations[ key ] = value;
_reverseTranslations[ value ] = key;
}
private void AddTranslation( TranslationKey key, string value )
{
var lookup = key.GetDictionaryLookupKey();
_translations[ lookup ] = value;
_reverseTranslations[ value ] = lookup;
}
private void QueueNewUntranslatedForClipboard( TranslationKey key )
{
if( Settings.CopyToClipboard && Features.SupportsClipboard )
{
if( !_textsToCopyToClipboard.Contains( key.RelevantText ) )
{
_textsToCopyToClipboard.Add( key.RelevantText );
_textsToCopyToClipboardOrdered.Add( key.RelevantText );
_clipboardUpdated = Time.realtimeSinceStartup;
}
}
}
private void QueueNewUntranslatedForDisk( TranslationKey key )
{
_newUntranslated.Add( key.GetDictionaryLookupKey() );
}
private void QueueNewTranslationForDisk( TranslationKey key, string value )
{
lock( _writeToFileSync )
{
_newTranslations[ key.GetDictionaryLookupKey() ] = value;
}
}
private void QueueNewTranslationForDisk( string key, string value )
{
lock( _writeToFileSync )
{
_newTranslations[ key ] = value;
}
}
private bool TryGetTranslation( TranslationKey key, out string value )
{
return TryGetTranslation( key.GetDictionaryLookupKey(), out value );
}
private bool TryGetTranslation( string key, out string value )
{
var result = _translations.TryGetValue( key, out value );
if( result )
{
return result;
}
else if( _staticTranslations.Count > 0 )
{
if( _staticTranslations.TryGetValue( key, out value ) )
{
QueueNewTranslationForDisk( key, value );
AddTranslation( key, value );
return true;
}
}
return result;
}
public bool TryGetReverseTranslation( string value, out string key )
{
return _reverseTranslations.TryGetValue( value, out key );
}
public string Hook_TextChanged_WithResult( object ui, string text )
{
if( !ui.IsKnownTextType() ) return null;
if( _textHooksEnabled && !_temporarilyDisabled )
{
return TranslateOrQueueWebJob( ui, text, false );
}
return null;
}
public string ExternalHook_TextChanged_WithResult( object ui, string text )
{
if( !ui.IsKnownTextType() ) return null;
if( _textHooksEnabled && !_temporarilyDisabled )
{
return TranslateOrQueueWebJob( ui, text, true );
}
return null;
}
public void Hook_TextChanged( object ui )
{
if( _textHooksEnabled && !_temporarilyDisabled )
{
TranslateOrQueueWebJob( ui, null, false );
}
}
public void Hook_ImageChangedOnComponent( object source, Texture2D texture = null, bool isPrefixHooked = false )
{
if( !_imageHooksEnabled ) return;
if( !source.IsKnownImageType() ) return;
HandleImage( source, texture, isPrefixHooked );
}
public void Hook_ImageChanged( Texture2D texture, bool isPrefixHooked = false )
{
if( !_imageHooksEnabled ) return;
if( texture == null ) return;
HandleImage( null, texture, isPrefixHooked );
}
private void SetTranslatedText( object ui, string translatedText, TextTranslationInfo info )
{
info?.SetTranslatedText( translatedText );
if( _isInTranslatedMode )
{
SetText( ui, translatedText, true, info );
}
}
public void Hook_HandleComponent( object ui )
{
if( _hasOverrideFont )
{
var info = ui.GetTextTranslationInfo();
if( _overrideFont )
{
info?.ChangeFont( ui );
}
else
{
info?.UnchangeFont( ui );
}
}
if( Settings.ForceUIResizing )
{
var info = ui.GetTextTranslationInfo();
if( info?.IsCurrentlySettingText == false )
{
// force UI resizing is highly problematic for NGUI because text should somehow
// be set after changing "resize" properties... brilliant stuff
if( ui.GetType() != ClrTypes.UILabel )
{
info?.ResizeUI( ui );
}
}
}
}
///
/// Sets the text of a UI text, while ensuring this will not fire a text changed event.
///
private void SetText( object ui, string text, bool isTranslated, TextTranslationInfo info )
{
if( !info?.IsCurrentlySettingText ?? true )
{
try
{
_textHooksEnabled = false;
if( info != null )
{
info.IsCurrentlySettingText = true;
}
if( Settings.EnableUIResizing || Settings.ForceUIResizing )
{
if( isTranslated || Settings.ForceUIResizing )
{
info?.ResizeUI( ui );
}
else
{
info?.UnresizeUI( ui );
}
}
// NGUI only behaves if you set the text after the resize behaviour
ui.SetText( text );
info?.ResetScrollIn( ui );
}
catch( TargetInvocationException )
{
// might happen with NGUI
}
catch( NullReferenceException )
{
// This is likely happened due to a scene change.
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while setting text on a component." );
}
finally
{
_textHooksEnabled = true;
if( info != null )
{
info.IsCurrentlySettingText = false;
}
}
}
}
///
/// Determines if a text should be translated.
///
private bool IsTranslatable( string str )
{
return _symbolCheck( str )
&& str.Length <= Settings.MaxCharactersPerTranslation
&& !_reverseTranslations.ContainsKey( str )
&& !Settings.IgnoreTextStartingWith.Any( x => str.StartsWithStrict( x ) );
}
private bool IsShortText( string str )
{
return str.Length <= ( Settings.MaxCharactersPerTranslation / 2 );
}
public bool ShouldTranslateImageComponent( object ui )
{
var component = ui as Component;
if( component != null )
{
// dummy check
var go = component.gameObject;
var ignore = go.HasIgnoredName();
if( ignore )
{
return false;
}
var behaviour = component as Behaviour;
if( behaviour?.isActiveAndEnabled == false )
{
return false;
}
}
return true;
}
public bool ShouldTranslateTextComponent( object ui, bool ignoreComponentState )
{
var component = ui as Component;
if( component != null )
{
// dummy check
var go = component.gameObject;
var ignore = go.HasIgnoredName();
if( ignore )
{
return false;
}
if( !ignoreComponentState )
{
var behaviour = component as Behaviour;
if( behaviour?.isActiveAndEnabled == false )
{
return false;
}
}
var inputField = component.gameObject.GetFirstComponentInSelfOrAncestor( ClrTypes.InputField )
?? component.gameObject.GetFirstComponentInSelfOrAncestor( ClrTypes.TMP_InputField );
return inputField == null;
}
return true;
}
private string TranslateOrQueueWebJob( object ui, string text, bool ignoreComponentState )
{
var info = ui.GetTextTranslationInfo();
if( _ongoingOperations.Contains( ui ) )
{
return TranslateImmediate( ui, text, info, ignoreComponentState );
}
var supportsStabilization = ui.SupportsStabilization();
if( Settings.Delay == 0 || !supportsStabilization )
{
return TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization, ignoreComponentState );
}
else
{
StartCoroutine(
DelayForSeconds( Settings.Delay, () =>
{
TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization, ignoreComponentState );
} ) );
}
return null;
}
public static bool IsCurrentlySetting( TextTranslationInfo info )
{
if( info == null ) return false;
return info.IsCurrentlySettingText;
}
private void HandleImage( object source, Texture2D texture, bool isPrefixHooked )
{
if( Settings.EnableTextureDumping )
{
try
{
DumpTexture( source, texture );
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while dumping texture." );
}
}
if( Settings.EnableTextureTranslation )
{
try
{
TranslateTexture( source, texture, isPrefixHooked, false );
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while translating texture." );
}
}
}
private void TranslateTexture( object ui, bool forceReload )
{
if( ui is Texture2D texture2d )
{
TranslateTexture( null, texture2d, false, forceReload );
}
else
{
TranslateTexture( ui, null, false, forceReload );
}
}
private void TranslateTexture( object source, Texture2D texture, bool isPrefixHooked, bool forceReload )
{
try
{
_imageHooksEnabled = false;
texture = texture ?? source.GetTexture();
if( texture == null ) return;
var tti = texture.GetTextureTranslationInfo();
var iti = source.GetImageTranslationInfo();
var key = tti.GetKey( texture );
if( string.IsNullOrEmpty( key ) ) return;
if( TryGetTranslatedImage( key, out var newData ) )
{
if( _isInTranslatedMode )
{
// handle texture
if( !tti.IsTranslated || forceReload )
{
try
{
texture.LoadImageEx( newData, tti.IsNonReadable( texture ) );
}
finally
{
tti.IsTranslated = true;
}
}
// handle containing component
if( iti != null )
{
if( !iti.IsTranslated || forceReload )
{
try
{
if( !isPrefixHooked )
{
source.SetAllDirtyEx();
}
}
finally
{
iti.IsTranslated = true;
}
}
}
}
}
else
{
// if we cannot find the texture, and the texture is considered translated... hmmm someone has removed a file
// handle texture
var originalData = tti.GetOriginalData( texture );
if( originalData != null )
{
if( tti.IsTranslated )
{
try
{
texture.LoadImageEx( originalData, tti.IsNonReadable( texture ) );
}
finally
{
tti.IsTranslated = true;
}
}
// handle containing component
if( iti != null )
{
if( iti.IsTranslated )
{
try
{
if( !isPrefixHooked )
{
source.SetAllDirtyEx();
}
}
finally
{
iti.IsTranslated = true;
}
}
}
}
}
if( !_isInTranslatedMode )
{
var originalData = tti.GetOriginalData( texture );
if( originalData != null )
{
// handle texture
if( tti.IsTranslated )
{
try
{
texture.LoadImageEx( originalData, tti.IsNonReadable( texture ) );
}
finally
{
tti.IsTranslated = false;
}
}
// handle containing component
if( iti != null )
{
if( iti.IsTranslated )
{
try
{
if( !isPrefixHooked )
{
source.SetAllDirtyEx();
}
}
finally
{
iti.IsTranslated = false;
}
}
}
}
}
}
finally
{
_imageHooksEnabled = true;
}
}
private void DumpTexture( object source, Texture2D texture )
{
try
{
_imageHooksEnabled = false;
texture = texture ?? source.GetTexture();
if( texture == null ) return;
var info = texture.GetTextureTranslationInfo();
if( info.HasDumpedAlternativeTexture ) return;
try
{
if( ShouldTranslate( texture ) )
{
var key = info.GetKey( texture );
if( string.IsNullOrEmpty( key ) ) return;
if( !IsImageRegistered( key ) )
{
var name = texture.GetTextureName();
//var format = "[" + texture.format.ToString() + "] ";
var originalData = info.GetOrCreateOriginalData( texture );
RegisterImageFromData( name, key, originalData );
}
}
}
finally
{
info.HasDumpedAlternativeTexture = true;
}
}
finally
{
_imageHooksEnabled = true;
}
}
private bool ShouldTranslate( Texture2D texture )
{
// convert to int so engine versions that does not have specific enums still work
var format = (int)texture.format;
// 1 = Alpha8
// 9 = R16
// 63 = R8
return format != 1
&& format != 9
&& format != 63;
}
private string TranslateImmediate( object ui, string text, TextTranslationInfo info, bool ignoreComponentState )
{
// Get the trimmed text
text = ( text ?? ui.GetText() ).TrimIfConfigured();
if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
{
info?.Reset( text );
var textKey = new TranslationKey( ui, text, ui.IsSpammingComponent(), false );
// if we already have translation loaded in our _translatios dictionary, simply load it and set text
string translation;
if( TryGetTranslation( textKey, out translation ) )
{
if( !string.IsNullOrEmpty( translation ) )
{
SetTranslatedText( ui, textKey.Untemplate( translation ), info );
return translation;
}
}
}
return null;
}
///
/// Translates the string of a UI text or queues it up to be translated
/// by the HTTP translation service.
///
private string TranslateOrQueueWebJobImmediate( object ui, string text, TextTranslationInfo info, bool supportsStabilization, bool ignoreComponentState, TranslationContext context = null )
{
text = text ?? ui.GetText();
// make sure text exists
var originalText = text;
if( context == null )
{
// Get the trimmed text
text = text.TrimIfConfigured();
}
// Ensure that we actually want to translate this text and its owning UI element.
if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
{
//Logger.Current.Debug( "START: " + ui.GetType().Name + ": " + text );
info?.Reset( originalText );
var isSpammer = ui.IsSpammingComponent();
var textKey = new TranslationKey( ui, text, isSpammer, context != null );
// if we already have translation loaded in our _translatios dictionary, simply load it and set text
string translation;
if( TryGetTranslation( textKey, out translation ) )
{
QueueNewUntranslatedForClipboard( textKey );
if( !string.IsNullOrEmpty( translation ) )
{
if( context == null ) // never set text if operation is contextualized (only a part translation)
{
SetTranslatedText( ui, textKey.Untemplate( translation ), info );
}
return translation;
}
}
else
{
if( context == null && ui.SupportsRichText() )
{
var parser = UnityTextParsers.GetTextParserByGameEngine();
if( parser != null )
{
var result = parser.Parse( text );
if( result.HasRichSyntax )
{
var isWhitelisted = ui.IsWhitelistedForImmediateRichTextTranslation();
translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, isWhitelisted );
if( translation != null )
{
SetTranslatedText( ui, translation, info );
return translation;
}
else if( isWhitelisted )
{
return null;
}
}
}
}
if( supportsStabilization && context == null ) // never stabilize a text that is contextualized or that does not support stabilization
{
// if we dont know what text to translate it to, we need to figure it out.
// this might take a while, so add the UI text component to the ongoing operations
// list, so we dont start multiple operations for it, as its text might be constantly
// changing.
_ongoingOperations.Add( ui );
// start a coroutine, that will execute once the string of the UI text has stopped
// changing. For all texts except 'story' texts, this will add a delay for exactly
// 0.5s to the translation. This is barely noticable.
//
// on the other hand, for 'story' texts, this will take the time that it takes
// for the text to stop 'scrolling' in.
try
{
StartCoroutine(
WaitForTextStablization(
ui: ui,
delay: 1.0f, // 1 second to prevent '1 second tickers' from getting translated
maxTries: 60, // 50 tries, about 1 minute
currentTries: 0,
onMaxTriesExceeded: () =>
{
_ongoingOperations.Remove( ui );
},
onTextStabilized: stabilizedText =>
{
_ongoingOperations.Remove( ui );
originalText = stabilizedText;
stabilizedText = stabilizedText.TrimIfConfigured();
if( !string.IsNullOrEmpty( stabilizedText ) && IsTranslatable( stabilizedText ) )
{
var stabilizedTextKey = new TranslationKey( ui, stabilizedText, false );
QueueNewUntranslatedForClipboard( stabilizedTextKey );
info?.Reset( originalText );
// once the text has stabilized, attempt to look it up
if( TryGetTranslation( stabilizedTextKey, out translation ) )
{
if( !string.IsNullOrEmpty( translation ) )
{
// stabilized, no need to untemplate
SetTranslatedText( ui, translation, info );
}
}
else
{
if( context == null && ui.SupportsRichText() )
{
var parser = UnityTextParsers.GetTextParserByGameEngine();
if( parser != null )
{
var result = parser.Parse( stabilizedText );
if( result.HasRichSyntax )
{
var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
if( translatedText != null )
{
// stabilized, no need to untemplate
SetTranslatedText( ui, translatedText, info );
}
return;
}
}
}
// Lets try not to spam a service that might not be there...
if( _endpoint != null )
{
if( !Settings.IsShutdown )
{
var job = GetOrCreateTranslationJobFor( ui, stabilizedTextKey, context );
job.Components.Add( ui );
}
}
else
{
QueueNewUntranslatedForDisk( stabilizedTextKey );
}
}
}
} ) );
}
catch( Exception )
{
_ongoingOperations.Remove( ui );
}
}
else if( !isSpammer || ( isSpammer && IsShortText( text ) ) )
{
if( context != null )
{
// if there is a context, this is a part-translation, which means it is not a candidate for scrolling-in text
if( _endpoint != null )
{
if( !Settings.IsShutdown )
{
// once the text has stabilized, attempt to look it up
var job = GetOrCreateTranslationJobFor( ui, textKey, context );
}
}
else
{
QueueNewUntranslatedForDisk( textKey );
}
}
else
{
StartCoroutine(
WaitForTextStablization(
textKey: textKey,
delay: 1.0f,
onTextStabilized: () =>
{
// Lets try not to spam a service that might not be there...
if( _endpoint != null )
{
// once the text has stabilized, attempt to look it up
if( !Settings.IsShutdown )
{
if( !TryGetTranslation( textKey, out translation ) )
{
var job = GetOrCreateTranslationJobFor( ui, textKey, context );
}
}
}
else
{
QueueNewUntranslatedForDisk( textKey );
}
} ) );
}
}
}
}
return null;
}
private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserResult result, bool allowStartJob )
{
Dictionary translations = new Dictionary();
// attempt to lookup ALL strings immediately; return result if possible; queue operations
foreach( var kvp in result.Arguments )
{
var key = kvp.Key;
var value = kvp.Value.TrimIfConfigured();
if( !string.IsNullOrEmpty( value ) && IsTranslatable( value ) )
{
string partTranslation;
if( TryGetTranslation( value, out partTranslation ) )
{
translations.Add( key, partTranslation );
}
else if( allowStartJob )
{
// incomplete, must start job
var context = new TranslationContext( ui, result );
TranslateOrQueueWebJobImmediate( ui, value, null, false, true, context );
}
}
else
{
// the value will do
translations.Add( key, value );
}
}
if( result.Arguments.Count == translations.Count )
{
return result.Untemplate( translations );
}
else
{
return null; // could not perform complete translation
}
}
///
/// Utility method that allows me to wait to call an action, until
/// the text has stopped changing. This is important for 'story'
/// mode text, which 'scrolls' into place slowly.
///
public IEnumerator WaitForTextStablization( object ui, float delay, int maxTries, int currentTries, Action onTextStabilized, Action onMaxTriesExceeded )
{
yield return 0; // wait a single frame to allow any external plugins to complete their hooking logic
bool succeeded = false;
while( currentTries < maxTries ) // shortcircuit
{
var beforeText = ui.GetText();
yield return new WaitForSeconds( delay );
var afterText = ui.GetText();
//Logger.Current.Debug( "WAITING: " + ui.GetType().Name + ": " + afterText );
if( beforeText == afterText )
{
onTextStabilized( afterText );
succeeded = true;
break;
}
currentTries++;
}
if( !succeeded )
{
onMaxTriesExceeded();
}
}
///
/// Utility method that allows me to wait to call an action, until
/// the text has stopped changing. This is important for 'story'
/// mode text, which 'scrolls' into place slowly. This version is
/// for global text, where the component cannot tell us if the text
/// has changed itself.
///
public IEnumerator WaitForTextStablization( TranslationKey textKey, float delay, Action onTextStabilized, Action onFailed = null )
{
var text = textKey.GetDictionaryLookupKey();
if( !_immediatelyTranslating.Contains( text ) )
{
_immediatelyTranslating.Add( text );
try
{
yield return new WaitForSeconds( delay );
bool succeeded = true;
foreach( var otherImmediatelyTranslating in _immediatelyTranslating )
{
if( text != otherImmediatelyTranslating )
{
if( text.RemindsOf( otherImmediatelyTranslating ) )
{
succeeded = false;
break;
}
}
}
if( succeeded )
{
onTextStabilized();
}
else
{
onFailed?.Invoke();
}
}
finally
{
_immediatelyTranslating.Remove( text );
}
}
}
public IEnumerator DelayForSeconds( float delay, Action onContinue )
{
yield return new WaitForSeconds( delay );
onContinue();
}
public void Awake()
{
if( !_initialized )
{
_initialized = true;
try
{
Initialize();
ManualHook();
}
catch( Exception e )
{
Logger.Current.Error( e, "An unexpected error occurred during plugin initialization." );
}
}
}
public void Start()
{
try
{
HooksSetup.InstallOverrideTextHooks();
}
catch( Exception e )
{
Logger.Current.Error( e, "An unexpected error occurred during plugin start." );
}
}
public void Update()
{
try
{
if( _endpoint != null )
{
_endpoint.OnUpdate();
}
if( Features.SupportsClipboard )
{
CopyToClipboard();
}
if( !Settings.IsShutdown )
{
PeriodicResetFrameCheck();
IncrementBatchOperations();
ResetThresholdTimerIfRequired();
KickoffTranslations();
FinishTranslations();
if( ClrTypes.AdvEngine != null && _nextAdvUpdate.HasValue && Time.time > _nextAdvUpdate )
{
_nextAdvUpdate = null;
UpdateUtageText();
}
}
if( Input.anyKey )
{
if( Settings.EnablePrintHierarchy && ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.Y ) )
{
PrintObjects();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.T ) )
{
ToggleTranslation();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.F ) )
{
ToggleFont();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.D ) )
{
DumpUntranslated();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.R ) )
{
ReloadTranslations();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.U ) )
{
ManualHook();
}
}
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred in Update callback. " );
}
}
// create this as a field instead of local var, to prevent new creation on EVERY game loop
private readonly List _kickedOff = new List();
private void KickoffTranslations()
{
if( _endpoint == null ) return;
if( Settings.EnableBatching && _endpoint.SupportsLineSplitting && !_batchLogicHasFailed && _unstartedJobs.Count > 1 && _availableBatchOperations > 0 )
{
while( _unstartedJobs.Count > 0 && _availableBatchOperations > 0 )
{
if( _endpoint.IsBusy ) break;
var kvps = _unstartedJobs.Take( Settings.BatchSize ).ToList();
var batch = new TranslationBatch();
foreach( var kvp in kvps )
{
var key = kvp.Key;
var job = kvp.Value;
_kickedOff.Add( key );
if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
batch.Add( job );
_ongoingJobs[ key ] = job;
}
if( !batch.IsEmpty )
{
_availableBatchOperations--;
StartCoroutine( _endpoint.Translate( batch.GetFullTranslationKey(), Settings.FromLanguage, Settings.Language, translatedText => OnBatchTranslationCompleted( batch, translatedText ),
() => OnTranslationFailed( batch ) ) );
}
}
}
else
{
foreach( var kvp in _unstartedJobs )
{
if( _endpoint.IsBusy ) break;
var key = kvp.Key;
var job = kvp.Value;
_kickedOff.Add( key );
// lets see if the text should still be translated before kicking anything off
if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
_ongoingJobs[ key ] = job;
StartCoroutine( _endpoint.Translate( job.Key.GetDictionaryLookupKey(), Settings.FromLanguage, Settings.Language, translatedText => OnSingleTranslationCompleted( job, translatedText ),
() => OnTranslationFailed( job ) ) );
}
}
for( int i = 0 ; i < _kickedOff.Count ; i++ )
{
_unstartedJobs.Remove( _kickedOff[ i ] );
}
_kickedOff.Clear();
}
public void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch )
{
_consecutiveErrors = 0;
var succeeded = batch.MatchWithTranslations( translatedTextBatch );
if( succeeded )
{
foreach( var tracker in batch.Trackers )
{
Settings.TranslationCount++;
var job = tracker.Job;
var translatedText = tracker.RawTranslatedText;
if( !string.IsNullOrEmpty( translatedText ) )
{
if( Settings.ForceSplitTextAfterCharacters > 0 )
{
translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
}
job.TranslatedText = job.Key.RepairTemplate( translatedText );
QueueNewTranslationForDisk( job.Key, translatedText );
_completedJobs.Add( job );
}
AddTranslation( job.Key, job.TranslatedText );
job.State = TranslationJobState.Succeeded;
_ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
}
}
else
{
// might as well re-add all translation jobs, and never do this again!
_batchLogicHasFailed = true;
foreach( var tracker in batch.Trackers )
{
Settings.TranslationCount++;
var key = tracker.Job.Key.GetDictionaryLookupKey();
if( !_unstartedJobs.ContainsKey( key ) )
{
_unstartedJobs[ key ] = tracker.Job;
}
_ongoingJobs.Remove( key );
}
Logger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
}
if( !Settings.IsShutdown )
{
if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
private void OnSingleTranslationCompleted( TranslationJob job, string translatedText )
{
Settings.TranslationCount++;
_consecutiveErrors = 0;
if( !string.IsNullOrEmpty( translatedText ) )
{
if( Settings.ForceSplitTextAfterCharacters > 0 )
{
translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
}
job.TranslatedText = job.Key.RepairTemplate( translatedText );
QueueNewTranslationForDisk( job.Key, translatedText );
_completedJobs.Add( job );
}
AddTranslation( job.Key, job.TranslatedText );
job.State = TranslationJobState.Succeeded;
_ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
if( !Settings.IsShutdown )
{
if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
private void OnTranslationFailed( TranslationJob job )
{
Settings.TranslationCount++; // counts as a translation
_consecutiveErrors++;
job.State = TranslationJobState.Failed;
_ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
if( !Settings.IsShutdown )
{
if( _consecutiveErrors >= Settings.MaxErrors )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
private void OnTranslationFailed( TranslationBatch batch )
{
Settings.TranslationCount++; // counts as a translation
_consecutiveErrors++;
foreach( var tracker in batch.Trackers )
{
tracker.Job.State = TranslationJobState.Failed;
_ongoingJobs.Remove( tracker.Job.Key.GetDictionaryLookupKey() );
}
if( !Settings.IsShutdown )
{
if( _consecutiveErrors >= Settings.MaxErrors )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
private void FinishTranslations()
{
if( _completedJobs.Count > 0 )
{
for( int i = _completedJobs.Count - 1 ; i >= 0 ; i-- )
{
var job = _completedJobs[ i ];
_completedJobs.RemoveAt( i );
foreach( var component in job.Components )
{
// update the original text, but only if it has not been chaanged already for some reason (could be other translator plugin or game itself)
try
{
var text = component.GetText().TrimIfConfigured();
if( text == job.Key.OriginalText )
{
var info = component.GetTextTranslationInfo();
SetTranslatedText( component, job.TranslatedText, info );
}
}
catch( NullReferenceException )
{
// might fail if compoent is no longer associated to game
}
}
// handle each context
foreach( var context in job.Contexts )
{
// are all jobs within this context completed? If so, we can set the text
if( context.Jobs.All( x => x.State == TranslationJobState.Succeeded ) )
{
try
{
var text = context.Component.GetText().TrimIfConfigured();
var result = context.Result;
Dictionary translations = new Dictionary();
var translatedText = TranslateOrQueueWebJobImmediateByParserResult( context.Component, result, false );
if( !string.IsNullOrEmpty( translatedText ) )
{
if( !_translations.ContainsKey( context.Result.OriginalText ) )
{
AddTranslation( context.Result.OriginalText, translatedText );
QueueNewTranslationForDisk( context.Result.OriginalText, translatedText );
}
if( text == result.OriginalText )
{
if( translatedText != null )
{
var info = context.Component.GetTextTranslationInfo();
SetTranslatedText( context.Component, translatedText, info );
}
}
}
}
catch( NullReferenceException )
{
}
}
}
// Utage support
if( ClrTypes.AdvEngine != null
&& job.OriginalSources.Any( x => ClrTypes.AdvCommand.IsAssignableFrom( x.GetType() ) ) )
{
_nextAdvUpdate = Time.time + 0.5f;
}
}
}
}
private void UpdateUtageText()
{
// After an object is destroyed, an equality check with null will return true. The variable does not go to null, you can still call GetInstanceID() on it, but the "==" operator is overloaded and behaves as expected.
if( _advEngine == null || _advEngine?.gameObject == null )
{
_advEngine = (Component)GameObject.FindObjectOfType( Constants.ClrTypes.AdvEngine );
}
if( _advEngine != null )
{
AccessTools.Method( Constants.ClrTypes.AdvEngine, "ChangeLanguage" )?.Invoke( _advEngine, new object[ 0 ] );
}
}
private void ReloadTranslations()
{
LoadTranslations();
foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
{
var ui = kvp.Key;
try
{
if( ui is Component component )
{
if( component.gameObject?.activeSelf ?? false )
{
var tti = kvp.Value as TextTranslationInfo;
if( tti != null && !string.IsNullOrEmpty( tti.OriginalText ) )
{
var key = new TranslationKey( kvp.Key, tti.OriginalText, false );
if( TryGetTranslation( key, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
{
SetTranslatedText( kvp.Key, translatedText, tti ); // no need to untemplatize the translated text
}
}
}
}
if( Settings.EnableTextureTranslation )
{
TranslateTexture( ui, true );
}
}
catch( Exception )
{
// not super pretty, no...
ObjectExtensions.Remove( ui );
}
}
}
private string CalculateDumpFileName()
{
int idx = 0;
string fileName = null;
do
{
idx++;
fileName = $"UntranslatedDump{idx}.txt";
}
while( File.Exists( fileName ) );
return fileName;
}
private void DumpUntranslated()
{
if( _newUntranslated.Count > 0 )
{
using( var stream = File.Open( CalculateDumpFileName(), FileMode.Append, FileAccess.Write ) )
using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
{
foreach( var untranslated in _newUntranslated )
{
writer.WriteLine( TextHelper.Encode( untranslated ) + '=' );
}
writer.Flush();
}
_newUntranslated.Clear();
}
}
private void ToggleFont()
{
if( _hasOverrideFont )
{
_overrideFont = !_overrideFont;
var objects = ObjectExtensions.GetAllRegisteredObjects();
Logger.Current.Info( $"Toggling fonts of {objects.Count} objects." );
if( _overrideFont )
{
// make sure we use the translated version of all texts
foreach( var kvp in objects )
{
var tti = kvp.Value as TextTranslationInfo;
if( tti != null )
{
var ui = kvp.Key;
try
{
if( ( ui as Component )?.gameObject?.activeSelf ?? false )
{
tti?.ChangeFont( ui );
}
}
catch( Exception )
{
// not super pretty, no...
ObjectExtensions.Remove( ui );
}
}
}
}
else
{
// make sure we use the original version of all texts
foreach( var kvp in objects )
{
var tti = kvp.Value as TextTranslationInfo;
var ui = kvp.Key;
try
{
if( ( ui as Component )?.gameObject?.activeSelf ?? false )
{
tti?.UnchangeFont( ui );
}
}
catch( Exception )
{
// not super pretty, no...
ObjectExtensions.Remove( ui );
}
}
}
}
}
private void ToggleTranslation()
{
_isInTranslatedMode = !_isInTranslatedMode;
var objects = ObjectExtensions.GetAllRegisteredObjects();
Logger.Current.Info( $"Toggling translations of {objects.Count} objects." );
// FIXME: Translate TEXTURES first??? Problem if texture is not related to a component!
if( _isInTranslatedMode )
{
// make sure we use the translated version of all texts
foreach( var kvp in objects )
{
var ui = kvp.Key;
try
{
if( ui is Component component )
{
if( component.gameObject?.activeSelf ?? false )
{
var tti = kvp.Value as TextTranslationInfo;
if( tti != null && tti.IsTranslated )
{
SetText( ui, tti.TranslatedText, true, tti );
}
}
}
if( Settings.EnableTextureTranslation && Settings.EnableTextureToggling )
{
TranslateTexture( ui, false );
}
}
catch( Exception )
{
// not super pretty, no...
ObjectExtensions.Remove( ui );
}
}
}
else
{
// make sure we use the original version of all texts
foreach( var kvp in objects )
{
var ui = kvp.Key;
try
{
if( ui is Component component )
{
if( component.gameObject?.activeSelf ?? false )
{
var tti = kvp.Value as TextTranslationInfo;
if( tti != null && tti.IsTranslated )
{
SetText( ui, tti.OriginalText, true, tti );
}
}
}
if( Settings.EnableTextureTranslation && Settings.EnableTextureToggling )
{
TranslateTexture( ui, false );
}
}
catch( Exception )
{
// not super pretty, no...
ObjectExtensions.Remove( ui );
}
}
}
}
private void CopyToClipboard()
{
if( Settings.CopyToClipboard
&& _textsToCopyToClipboardOrdered.Count > 0
&& Time.realtimeSinceStartup - _clipboardUpdated > Settings.ClipboardDebounceTime )
{
try
{
var builder = new StringBuilder();
foreach( var text in _textsToCopyToClipboardOrdered )
{
if( text.Length + builder.Length > Settings.MaxClipboardCopyCharacters ) break;
builder.AppendLine( text );
}
TextEditor editor = (TextEditor)GUIUtility.GetStateObject( typeof( TextEditor ), GUIUtility.keyboardControl );
editor.text = builder.ToString();
editor.SelectAll();
editor.Copy();
}
catch( Exception e )
{
Logger.Current.Error( e, "An error while copying text to clipboard." );
}
finally
{
_textsToCopyToClipboard.Clear();
_textsToCopyToClipboardOrdered.Clear();
}
}
}
private void PrintObjects()
{
using( var stream = File.Open( Path.Combine( Environment.CurrentDirectory, "hierarchy.txt" ), FileMode.Create ) )
using( var writer = new StreamWriter( stream ) )
{
foreach( var root in GetAllRoots() )
{
TraverseChildren( writer, root, "" );
}
writer.Flush();
}
}
private void ManualHook()
{
ManualHookForComponents();
ManualHookForTextures();
}
private void ManualHookForComponents()
{
foreach( var root in GetAllRoots() )
{
TraverseChildrenManualHook( root );
}
}
private void ManualHookForTextures()
{
if( Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureTranslation || Settings.EnableTextureDumping ) )
{
// scan all textures and update
var textures = Resources.FindObjectsOfTypeAll();
foreach( var texture in textures )
{
Hook_ImageChanged( texture );
}
//// scan all components and set dirty
//var components = GameObject.FindObjectsOfType();
//foreach( var component in components )
//{
// component.SetAllDirtyEx();
//}
}
}
private IEnumerable GetAllRoots()
{
var objects = GameObject.FindObjectsOfType();
foreach( var obj in objects )
{
if( obj.transform != null && obj.transform.parent == null )
{
yield return obj;
}
}
}
private void TraverseChildren( StreamWriter writer, GameObject obj, string identation )
{
if( obj != null )
{
var layer = LayerMask.LayerToName( obj.layer );
var components = string.Join( ", ", obj.GetComponents().Select( x => x?.GetType()?.Name ).Where( x => x != null ).ToArray() );
var line = string.Format( "{0,-50} {1,100}",
identation + obj.name + " [" + layer + "]",
components );
writer.WriteLine( line );
if( obj.transform != null )
{
for( int i = 0 ; i < obj.transform.childCount ; i++ )
{
var child = obj.transform.GetChild( i );
TraverseChildren( writer, child.gameObject, identation + " " );
}
}
}
}
private void TraverseChildrenManualHook( GameObject obj )
{
if( obj != null )
{
var components = obj.GetComponents();
foreach( var component in components )
{
if( component.IsKnownTextType() )
{
Hook_TextChanged( component );
}
if( Settings.EnableTextureTranslation || Settings.EnableTextureDumping )
{
if( component.IsKnownImageType() )
{
Hook_ImageChangedOnComponent( component );
}
}
}
if( obj.transform != null )
{
for( int i = 0 ; i < obj.transform.childCount ; i++ )
{
var child = obj.transform.GetChild( i );
TraverseChildrenManualHook( child.gameObject );
}
}
}
}
public void DisableAutoTranslator()
{
_temporarilyDisabled = true;
}
public void EnableAutoTranslator()
{
_temporarilyDisabled = false;
}
}
}