AutoTranslationPlugin.cs 96 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Reflection;
  8. using System.Text;
  9. using System.Text.RegularExpressions;
  10. using System.Threading;
  11. using ExIni;
  12. using UnityEngine;
  13. using UnityEngine.UI;
  14. using System.Globalization;
  15. using XUnity.AutoTranslator.Plugin.Core.Extensions;
  16. using UnityEngine.EventSystems;
  17. using XUnity.AutoTranslator.Plugin.Core.Configuration;
  18. using XUnity.AutoTranslator.Plugin.Core.Utilities;
  19. using XUnity.AutoTranslator.Plugin.Core.Web;
  20. using XUnity.AutoTranslator.Plugin.Core.Hooks;
  21. using XUnity.AutoTranslator.Plugin.Core.Hooks.TextMeshPro;
  22. using XUnity.AutoTranslator.Plugin.Core.Hooks.UGUI;
  23. using XUnity.AutoTranslator.Plugin.Core.Hooks.NGUI;
  24. using UnityEngine.SceneManagement;
  25. using XUnity.AutoTranslator.Plugin.Core.Constants;
  26. using XUnity.AutoTranslator.Plugin.Core.Debugging;
  27. using XUnity.AutoTranslator.Plugin.Core.Batching;
  28. using Harmony;
  29. using XUnity.AutoTranslator.Plugin.Core.Parsing;
  30. using System.Diagnostics;
  31. using XUnity.AutoTranslator.Plugin.Core.UI;
  32. using XUnity.AutoTranslator.Plugin.Core.Endpoints;
  33. using XUnity.AutoTranslator.Plugin.Core.Web.Internal;
  34. namespace XUnity.AutoTranslator.Plugin.Core
  35. {
  36. public class AutoTranslationPlugin : MonoBehaviour
  37. {
  38. private static readonly char[][] TranslationSplitters = new char[][] { new char[] { '\t' }, new char[] { '=' } };
  39. /// <summary>
  40. /// Allow the instance to be accessed statically, as only one will exist.
  41. /// </summary>
  42. internal static AutoTranslationPlugin Current;
  43. private XuaWindow _window;
  44. /// <summary>
  45. /// These are the currently running translation jobs (being translated by an http request).
  46. /// </summary>
  47. private List<TranslationJob> _completedJobs = new List<TranslationJob>();
  48. private Dictionary<string, TranslationJob> _unstartedJobs = new Dictionary<string, TranslationJob>();
  49. private Dictionary<string, TranslationJob> _ongoingJobs = new Dictionary<string, TranslationJob>();
  50. /// <summary>
  51. /// All the translations are stored in this dictionary.
  52. /// </summary>
  53. private Dictionary<string, string> _staticTranslations = new Dictionary<string, string>();
  54. private Dictionary<string, string> _translations = new Dictionary<string, string>();
  55. private Dictionary<string, string> _reverseTranslations = new Dictionary<string, string>();
  56. /// <summary>
  57. /// These are the new translations that has not yet been persisted to the file system.
  58. /// </summary>
  59. private object _writeToFileSync = new object();
  60. private Dictionary<string, string> _newTranslations = new Dictionary<string, string>();
  61. private HashSet<string> _newUntranslated = new HashSet<string>();
  62. /// <summary>
  63. /// Keeps track of things to copy to clipboard.
  64. /// </summary>
  65. private List<string> _textsToCopyToClipboardOrdered = new List<string>();
  66. private HashSet<string> _textsToCopyToClipboard = new HashSet<string>();
  67. private float _clipboardUpdated = 0.0f;
  68. /// <summary>
  69. /// The number of http translation errors that has occurred up until now.
  70. /// </summary>
  71. private int _consecutiveErrors = 0;
  72. /// <summary>
  73. /// This is a hash set that contains all Text components that is currently being worked on by
  74. /// the translation plugin.
  75. /// </summary>
  76. private HashSet<object> _ongoingOperations = new HashSet<object>();
  77. /// <summary>
  78. /// This function will check if there are symbols of a given language contained in a string.
  79. /// </summary>
  80. private Func<string, bool> _symbolCheck;
  81. /// <summary>
  82. /// Texts currently being scheduled for translation by 'immediate' components.
  83. /// </summary>
  84. private HashSet<string> _immediatelyTranslating = new HashSet<string>();
  85. private Dictionary<string, byte[]> _translatedImages = new Dictionary<string, byte[]>( StringComparer.InvariantCultureIgnoreCase );
  86. private HashSet<string> _untranslatedImages = new HashSet<string>();
  87. private readonly List<string> _kickedOff = new List<string>();
  88. private Component _advEngine;
  89. private float? _nextAdvUpdate;
  90. private HttpSecurity _httpSecurity;
  91. private List<ConfiguredEndpoint> _configuredEndpoints;
  92. private ConfiguredEndpoint _endpoint;
  93. private int[] _currentTranslationsQueuedPerSecondRollingWindow = new int[ Settings.TranslationQueueWatchWindow ];
  94. private float? _timeExceededThreshold;
  95. private float _translationsQueuedPerSecond;
  96. private bool _isInTranslatedMode = true;
  97. private bool _textHooksEnabled = true;
  98. private bool _imageHooksEnabled = true;
  99. private bool _batchLogicHasFailed = false;
  100. private int _availableBatchOperations = Settings.MaxAvailableBatchOperations;
  101. private float _batchOperationSecondCounter = 0;
  102. private string[] _previouslyQueuedText = new string[ Settings.PreviousTextStaggerCount ];
  103. private int _staggerTextCursor = 0;
  104. private int _concurrentStaggers = 0;
  105. private int _frameForLastQueuedTranslation = -1;
  106. private int _consecutiveFramesTranslated = 0;
  107. private int _secondForQueuedTranslation = -1;
  108. private int _consecutiveSecondsTranslated = 0;
  109. private bool _hasOverrideFont = false;
  110. private bool _overrideFont = false;
  111. private bool _initialized = false;
  112. private bool _temporarilyDisabled = false;
  113. private string _requireSpriteRendererCheckCausedBy = null;
  114. private int _lastSpriteUpdateFrame = -1;
  115. private bool _isCalledFromSceneManager = false;
  116. public void Initialize()
  117. {
  118. Current = this;
  119. if( Logger.Current == null )
  120. {
  121. Logger.Current = new ConsoleLogger();
  122. }
  123. try
  124. {
  125. Settings.Configure();
  126. }
  127. catch( Exception e )
  128. {
  129. Logger.Current.Error( e, "An error occurred during configuration. Shutting plugin down." );
  130. Settings.IsShutdown = true;
  131. Settings.IsShutdownFatal = true;
  132. return;
  133. }
  134. if( Settings.EnableConsole ) DebugConsole.Enable();
  135. HooksSetup.InstallTextHooks();
  136. HooksSetup.InstallImageHooks();
  137. HooksSetup.InstallTextGetterCompatHooks();
  138. _httpSecurity = new HttpSecurity();
  139. try
  140. {
  141. var context = new InitializationContext( Config.Current, _httpSecurity );
  142. _configuredEndpoints = KnownEndpoints.CreateEndpoints( gameObject, context )
  143. .OrderBy( x => x.Error != null )
  144. .ThenBy( x => x.Endpoint.FriendlyName )
  145. .ToList();
  146. }
  147. catch( Exception e )
  148. {
  149. Logger.Current.Error( e, "An error occurred while constructing endpoints. Shutting plugin down." );
  150. Settings.IsShutdown = true;
  151. Settings.IsShutdownFatal = true;
  152. return;
  153. }
  154. try
  155. {
  156. var primaryEndpoint = _configuredEndpoints.FirstOrDefault( x => x.Endpoint.Id == Settings.ServiceEndpoint );
  157. if( primaryEndpoint == null ) throw new Exception( "The primary endpoint was not properly configured." );
  158. if( primaryEndpoint.Error != null ) throw new Exception( "The primary endpoint was not properly configured.", primaryEndpoint.Error );
  159. _endpoint = primaryEndpoint;
  160. }
  161. catch( Exception e )
  162. {
  163. Logger.Current.Error( e, "An unexpected error occurred during initialization of endpoint." );
  164. }
  165. // TODO: Perhaps some bleeding edge check to see if this is required?
  166. var callback = _httpSecurity.GetCertificateValidationCheck();
  167. if( callback != null )
  168. {
  169. ServicePointManager.ServerCertificateValidationCallback += callback;
  170. }
  171. // Save again because configuration may be modified by endpoints
  172. try
  173. {
  174. Config.Current.SaveConfig();
  175. }
  176. catch( Exception e )
  177. {
  178. Logger.Current.Error( e, "An error occurred during while saving configuration." );
  179. }
  180. if( !LanguageHelper.IsFromLanguageSupported( Settings.FromLanguage ) )
  181. {
  182. Logger.Current.Error( $"The plugin has been configured to use the 'FromLanguage={Settings.FromLanguage}'. This language is not supported. Shutting plugin down." );
  183. _endpoint = null;
  184. Settings.IsShutdown = true;
  185. Settings.IsShutdownFatal = true;
  186. }
  187. _symbolCheck = LanguageHelper.GetSymbolCheck( Settings.FromLanguage );
  188. if( !string.IsNullOrEmpty( Settings.OverrideFont ) )
  189. {
  190. var available = Font.GetOSInstalledFontNames();
  191. if( !available.Contains( Settings.OverrideFont ) )
  192. {
  193. Logger.Current.Error( $"The specified override font is not available. Available fonts: " + string.Join( ", ", available ) );
  194. Settings.OverrideFont = null;
  195. }
  196. else
  197. {
  198. _hasOverrideFont = true;
  199. }
  200. _overrideFont = _hasOverrideFont;
  201. }
  202. try
  203. {
  204. EnableSceneLoadScan();
  205. }
  206. catch( Exception e )
  207. {
  208. Logger.Current.Error( e, "An error occurred while settings up texture scene-load scans." );
  209. }
  210. LoadTranslations();
  211. LoadStaticTranslations();
  212. _window = new XuaWindow(
  213. new List<ToggleViewModel>
  214. {
  215. new ToggleViewModel(
  216. " Translated",
  217. "<b>TRANSLATED</b>\nThe plugin currently displays translated texts. Disabling this does not mean the plugin will no longer perform translations, just that they will not be displayed.",
  218. "<b>NOT TRANSLATED</b>\nThe plugin currently displays untranslated texts.",
  219. ToggleTranslation, () => _isInTranslatedMode )
  220. },
  221. _configuredEndpoints.Select( x =>
  222. new TranslatorDropdownOptionViewModel( () => x == _endpoint, x, OnEndpointSelected ) ).ToList(),
  223. new List<ButtonViewModel>
  224. {
  225. new ButtonViewModel( "Reboot", "<b>REBOOT PLUGIN</b>\nReboots the plugin if it has been shutdown. This only works if the plugin was shut down due to consequtive errors towards the translation endpoint.", RebootPlugin, () => Settings.IsShutdown && !Settings.IsShutdownFatal ),
  226. new ButtonViewModel( "Reload", "<b>RELOAD TRANSLATION</b>\nReloads all translation text files and texture files from disk.", ReloadTranslations, null ),
  227. new ButtonViewModel( "Hook", "<b>MANUAL HOOK</b>\nTraverses the unity object tree for looking for anything that can be translated. Performs a translation if something is found.", ManualHook, null )
  228. },
  229. new List<LabelViewModel>
  230. {
  231. new LabelViewModel( "Version: ", () => PluginData.Version ),
  232. new LabelViewModel( "Status: ", () => Settings.IsShutdown ? "Shutdown" : "Running" ),
  233. new LabelViewModel( "Served translations: ", () => $"{Settings.TranslationCount} / {Settings.MaxTranslationsBeforeShutdown}" ),
  234. new LabelViewModel( "Queued translations: ", () => $"{(_unstartedJobs.Count + _ongoingJobs.Count)} / {Settings.MaxUnstartedJobs}" ),
  235. new LabelViewModel( "Error'ed translations: ", () => $"{_consecutiveErrors} / {Settings.MaxErrors}" ),
  236. } );
  237. UnityTextParsers.Initialize( text => IsTranslatable( text ) && IsBelowMaxLength( text ) );
  238. // start a thread that will periodically removed unused references
  239. var t1 = new Thread( MaintenanceLoop );
  240. t1.IsBackground = true;
  241. t1.Start();
  242. // start a thread that will periodically save new translations
  243. var t2 = new Thread( SaveTranslationsLoop );
  244. t2.IsBackground = true;
  245. t2.Start();
  246. }
  247. private void OnEndpointSelected( ConfiguredEndpoint endpoint )
  248. {
  249. _endpoint = endpoint;
  250. if( Settings.IsShutdown && !Settings.IsShutdownFatal )
  251. {
  252. RebootPlugin();
  253. ManualHook();
  254. }
  255. }
  256. private IEnumerable<string> GetTranslationFiles()
  257. {
  258. return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ).Parameterize(), $"*.txt", SearchOption.AllDirectories )
  259. .Select( x => x.Replace( "/", "\\" ) );
  260. }
  261. private IEnumerable<string> GetTextureFiles()
  262. {
  263. return Directory.GetFiles( Path.Combine( Config.Current.DataPath, Settings.TextureDirectory ).Parameterize(), $"*.png", SearchOption.AllDirectories )
  264. .Select( x => x.Replace( "/", "\\" ) );
  265. }
  266. private void MaintenanceLoop( object state )
  267. {
  268. while( true )
  269. {
  270. try
  271. {
  272. ObjectReferenceMapper.Cull();
  273. }
  274. catch( Exception e )
  275. {
  276. Logger.Current.Error( e, "An unexpected error occurred while removing GC'ed resources." );
  277. }
  278. Thread.Sleep( 1000 * 60 );
  279. }
  280. }
  281. private void SaveTranslationsLoop( object state )
  282. {
  283. try
  284. {
  285. while( true )
  286. {
  287. if( _newTranslations.Count > 0 )
  288. {
  289. lock( _writeToFileSync )
  290. {
  291. if( _newTranslations.Count > 0 )
  292. {
  293. using( var stream = File.Open( Settings.AutoTranslationsFilePath, FileMode.Append, FileAccess.Write ) )
  294. using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
  295. {
  296. foreach( var kvp in _newTranslations )
  297. {
  298. writer.WriteLine( TextHelper.Encode( kvp.Key ) + '=' + TextHelper.Encode( kvp.Value ) );
  299. }
  300. writer.Flush();
  301. }
  302. _newTranslations.Clear();
  303. }
  304. }
  305. }
  306. else
  307. {
  308. Thread.Sleep( 5000 );
  309. }
  310. }
  311. }
  312. catch( Exception e )
  313. {
  314. Logger.Current.Error( e, "An error occurred while saving translations to disk." );
  315. }
  316. }
  317. private void EnableSceneLoadScan()
  318. {
  319. Logger.Current.Info( "Probing whether OnLevelWasLoaded or SceneManager is supported in this version of Unity. Any warnings related to OnLevelWasLoaded coming from Unity can safely be ignored." );
  320. if( Features.SupportsScenes )
  321. {
  322. Logger.Current.Info( "SceneManager is supported in this version of Unity." );
  323. EnableSceneLoadScanInternal();
  324. }
  325. else
  326. {
  327. Logger.Current.Info( "SceneManager is not supported in this version of Unity. Falling back to OnLevelWasLoaded and Application level API." );
  328. }
  329. }
  330. private void EnableSceneLoadScanInternal()
  331. {
  332. // do this in a different class to avoid having an anonymous method with references to the "Scene" class
  333. SceneManagerLoader.EnableSceneLoadScanInternal( this );
  334. }
  335. internal void OnLevelWasLoadedFromSceneManager( int id )
  336. {
  337. try
  338. {
  339. _isCalledFromSceneManager = true;
  340. OnLevelWasLoaded( id );
  341. }
  342. finally
  343. {
  344. _isCalledFromSceneManager = false;
  345. }
  346. }
  347. private void OnLevelWasLoaded( int id )
  348. {
  349. if( !Features.SupportsScenes || ( Features.SupportsScenes && _isCalledFromSceneManager ) )
  350. {
  351. if( Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureDumping || Settings.EnableTextureTranslation ) )
  352. {
  353. Logger.Current.Info( "Performing texture lookup during scene load..." );
  354. var startTime = Time.realtimeSinceStartup;
  355. ManualHookForTextures();
  356. var endTime = Time.realtimeSinceStartup;
  357. Logger.Current.Info( $"Finished texture lookup (took {Math.Round( endTime - startTime, 2 )} seconds)" );
  358. }
  359. }
  360. }
  361. /// <summary>
  362. /// Loads the translations found in Translation.{lang}.txt
  363. /// </summary>
  364. private void LoadTranslations()
  365. {
  366. try
  367. {
  368. lock( _writeToFileSync )
  369. {
  370. Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TranslationDirectory ).Parameterize() );
  371. Directory.CreateDirectory( Path.GetDirectoryName( Settings.AutoTranslationsFilePath ) );
  372. var mainTranslationFile = Settings.AutoTranslationsFilePath;
  373. LoadTranslationsInFile( mainTranslationFile );
  374. foreach( var fullFileName in GetTranslationFiles().Reverse().Except( new[] { mainTranslationFile } ) )
  375. {
  376. LoadTranslationsInFile( fullFileName );
  377. }
  378. }
  379. if( Settings.EnableTextureTranslation || Settings.EnableTextureDumping )
  380. {
  381. _translatedImages.Clear();
  382. _untranslatedImages.Clear();
  383. Directory.CreateDirectory( Path.Combine( Config.Current.DataPath, Settings.TextureDirectory ).Parameterize() );
  384. foreach( var fullFileName in GetTextureFiles() )
  385. {
  386. RegisterImageFromFile( fullFileName );
  387. }
  388. }
  389. }
  390. catch( Exception e )
  391. {
  392. Logger.Current.Error( e, "An error occurred while loading translations." );
  393. }
  394. }
  395. private void RegisterImageFromFile( string fullFileName )
  396. {
  397. var fileName = Path.GetFileNameWithoutExtension( fullFileName );
  398. var startHash = fileName.LastIndexOf( "[" );
  399. var endHash = fileName.LastIndexOf( "]" );
  400. if( endHash > -1 && startHash > -1 && endHash > startHash )
  401. {
  402. var takeFrom = startHash + 1;
  403. // load based on whether or not the key is image hashed
  404. var parts = fileName.Substring( takeFrom, endHash - takeFrom ).Split( '-' );
  405. string key;
  406. string originalHash;
  407. if( parts.Length == 1 )
  408. {
  409. key = parts[ 0 ];
  410. originalHash = parts[ 0 ];
  411. }
  412. else if( parts.Length == 2 )
  413. {
  414. key = parts[ 0 ];
  415. originalHash = parts[ 1 ];
  416. }
  417. else
  418. {
  419. Logger.Current.Warn( $"Image not loaded (unknown hash): {fullFileName}." );
  420. return;
  421. }
  422. var data = File.ReadAllBytes( fullFileName );
  423. var currentHash = HashHelper.Compute( data );
  424. var isModified = StringComparer.InvariantCultureIgnoreCase.Compare( originalHash, currentHash ) != 0;
  425. // only load images that someone has modified!
  426. if( Settings.LoadUnmodifiedTextures || isModified )
  427. {
  428. RegisterTranslatedImage( key, data );
  429. Logger.Current.Debug( $"Image loaded: {fullFileName}." );
  430. }
  431. else
  432. {
  433. RegisterUntranslatedImage( key );
  434. Logger.Current.Warn( $"Image not loaded (unmodified): {fullFileName}." );
  435. }
  436. //if( Settings.DeleteUnmodifiedTextures && !isModified )
  437. //{
  438. // try
  439. // {
  440. // File.Delete( fullFileName );
  441. // Logger.Current.Warn( $"Image deleted (unmodified): {fullFileName}." );
  442. // }
  443. // catch( Exception e )
  444. // {
  445. // Logger.Current.Warn( e, $"An error occurred while trying to delete unmodified image: {fullFileName}." );
  446. // }
  447. //}
  448. }
  449. else
  450. {
  451. Logger.Current.Warn( $"Image not loaded (no hash): {fullFileName}." );
  452. }
  453. }
  454. private void RegisterImageFromData( string textureName, string key, byte[] data )
  455. {
  456. var name = textureName.SanitizeForFileSystem();
  457. var root = Path.Combine( Config.Current.DataPath, Settings.TextureDirectory ).Parameterize();
  458. var originalHash = HashHelper.Compute( data );
  459. // allow hash and key to be the same; only store one of them then!
  460. string fileName;
  461. if( key == originalHash )
  462. {
  463. fileName = name + " [" + key + "].png";
  464. }
  465. else
  466. {
  467. fileName = name + " [" + key + "-" + originalHash + "].png";
  468. }
  469. var fullName = Path.Combine( root, fileName );
  470. File.WriteAllBytes( fullName, data );
  471. Logger.Current.Info( "Dumped texture file: " + fileName );
  472. if( Settings.LoadUnmodifiedTextures )
  473. {
  474. RegisterTranslatedImage( key, data );
  475. }
  476. else
  477. {
  478. RegisterUntranslatedImage( key );
  479. }
  480. }
  481. private void RegisterTranslatedImage( string key, byte[] data )
  482. {
  483. _translatedImages[ key ] = data;
  484. }
  485. private void RegisterUntranslatedImage( string key )
  486. {
  487. _untranslatedImages.Add( key );
  488. }
  489. private void LoadTranslationsInFile( string fullFileName )
  490. {
  491. if( File.Exists( fullFileName ) )
  492. {
  493. Logger.Current.Debug( $"Loading texts: {fullFileName}." );
  494. string[] translations = File.ReadAllLines( fullFileName, Encoding.UTF8 );
  495. foreach( string translation in translations )
  496. {
  497. for( int i = 0 ; i < TranslationSplitters.Length ; i++ )
  498. {
  499. var splitter = TranslationSplitters[ i ];
  500. string[] kvp = translation.Split( splitter, StringSplitOptions.None );
  501. if( kvp.Length == 2 )
  502. {
  503. string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
  504. string value = TextHelper.Decode( kvp[ 1 ] );
  505. if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) && IsTranslatable( key ) )
  506. {
  507. AddTranslation( key, value );
  508. break;
  509. }
  510. }
  511. }
  512. }
  513. }
  514. }
  515. private void LoadStaticTranslations()
  516. {
  517. if( Settings.UseStaticTranslations && Settings.FromLanguage == Settings.DefaultFromLanguage && Settings.Language == Settings.DefaultLanguage )
  518. {
  519. var tab = new char[] { '\t' };
  520. var equals = new char[] { '=' };
  521. var splitters = new char[][] { tab, equals };
  522. // load static translations from previous titles
  523. string[] translations = Properties.Resources.StaticTranslations.Split( new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries );
  524. foreach( string translation in translations )
  525. {
  526. for( int i = 0 ; i < splitters.Length ; i++ )
  527. {
  528. var splitter = splitters[ i ];
  529. string[] kvp = translation.Split( splitter, StringSplitOptions.None );
  530. if( kvp.Length >= 2 )
  531. {
  532. string key = TextHelper.Decode( kvp[ 0 ].TrimIfConfigured() );
  533. string value = TextHelper.Decode( kvp[ 1 ].TrimIfConfigured() );
  534. if( !string.IsNullOrEmpty( key ) && !string.IsNullOrEmpty( value ) )
  535. {
  536. _staticTranslations[ key ] = value;
  537. break;
  538. }
  539. }
  540. }
  541. }
  542. }
  543. }
  544. private TranslationJob GetOrCreateTranslationJobFor( object ui, TranslationKey key, TranslationContext context )
  545. {
  546. var lookupKey = key.GetDictionaryLookupKey();
  547. if( _unstartedJobs.TryGetValue( lookupKey, out TranslationJob unstartedJob ) )
  548. {
  549. unstartedJob.Associate( context );
  550. return unstartedJob;
  551. }
  552. if( _ongoingJobs.TryGetValue( lookupKey, out TranslationJob ongoingJob ) )
  553. {
  554. ongoingJob.Associate( context );
  555. return ongoingJob;
  556. }
  557. foreach( var completedJob in _completedJobs )
  558. {
  559. if( completedJob.Key.GetDictionaryLookupKey() == lookupKey )
  560. {
  561. completedJob.Associate( context );
  562. return completedJob;
  563. }
  564. }
  565. Logger.Current.Debug( "Queued translation for: " + lookupKey );
  566. ongoingJob = new TranslationJob( key );
  567. if( ui != null )
  568. {
  569. ongoingJob.OriginalSources.Add( ui );
  570. }
  571. ongoingJob.Associate( context );
  572. _unstartedJobs.Add( lookupKey, ongoingJob );
  573. CheckStaggerText( lookupKey );
  574. CheckConsecutiveFrames();
  575. CheckConsecutiveSeconds();
  576. CheckThresholds();
  577. return ongoingJob;
  578. }
  579. private void CheckConsecutiveSeconds()
  580. {
  581. var currentSecond = (int)Time.time;
  582. var lastSecond = currentSecond - 1;
  583. if( lastSecond == _secondForQueuedTranslation )
  584. {
  585. // we also queued something last frame, lets increment our counter
  586. _consecutiveSecondsTranslated++;
  587. if( _consecutiveSecondsTranslated > Settings.MaximumConsecutiveSecondsTranslated )
  588. {
  589. // Shutdown, this wont be tolerated!!!
  590. _unstartedJobs.Clear();
  591. _completedJobs.Clear();
  592. _ongoingJobs.Clear();
  593. Settings.IsShutdown = true;
  594. Settings.IsShutdownFatal = true;
  595. Logger.Current.Error( $"SPAM DETECTED: Translations were queued every second for more than {Settings.MaximumConsecutiveSecondsTranslated} consecutive seconds. Shutting down plugin." );
  596. }
  597. }
  598. else if( currentSecond == _secondForQueuedTranslation )
  599. {
  600. // do nothing, there may be multiple translations per frame, that wont increase this counter
  601. }
  602. else
  603. {
  604. // but if multiple Update frames has passed, we will reset the counter
  605. _consecutiveSecondsTranslated = 0;
  606. }
  607. _secondForQueuedTranslation = currentSecond;
  608. }
  609. private void CheckConsecutiveFrames()
  610. {
  611. var currentFrame = Time.frameCount;
  612. var lastFrame = currentFrame - 1;
  613. if( lastFrame == _frameForLastQueuedTranslation )
  614. {
  615. // we also queued something last frame, lets increment our counter
  616. _consecutiveFramesTranslated++;
  617. if( _consecutiveFramesTranslated > Settings.MaximumConsecutiveFramesTranslated )
  618. {
  619. // Shutdown, this wont be tolerated!!!
  620. _unstartedJobs.Clear();
  621. _completedJobs.Clear();
  622. _ongoingJobs.Clear();
  623. Settings.IsShutdown = true;
  624. Settings.IsShutdownFatal = true;
  625. Logger.Current.Error( $"SPAM DETECTED: Translations were queued every frame for more than {Settings.MaximumConsecutiveFramesTranslated} consecutive frames. Shutting down plugin." );
  626. }
  627. }
  628. else if( currentFrame == _frameForLastQueuedTranslation )
  629. {
  630. // do nothing, there may be multiple translations per frame, that wont increase this counter
  631. }
  632. else if( _consecutiveFramesTranslated > 0 )
  633. {
  634. // but if multiple Update frames has passed, we will reset the counter
  635. _consecutiveFramesTranslated--;
  636. }
  637. _frameForLastQueuedTranslation = currentFrame;
  638. }
  639. private void PeriodicResetFrameCheck()
  640. {
  641. var currentSecond = (int)Time.time;
  642. if( currentSecond % 100 == 0 )
  643. {
  644. _consecutiveFramesTranslated = 0;
  645. }
  646. }
  647. private void CheckStaggerText( string untranslatedText )
  648. {
  649. bool wasProblematic = false;
  650. for( int i = 0 ; i < _previouslyQueuedText.Length ; i++ )
  651. {
  652. var previouslyQueuedText = _previouslyQueuedText[ i ];
  653. if( previouslyQueuedText != null )
  654. {
  655. if( untranslatedText.RemindsOf( previouslyQueuedText ) )
  656. {
  657. wasProblematic = true;
  658. break;
  659. }
  660. }
  661. }
  662. if( wasProblematic )
  663. {
  664. _concurrentStaggers++;
  665. if( _concurrentStaggers > Settings.MaximumStaggers )
  666. {
  667. _unstartedJobs.Clear();
  668. _completedJobs.Clear();
  669. _ongoingJobs.Clear();
  670. Settings.IsShutdown = true;
  671. Settings.IsShutdownFatal = true;
  672. Logger.Current.Error( $"SPAM DETECTED: Text that is 'scrolling in' is being translated. Disable that feature. Shutting down plugin." );
  673. }
  674. }
  675. else
  676. {
  677. _concurrentStaggers = 0;
  678. }
  679. _previouslyQueuedText[ _staggerTextCursor % _previouslyQueuedText.Length ] = untranslatedText;
  680. _staggerTextCursor++;
  681. }
  682. private void CheckThresholds()
  683. {
  684. if( _unstartedJobs.Count > Settings.MaxUnstartedJobs )
  685. {
  686. _unstartedJobs.Clear();
  687. _completedJobs.Clear();
  688. _ongoingJobs.Clear();
  689. Settings.IsShutdown = true;
  690. Settings.IsShutdownFatal = true;
  691. Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxUnstartedJobs} queued for translations due to unknown reasons. Shutting down plugin." );
  692. }
  693. var previousIdx = ( (int)( Time.time - Time.deltaTime ) ) % Settings.TranslationQueueWatchWindow;
  694. var newIdx = ( (int)Time.time ) % Settings.TranslationQueueWatchWindow;
  695. if( previousIdx != newIdx )
  696. {
  697. _currentTranslationsQueuedPerSecondRollingWindow[ newIdx ] = 0;
  698. }
  699. _currentTranslationsQueuedPerSecondRollingWindow[ newIdx ]++;
  700. var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
  701. _translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
  702. if( _translationsQueuedPerSecond > Settings.MaxTranslationsQueuedPerSecond )
  703. {
  704. if( !_timeExceededThreshold.HasValue )
  705. {
  706. _timeExceededThreshold = Time.time;
  707. }
  708. if( Time.time - _timeExceededThreshold.Value > Settings.MaxSecondsAboveTranslationThreshold )
  709. {
  710. _unstartedJobs.Clear();
  711. _completedJobs.Clear();
  712. _ongoingJobs.Clear();
  713. Settings.IsShutdown = true;
  714. Settings.IsShutdownFatal = true;
  715. Logger.Current.Error( $"SPAM DETECTED: More than {Settings.MaxTranslationsQueuedPerSecond} translations per seconds queued for a {Settings.MaxSecondsAboveTranslationThreshold} second period. Shutting down plugin." );
  716. }
  717. }
  718. else
  719. {
  720. _timeExceededThreshold = null;
  721. }
  722. }
  723. private void IncrementBatchOperations()
  724. {
  725. _batchOperationSecondCounter += Time.deltaTime;
  726. if( _batchOperationSecondCounter > Settings.IncreaseBatchOperationsEvery )
  727. {
  728. if( _availableBatchOperations < Settings.MaxAvailableBatchOperations )
  729. {
  730. _availableBatchOperations++;
  731. }
  732. _batchOperationSecondCounter = 0;
  733. }
  734. }
  735. private void ResetThresholdTimerIfRequired()
  736. {
  737. var previousIdx = ( (int)( Time.time - Time.deltaTime ) ) % Settings.TranslationQueueWatchWindow;
  738. var newIdx = ( (int)Time.time ) % Settings.TranslationQueueWatchWindow;
  739. if( previousIdx != newIdx )
  740. {
  741. _currentTranslationsQueuedPerSecondRollingWindow[ newIdx ] = 0;
  742. }
  743. var translationsInWindow = _currentTranslationsQueuedPerSecondRollingWindow.Sum();
  744. _translationsQueuedPerSecond = (float)translationsInWindow / Settings.TranslationQueueWatchWindow;
  745. if( _translationsQueuedPerSecond <= Settings.MaxTranslationsQueuedPerSecond )
  746. {
  747. _timeExceededThreshold = null;
  748. }
  749. }
  750. private bool IsImageRegistered( string key )
  751. {
  752. return _translatedImages.ContainsKey( key ) || _untranslatedImages.Contains( key );
  753. }
  754. private bool TryGetTranslatedImage( string key, out byte[] data )
  755. {
  756. return _translatedImages.TryGetValue( key, out data );
  757. }
  758. private void AddTranslation( string key, string value )
  759. {
  760. _translations[ key ] = value;
  761. _reverseTranslations[ value ] = key;
  762. }
  763. private void AddTranslation( TranslationKey key, string value )
  764. {
  765. var lookup = key.GetDictionaryLookupKey();
  766. _translations[ lookup ] = value;
  767. _reverseTranslations[ value ] = lookup;
  768. }
  769. private void UpdateSpriteRenderers()
  770. {
  771. if( Settings.EnableSpriteRendererHooking && ( Settings.EnableTextureTranslation || Settings.EnableTextureDumping ) )
  772. {
  773. if( _requireSpriteRendererCheckCausedBy != null )
  774. {
  775. try
  776. {
  777. var start = Time.realtimeSinceStartup;
  778. var spriteRenderers = GameObject.FindObjectsOfType<SpriteRenderer>();
  779. foreach( var sr in spriteRenderers )
  780. {
  781. // simulate a hook
  782. Hook_ImageChangedOnComponent( sr, null, false, false );
  783. }
  784. var end = Time.realtimeSinceStartup;
  785. var delta = Math.Round( end - start, 2 );
  786. Logger.Current.Debug( $"Update SpriteRenderers caused by {_requireSpriteRendererCheckCausedBy} component (took " + delta + " seconds)" );
  787. }
  788. finally
  789. {
  790. _requireSpriteRendererCheckCausedBy = null;
  791. }
  792. }
  793. }
  794. }
  795. private void QueueNewUntranslatedForClipboard( TranslationKey key )
  796. {
  797. if( Settings.CopyToClipboard && Features.SupportsClipboard )
  798. {
  799. if( !_textsToCopyToClipboard.Contains( key.RelevantText ) )
  800. {
  801. _textsToCopyToClipboard.Add( key.RelevantText );
  802. _textsToCopyToClipboardOrdered.Add( key.RelevantText );
  803. _clipboardUpdated = Time.realtimeSinceStartup;
  804. }
  805. }
  806. }
  807. private void QueueNewUntranslatedForDisk( TranslationKey key )
  808. {
  809. _newUntranslated.Add( key.GetDictionaryLookupKey() );
  810. }
  811. private void QueueNewTranslationForDisk( TranslationKey key, string value )
  812. {
  813. lock( _writeToFileSync )
  814. {
  815. _newTranslations[ key.GetDictionaryLookupKey() ] = value;
  816. }
  817. }
  818. private void QueueNewTranslationForDisk( string key, string value )
  819. {
  820. lock( _writeToFileSync )
  821. {
  822. _newTranslations[ key ] = value;
  823. }
  824. }
  825. private bool TryGetTranslation( TranslationKey key, out string value )
  826. {
  827. return TryGetTranslation( key.GetDictionaryLookupKey(), out value );
  828. }
  829. private bool TryGetTranslation( string key, out string value )
  830. {
  831. var result = _translations.TryGetValue( key, out value );
  832. if( result )
  833. {
  834. return result;
  835. }
  836. else if( _staticTranslations.Count > 0 )
  837. {
  838. if( _staticTranslations.TryGetValue( key, out value ) )
  839. {
  840. QueueNewTranslationForDisk( key, value );
  841. AddTranslation( key, value );
  842. return true;
  843. }
  844. }
  845. return result;
  846. }
  847. internal bool TryGetReverseTranslation( string value, out string key )
  848. {
  849. return _reverseTranslations.TryGetValue( value, out key );
  850. }
  851. internal string Hook_TextChanged_WithResult( object ui, string text )
  852. {
  853. if( !ui.IsKnownTextType() ) return null;
  854. if( _textHooksEnabled && !_temporarilyDisabled )
  855. {
  856. return TranslateOrQueueWebJob( ui, text, false );
  857. }
  858. return null;
  859. }
  860. internal string ExternalHook_TextChanged_WithResult( object ui, string text )
  861. {
  862. if( !ui.IsKnownTextType() ) return null;
  863. if( _textHooksEnabled && !_temporarilyDisabled )
  864. {
  865. return TranslateOrQueueWebJob( ui, text, true );
  866. }
  867. return null;
  868. }
  869. internal void Hook_TextChanged( object ui, bool onEnable )
  870. {
  871. if( _textHooksEnabled && !_temporarilyDisabled )
  872. {
  873. TranslateOrQueueWebJob( ui, null, false );
  874. }
  875. if( onEnable )
  876. {
  877. CheckSpriteRenderer( ui );
  878. }
  879. }
  880. internal void Hook_ImageChangedOnComponent( object source, Texture2D texture, bool isPrefixHooked, bool onEnable )
  881. {
  882. if( !_imageHooksEnabled ) return;
  883. if( !source.IsKnownImageType() ) return;
  884. HandleImage( source, texture, isPrefixHooked );
  885. if( onEnable )
  886. {
  887. CheckSpriteRenderer( source );
  888. }
  889. }
  890. internal void Hook_ImageChanged( Texture2D texture, bool isPrefixHooked )
  891. {
  892. if( !_imageHooksEnabled ) return;
  893. if( texture == null ) return;
  894. HandleImage( null, texture, isPrefixHooked );
  895. }
  896. private void SetTranslatedText( object ui, string translatedText, TextTranslationInfo info )
  897. {
  898. info?.SetTranslatedText( translatedText );
  899. if( _isInTranslatedMode )
  900. {
  901. SetText( ui, translatedText, true, info );
  902. }
  903. }
  904. internal void Hook_HandleComponent( object ui )
  905. {
  906. if( _hasOverrideFont )
  907. {
  908. var info = ui.GetOrCreateTextTranslationInfo();
  909. if( _overrideFont )
  910. {
  911. info?.ChangeFont( ui );
  912. }
  913. else
  914. {
  915. info?.UnchangeFont( ui );
  916. }
  917. }
  918. if( Settings.ForceUIResizing )
  919. {
  920. var info = ui.GetOrCreateTextTranslationInfo();
  921. if( info?.IsCurrentlySettingText == false )
  922. {
  923. // force UI resizing is highly problematic for NGUI because text should somehow
  924. // be set after changing "resize" properties... brilliant stuff
  925. if( ui.GetType() != ClrTypes.UILabel )
  926. {
  927. info?.ResizeUI( ui );
  928. }
  929. }
  930. }
  931. }
  932. private void CheckSpriteRenderer( object ui )
  933. {
  934. if( Settings.EnableSpriteRendererHooking )
  935. {
  936. var currentFrame = Time.frameCount;
  937. var lastFrame = currentFrame - 1;
  938. if( lastFrame != _lastSpriteUpdateFrame && currentFrame != _lastSpriteUpdateFrame )
  939. {
  940. _requireSpriteRendererCheckCausedBy = ui?.GetType().Name;
  941. }
  942. _lastSpriteUpdateFrame = currentFrame;
  943. }
  944. }
  945. /// <summary>
  946. /// Sets the text of a UI text, while ensuring this will not fire a text changed event.
  947. /// </summary>
  948. private void SetText( object ui, string text, bool isTranslated, TextTranslationInfo info )
  949. {
  950. if( !info?.IsCurrentlySettingText ?? true )
  951. {
  952. try
  953. {
  954. _textHooksEnabled = false;
  955. if( info != null )
  956. {
  957. info.IsCurrentlySettingText = true;
  958. }
  959. if( Settings.EnableUIResizing || Settings.ForceUIResizing )
  960. {
  961. if( isTranslated || Settings.ForceUIResizing )
  962. {
  963. info?.ResizeUI( ui );
  964. }
  965. else
  966. {
  967. info?.UnresizeUI( ui );
  968. }
  969. }
  970. // NGUI only behaves if you set the text after the resize behaviour
  971. ui.SetText( text );
  972. info?.ResetScrollIn( ui );
  973. }
  974. catch( TargetInvocationException )
  975. {
  976. // might happen with NGUI
  977. }
  978. catch( NullReferenceException )
  979. {
  980. // This is likely happened due to a scene change.
  981. }
  982. catch( Exception e )
  983. {
  984. Logger.Current.Error( e, "An error occurred while setting text on a component." );
  985. }
  986. finally
  987. {
  988. _textHooksEnabled = true;
  989. if( info != null )
  990. {
  991. info.IsCurrentlySettingText = false;
  992. }
  993. }
  994. }
  995. }
  996. /// <summary>
  997. /// Determines if a text should be translated.
  998. /// </summary>
  999. private bool IsTranslatable( string str )
  1000. {
  1001. return _symbolCheck( str )
  1002. //&& str.Length <= Settings.MaxCharactersPerTranslation
  1003. && !_reverseTranslations.ContainsKey( str )
  1004. && !Settings.IgnoreTextStartingWith.Any( x => str.StartsWithStrict( x ) );
  1005. }
  1006. private bool IsBelowMaxLength( string str )
  1007. {
  1008. return str.Length <= Settings.MaxCharactersPerTranslation;
  1009. }
  1010. private bool IsBelowMaxLengthStrict( string str )
  1011. {
  1012. return str.Length <= ( Settings.MaxCharactersPerTranslation / 2 );
  1013. }
  1014. private bool ShouldTranslateImageComponent( object ui )
  1015. {
  1016. var component = ui as Component;
  1017. if( component != null )
  1018. {
  1019. // dummy check
  1020. var go = component.gameObject;
  1021. var ignore = go.HasIgnoredName();
  1022. if( ignore )
  1023. {
  1024. return false;
  1025. }
  1026. var behaviour = component as Behaviour;
  1027. if( behaviour?.isActiveAndEnabled == false )
  1028. {
  1029. return false;
  1030. }
  1031. }
  1032. return true;
  1033. }
  1034. private bool ShouldTranslateTextComponent( object ui, bool ignoreComponentState )
  1035. {
  1036. var component = ui as Component;
  1037. if( component != null )
  1038. {
  1039. // dummy check
  1040. var go = component.gameObject;
  1041. var ignore = go.HasIgnoredName();
  1042. if( ignore )
  1043. {
  1044. return false;
  1045. }
  1046. if( !ignoreComponentState )
  1047. {
  1048. var behaviour = component as Behaviour;
  1049. if( behaviour?.isActiveAndEnabled == false )
  1050. {
  1051. return false;
  1052. }
  1053. }
  1054. var inputField = component.gameObject.GetFirstComponentInSelfOrAncestor( ClrTypes.InputField )
  1055. ?? component.gameObject.GetFirstComponentInSelfOrAncestor( ClrTypes.TMP_InputField );
  1056. return inputField == null;
  1057. }
  1058. return true;
  1059. }
  1060. private string TranslateOrQueueWebJob( object ui, string text, bool ignoreComponentState )
  1061. {
  1062. var info = ui.GetOrCreateTextTranslationInfo();
  1063. if( _ongoingOperations.Contains( ui ) )
  1064. {
  1065. return TranslateImmediate( ui, text, info, ignoreComponentState );
  1066. }
  1067. var supportsStabilization = ui.SupportsStabilization();
  1068. if( Settings.Delay == 0 || !supportsStabilization )
  1069. {
  1070. return TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization, ignoreComponentState );
  1071. }
  1072. else
  1073. {
  1074. StartCoroutine(
  1075. DelayForSeconds( Settings.Delay, () =>
  1076. {
  1077. TranslateOrQueueWebJobImmediate( ui, text, info, supportsStabilization, ignoreComponentState );
  1078. } ) );
  1079. }
  1080. return null;
  1081. }
  1082. private static bool IsCurrentlySetting( TextTranslationInfo info )
  1083. {
  1084. if( info == null ) return false;
  1085. return info.IsCurrentlySettingText;
  1086. }
  1087. private void HandleImage( object source, Texture2D texture, bool isPrefixHooked )
  1088. {
  1089. if( Settings.EnableTextureDumping )
  1090. {
  1091. try
  1092. {
  1093. DumpTexture( source, texture );
  1094. }
  1095. catch( Exception e )
  1096. {
  1097. Logger.Current.Error( e, "An error occurred while dumping texture." );
  1098. }
  1099. }
  1100. if( Settings.EnableTextureTranslation )
  1101. {
  1102. try
  1103. {
  1104. TranslateTexture( source, texture, isPrefixHooked, null );
  1105. }
  1106. catch( Exception e )
  1107. {
  1108. Logger.Current.Error( e, "An error occurred while translating texture." );
  1109. }
  1110. }
  1111. }
  1112. private void TranslateTexture( object ui, TextureReloadContext context )
  1113. {
  1114. if( ui is Texture2D texture2d )
  1115. {
  1116. TranslateTexture( null, texture2d, false, context );
  1117. }
  1118. else
  1119. {
  1120. TranslateTexture( ui, null, false, context );
  1121. }
  1122. }
  1123. private void TranslateTexture( object source, Texture2D texture, bool isPrefixHooked, TextureReloadContext context )
  1124. {
  1125. try
  1126. {
  1127. _imageHooksEnabled = false;
  1128. texture = texture ?? source.GetTexture();
  1129. if( texture == null ) return;
  1130. var tti = texture.GetOrCreateTextureTranslationInfo();
  1131. var iti = source.GetOrCreateImageTranslationInfo();
  1132. var key = tti.GetKey( texture );
  1133. if( string.IsNullOrEmpty( key ) ) return;
  1134. bool hasContext = context != null;
  1135. bool forceReload = false;
  1136. if( hasContext )
  1137. {
  1138. forceReload = context.RegisterTextureInContextAndDetermineWhetherToReload( texture );
  1139. }
  1140. if( TryGetTranslatedImage( key, out var newData ) )
  1141. {
  1142. if( _isInTranslatedMode )
  1143. {
  1144. // handle texture
  1145. if( !tti.IsTranslated || forceReload )
  1146. {
  1147. try
  1148. {
  1149. texture.LoadImageEx( newData, tti.IsNonReadable( texture ) );
  1150. }
  1151. finally
  1152. {
  1153. tti.IsTranslated = true;
  1154. }
  1155. }
  1156. // handle containing component
  1157. if( iti != null )
  1158. {
  1159. if( !iti.IsTranslated || hasContext )
  1160. {
  1161. try
  1162. {
  1163. if( !isPrefixHooked )
  1164. {
  1165. source.SetAllDirtyEx();
  1166. }
  1167. }
  1168. finally
  1169. {
  1170. iti.IsTranslated = true;
  1171. }
  1172. }
  1173. }
  1174. }
  1175. }
  1176. else
  1177. {
  1178. // if we cannot find the texture, and the texture is considered translated... hmmm someone has removed a file
  1179. // handle texture
  1180. var originalData = tti.GetOriginalData( texture );
  1181. if( originalData != null )
  1182. {
  1183. if( tti.IsTranslated )
  1184. {
  1185. try
  1186. {
  1187. texture.LoadImageEx( originalData, tti.IsNonReadable( texture ) );
  1188. }
  1189. finally
  1190. {
  1191. tti.IsTranslated = true;
  1192. }
  1193. }
  1194. // handle containing component
  1195. if( iti != null )
  1196. {
  1197. if( iti.IsTranslated )
  1198. {
  1199. try
  1200. {
  1201. if( !isPrefixHooked )
  1202. {
  1203. source.SetAllDirtyEx();
  1204. }
  1205. }
  1206. finally
  1207. {
  1208. iti.IsTranslated = true;
  1209. }
  1210. }
  1211. }
  1212. }
  1213. }
  1214. if( !_isInTranslatedMode )
  1215. {
  1216. var originalData = tti.GetOriginalData( texture );
  1217. if( originalData != null )
  1218. {
  1219. // handle texture
  1220. if( tti.IsTranslated )
  1221. {
  1222. try
  1223. {
  1224. texture.LoadImageEx( originalData, tti.IsNonReadable( texture ) );
  1225. }
  1226. finally
  1227. {
  1228. tti.IsTranslated = false;
  1229. }
  1230. }
  1231. // handle containing component
  1232. if( iti != null )
  1233. {
  1234. if( iti.IsTranslated )
  1235. {
  1236. try
  1237. {
  1238. if( !isPrefixHooked )
  1239. {
  1240. source.SetAllDirtyEx();
  1241. }
  1242. }
  1243. finally
  1244. {
  1245. iti.IsTranslated = false;
  1246. }
  1247. }
  1248. }
  1249. }
  1250. }
  1251. if( forceReload )
  1252. {
  1253. Logger.Current.Info( $"Reloaded texture: {texture.name} ({key})." );
  1254. }
  1255. }
  1256. finally
  1257. {
  1258. _imageHooksEnabled = true;
  1259. }
  1260. }
  1261. private void DumpTexture( object source, Texture2D texture )
  1262. {
  1263. try
  1264. {
  1265. _imageHooksEnabled = false;
  1266. texture = texture ?? source.GetTexture();
  1267. if( texture == null ) return;
  1268. var info = texture.GetOrCreateTextureTranslationInfo();
  1269. if( info.HasDumpedAlternativeTexture ) return;
  1270. try
  1271. {
  1272. if( ShouldTranslate( texture ) )
  1273. {
  1274. var key = info.GetKey( texture );
  1275. if( string.IsNullOrEmpty( key ) ) return;
  1276. if( !IsImageRegistered( key ) )
  1277. {
  1278. var name = texture.GetTextureName();
  1279. //var format = "[" + texture.format.ToString() + "] ";
  1280. var originalData = info.GetOrCreateOriginalData( texture );
  1281. RegisterImageFromData( name, key, originalData );
  1282. }
  1283. }
  1284. }
  1285. finally
  1286. {
  1287. info.HasDumpedAlternativeTexture = true;
  1288. }
  1289. }
  1290. finally
  1291. {
  1292. _imageHooksEnabled = true;
  1293. }
  1294. }
  1295. private bool ShouldTranslate( Texture2D texture )
  1296. {
  1297. // convert to int so engine versions that does not have specific enums still work
  1298. var format = (int)texture.format;
  1299. // 1 = Alpha8
  1300. // 9 = R16
  1301. // 63 = R8
  1302. return format != 1
  1303. && format != 9
  1304. && format != 63;
  1305. }
  1306. private string TranslateImmediate( object ui, string text, TextTranslationInfo info, bool ignoreComponentState )
  1307. {
  1308. // Get the trimmed text
  1309. text = ( text ?? ui.GetText() ).TrimIfConfigured();
  1310. if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
  1311. {
  1312. info?.Reset( text );
  1313. var textKey = new TranslationKey( ui, text, ui.IsSpammingComponent(), false );
  1314. // if we already have translation loaded in our _translatios dictionary, simply load it and set text
  1315. string translation;
  1316. if( TryGetTranslation( textKey, out translation ) )
  1317. {
  1318. if( !string.IsNullOrEmpty( translation ) )
  1319. {
  1320. SetTranslatedText( ui, textKey.Untemplate( translation ), info );
  1321. return translation;
  1322. }
  1323. }
  1324. else
  1325. {
  1326. if( UnityTextParsers.GameLogTextParser.CanApply( ui ) )
  1327. {
  1328. var result = UnityTextParsers.GameLogTextParser.Parse( text );
  1329. if( result.Succeeded )
  1330. {
  1331. translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
  1332. if( translation != null )
  1333. {
  1334. SetTranslatedText( ui, translation, info );
  1335. return translation;
  1336. }
  1337. }
  1338. }
  1339. }
  1340. }
  1341. return null;
  1342. }
  1343. /// <summary>
  1344. /// Translates the string of a UI text or queues it up to be translated
  1345. /// by the HTTP translation service.
  1346. /// </summary>
  1347. private string TranslateOrQueueWebJobImmediate( object ui, string text, TextTranslationInfo info, bool supportsStabilization, bool ignoreComponentState, TranslationContext context = null )
  1348. {
  1349. text = text ?? ui.GetText();
  1350. // make sure text exists
  1351. var originalText = text;
  1352. if( context == null )
  1353. {
  1354. // Get the trimmed text
  1355. text = text.TrimIfConfigured();
  1356. }
  1357. // Ensure that we actually want to translate this text and its owning UI element.
  1358. if( !string.IsNullOrEmpty( text ) && IsTranslatable( text ) && ShouldTranslateTextComponent( ui, ignoreComponentState ) && !IsCurrentlySetting( info ) )
  1359. {
  1360. //Logger.Current.Debug( "START: " + ui.GetType().Name + ": " + text );
  1361. info?.Reset( originalText );
  1362. var isSpammer = ui.IsSpammingComponent();
  1363. var textKey = new TranslationKey( ui, text, isSpammer, context != null );
  1364. // if we already have translation loaded in our _translatios dictionary, simply load it and set text
  1365. string translation;
  1366. if( TryGetTranslation( textKey, out translation ) )
  1367. {
  1368. QueueNewUntranslatedForClipboard( textKey );
  1369. if( !string.IsNullOrEmpty( translation ) )
  1370. {
  1371. if( context == null ) // never set text if operation is contextualized (only a part translation)
  1372. {
  1373. SetTranslatedText( ui, textKey.Untemplate( translation ), info );
  1374. }
  1375. return translation;
  1376. }
  1377. }
  1378. else
  1379. {
  1380. if( context == null )
  1381. {
  1382. if( UnityTextParsers.GameLogTextParser.CanApply( ui ) )
  1383. {
  1384. var result = UnityTextParsers.GameLogTextParser.Parse( text );
  1385. if( result.Succeeded )
  1386. {
  1387. translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, false );
  1388. if( translation != null )
  1389. {
  1390. SetTranslatedText( ui, translation, info );
  1391. return translation;
  1392. }
  1393. }
  1394. }
  1395. else if( UnityTextParsers.RichTextParser.CanApply( ui ) && IsBelowMaxLength( text ) )
  1396. {
  1397. var result = UnityTextParsers.RichTextParser.Parse( text );
  1398. if( result.Succeeded )
  1399. {
  1400. var isWhitelisted = ui.IsWhitelistedForImmediateRichTextTranslation();
  1401. translation = TranslateOrQueueWebJobImmediateByParserResult( ui, result, isWhitelisted );
  1402. if( translation != null )
  1403. {
  1404. SetTranslatedText( ui, translation, info );
  1405. return translation;
  1406. }
  1407. else if( isWhitelisted )
  1408. {
  1409. return null;
  1410. }
  1411. }
  1412. }
  1413. }
  1414. if( supportsStabilization && context == null ) // never stabilize a text that is contextualized or that does not support stabilization
  1415. {
  1416. // if we dont know what text to translate it to, we need to figure it out.
  1417. // this might take a while, so add the UI text component to the ongoing operations
  1418. // list, so we dont start multiple operations for it, as its text might be constantly
  1419. // changing.
  1420. _ongoingOperations.Add( ui );
  1421. // start a coroutine, that will execute once the string of the UI text has stopped
  1422. // changing. For all texts except 'story' texts, this will add a delay for exactly
  1423. // 0.5s to the translation. This is barely noticable.
  1424. //
  1425. // on the other hand, for 'story' texts, this will take the time that it takes
  1426. // for the text to stop 'scrolling' in.
  1427. try
  1428. {
  1429. StartCoroutine(
  1430. WaitForTextStablization(
  1431. ui: ui,
  1432. delay: 1.0f, // 1 second to prevent '1 second tickers' from getting translated
  1433. maxTries: 60, // 50 tries, about 1 minute
  1434. currentTries: 0,
  1435. onMaxTriesExceeded: () =>
  1436. {
  1437. _ongoingOperations.Remove( ui );
  1438. },
  1439. onTextStabilized: stabilizedText =>
  1440. {
  1441. _ongoingOperations.Remove( ui );
  1442. originalText = stabilizedText;
  1443. stabilizedText = stabilizedText.TrimIfConfigured();
  1444. if( !string.IsNullOrEmpty( stabilizedText ) && IsTranslatable( stabilizedText ) )
  1445. {
  1446. var stabilizedTextKey = new TranslationKey( ui, stabilizedText, false );
  1447. QueueNewUntranslatedForClipboard( stabilizedTextKey );
  1448. info?.Reset( originalText );
  1449. // once the text has stabilized, attempt to look it up
  1450. if( TryGetTranslation( stabilizedTextKey, out translation ) )
  1451. {
  1452. if( !string.IsNullOrEmpty( translation ) )
  1453. {
  1454. // stabilized, no need to untemplate
  1455. SetTranslatedText( ui, translation, info );
  1456. }
  1457. }
  1458. else
  1459. {
  1460. if( context == null )
  1461. {
  1462. if( UnityTextParsers.GameLogTextParser.CanApply( ui ) )
  1463. {
  1464. var result = UnityTextParsers.GameLogTextParser.Parse( stabilizedText );
  1465. if( result.Succeeded )
  1466. {
  1467. var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
  1468. if( translatedText != null )
  1469. {
  1470. // stabilized, no need to untemplate
  1471. SetTranslatedText( ui, translatedText, info );
  1472. }
  1473. return;
  1474. }
  1475. }
  1476. else if( UnityTextParsers.RichTextParser.CanApply( ui ) && IsBelowMaxLength( stabilizedText ) )
  1477. {
  1478. var result = UnityTextParsers.RichTextParser.Parse( stabilizedText );
  1479. if( result.Succeeded )
  1480. {
  1481. var translatedText = TranslateOrQueueWebJobImmediateByParserResult( ui, result, true );
  1482. if( translatedText != null )
  1483. {
  1484. // stabilized, no need to untemplate
  1485. SetTranslatedText( ui, translatedText, info );
  1486. }
  1487. return;
  1488. }
  1489. }
  1490. }
  1491. // Lets try not to spam a service that might not be there...
  1492. if( _endpoint != null )
  1493. {
  1494. if( IsBelowMaxLength( stabilizedText ) )
  1495. {
  1496. if( !Settings.IsShutdown )
  1497. {
  1498. var job = GetOrCreateTranslationJobFor( ui, stabilizedTextKey, context );
  1499. job.Components.Add( ui );
  1500. }
  1501. }
  1502. }
  1503. else
  1504. {
  1505. QueueNewUntranslatedForDisk( stabilizedTextKey );
  1506. }
  1507. }
  1508. }
  1509. } ) );
  1510. }
  1511. catch( Exception )
  1512. {
  1513. _ongoingOperations.Remove( ui );
  1514. }
  1515. }
  1516. else if( !isSpammer || ( isSpammer && IsBelowMaxLengthStrict( text ) ) )
  1517. {
  1518. if( context != null )
  1519. {
  1520. // if there is a context, this is a part-translation, which means it is not a candidate for scrolling-in text
  1521. if( _endpoint != null )
  1522. {
  1523. if( !Settings.IsShutdown )
  1524. {
  1525. // once the text has stabilized, attempt to look it up
  1526. var job = GetOrCreateTranslationJobFor( ui, textKey, context );
  1527. }
  1528. }
  1529. else
  1530. {
  1531. QueueNewUntranslatedForDisk( textKey );
  1532. }
  1533. }
  1534. else
  1535. {
  1536. StartCoroutine(
  1537. WaitForTextStablization(
  1538. textKey: textKey,
  1539. delay: 1.0f,
  1540. onTextStabilized: () =>
  1541. {
  1542. // Lets try not to spam a service that might not be there...
  1543. if( _endpoint != null )
  1544. {
  1545. // once the text has stabilized, attempt to look it up
  1546. if( !Settings.IsShutdown )
  1547. {
  1548. if( !TryGetTranslation( textKey, out translation ) )
  1549. {
  1550. var job = GetOrCreateTranslationJobFor( ui, textKey, context );
  1551. }
  1552. }
  1553. }
  1554. else
  1555. {
  1556. QueueNewUntranslatedForDisk( textKey );
  1557. }
  1558. } ) );
  1559. }
  1560. }
  1561. }
  1562. }
  1563. return null;
  1564. }
  1565. private string TranslateOrQueueWebJobImmediateByParserResult( object ui, ParserResult result, bool allowStartJob )
  1566. {
  1567. Dictionary<string, string> translations = new Dictionary<string, string>();
  1568. // attempt to lookup ALL strings immediately; return result if possible; queue operations
  1569. foreach( var kvp in result.Arguments )
  1570. {
  1571. var key = kvp.Key;
  1572. var value = kvp.Value.TrimIfConfigured();
  1573. if( !string.IsNullOrEmpty( value ) && IsTranslatable( value ) && IsBelowMaxLength( value ) )
  1574. {
  1575. string partTranslation;
  1576. if( TryGetTranslation( value, out partTranslation ) )
  1577. {
  1578. translations.Add( key, partTranslation );
  1579. }
  1580. else if( allowStartJob )
  1581. {
  1582. // incomplete, must start job
  1583. var context = new TranslationContext( ui, result );
  1584. TranslateOrQueueWebJobImmediate( ui, value, null, false, true, context );
  1585. }
  1586. }
  1587. else
  1588. {
  1589. // the value will do
  1590. translations.Add( key, value );
  1591. }
  1592. }
  1593. if( result.Arguments.Count == translations.Count )
  1594. {
  1595. return result.Untemplate( translations );
  1596. }
  1597. else
  1598. {
  1599. return null; // could not perform complete translation
  1600. }
  1601. }
  1602. /// <summary>
  1603. /// Utility method that allows me to wait to call an action, until
  1604. /// the text has stopped changing. This is important for 'story'
  1605. /// mode text, which 'scrolls' into place slowly.
  1606. /// </summary>
  1607. private IEnumerator WaitForTextStablization( object ui, float delay, int maxTries, int currentTries, Action<string> onTextStabilized, Action onMaxTriesExceeded )
  1608. {
  1609. yield return 0; // wait a single frame to allow any external plugins to complete their hooking logic
  1610. bool succeeded = false;
  1611. while( currentTries < maxTries ) // shortcircuit
  1612. {
  1613. var beforeText = ui.GetText();
  1614. yield return new WaitForSeconds( delay );
  1615. var afterText = ui.GetText();
  1616. //Logger.Current.Debug( "WAITING: " + ui.GetType().Name + ": " + afterText );
  1617. if( beforeText == afterText )
  1618. {
  1619. onTextStabilized( afterText );
  1620. succeeded = true;
  1621. break;
  1622. }
  1623. currentTries++;
  1624. }
  1625. if( !succeeded )
  1626. {
  1627. onMaxTriesExceeded();
  1628. }
  1629. }
  1630. /// <summary>
  1631. /// Utility method that allows me to wait to call an action, until
  1632. /// the text has stopped changing. This is important for 'story'
  1633. /// mode text, which 'scrolls' into place slowly. This version is
  1634. /// for global text, where the component cannot tell us if the text
  1635. /// has changed itself.
  1636. /// </summary>
  1637. private IEnumerator WaitForTextStablization( TranslationKey textKey, float delay, Action onTextStabilized, Action onFailed = null )
  1638. {
  1639. var text = textKey.GetDictionaryLookupKey();
  1640. if( !_immediatelyTranslating.Contains( text ) )
  1641. {
  1642. _immediatelyTranslating.Add( text );
  1643. try
  1644. {
  1645. yield return new WaitForSeconds( delay );
  1646. bool succeeded = true;
  1647. foreach( var otherImmediatelyTranslating in _immediatelyTranslating )
  1648. {
  1649. if( text != otherImmediatelyTranslating )
  1650. {
  1651. if( text.RemindsOf( otherImmediatelyTranslating ) )
  1652. {
  1653. succeeded = false;
  1654. break;
  1655. }
  1656. }
  1657. }
  1658. if( succeeded )
  1659. {
  1660. onTextStabilized();
  1661. }
  1662. else
  1663. {
  1664. onFailed?.Invoke();
  1665. }
  1666. }
  1667. finally
  1668. {
  1669. _immediatelyTranslating.Remove( text );
  1670. }
  1671. }
  1672. }
  1673. private IEnumerator DelayForSeconds( float delay, Action onContinue )
  1674. {
  1675. yield return new WaitForSeconds( delay );
  1676. onContinue();
  1677. }
  1678. void Awake()
  1679. {
  1680. if( !_initialized )
  1681. {
  1682. _initialized = true;
  1683. try
  1684. {
  1685. Initialize();
  1686. ManualHook();
  1687. }
  1688. catch( Exception e )
  1689. {
  1690. Logger.Current.Error( e, "An unexpected error occurred during plugin initialization." );
  1691. }
  1692. }
  1693. }
  1694. void Start()
  1695. {
  1696. try
  1697. {
  1698. HooksSetup.InstallOverrideTextHooks();
  1699. }
  1700. catch( Exception e )
  1701. {
  1702. Logger.Current.Error( e, "An unexpected error occurred during plugin start." );
  1703. }
  1704. }
  1705. void Update()
  1706. {
  1707. try
  1708. {
  1709. // perform this check every 100 frames!
  1710. if( Time.frameCount % 100 == 0 )
  1711. {
  1712. ConnectionTrackingWebClient.CheckServicePoints();
  1713. }
  1714. if( Features.SupportsClipboard )
  1715. {
  1716. CopyToClipboard();
  1717. }
  1718. if( !Settings.IsShutdown )
  1719. {
  1720. UpdateSpriteRenderers();
  1721. PeriodicResetFrameCheck();
  1722. IncrementBatchOperations();
  1723. ResetThresholdTimerIfRequired();
  1724. KickoffTranslations();
  1725. FinishTranslations();
  1726. if( ClrTypes.AdvEngine != null && _nextAdvUpdate.HasValue && Time.time > _nextAdvUpdate )
  1727. {
  1728. _nextAdvUpdate = null;
  1729. UpdateUtageText();
  1730. }
  1731. }
  1732. if( Input.anyKey )
  1733. {
  1734. var isAltPressed = Input.GetKey( KeyCode.LeftAlt ) || Input.GetKey( KeyCode.RightAlt );
  1735. if( Settings.EnablePrintHierarchy && isAltPressed && Input.GetKeyDown( KeyCode.Y ) )
  1736. {
  1737. PrintObjects();
  1738. }
  1739. else if( isAltPressed && Input.GetKeyDown( KeyCode.T ) )
  1740. {
  1741. ToggleTranslation();
  1742. }
  1743. else if( isAltPressed && Input.GetKeyDown( KeyCode.F ) )
  1744. {
  1745. ToggleFont();
  1746. }
  1747. else if( isAltPressed && Input.GetKeyDown( KeyCode.D ) )
  1748. {
  1749. DumpUntranslated();
  1750. }
  1751. else if( isAltPressed && Input.GetKeyDown( KeyCode.R ) )
  1752. {
  1753. ReloadTranslations();
  1754. }
  1755. else if( isAltPressed && Input.GetKeyDown( KeyCode.U ) )
  1756. {
  1757. ManualHook();
  1758. }
  1759. else if( isAltPressed && Input.GetKeyDown( KeyCode.Q ) )
  1760. {
  1761. RebootPlugin();
  1762. }
  1763. else if( isAltPressed && ( Input.GetKeyDown( KeyCode.Alpha0 ) || Input.GetKeyDown( KeyCode.Keypad0 ) ) )
  1764. {
  1765. _window.IsShown = !_window.IsShown;
  1766. }
  1767. }
  1768. }
  1769. catch( Exception e )
  1770. {
  1771. Logger.Current.Error( e, "An error occurred in Update callback. " );
  1772. }
  1773. }
  1774. void OnGUI()
  1775. {
  1776. try
  1777. {
  1778. DisableAutoTranslator();
  1779. if( _window.IsShown ) _window.OnGUI();
  1780. }
  1781. finally
  1782. {
  1783. EnableAutoTranslator();
  1784. }
  1785. }
  1786. private void RebootPlugin()
  1787. {
  1788. if( Settings.IsShutdown )
  1789. {
  1790. if( !Settings.IsShutdownFatal )
  1791. {
  1792. _consecutiveErrors = 0;
  1793. Settings.IsShutdown = false;
  1794. Logger.Current.Info( "Rebooted Auto Translator." );
  1795. }
  1796. else
  1797. {
  1798. Logger.Current.Info( "Cannot reboot Auto Translator because the error that caused the shutdown is bad behaviour by the game." );
  1799. }
  1800. }
  1801. else
  1802. {
  1803. Logger.Current.Info( "Cannot reboot Auto Translator because it has not been shut down." );
  1804. }
  1805. }
  1806. private void KickoffTranslations()
  1807. {
  1808. if( _endpoint == null ) return;
  1809. if( Settings.EnableBatching && _endpoint.Endpoint.SupportsLineSplitting() && !_batchLogicHasFailed && _unstartedJobs.Count > 1 && _availableBatchOperations > 0 )
  1810. {
  1811. while( _unstartedJobs.Count > 0 && _availableBatchOperations > 0 )
  1812. {
  1813. if( _endpoint.IsBusy ) break;
  1814. var kvps = _unstartedJobs.Take( Settings.BatchSize ).ToList();
  1815. var batch = new TranslationBatch();
  1816. foreach( var kvp in kvps )
  1817. {
  1818. var key = kvp.Key;
  1819. var job = kvp.Value;
  1820. _kickedOff.Add( key );
  1821. if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
  1822. batch.Add( job );
  1823. _ongoingJobs[ key ] = job;
  1824. }
  1825. if( !batch.IsEmpty )
  1826. {
  1827. _availableBatchOperations--;
  1828. var untranslatedText = batch.GetFullTranslationKey();
  1829. Logger.Current.Debug( "Starting translation for: " + untranslatedText );
  1830. StartCoroutine( _endpoint.Translate( untranslatedText, Settings.FromLanguage, Settings.Language, translatedText => OnBatchTranslationCompleted( batch, translatedText ),
  1831. ( msg, e ) => OnTranslationFailed( batch, msg, e ) ) );
  1832. }
  1833. }
  1834. }
  1835. else
  1836. {
  1837. foreach( var kvp in _unstartedJobs )
  1838. {
  1839. if( _endpoint.IsBusy ) break;
  1840. var key = kvp.Key;
  1841. var job = kvp.Value;
  1842. _kickedOff.Add( key );
  1843. // lets see if the text should still be translated before kicking anything off
  1844. if( !job.AnyComponentsStillHasOriginalUntranslatedTextOrContextual() ) continue;
  1845. _ongoingJobs[ key ] = job;
  1846. var untranslatedText = job.Key.GetDictionaryLookupKey();
  1847. Logger.Current.Debug( "Starting translation for: " + untranslatedText );
  1848. StartCoroutine( _endpoint.Translate( untranslatedText, Settings.FromLanguage, Settings.Language, translatedText => OnSingleTranslationCompleted( job, translatedText ),
  1849. ( msg, e ) => OnTranslationFailed( job, msg, e ) ) );
  1850. }
  1851. }
  1852. for( int i = 0 ; i < _kickedOff.Count ; i++ )
  1853. {
  1854. _unstartedJobs.Remove( _kickedOff[ i ] );
  1855. }
  1856. _kickedOff.Clear();
  1857. }
  1858. private void OnBatchTranslationCompleted( TranslationBatch batch, string translatedTextBatch )
  1859. {
  1860. _consecutiveErrors = 0;
  1861. Logger.Current.Debug( $"Translation for '{batch.GetFullTranslationKey()}' succeded. Result: {translatedTextBatch}" );
  1862. var succeeded = batch.MatchWithTranslations( translatedTextBatch );
  1863. if( succeeded )
  1864. {
  1865. foreach( var tracker in batch.Trackers )
  1866. {
  1867. Settings.TranslationCount++;
  1868. var job = tracker.Job;
  1869. var translatedText = tracker.RawTranslatedText;
  1870. if( !string.IsNullOrEmpty( translatedText ) )
  1871. {
  1872. if( Settings.ForceSplitTextAfterCharacters > 0 )
  1873. {
  1874. translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
  1875. }
  1876. job.TranslatedText = job.Key.RepairTemplate( translatedText );
  1877. QueueNewTranslationForDisk( job.Key, translatedText );
  1878. _completedJobs.Add( job );
  1879. }
  1880. AddTranslation( job.Key, job.TranslatedText );
  1881. job.State = TranslationJobState.Succeeded;
  1882. _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
  1883. }
  1884. }
  1885. else
  1886. {
  1887. // might as well re-add all translation jobs, and never do this again!
  1888. _batchLogicHasFailed = true;
  1889. foreach( var tracker in batch.Trackers )
  1890. {
  1891. Settings.TranslationCount++;
  1892. var key = tracker.Job.Key.GetDictionaryLookupKey();
  1893. if( !_unstartedJobs.ContainsKey( key ) )
  1894. {
  1895. _unstartedJobs[ key ] = tracker.Job;
  1896. }
  1897. _ongoingJobs.Remove( key );
  1898. }
  1899. Logger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
  1900. }
  1901. if( !Settings.IsShutdown )
  1902. {
  1903. if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
  1904. {
  1905. Settings.IsShutdown = true;
  1906. Settings.IsShutdownFatal = true;
  1907. Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
  1908. _unstartedJobs.Clear();
  1909. _completedJobs.Clear();
  1910. _ongoingJobs.Clear();
  1911. }
  1912. }
  1913. }
  1914. private void OnSingleTranslationCompleted( TranslationJob job, string translatedText )
  1915. {
  1916. Settings.TranslationCount++;
  1917. Logger.Current.Debug( $"Translation for '{job.Key.GetDictionaryLookupKey()}' succeded. Result: {translatedText}" );
  1918. _consecutiveErrors = 0;
  1919. if( !string.IsNullOrEmpty( translatedText ) )
  1920. {
  1921. if( Settings.ForceSplitTextAfterCharacters > 0 )
  1922. {
  1923. translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
  1924. }
  1925. job.TranslatedText = job.Key.RepairTemplate( translatedText );
  1926. QueueNewTranslationForDisk( job.Key, translatedText );
  1927. _completedJobs.Add( job );
  1928. }
  1929. AddTranslation( job.Key, job.TranslatedText );
  1930. job.State = TranslationJobState.Succeeded;
  1931. _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
  1932. if( !Settings.IsShutdown )
  1933. {
  1934. if( Settings.TranslationCount > Settings.MaxTranslationsBeforeShutdown )
  1935. {
  1936. Settings.IsShutdown = true;
  1937. Settings.IsShutdownFatal = true;
  1938. Logger.Current.Error( $"Maximum translations ({Settings.MaxTranslationsBeforeShutdown}) per session reached. Shutting plugin down." );
  1939. _unstartedJobs.Clear();
  1940. _completedJobs.Clear();
  1941. _ongoingJobs.Clear();
  1942. }
  1943. }
  1944. }
  1945. private void OnTranslationFailed( TranslationJob job, string error, Exception e )
  1946. {
  1947. if( e == null )
  1948. {
  1949. Logger.Current.Error( error );
  1950. }
  1951. else
  1952. {
  1953. Logger.Current.Error( e, error );
  1954. }
  1955. Settings.TranslationCount++; // counts as a translation
  1956. _consecutiveErrors++;
  1957. job.State = TranslationJobState.Failed;
  1958. _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
  1959. if( !Settings.IsShutdown )
  1960. {
  1961. if( _consecutiveErrors >= Settings.MaxErrors )
  1962. {
  1963. Settings.IsShutdown = true;
  1964. Logger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
  1965. _unstartedJobs.Clear();
  1966. _completedJobs.Clear();
  1967. _ongoingJobs.Clear();
  1968. }
  1969. }
  1970. }
  1971. private void OnTranslationFailed( TranslationBatch batch, string error, Exception e )
  1972. {
  1973. if( e == null )
  1974. {
  1975. Logger.Current.Error( error );
  1976. }
  1977. else
  1978. {
  1979. Logger.Current.Error( e, error );
  1980. }
  1981. Settings.TranslationCount++; // counts as a translation
  1982. _consecutiveErrors++;
  1983. _batchLogicHasFailed = true;
  1984. foreach( var tracker in batch.Trackers )
  1985. {
  1986. tracker.Job.State = TranslationJobState.Failed;
  1987. _ongoingJobs.Remove( tracker.Job.Key.GetDictionaryLookupKey() );
  1988. }
  1989. if( !Settings.IsShutdown )
  1990. {
  1991. if( _consecutiveErrors >= Settings.MaxErrors )
  1992. {
  1993. Settings.IsShutdown = true;
  1994. Logger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
  1995. _unstartedJobs.Clear();
  1996. _completedJobs.Clear();
  1997. _ongoingJobs.Clear();
  1998. }
  1999. }
  2000. }
  2001. private void FinishTranslations()
  2002. {
  2003. if( _completedJobs.Count > 0 )
  2004. {
  2005. for( int i = _completedJobs.Count - 1 ; i >= 0 ; i-- )
  2006. {
  2007. var job = _completedJobs[ i ];
  2008. _completedJobs.RemoveAt( i );
  2009. foreach( var component in job.Components )
  2010. {
  2011. // update the original text, but only if it has not been chaanged already for some reason (could be other translator plugin or game itself)
  2012. try
  2013. {
  2014. var text = component.GetText().TrimIfConfigured();
  2015. if( text == job.Key.OriginalText )
  2016. {
  2017. var info = component.GetOrCreateTextTranslationInfo();
  2018. SetTranslatedText( component, job.TranslatedText, info );
  2019. }
  2020. }
  2021. catch( NullReferenceException )
  2022. {
  2023. // might fail if compoent is no longer associated to game
  2024. }
  2025. }
  2026. // handle each context
  2027. foreach( var context in job.Contexts )
  2028. {
  2029. // are all jobs within this context completed? If so, we can set the text
  2030. if( context.Jobs.All( x => x.State == TranslationJobState.Succeeded ) )
  2031. {
  2032. try
  2033. {
  2034. var text = context.Component.GetText().TrimIfConfigured();
  2035. var result = context.Result;
  2036. Dictionary<string, string> translations = new Dictionary<string, string>();
  2037. var translatedText = TranslateOrQueueWebJobImmediateByParserResult( context.Component, result, false );
  2038. if( !string.IsNullOrEmpty( translatedText ) )
  2039. {
  2040. if( result.PersistCombinedResult && !_translations.ContainsKey( context.Result.OriginalText ) )
  2041. {
  2042. AddTranslation( context.Result.OriginalText, translatedText );
  2043. QueueNewTranslationForDisk( context.Result.OriginalText, translatedText );
  2044. }
  2045. if( text == result.OriginalText )
  2046. {
  2047. if( translatedText != null )
  2048. {
  2049. var info = context.Component.GetOrCreateTextTranslationInfo();
  2050. SetTranslatedText( context.Component, translatedText, info );
  2051. }
  2052. }
  2053. }
  2054. }
  2055. catch( NullReferenceException )
  2056. {
  2057. }
  2058. }
  2059. }
  2060. // Utage support
  2061. if( ClrTypes.AdvEngine != null
  2062. && job.OriginalSources.Any( x => ClrTypes.AdvCommand.IsAssignableFrom( x.GetType() ) ) )
  2063. {
  2064. _nextAdvUpdate = Time.time + 0.5f;
  2065. }
  2066. }
  2067. }
  2068. }
  2069. private void UpdateUtageText()
  2070. {
  2071. // 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.
  2072. if( _advEngine == null || _advEngine?.gameObject == null )
  2073. {
  2074. _advEngine = (Component)GameObject.FindObjectOfType( Constants.ClrTypes.AdvEngine );
  2075. }
  2076. if( _advEngine != null )
  2077. {
  2078. AccessTools.Method( Constants.ClrTypes.AdvEngine, "ChangeLanguage" )?.Invoke( _advEngine, new object[ 0 ] );
  2079. }
  2080. }
  2081. private void ReloadTranslations()
  2082. {
  2083. LoadTranslations();
  2084. var context = new TextureReloadContext();
  2085. foreach( var kvp in ObjectReferenceMapper.GetAllRegisteredObjects() )
  2086. {
  2087. var ui = kvp.Key;
  2088. try
  2089. {
  2090. if( ui is Component component )
  2091. {
  2092. if( component.gameObject?.activeSelf ?? false )
  2093. {
  2094. var tti = kvp.Value as TextTranslationInfo;
  2095. if( tti != null && !string.IsNullOrEmpty( tti.OriginalText ) )
  2096. {
  2097. var key = new TranslationKey( kvp.Key, tti.OriginalText, false );
  2098. if( TryGetTranslation( key, out string translatedText ) && !string.IsNullOrEmpty( translatedText ) )
  2099. {
  2100. SetTranslatedText( kvp.Key, translatedText, tti ); // no need to untemplatize the translated text
  2101. }
  2102. }
  2103. }
  2104. }
  2105. if( Settings.EnableTextureTranslation )
  2106. {
  2107. TranslateTexture( ui, context );
  2108. }
  2109. }
  2110. catch( Exception )
  2111. {
  2112. // not super pretty, no...
  2113. ObjectReferenceMapper.Remove( ui );
  2114. }
  2115. }
  2116. }
  2117. private string CalculateDumpFileName()
  2118. {
  2119. int idx = 0;
  2120. string fileName = null;
  2121. do
  2122. {
  2123. idx++;
  2124. fileName = $"UntranslatedDump{idx}.txt";
  2125. }
  2126. while( File.Exists( fileName ) );
  2127. return fileName;
  2128. }
  2129. private void DumpUntranslated()
  2130. {
  2131. if( _newUntranslated.Count > 0 )
  2132. {
  2133. using( var stream = File.Open( CalculateDumpFileName(), FileMode.Append, FileAccess.Write ) )
  2134. using( var writer = new StreamWriter( stream, Encoding.UTF8 ) )
  2135. {
  2136. foreach( var untranslated in _newUntranslated )
  2137. {
  2138. writer.WriteLine( TextHelper.Encode( untranslated ) + '=' );
  2139. }
  2140. writer.Flush();
  2141. }
  2142. _newUntranslated.Clear();
  2143. }
  2144. }
  2145. private void ToggleFont()
  2146. {
  2147. if( _hasOverrideFont )
  2148. {
  2149. _overrideFont = !_overrideFont;
  2150. var objects = ObjectReferenceMapper.GetAllRegisteredObjects();
  2151. Logger.Current.Info( $"Toggling fonts of {objects.Count} objects." );
  2152. if( _overrideFont )
  2153. {
  2154. // make sure we use the translated version of all texts
  2155. foreach( var kvp in objects )
  2156. {
  2157. var tti = kvp.Value as TextTranslationInfo;
  2158. if( tti != null )
  2159. {
  2160. var ui = kvp.Key;
  2161. try
  2162. {
  2163. if( ( ui as Component )?.gameObject?.activeSelf ?? false )
  2164. {
  2165. tti?.ChangeFont( ui );
  2166. }
  2167. }
  2168. catch( Exception )
  2169. {
  2170. // not super pretty, no...
  2171. ObjectReferenceMapper.Remove( ui );
  2172. }
  2173. }
  2174. }
  2175. }
  2176. else
  2177. {
  2178. // make sure we use the original version of all texts
  2179. foreach( var kvp in objects )
  2180. {
  2181. var tti = kvp.Value as TextTranslationInfo;
  2182. var ui = kvp.Key;
  2183. try
  2184. {
  2185. if( ( ui as Component )?.gameObject?.activeSelf ?? false )
  2186. {
  2187. tti?.UnchangeFont( ui );
  2188. }
  2189. }
  2190. catch( Exception )
  2191. {
  2192. // not super pretty, no...
  2193. ObjectReferenceMapper.Remove( ui );
  2194. }
  2195. }
  2196. }
  2197. }
  2198. }
  2199. private void ToggleTranslation()
  2200. {
  2201. _isInTranslatedMode = !_isInTranslatedMode;
  2202. var objects = ObjectReferenceMapper.GetAllRegisteredObjects();
  2203. Logger.Current.Info( $"Toggling translations of {objects.Count} objects." );
  2204. if( _isInTranslatedMode )
  2205. {
  2206. // make sure we use the translated version of all texts
  2207. foreach( var kvp in objects )
  2208. {
  2209. var ui = kvp.Key;
  2210. try
  2211. {
  2212. if( ui is Component component )
  2213. {
  2214. if( component.gameObject?.activeSelf ?? false )
  2215. {
  2216. var tti = kvp.Value as TextTranslationInfo;
  2217. if( tti != null && tti.IsTranslated )
  2218. {
  2219. SetText( ui, tti.TranslatedText, true, tti );
  2220. }
  2221. }
  2222. }
  2223. if( Settings.EnableTextureTranslation && Settings.EnableTextureToggling )
  2224. {
  2225. TranslateTexture( ui, null );
  2226. }
  2227. }
  2228. catch( Exception )
  2229. {
  2230. // not super pretty, no...
  2231. ObjectReferenceMapper.Remove( ui );
  2232. }
  2233. }
  2234. }
  2235. else
  2236. {
  2237. // make sure we use the original version of all texts
  2238. foreach( var kvp in objects )
  2239. {
  2240. var ui = kvp.Key;
  2241. try
  2242. {
  2243. if( ui is Component component )
  2244. {
  2245. if( component.gameObject?.activeSelf ?? false )
  2246. {
  2247. var tti = kvp.Value as TextTranslationInfo;
  2248. if( tti != null && tti.IsTranslated )
  2249. {
  2250. SetText( ui, tti.OriginalText, true, tti );
  2251. }
  2252. }
  2253. }
  2254. if( Settings.EnableTextureTranslation && Settings.EnableTextureToggling )
  2255. {
  2256. TranslateTexture( ui, null );
  2257. }
  2258. }
  2259. catch( Exception )
  2260. {
  2261. // not super pretty, no...
  2262. ObjectReferenceMapper.Remove( ui );
  2263. }
  2264. }
  2265. }
  2266. }
  2267. private void CopyToClipboard()
  2268. {
  2269. if( Settings.CopyToClipboard
  2270. && _textsToCopyToClipboardOrdered.Count > 0
  2271. && Time.realtimeSinceStartup - _clipboardUpdated > Settings.ClipboardDebounceTime )
  2272. {
  2273. try
  2274. {
  2275. var builder = new StringBuilder();
  2276. foreach( var text in _textsToCopyToClipboardOrdered )
  2277. {
  2278. if( text.Length + builder.Length > Settings.MaxClipboardCopyCharacters ) break;
  2279. builder.AppendLine( text );
  2280. }
  2281. TextEditor editor = (TextEditor)GUIUtility.GetStateObject( typeof( TextEditor ), GUIUtility.keyboardControl );
  2282. editor.text = builder.ToString();
  2283. editor.SelectAll();
  2284. editor.Copy();
  2285. }
  2286. catch( Exception e )
  2287. {
  2288. Logger.Current.Error( e, "An error while copying text to clipboard." );
  2289. }
  2290. finally
  2291. {
  2292. _textsToCopyToClipboard.Clear();
  2293. _textsToCopyToClipboardOrdered.Clear();
  2294. }
  2295. }
  2296. }
  2297. private void PrintObjects()
  2298. {
  2299. using( var stream = File.Open( Path.Combine( Environment.CurrentDirectory, "hierarchy.txt" ), FileMode.Create ) )
  2300. using( var writer = new StreamWriter( stream ) )
  2301. {
  2302. foreach( var root in GetAllRoots() )
  2303. {
  2304. TraverseChildren( writer, root, "" );
  2305. }
  2306. writer.Flush();
  2307. }
  2308. }
  2309. private void ManualHook()
  2310. {
  2311. ManualHookForComponents();
  2312. ManualHookForTextures();
  2313. }
  2314. private void ManualHookForComponents()
  2315. {
  2316. foreach( var root in GetAllRoots() )
  2317. {
  2318. TraverseChildrenManualHook( root );
  2319. }
  2320. }
  2321. private void ManualHookForTextures()
  2322. {
  2323. if( Settings.EnableTextureScanOnSceneLoad && ( Settings.EnableTextureTranslation || Settings.EnableTextureDumping ) )
  2324. {
  2325. // scan all textures and update
  2326. var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
  2327. foreach( var texture in textures )
  2328. {
  2329. Hook_ImageChanged( texture, false );
  2330. }
  2331. //// scan all components and set dirty
  2332. //var components = GameObject.FindObjectsOfType<Component>();
  2333. //foreach( var component in components )
  2334. //{
  2335. // component.SetAllDirtyEx();
  2336. //}
  2337. }
  2338. }
  2339. private IEnumerable<GameObject> GetAllRoots()
  2340. {
  2341. var objects = GameObject.FindObjectsOfType<GameObject>();
  2342. foreach( var obj in objects )
  2343. {
  2344. if( obj.transform != null && obj.transform.parent == null )
  2345. {
  2346. yield return obj;
  2347. }
  2348. }
  2349. }
  2350. private void TraverseChildren( StreamWriter writer, GameObject obj, string identation )
  2351. {
  2352. if( obj != null )
  2353. {
  2354. var layer = LayerMask.LayerToName( obj.layer );
  2355. var components = string.Join( ", ", obj.GetComponents<Component>().Select( x => x?.GetType()?.Name ).Where( x => x != null ).ToArray() );
  2356. var line = string.Format( "{0,-50} {1,100}",
  2357. identation + obj.name + " [" + layer + "]",
  2358. components );
  2359. writer.WriteLine( line );
  2360. if( obj.transform != null )
  2361. {
  2362. for( int i = 0 ; i < obj.transform.childCount ; i++ )
  2363. {
  2364. var child = obj.transform.GetChild( i );
  2365. TraverseChildren( writer, child.gameObject, identation + " " );
  2366. }
  2367. }
  2368. }
  2369. }
  2370. private void TraverseChildrenManualHook( GameObject obj )
  2371. {
  2372. if( obj != null )
  2373. {
  2374. var components = obj.GetComponents<Component>();
  2375. foreach( var component in components )
  2376. {
  2377. if( component.IsKnownTextType() )
  2378. {
  2379. Hook_TextChanged( component, false );
  2380. }
  2381. if( Settings.EnableTextureTranslation || Settings.EnableTextureDumping )
  2382. {
  2383. if( component.IsKnownImageType() )
  2384. {
  2385. Hook_ImageChangedOnComponent( component, null, false, false );
  2386. }
  2387. }
  2388. }
  2389. if( obj.transform != null )
  2390. {
  2391. for( int i = 0 ; i < obj.transform.childCount ; i++ )
  2392. {
  2393. var child = obj.transform.GetChild( i );
  2394. TraverseChildrenManualHook( child.gameObject );
  2395. }
  2396. }
  2397. }
  2398. }
  2399. public void DisableAutoTranslator()
  2400. {
  2401. _temporarilyDisabled = true;
  2402. }
  2403. public void EnableAutoTranslator()
  2404. {
  2405. _temporarilyDisabled = false;
  2406. }
  2407. void OnApplicationQuit()
  2408. {
  2409. if( _configuredEndpoints == null ) return;
  2410. foreach( var ce in _configuredEndpoints )
  2411. {
  2412. try
  2413. {
  2414. if( ce.Endpoint is IDisposable disposable ) disposable.Dispose();
  2415. }
  2416. catch( Exception e )
  2417. {
  2418. Logger.Current.Error( e, "An error occurred while disposing endpoint." );
  2419. }
  2420. }
  2421. }
  2422. }
  2423. internal static class SceneManagerLoader
  2424. {
  2425. public static void EnableSceneLoadScanInternal( AutoTranslationPlugin plugin )
  2426. {
  2427. // specified in own method, because of chance that this has changed through Unity lifetime
  2428. SceneManager.sceneLoaded += ( arg1, arg2 ) => plugin.OnLevelWasLoadedFromSceneManager( arg1.buildIndex );
  2429. }
  2430. }
  2431. }