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
{
///
/// 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 = Time.realtimeSinceStartup;
///
/// 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;
private object _advEngine;
private float? _nextAdvUpdate;
private IKnownEndpoint _endpoint;
private int[] _currentTranslationsQueuedPerSecondRollingWindow = new int[ Settings.TranslationQueueWatchWindow ];
private float? _timeExceededThreshold;
private float _translationsQueuedPerSecond;
private bool _isInTranslatedMode = true;
private bool _hooksEnabled = true;
private bool _batchLogicHasFailed = false;
private int _availableBatchOperations = Settings.MaxAvailableBatchOperations;
private float _batchOperationSecondCounter = 0;
private string _previouslyQueuedText = null;
private int _concurrentStaggers = 0;
private int _frameForLastQueuedTranslation = -1;
private int _consecutiveFramesQueued = 0;
public void Initialize()
{
Current = this;
Logger.Current = new ConsoleLogger();
Settings.Configure();
if( Settings.EnableConsole ) DebugConsole.Enable();
HooksSetup.InstallHooks();
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 );
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 string[] GetTranslationFiles()
{
return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ), $"*.txt", SearchOption.AllDirectories ) // FIXME: Add $"*{Language}.txt"
.Union( new[] { Settings.AutoTranslationsFilePath } )
.Select( x => x.Replace( "/", "\\" ) )
.Distinct()
.OrderBy( x => x )
.ToArray();
}
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." );
}
}
///
/// 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 tab = new char[] { '\t' };
var equals = new char[] { '=' };
var splitters = new char[][] { tab, equals };
foreach( var fullFileName in GetTranslationFiles() )
{
if( File.Exists( fullFileName ) )
{
string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
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 ) )
{
AddTranslation( key, value );
break;
}
}
}
}
}
}
}
}
catch( Exception e )
{
Logger.Current.Error( e, "An error occurred while loading translations." );
}
}
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();
CheckThresholds();
return ongoingJob;
}
public void CheckConsecutiveFrames()
{
var currentFrame = Time.frameCount;
var lastFrame = currentFrame - 1;
if( lastFrame == _frameForLastQueuedTranslation )
{
// we also queued something last frame, lets increment our counter
_consecutiveFramesQueued++;
if( _consecutiveFramesQueued > Settings.MaximumConcurrentFrameTranslations )
{
// 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.MaximumConcurrentFrameTranslations} consecutive frames. Shutting down plugin." );
}
}
else if( currentFrame == _frameForLastQueuedTranslation )
{
// 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
_consecutiveFramesQueued = 0;
}
_frameForLastQueuedTranslation = currentFrame;
}
private void CheckStaggerText( string untranslatedText )
{
if( _previouslyQueuedText != null )
{
if( untranslatedText.StartsWith( _previouslyQueuedText ) )
{
_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 = untranslatedText;
}
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 void AddTranslation( string key, string value )
{
_translations[ key ] = value;
_reverseTranslations[ value ] = key;
}
private void AddTranslation( TranslationKey key, string value )
{
_translations[ key.GetDictionaryLookupKey() ] = value;
_reverseTranslations[ value ] = key.GetDictionaryLookupKey();
}
private void QueueNewUntranslatedForClipboard( TranslationKey key )
{
if( Settings.CopyToClipboard )
{
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 )
{
var lookup = key.GetDictionaryLookupKey();
var result = _translations.TryGetValue( lookup, out value );
if( result )
{
return result;
}
else if( _staticTranslations.Count > 0 )
{
if( _staticTranslations.TryGetValue( lookup, out value ) )
{
QueueNewTranslationForDisk( lookup, value );
AddTranslation( lookup, 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.IsKnownType() ) return null;
if( _hooksEnabled )
{
return TranslateOrQueueWebJob( ui, text, true );
}
return null;
}
public void Hook_TextChanged( object ui )
{
if( _hooksEnabled )
{
TranslateOrQueueWebJob( ui, null, false );
}
}
public void Hook_TextInitialized( object ui )
{
if( _hooksEnabled )
{
TranslateOrQueueWebJob( ui, null, true );
}
}
private void SetTranslatedText( object ui, string translatedText, TranslationInfo info )
{
info?.SetTranslatedText( translatedText );
if( _isInTranslatedMode )
{
SetText( ui, translatedText, true, info );
}
}
///
/// 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, TranslationInfo info )
{
if( !info?.IsCurrentlySettingText ?? true )
{
try
{
// TODO: Disable ANY Hook
_hooksEnabled = false;
if( info != null )
{
info.IsCurrentlySettingText = true;
}
ui.SetText( text );
if( Settings.EnableUIResizing )
{
if( isTranslated )
{
info?.ResizeUI( ui );
}
else
{
info?.UnresizeUI( ui );
}
}
}
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
{
_hooksEnabled = 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 );
}
public bool ShouldTranslate( object ui )
{
var cui = ui as Component;
if( cui != null )
{
var go = cui.gameObject;
var isDummy = go.IsDummy();
if( isDummy )
{
return false;
}
var inputField = cui.gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.InputField )
?? cui.gameObject.GetFirstComponentInSelfOrAncestor( Constants.Types.TMP_InputField );
return inputField == null;
}
return true;
}
private string TranslateOrQueueWebJob( object ui, string text, bool isAwakening )
{
var info = ui.GetTranslationInfo( isAwakening );
if( !info?.IsAwake ?? false )
{
return null;
}
if( _ongoingOperations.Contains( ui ) )
{
return TranslateImmediate( ui, text, info );
}
var supportsStabilization = ui.SupportsStabilization();
if( Settings.Delay == 0 || !supportsStabilization )
{
return TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization );
}
else
{
StartCoroutine(
DelayForSeconds( Settings.Delay, () =>
{
TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization );
} ) );
}
return null;
}
public static bool IsCurrentlySetting( TranslationInfo info )
{
if( info == null ) return false;
return info.IsCurrentlySettingText;
}
private string TranslateImmediate( object ui, string text, TranslationInfo info )
{
// Get the trimmed text
text = ( text ?? ui.GetText() ).TrimIfConfigured();
if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslate( ui ) && !IsCurrentlySetting( info ) )
{
info?.Reset( text );
var textKey = new TranslationKey( 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, TranslationInfo info, bool supportsStabilization, TranslationContext context = null )
{
// Get the trimmed text
text = ( text ?? ui.GetText() ).TrimIfConfigured();
// Ensure that we actually want to translate this text and its owning UI element.
if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslate( ui ) && !IsCurrentlySetting( info ) )
{
info?.Reset( text );
var textKey = new TranslationKey( text, ui.IsSpammingComponent(), 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 ) )
{
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 )
{
translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
if( translation != null )
{
SetTranslatedText( ui, translation, info ); // get rid of textKey here!!
}
return translation;
}
}
}
if( supportsStabilization && context == null ) // never stabilize a text that is contextualized or that does not support stabilization
{
// if we dont know what text to translate it to, we need to figure it out.
// this might take a while, so add the UI text component to the ongoing operations
// 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 );
if( !string.IsNullOrEmpty( stabilizedText ) && IsTranslatable( stabilizedText ) )
{
var stabilizedTextKey = new TranslationKey( stabilizedText, false );
QueueNewUntranslatedForClipboard( stabilizedTextKey );
info?.Reset( stabilizedText );
// 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( _consecutiveErrors < Settings.MaxErrors && !Settings.IsShutdown )
{
var job = GetOrCreateTranslationJobFor( ui, stabilizedTextKey, context );
job.Components.Add( ui );
}
}
else
{
QueueNewUntranslatedForDisk( stabilizedTextKey );
}
}
}
} ) );
}
catch( Exception )
{
_ongoingOperations.Remove( ui );
}
}
else
{
// Lets try not to spam a service that might not be there...
if( _endpoint != null )
{
if( _consecutiveErrors < Settings.MaxErrors && !Settings.IsShutdown )
{
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 ) )
{
var valueKey = new TranslationKey( value, false, true );
string partTranslation;
if( TryGetTranslation( valueKey, out partTranslation ) )
{
translations.Add( key, partTranslation );
}
else if( allowStartJob )
{
// incomplete, must start job
var context = new TranslationContext( ui, result );
TranslateOrQueueWebJobImmediate( null, value, null, false, context );
}
}
else
{
// the value will do
translations.Add( key, value );
}
}
if( result.Arguments.Count == translations.Count )
{
return result.Untemplate( translations );
}
else
{
return null; // could not perform complete translation
}
}
///
/// 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();
if( beforeText == afterText )
{
onTextStabilized( afterText.TrimIfConfigured() );
succeeded = true;
break;
}
currentTries++;
}
if( !succeeded )
{
onMaxTriesExceeded();
}
}
public IEnumerator DelayForSeconds( float delay, Action onContinue )
{
yield return new WaitForSeconds( delay );
onContinue();
}
public void Update()
{
try
{
if( _endpoint != null )
{
_endpoint.OnUpdate();
}
CopyToClipboard();
if( !Settings.IsShutdown )
{
IncrementBatchOperations();
ResetThresholdTimerIfRequired();
KickoffTranslations();
FinishTranslations();
if( _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.D ) )
{
DumpUntranslated();
}
else if( ( Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt ) ) && Input.GetKeyDown( KeyCode.R ) )
{
ReloadTranslations();
}
}
}
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 )
{
Settings.TranslationCount++;
if( !Settings.IsShutdown )
{
if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
}
}
_consecutiveErrors = 0;
var succeeded = batch.MatchWithTranslations( translatedTextBatch );
if( succeeded )
{
foreach( var tracker in batch.Trackers )
{
var job = tracker.Job;
var translatedText = tracker.RawTranslatedText;
if( !string.IsNullOrEmpty( translatedText ) )
{
if( Settings.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 )
{
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." );
}
}
private void OnSingleTranslationCompleted( TranslationJob job, string translatedText )
{
Settings.TranslationCount++;
if( !Settings.IsShutdown )
{
if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
{
Settings.IsShutdown = true;
Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
}
}
_consecutiveErrors = 0;
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() );
}
private void OnTranslationFailed( TranslationJob job )
{
_consecutiveErrors++;
job.State = TranslationJobState.Failed;
_ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
if( !Settings.IsShutdown )
{
if( _consecutiveErrors > Settings.MaxErrors )
{
if( _endpoint.ShouldGetSecondChanceAfterFailure() )
{
Logger.Current.Warn( $"More than {Settings.MaxErrors} consecutive errors occurred. Entering fallback mode." );
_consecutiveErrors = 0;
}
else
{
Settings.IsShutdown = true;
Logger.Current.Error( $"More than {Settings.MaxErrors} consecutive errors occurred. Shutting down plugin." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
}
private void OnTranslationFailed( TranslationBatch batch )
{
_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 )
{
if( _endpoint.ShouldGetSecondChanceAfterFailure() )
{
Logger.Current.Warn( $"More than {Settings.MaxErrors} consecutive errors occurred. Entering fallback mode." );
_consecutiveErrors = 0;
}
else
{
Settings.IsShutdown = true;
Logger.Current.Error( $"More than {Settings.MaxErrors} consecutive errors occurred. Shutting down plugin." );
_unstartedJobs.Clear();
_completedJobs.Clear();
_ongoingJobs.Clear();
}
}
}
}
private void FinishTranslations()
{
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.GetTranslationInfo( false );
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( null, 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.GetTranslationInfo( false );
SetTranslatedText( context.Component, translatedText, info );
}
}
}
}
catch( NullReferenceException )
{
}
}
}
// Utage support
if( Constants.Types.AdvEngine != null
&& job.OriginalSources.Any( x => Constants.Types.AdvCommand.IsAssignableFrom( x.GetType() ) ) )
{
_nextAdvUpdate = Time.time + 0.5f;
}
}
}
}
private void UpdateUtageText()
{
if( _advEngine == null )
{
_advEngine = GameObject.FindObjectOfType( Constants.Types.AdvEngine );
}
if( _advEngine != null )
{
AccessTools.Method( Constants.Types.AdvEngine, "ChangeLanguage" )?.Invoke( _advEngine, new object[ 0 ] );
}
}
private void ReloadTranslations()
{
LoadTranslations();
foreach( var kvp in ObjectExtensions.GetAllRegisteredObjects() )
{
var info = kvp.Value as TranslationInfo;
if( info != null && !string.IsNullOrEmpty( info.OriginalText ) )
{
var key = new TranslationKey( info.OriginalText, false );
if( TryGetTranslation( key, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
{
SetTranslatedText( kvp.Key, translatedText, info ); // no need to untemplatize the translated text
}
}
}
}
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 ToggleTranslation()
{
_isInTranslatedMode = !_isInTranslatedMode;
var objects = ObjectExtensions.GetAllRegisteredObjects();
Logger.Current.Info( $"Toggling translations of {objects.Count} objects." );
if( _isInTranslatedMode )
{
// make sure we use the translated version of all texts
foreach( var kvp in objects )
{
var ui = kvp.Key;
try
{
if( ( ui as Component )?.gameObject?.activeSelf ?? false )
{
var info = (TranslationInfo)kvp.Value;
if( info != null && info.IsTranslated )
{
SetText( ui, info.TranslatedText, true, info );
}
}
}
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 as Component )?.gameObject?.activeSelf ?? false )
{
var info = (TranslationInfo)kvp.Value;
if( info != null && info.IsTranslated )
{
SetText( ui, info.OriginalText, true, info );
}
}
}
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 IEnumerable GetAllRoots()
{
var objects = GameObject.FindObjectsOfType();
foreach( var obj in objects )
{
if( obj.transform.parent == null )
{
yield return obj;
}
}
}
private void TraverseChildren( StreamWriter writer, GameObject obj, string identation )
{
var layer = LayerMask.LayerToName( obj.gameObject.layer );
var components = string.Join( ", ", obj.GetComponents().Select( x => x.GetType().Name ).ToArray() );
var line = string.Format( "{0,-50} {1,100}",
identation + obj.gameObject.name + " [" + layer + "]",
components );
writer.WriteLine( line );
for( int i = 0 ; i < obj.transform.childCount ; i++ )
{
var child = obj.transform.GetChild( i );
TraverseChildren( writer, child.gameObject, identation + " " );
}
}
}
}