using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Timers; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Jotunn.Managers; using Localization.DevTools; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using ServerSync; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("Localization")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+3b3b4aed6420cc2f72f6bdae0dea94042e81068d")] [assembly: AssemblyProduct("Localization")] [assembly: AssemblyTitle("Localization")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Localization { internal sealed class BoundedStringDictionary { private readonly int capacity; private readonly Dictionary values; private readonly Queue insertionOrder; public int Count => values.Count; public BoundedStringDictionary(int capacity, IEqualityComparer comparer) { this.capacity = ((capacity <= 0) ? 1 : capacity); values = new Dictionary(this.capacity, comparer ?? StringComparer.Ordinal); insertionOrder = new Queue(this.capacity); } public bool TryGetValue(string key, out TValue value) { return values.TryGetValue(key, out value); } public bool Set(string key, TValue value, out KeyValuePair evictedEntry) { evictedEntry = default(KeyValuePair); if (values.ContainsKey(key)) { values[key] = value; return false; } EvictIfNeeded(out evictedEntry); values.Add(key, value); insertionOrder.Enqueue(key); return evictedEntry.Key != null; } public void Clear() { values.Clear(); insertionOrder.Clear(); } private void EvictIfNeeded(out KeyValuePair evictedEntry) { evictedEntry = default(KeyValuePair); while (values.Count >= capacity && insertionOrder.Count > 0) { string key = insertionOrder.Dequeue(); if (values.TryGetValue(key, out var value)) { values.Remove(key); evictedEntry = new KeyValuePair(key, value); break; } } } } internal sealed class BoundedStringSet { private readonly int capacity; private readonly HashSet values; private readonly Queue insertionOrder; public int Count => values.Count; public BoundedStringSet(int capacity, IEqualityComparer comparer) { this.capacity = ((capacity <= 0) ? 1 : capacity); values = new HashSet(comparer ?? StringComparer.Ordinal); insertionOrder = new Queue(this.capacity); } public bool Contains(string key) { return values.Contains(key); } public bool Add(string key, out string evictedKey) { evictedKey = null; if (values.Contains(key)) { return false; } EvictIfNeeded(out evictedKey); values.Add(key); insertionOrder.Enqueue(key); return true; } public void Clear() { values.Clear(); insertionOrder.Clear(); } private void EvictIfNeeded(out string evictedKey) { evictedKey = null; while (values.Count >= capacity && insertionOrder.Count > 0) { string text = insertionOrder.Dequeue(); if (values.Remove(text)) { evictedKey = text; break; } } } } internal sealed class CompiledTemplateRule { private readonly struct TemplateToken { public bool IsPlaceholder { get; } public string Value { get; } public bool IsFunctionCall { get; } public PlaceholderConstraint Constraint { get; } public TemplateToken(bool isPlaceholder, string value, bool isFunctionCall = false, PlaceholderConstraint constraint = null) { IsPlaceholder = isPlaceholder; Value = value; IsFunctionCall = isFunctionCall; Constraint = constraint; } } private sealed class PlaceholderConstraint { public PlaceholderConstraintKind Kind { get; } public string Signature { get; } private IntegerRange WordCountRange { get; } private IntegerRange IntegerDigitRange { get; } private IntegerRange FractionDigitRange { get; } private NumericPredicate[] NumericPredicates { get; } private string[] OneOfValues { get; } private PlaceholderConstraint(PlaceholderConstraintKind kind, string signature, IntegerRange wordCountRange, IntegerRange integerDigitRange, IntegerRange fractionDigitRange, NumericPredicate[] numericPredicates, string[] oneOfValues) { Kind = kind; Signature = signature; WordCountRange = wordCountRange; IntegerDigitRange = integerDigitRange; FractionDigitRange = fractionDigitRange; NumericPredicates = numericPredicates ?? Array.Empty(); OneOfValues = oneOfValues ?? Array.Empty(); } public static PlaceholderConstraint CreateWord() { return new PlaceholderConstraint(PlaceholderConstraintKind.Word, "word", IntegerRange.Exact(1), default(IntegerRange), default(IntegerRange), Array.Empty(), Array.Empty()); } public static PlaceholderConstraint CreateWords(IntegerRange countRange) { return new PlaceholderConstraint(PlaceholderConstraintKind.Words, "words(count=" + countRange.ToSignature() + ")", countRange, default(IntegerRange), default(IntegerRange), Array.Empty(), Array.Empty()); } public static PlaceholderConstraint CreateInteger(IntegerRange digitRange, NumericPredicate[] predicates) { return new PlaceholderConstraint(PlaceholderConstraintKind.Integer, BuildIntegerSignature(digitRange, predicates), default(IntegerRange), digitRange, default(IntegerRange), predicates, Array.Empty()); } public static PlaceholderConstraint CreateFloat(IntegerRange integerDigitRange, IntegerRange fractionDigitRange, NumericPredicate[] predicates) { return new PlaceholderConstraint(PlaceholderConstraintKind.Float, BuildFloatSignature(integerDigitRange, fractionDigitRange, predicates), default(IntegerRange), integerDigitRange, fractionDigitRange, predicates, Array.Empty()); } public static PlaceholderConstraint CreateOneOf(string[] values) { return new PlaceholderConstraint(PlaceholderConstraintKind.OneOf, "oneof(" + string.Join("|", values) + ")", default(IntegerRange), default(IntegerRange), default(IntegerRange), Array.Empty(), values); } public bool IsMatch(string value) { if (value == null) { return false; } return IsMatch(value, 0, value.Length); } public bool IsMatch(string value, int startIndex, int length) { switch (Kind) { case PlaceholderConstraintKind.Word: case PlaceholderConstraintKind.Words: return MatchesWords(value, startIndex, length, WordCountRange); case PlaceholderConstraintKind.Integer: return MatchesInteger(value, startIndex, length, IntegerDigitRange, NumericPredicates); case PlaceholderConstraintKind.Float: return MatchesFloat(value, startIndex, length, IntegerDigitRange, FractionDigitRange, NumericPredicates); case PlaceholderConstraintKind.OneOf: return MatchesOneOf(value, startIndex, length, OneOfValues); default: return false; } } private static string BuildIntegerSignature(IntegerRange digitRange, NumericPredicate[] predicates) { if (predicates == null || predicates.Length == 0) { if (digitRange.IsAtLeastOne()) { return "int"; } return "int(digits=" + digitRange.ToSignature() + ")"; } if (digitRange.IsAtLeastOne()) { return "int(where=" + JoinPredicates(predicates) + ")"; } return "int(digits=" + digitRange.ToSignature() + ",where=" + JoinPredicates(predicates) + ")"; } private static string BuildFloatSignature(IntegerRange integerDigitRange, IntegerRange fractionDigitRange, NumericPredicate[] predicates) { List list = new List(3); if (!integerDigitRange.IsAtLeastOne()) { list.Add("int=" + integerDigitRange.ToSignature()); } if (!fractionDigitRange.IsAtLeastZero()) { list.Add("frac=" + fractionDigitRange.ToSignature()); } if (predicates != null && predicates.Length != 0) { list.Add("where=" + JoinPredicates(predicates)); } if (list.Count == 0) { return "float"; } return "float(" + string.Join(",", list) + ")"; } private static string JoinPredicates(NumericPredicate[] predicates) { if (predicates == null || predicates.Length == 0) { return string.Empty; } string[] array = new string[predicates.Length]; for (int i = 0; i < predicates.Length; i++) { array[i] = predicates[i].ToSignature(); } return string.Join("&&", array); } private static bool MatchesWords(string value, int startIndex, int length, IntegerRange countRange) { if (string.IsNullOrEmpty(value) || length <= 0) { return false; } int num = startIndex + length; if (char.IsWhiteSpace(value[startIndex]) || char.IsWhiteSpace(value[num - 1])) { return false; } int num2 = 0; bool flag = false; for (int i = startIndex; i < num; i++) { char c = value[i]; if (char.IsWhiteSpace(c)) { flag = false; continue; } if (!IsWordCharacter(c)) { return false; } if (!flag) { num2++; flag = true; } } return countRange.Contains(num2); } private static bool MatchesInteger(string value, int startIndex, int length, IntegerRange digitRange, NumericPredicate[] predicates) { bool flag = predicates != null && predicates.Length != 0; if (!TryParseInteger(value, startIndex, length, flag, out var digitCount, out var numericValue)) { return false; } if (!digitRange.Contains(digitCount)) { return false; } if (flag) { return MatchesNumericPredicates(numericValue, predicates); } return true; } private static bool MatchesFloat(string value, int startIndex, int length, IntegerRange integerDigitRange, IntegerRange fractionDigitRange, NumericPredicate[] predicates) { bool flag = predicates != null && predicates.Length != 0; if (!TryParseFloat(value, startIndex, length, flag, out var integerDigitCount, out var fractionDigitCount, out var numericValue)) { return false; } if (!integerDigitRange.Contains(integerDigitCount) || !fractionDigitRange.Contains(fractionDigitCount)) { return false; } if (flag) { return MatchesNumericPredicates(numericValue, predicates); } return true; } private static bool MatchesOneOf(string value, int startIndex, int length, string[] oneOfValues) { if (string.IsNullOrEmpty(value) || length <= 0) { return false; } foreach (string text in oneOfValues) { if (text.Length == length && string.Compare(value, startIndex, text, 0, length, StringComparison.OrdinalIgnoreCase) == 0) { return true; } } return false; } private static bool MatchesNumericPredicates(decimal numericValue, NumericPredicate[] predicates) { if (predicates == null || predicates.Length == 0) { return true; } for (int i = 0; i < predicates.Length; i++) { if (!predicates[i].Matches(numericValue)) { return false; } } return true; } private static bool TryParseInteger(string value, int startIndex, int length, bool requireNumericValue, out int digitCount, out decimal numericValue) { digitCount = 0; numericValue = default(decimal); if (string.IsNullOrEmpty(value) || length <= 0) { return false; } int num = ((value[startIndex] == '+' || value[startIndex] == '-') ? 1 : 0); if (num >= length) { return false; } for (int i = startIndex + num; i < startIndex + length; i++) { if (!char.IsDigit(value[i])) { return false; } } digitCount = length - num; if (!requireNumericValue) { return true; } return decimal.TryParse(value.Substring(startIndex, length), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out numericValue); } private static bool TryParseFloat(string value, int startIndex, int length, bool requireNumericValue, out int integerDigitCount, out int fractionDigitCount, out decimal numericValue) { integerDigitCount = 0; fractionDigitCount = 0; numericValue = default(decimal); if (string.IsNullOrEmpty(value) || length <= 0) { return false; } int num = ((value[startIndex] == '+' || value[startIndex] == '-') ? 1 : 0); if (num >= length) { return false; } int num2 = -1; int num3 = startIndex + length; for (int i = startIndex + num; i < num3; i++) { char c = value[i]; if (c == '.') { if (num2 >= 0) { return false; } num2 = i; } else if (!char.IsDigit(c)) { return false; } } if (num2 < 0) { integerDigitCount = length - num; if (integerDigitCount <= 0) { return false; } if (!requireNumericValue) { return true; } return decimal.TryParse(value.Substring(startIndex, length), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out numericValue); } integerDigitCount = num2 - startIndex - num; fractionDigitCount = num3 - num2 - 1; if (integerDigitCount <= 0 || fractionDigitCount <= 0) { return false; } if (!requireNumericValue) { return true; } return decimal.TryParse(value.Substring(startIndex, length), NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out numericValue); } } private enum PlaceholderConstraintKind { Word, Words, Integer, Float, OneOf } private enum NumericComparisonOperator { LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual, Equal, NotEqual } private readonly struct IntegerRange { public bool HasMinimum { get; } public int Minimum { get; } public bool HasMaximum { get; } public int Maximum { get; } public IntegerRange(bool hasMinimum, int minimum, bool hasMaximum, int maximum) { HasMinimum = hasMinimum; Minimum = minimum; HasMaximum = hasMaximum; Maximum = maximum; } public static IntegerRange Exact(int value) { return new IntegerRange(hasMinimum: true, value, hasMaximum: true, value); } public static IntegerRange AtLeast(int minimum) { return new IntegerRange(hasMinimum: true, minimum, hasMaximum: false, 0); } public bool Contains(int value) { if (HasMinimum && value < Minimum) { return false; } if (HasMaximum && value > Maximum) { return false; } return true; } public bool IsAtLeastOne() { if (HasMinimum && Minimum == 1) { return !HasMaximum; } return false; } public bool IsAtLeastZero() { if (HasMinimum && Minimum == 0) { return !HasMaximum; } return false; } public string ToSignature() { if (HasMinimum && HasMaximum && Minimum == Maximum) { return Minimum.ToString(CultureInfo.InvariantCulture); } if (HasMinimum && HasMaximum) { return Minimum.ToString(CultureInfo.InvariantCulture) + ".." + Maximum.ToString(CultureInfo.InvariantCulture); } if (HasMinimum) { return Minimum.ToString(CultureInfo.InvariantCulture) + ".."; } if (HasMaximum) { return ".." + Maximum.ToString(CultureInfo.InvariantCulture); } return ".."; } } private readonly struct NumericPredicate { public NumericComparisonOperator ComparisonOperator { get; } public decimal ComparisonValue { get; } public NumericPredicate(NumericComparisonOperator comparisonOperator, decimal comparisonValue) { ComparisonOperator = comparisonOperator; ComparisonValue = comparisonValue; } public bool Matches(decimal value) { return ComparisonOperator switch { NumericComparisonOperator.LessThan => value < ComparisonValue, NumericComparisonOperator.LessThanOrEqual => value <= ComparisonValue, NumericComparisonOperator.GreaterThan => value > ComparisonValue, NumericComparisonOperator.GreaterThanOrEqual => value >= ComparisonValue, NumericComparisonOperator.Equal => value == ComparisonValue, _ => value != ComparisonValue, }; } public string ToSignature() { return ComparisonOperator switch { NumericComparisonOperator.LessThan => "<", NumericComparisonOperator.LessThanOrEqual => "<=", NumericComparisonOperator.GreaterThan => ">", NumericComparisonOperator.GreaterThanOrEqual => ">=", NumericComparisonOperator.Equal => "==", _ => "!=", } + ComparisonValue.ToString(CultureInfo.InvariantCulture); } } private const string FunctionStartToken = "{{"; private const string FunctionEndToken = "}}"; private const string LocalizeFunctionName = "localize"; private const string ConstraintOptionCount = "count"; private const string ConstraintOptionDigits = "digits"; private const string ConstraintOptionFractionDigits = "frac"; private const string ConstraintOptionIntegerDigits = "int"; private const string ConstraintOptionWhere = "where"; private readonly TemplateToken[] matchTokens; private readonly TemplateToken[] replaceTokens; private readonly string[] nextLiteralValues; private readonly string leadingLiteralPrefix; private readonly string trailingLiteralSuffix; private readonly string longestLiteralAnchor; private readonly int minimumInputLength; private readonly int placeholderCount; private readonly int uniquePlaceholderCount; private readonly int literalSegmentCount; private readonly bool startsWithPlaceholder; private readonly bool endsWithPlaceholder; private readonly bool hasRepeatedPlaceholderNames; private readonly bool hasConstrainedPlaceholders; private readonly int[] matchPlaceholderSlots; private readonly int[] replacePlaceholderSlots; public string Signature { get; } public string ReplaceTemplate { get; } public string SourcePath { get; } public string LeadingLiteralPrefix => leadingLiteralPrefix; public string TrailingLiteralSuffix => trailingLiteralSuffix; public string LongestLiteralAnchor => longestLiteralAnchor; public int MinimumInputLength => minimumInputLength; public int PlaceholderCount => placeholderCount; public int LiteralSegmentCount => literalSegmentCount; public bool StartsWithPlaceholder => startsWithPlaceholder; public bool EndsWithPlaceholder => endsWithPlaceholder; public bool HasRepeatedPlaceholderNames => hasRepeatedPlaceholderNames; internal bool UsesConstrainedPlaceholders => hasConstrainedPlaceholders; internal int DispatchOrder { get; private set; } private CompiledTemplateRule(string signature, string replaceTemplate, string sourcePath, TemplateToken[] matchTokens, TemplateToken[] replaceTokens) { Signature = signature; ReplaceTemplate = replaceTemplate ?? string.Empty; SourcePath = sourcePath ?? string.Empty; this.matchTokens = matchTokens; this.replaceTokens = replaceTokens; nextLiteralValues = BuildNextLiteralValues(matchTokens); InitializeMatchMetadata(matchTokens, out leadingLiteralPrefix, out trailingLiteralSuffix, out longestLiteralAnchor, out minimumInputLength, out placeholderCount, out literalSegmentCount, out startsWithPlaceholder, out endsWithPlaceholder, out hasRepeatedPlaceholderNames); hasConstrainedPlaceholders = HasConstrainedPlaceholders(matchTokens); BuildPlaceholderSlots(matchTokens, replaceTokens, out matchPlaceholderSlots, out replacePlaceholderSlots, out uniquePlaceholderCount); } public static bool TryCreate(string matchTemplate, string replaceTemplate, out CompiledTemplateRule rule, out string error) { return TryCreate(matchTemplate, replaceTemplate, null, out rule, out error); } public static bool TryCreate(string matchTemplate, string replaceTemplate, string sourcePath, out CompiledTemplateRule rule, out string error) { rule = null; error = null; if (!TryTokenize(matchTemplate, requirePlaceholder: true, allowFunctions: false, allowConstraints: true, allowAdjacentPlaceholders: false, out var tokens, out error)) { return false; } if (!TryNormalizeMatchTokens(matchTemplate, tokens, out var normalizedTokens, out error)) { return false; } if (!TryValidateRepeatedPlaceholderConstraints(matchTemplate, normalizedTokens, out error)) { return false; } if (!TryTokenize(replaceTemplate, requirePlaceholder: false, allowFunctions: true, allowConstraints: false, allowAdjacentPlaceholders: true, out var tokens2, out error)) { return false; } rule = new CompiledTemplateRule(matchTemplate, replaceTemplate, sourcePath, normalizedTokens, tokens2); return true; } public bool TryTranslate(TextMatchNormalizer.NormalizedText normalizedInput, out string translated) { translated = null; string value = normalizedInput.Value; if (value == null || value.Length < minimumInputLength) { return false; } if (leadingLiteralPrefix != null && !StartsWith(value, 0, leadingLiteralPrefix)) { return false; } if (trailingLiteralSuffix != null && !EndsWith(value, trailingLiteralSuffix)) { return false; } int[] array = ((uniquePlaceholderCount == 0) ? Array.Empty() : new int[uniquePlaceholderCount]); int[] captureLengths = ((uniquePlaceholderCount == 0) ? Array.Empty() : new int[uniquePlaceholderCount]); for (int i = 0; i < array.Length; i++) { array[i] = -1; } if (!(hasConstrainedPlaceholders ? TryTranslateConstrained(value, 0, 0, array, captureLengths) : TryTranslateFast(value, array, captureLengths))) { return false; } translated = normalizedInput.WrapWholeMatch(BuildTranslatedOutput(value, array, captureLengths)); return true; } private bool TryTranslateFast(string normalizedValue, int[] captureStarts, int[] captureLengths) { int num = 0; for (int i = 0; i < matchTokens.Length; i++) { TemplateToken token = matchTokens[i]; if (!token.IsPlaceholder) { if (!StartsWith(normalizedValue, num, token.Value)) { return false; } num += token.Value.Length; continue; } string text = nextLiteralValues[i]; int captureStartIndex = num; int captureLength; if (text == null) { captureLength = normalizedValue.Length - num; num = normalizedValue.Length; } else { int num2 = normalizedValue.IndexOf(text, num, StringComparison.OrdinalIgnoreCase); if (num2 < 0) { return false; } captureLength = num2 - num; num = num2; } if (!TryStoreCapture(token, matchPlaceholderSlots[i], normalizedValue, captureStartIndex, captureLength, captureStarts, captureLengths, out var _, out var _, out var _)) { return false; } } return num == normalizedValue.Length; } private bool TryTranslateConstrained(string normalizedValue, int tokenIndex, int position, int[] captureStarts, int[] captureLengths) { if (tokenIndex >= matchTokens.Length) { return position == normalizedValue.Length; } TemplateToken templateToken = matchTokens[tokenIndex]; if (!templateToken.IsPlaceholder) { if (!StartsWith(normalizedValue, position, templateToken.Value)) { return false; } return TryTranslateConstrained(normalizedValue, tokenIndex + 1, position + templateToken.Value.Length, captureStarts, captureLengths); } string text = nextLiteralValues[tokenIndex]; if (text == null) { return TryMatchPlaceholderCandidate(normalizedValue, tokenIndex, position, normalizedValue.Length, captureStarts, captureLengths); } int num = position; while (num <= normalizedValue.Length) { int num2 = normalizedValue.IndexOf(text, num, StringComparison.OrdinalIgnoreCase); if (num2 < 0) { return false; } if (TryMatchPlaceholderCandidate(normalizedValue, tokenIndex, position, num2, captureStarts, captureLengths)) { return true; } num = num2 + 1; } return false; } private bool TryMatchPlaceholderCandidate(string normalizedValue, int tokenIndex, int captureStartIndex, int nextPosition, int[] captureStarts, int[] captureLengths) { int num = matchPlaceholderSlots[tokenIndex]; int captureLength = nextPosition - captureStartIndex; if (!TryStoreCapture(matchTokens[tokenIndex], num, normalizedValue, captureStartIndex, captureLength, captureStarts, captureLengths, out var previousStart, out var previousLength, out var assignedNew)) { return false; } bool num2 = TryTranslateConstrained(normalizedValue, tokenIndex + 1, nextPosition, captureStarts, captureLengths); if (!num2 && assignedNew) { captureStarts[num] = previousStart; captureLengths[num] = previousLength; } return num2; } private bool TryStoreCapture(TemplateToken token, int captureSlot, string normalizedValue, int captureStartIndex, int captureLength, int[] captureStarts, int[] captureLengths, out int previousStart, out int previousLength, out bool assignedNew) { previousStart = captureStarts[captureSlot]; previousLength = captureLengths[captureSlot]; assignedNew = false; if (previousStart >= 0 && !SegmentsEqual(normalizedValue, previousStart, previousLength, captureStartIndex, captureLength)) { return false; } if (token.Constraint != null && !token.Constraint.IsMatch(normalizedValue, captureStartIndex, captureLength)) { return false; } if (previousStart < 0) { captureStarts[captureSlot] = captureStartIndex; captureLengths[captureSlot] = captureLength; assignedNew = true; } return true; } private string BuildTranslatedOutput(string normalizedValue, int[] captureStarts, int[] captureLengths) { StringBuilder stringBuilder = new StringBuilder(normalizedValue.Length + 16); for (int i = 0; i < replaceTokens.Length; i++) { TemplateToken templateToken = replaceTokens[i]; if (!templateToken.IsPlaceholder) { stringBuilder.Append(templateToken.Value); continue; } int num = replacePlaceholderSlots[i]; if (num < 0 || num >= captureStarts.Length) { continue; } int num2 = captureStarts[num]; if (num2 >= 0) { int num3 = captureLengths[num]; if (templateToken.IsFunctionCall) { stringBuilder.Append(TranslationRuntime.ResolveTemplatePlaceholder(normalizedValue.Substring(num2, num3))); } else { stringBuilder.Append(normalizedValue, num2, num3); } } } return stringBuilder.ToString(); } private static bool SegmentsEqual(string value, int leftStart, int leftLength, int rightStart, int rightLength) { if (leftLength != rightLength) { return false; } if (leftLength == 0) { return true; } return string.Compare(value, leftStart, value, rightStart, leftLength, StringComparison.Ordinal) == 0; } private static bool HasConstrainedPlaceholders(TemplateToken[] tokens) { for (int i = 0; i < tokens.Length; i++) { if (tokens[i].Constraint != null) { return true; } } return false; } private static bool TryValidateRepeatedPlaceholderConstraints(string template, TemplateToken[] matchTokens, out string error) { error = null; Dictionary dictionary = null; for (int i = 0; i < matchTokens.Length; i++) { TemplateToken templateToken = matchTokens[i]; if (!templateToken.IsPlaceholder) { continue; } if (dictionary == null) { dictionary = new Dictionary(StringComparer.Ordinal); } string text = ((templateToken.Constraint == null) ? string.Empty : templateToken.Constraint.Signature); if (dictionary.TryGetValue(templateToken.Value, out var value)) { if (!string.Equals(value, text, StringComparison.Ordinal)) { error = "Template '" + template + "' uses placeholder '" + templateToken.Value + "' with conflicting constraints."; return false; } } else { dictionary[templateToken.Value] = text; } } return true; } private static void BuildPlaceholderSlots(TemplateToken[] matchTokens, TemplateToken[] replaceTokens, out int[] matchPlaceholderSlots, out int[] replacePlaceholderSlots, out int uniquePlaceholderCount) { matchPlaceholderSlots = new int[matchTokens.Length]; replacePlaceholderSlots = new int[replaceTokens.Length]; uniquePlaceholderCount = 0; Dictionary dictionary = null; for (int i = 0; i < matchTokens.Length; i++) { if (matchTokens[i].IsPlaceholder) { if (dictionary == null) { dictionary = new Dictionary(StringComparer.Ordinal); } if (!dictionary.TryGetValue(matchTokens[i].Value, out var value)) { value = uniquePlaceholderCount; uniquePlaceholderCount++; dictionary[matchTokens[i].Value] = value; } matchPlaceholderSlots[i] = value; } } for (int j = 0; j < replaceTokens.Length; j++) { if (replaceTokens[j].IsPlaceholder) { replacePlaceholderSlots[j] = -1; if (dictionary != null && dictionary.TryGetValue(replaceTokens[j].Value, out var value2)) { replacePlaceholderSlots[j] = value2; } } } } private static bool TryNormalizeMatchTokens(string template, TemplateToken[] sourceTokens, out TemplateToken[] normalizedTokens, out string error) { List list = new List(sourceTokens.Length); for (int i = 0; i < sourceTokens.Length; i++) { TemplateToken item = sourceTokens[i]; if (item.IsPlaceholder) { if (list.Count > 0 && list[list.Count - 1].IsPlaceholder) { normalizedTokens = Array.Empty(); error = "Template '" + template + "' becomes ambiguous after stripping rich-text tags."; return false; } list.Add(item); } else { string value = TextMatchNormalizer.Normalize(item.Value); if (!string.IsNullOrEmpty(value)) { list.Add(new TemplateToken(isPlaceholder: false, value)); } } } if (list.Count == 0) { normalizedTokens = Array.Empty(); error = "Template '" + template + "' becomes empty after stripping rich-text tags."; return false; } normalizedTokens = list.ToArray(); error = null; return true; } internal void SetDispatchOrder(int dispatchOrder) { DispatchOrder = dispatchOrder; } private static string[] BuildNextLiteralValues(TemplateToken[] tokens) { string[] array = new string[tokens.Length]; string text = null; for (int num = tokens.Length - 1; num >= 0; num--) { array[num] = text; if (!tokens[num].IsPlaceholder) { text = tokens[num].Value; } } return array; } private static bool StartsWith(string text, int startIndex, string value) { if (startIndex + value.Length > text.Length) { return false; } return string.Compare(text, startIndex, value, 0, value.Length, StringComparison.OrdinalIgnoreCase) == 0; } private static bool EndsWith(string text, string value) { if (value.Length > text.Length) { return false; } return string.Compare(text, text.Length - value.Length, value, 0, value.Length, StringComparison.OrdinalIgnoreCase) == 0; } private static void InitializeMatchMetadata(TemplateToken[] tokens, out string leadingLiteralPrefix, out string trailingLiteralSuffix, out string longestLiteralAnchor, out int minimumInputLength, out int placeholderCount, out int literalSegmentCount, out bool startsWithPlaceholder, out bool endsWithPlaceholder, out bool hasRepeatedPlaceholderNames) { leadingLiteralPrefix = null; trailingLiteralSuffix = null; longestLiteralAnchor = null; minimumInputLength = 0; placeholderCount = 0; literalSegmentCount = 0; startsWithPlaceholder = tokens.Length != 0 && tokens[0].IsPlaceholder; endsWithPlaceholder = tokens.Length != 0 && tokens[^1].IsPlaceholder; hasRepeatedPlaceholderNames = false; HashSet hashSet = null; for (int i = 0; i < tokens.Length; i++) { TemplateToken templateToken = tokens[i]; if (templateToken.IsPlaceholder) { placeholderCount++; if (hashSet == null) { hashSet = new HashSet(StringComparer.Ordinal); } if (!hashSet.Add(templateToken.Value)) { hasRepeatedPlaceholderNames = true; } continue; } literalSegmentCount++; minimumInputLength += templateToken.Value.Length; if (i == 0) { leadingLiteralPrefix = templateToken.Value; } if (i == tokens.Length - 1) { trailingLiteralSuffix = templateToken.Value; } if (longestLiteralAnchor == null || templateToken.Value.Length > longestLiteralAnchor.Length) { longestLiteralAnchor = templateToken.Value; } } } private static bool TryTokenize(string template, bool requirePlaceholder, bool allowFunctions, bool allowConstraints, bool allowAdjacentPlaceholders, out TemplateToken[] tokens, out string error) { tokens = Array.Empty(); error = null; if (string.IsNullOrWhiteSpace(template)) { error = "Template cannot be empty."; return false; } List list = new List(); int num = 0; bool flag = false; while (num < template.Length) { int num2 = template.IndexOf("{{", num, StringComparison.Ordinal); if (num2 < 0) { list.Add(new TemplateToken(isPlaceholder: false, template.Substring(num))); num = template.Length; break; } if (num2 > num) { list.Add(new TemplateToken(isPlaceholder: false, template.Substring(num, num2 - num))); } if (!TryParseToken(template, num2, allowFunctions, allowConstraints, out var token, out var nextCursor, out error)) { return false; } num = nextCursor; if (!allowAdjacentPlaceholders && list.Count > 0 && list[list.Count - 1].IsPlaceholder) { error = "Template '" + template + "' cannot contain adjacent placeholders."; return false; } list.Add(token); flag = true; } if (requirePlaceholder && !flag) { error = "Template '" + template + "' must contain at least one placeholder."; return false; } if (list.Count == 0) { list.Add(new TemplateToken(isPlaceholder: false, template)); } tokens = list.ToArray(); return true; } private static bool IsValidPlaceholderName(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } if (!IsValidPlaceholderStartCharacter(value[0])) { return false; } for (int i = 1; i < value.Length; i++) { if (!IsValidPlaceholderCharacter(value[i])) { return false; } } return true; } private static bool TryParseToken(string template, int tokenStart, bool allowFunctions, bool allowConstraints, out TemplateToken token, out int nextCursor, out string error) { token = default(TemplateToken); nextCursor = tokenStart; error = null; int num = template.IndexOf("}}", tokenStart + "{{".Length, StringComparison.Ordinal); if (num < 0) { error = "Template '" + template + "' has an unclosed function token."; return false; } string text = template.Substring(tokenStart + "{{".Length, num - tokenStart - "{{".Length).Trim(); if (string.IsNullOrWhiteSpace(text)) { error = "Template '" + template + "' contains an empty function token."; return false; } if (text[0] == '$') { if (!TryParsePlaceholderToken(template, text, allowConstraints, out token, out error)) { return false; } nextCursor = num + "}}".Length; return true; } if (!allowFunctions) { error = "Template '" + template + "' cannot use function tokens in the match template."; return false; } int num2 = text.IndexOf('('); int num3 = text.LastIndexOf(')'); if (num2 <= 0 || num3 != text.Length - 1) { error = "Template '" + template + "' has an invalid function token '" + text + "'."; return false; } string text2 = text.Substring(0, num2).Trim(); if (!string.Equals(text2, "localize", StringComparison.Ordinal)) { error = "Template '" + template + "' uses an unsupported function '" + text2 + "'."; return false; } string text3 = text.Substring(num2 + 1, num3 - num2 - 1).Trim(); if (string.IsNullOrWhiteSpace(text3)) { error = "Template '" + template + "' function '" + text2 + "' requires one placeholder argument."; return false; } string[] array = text3.Split(new char[1] { ',' }); if (array.Length != 1) { error = "Template '" + template + "' function '" + text2 + "' currently supports exactly one placeholder argument."; return false; } string text4 = array[0].Trim(); if (text4.Length < 2 || text4[0] != '$') { error = "Template '" + template + "' function '" + text2 + "' must reference a placeholder like $name."; return false; } string text5 = text4.Substring(1).Trim(); if (!IsValidPlaceholderName(text5)) { error = "Template '" + template + "' has an invalid placeholder name '" + text5 + "'."; return false; } token = new TemplateToken(isPlaceholder: true, text5, isFunctionCall: true); nextCursor = num + "}}".Length; return true; } private static bool TryParsePlaceholderToken(string template, string tokenContent, bool allowConstraints, out TemplateToken token, out string error) { token = default(TemplateToken); error = null; string text = tokenContent.Substring(1).Trim(); int num = text.IndexOf(':'); string text2 = ((num < 0) ? text : text.Substring(0, num).Trim()); if (!IsValidPlaceholderName(text2)) { error = "Template '" + template + "' has an invalid placeholder name '" + text2 + "'."; return false; } PlaceholderConstraint constraint = null; if (num >= 0) { if (!allowConstraints) { error = "Template '" + template + "' cannot use constrained placeholders in the replacement template."; return false; } string text3 = text.Substring(num + 1).Trim(); if (string.IsNullOrWhiteSpace(text3)) { error = "Template '" + template + "' placeholder '" + text2 + "' has an empty constraint."; return false; } if (!TryParsePlaceholderConstraint(template, text2, text3, out constraint, out error)) { return false; } } token = new TemplateToken(isPlaceholder: true, text2, isFunctionCall: false, constraint); return true; } private static bool TryParsePlaceholderConstraint(string template, string placeholderName, string constraintText, out PlaceholderConstraint constraint, out string error) { constraint = null; error = null; int num = constraintText.IndexOf('('); string text = constraintText; string text2 = null; if (num >= 0) { int num2 = constraintText.LastIndexOf(')'); if (num == 0 || num2 != constraintText.Length - 1) { error = "Template '" + template + "' placeholder '" + placeholderName + "' has an invalid constraint '" + constraintText + "'."; return false; } text = constraintText.Substring(0, num).Trim(); text2 = constraintText.Substring(num + 1, num2 - num - 1).Trim(); } if (string.IsNullOrWhiteSpace(text)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' has an empty constraint type."; return false; } if (string.Equals(text, "word", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrWhiteSpace(text2)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint 'word' does not accept options."; return false; } constraint = PlaceholderConstraint.CreateWord(); return true; } if (string.Equals(text, "words", StringComparison.OrdinalIgnoreCase)) { if (!TryParseConstraintOptions(template, placeholderName, text, text2, out var options, out error)) { return false; } IntegerRange range = IntegerRange.AtLeast(1); if (options.TryGetValue("count", out var value) && !TryParseIntegerRangeOption(template, placeholderName, text, "count", value, allowZeroMinimum: false, out range, out error)) { return false; } if (!EnsureOnlySupportedOptions(template, placeholderName, text, options, "count", out error)) { return false; } constraint = PlaceholderConstraint.CreateWords(range); return true; } if (string.Equals(text, "int", StringComparison.OrdinalIgnoreCase)) { if (!TryParseConstraintOptions(template, placeholderName, text, text2, out var options2, out error)) { return false; } IntegerRange range2 = IntegerRange.AtLeast(1); if (options2.TryGetValue("digits", out var value2) && !TryParseIntegerRangeOption(template, placeholderName, text, "digits", value2, allowZeroMinimum: false, out range2, out error)) { return false; } NumericPredicate[] predicates = Array.Empty(); if (options2.TryGetValue("where", out var value3) && !TryParseNumericPredicates(template, placeholderName, text, value3, out predicates, out error)) { return false; } if (!EnsureOnlySupportedOptions(template, placeholderName, text, options2, "digits", "where", out error)) { return false; } constraint = PlaceholderConstraint.CreateInteger(range2, predicates); return true; } if (string.Equals(text, "float", StringComparison.OrdinalIgnoreCase)) { if (!TryParseConstraintOptions(template, placeholderName, text, text2, out var options3, out error)) { return false; } IntegerRange range3 = IntegerRange.AtLeast(1); if (options3.TryGetValue("int", out var value4) && !TryParseIntegerRangeOption(template, placeholderName, text, "int", value4, allowZeroMinimum: false, out range3, out error)) { return false; } IntegerRange range4 = IntegerRange.AtLeast(0); if (options3.TryGetValue("frac", out var value5) && !TryParseIntegerRangeOption(template, placeholderName, text, "frac", value5, allowZeroMinimum: true, out range4, out error)) { return false; } NumericPredicate[] predicates2 = Array.Empty(); if (options3.TryGetValue("where", out var value6) && !TryParseNumericPredicates(template, placeholderName, text, value6, out predicates2, out error)) { return false; } if (!EnsureOnlySupportedOptions(template, placeholderName, text, options3, "int", "frac", "where", out error)) { return false; } constraint = PlaceholderConstraint.CreateFloat(range3, range4, predicates2); return true; } if (string.Equals(text, "oneof", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(text2)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint 'oneof' requires at least one value."; return false; } string[] array = text2.Split(new char[1] { '|' }); List list = new List(array.Length); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); if (string.IsNullOrEmpty(text3)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint 'oneof' contains an empty option."; return false; } list.Add(text3); } constraint = PlaceholderConstraint.CreateOneOf(list.ToArray()); return true; } error = "Template '" + template + "' placeholder '" + placeholderName + "' uses an unsupported constraint type '" + text + "'."; return false; } private static bool TryParseConstraintOptions(string template, string placeholderName, string constraintType, string argumentText, out Dictionary options, out string error) { options = new Dictionary(StringComparer.OrdinalIgnoreCase); error = null; if (string.IsNullOrWhiteSpace(argumentText)) { return true; } string[] array = argumentText.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (string.IsNullOrEmpty(text)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' contains an empty option."; return false; } int num = text.IndexOf('='); if (num <= 0 || num == text.Length - 1) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' has an invalid option '" + text + "'."; return false; } string text2 = text.Substring(0, num).Trim(); string value = text.Substring(num + 1).Trim(); if (string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(value)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' has an invalid option '" + text + "'."; return false; } if (options.ContainsKey(text2)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' repeats option '" + text2 + "'."; return false; } options[text2] = value; } return true; } private static bool EnsureOnlySupportedOptions(string template, string placeholderName, string constraintType, Dictionary options, string supportedOption, out string error) { return EnsureOnlySupportedOptions(template, placeholderName, constraintType, options, new string[1] { supportedOption }, out error); } private static bool EnsureOnlySupportedOptions(string template, string placeholderName, string constraintType, Dictionary options, string supportedOptionOne, string supportedOptionTwo, out string error) { return EnsureOnlySupportedOptions(template, placeholderName, constraintType, options, new string[2] { supportedOptionOne, supportedOptionTwo }, out error); } private static bool EnsureOnlySupportedOptions(string template, string placeholderName, string constraintType, Dictionary options, string supportedOptionOne, string supportedOptionTwo, string supportedOptionThree, out string error) { return EnsureOnlySupportedOptions(template, placeholderName, constraintType, options, new string[3] { supportedOptionOne, supportedOptionTwo, supportedOptionThree }, out error); } private static bool EnsureOnlySupportedOptions(string template, string placeholderName, string constraintType, Dictionary options, string[] supportedOptions, out string error) { error = null; HashSet hashSet = new HashSet(supportedOptions, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair option in options) { if (!hashSet.Contains(option.Key)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' does not support option '" + option.Key + "'."; return false; } } return true; } private static bool TryParseIntegerRangeOption(string template, string placeholderName, string constraintType, string optionName, string valueText, bool allowZeroMinimum, out IntegerRange range, out string error) { if (!TryParseIntegerRange(valueText, allowZeroMinimum, out range, out error)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' has an invalid '" + optionName + "' range: " + error; return false; } return true; } private static bool TryParseIntegerRange(string valueText, bool allowZeroMinimum, out IntegerRange range, out string error) { range = default(IntegerRange); error = null; if (string.IsNullOrWhiteSpace(valueText)) { error = "range cannot be empty."; return false; } string text = valueText.Trim(); int num = text.IndexOf("..", StringComparison.Ordinal); if (num < 0) { if (!TryParseBoundValue(text, allowZeroMinimum, out var value, out error)) { return false; } range = IntegerRange.Exact(value); return true; } string text2 = text.Substring(0, num).Trim(); string text3 = text.Substring(num + 2).Trim(); bool flag = !string.IsNullOrEmpty(text2); bool flag2 = !string.IsNullOrEmpty(text3); if (!flag && !flag2) { error = "range must specify at least one bound."; return false; } int value2 = 0; if (flag && !TryParseBoundValue(text2, allowZeroMinimum, out value2, out error)) { return false; } int value3 = 0; if (flag2 && !TryParseBoundValue(text3, allowZeroMinimum: true, out value3, out error)) { return false; } if (flag && flag2 && value2 > value3) { error = "range minimum cannot be greater than maximum."; return false; } range = new IntegerRange(flag, value2, flag2, value3); return true; } private static bool TryParseBoundValue(string valueText, bool allowZeroMinimum, out int value, out string error) { value = 0; error = null; if (!int.TryParse(valueText, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { error = "'" + valueText + "' is not a valid integer."; return false; } if (value < 0) { error = "range values must be non-negative."; return false; } if (!allowZeroMinimum && value == 0) { error = "range values must be greater than zero."; return false; } return true; } private static bool TryParseNumericPredicates(string template, string placeholderName, string constraintType, string whereText, out NumericPredicate[] predicates, out string error) { predicates = Array.Empty(); error = null; if (string.IsNullOrWhiteSpace(whereText)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' has an empty 'where' clause."; return false; } string[] array = whereText.Split(new string[1] { "&&" }, StringSplitOptions.None); List list = new List(array.Length); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (string.IsNullOrEmpty(text)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' contains an empty numeric predicate."; return false; } if (!TryParseNumericPredicate(text, out var predicate, out var error2)) { error = "Template '" + template + "' placeholder '" + placeholderName + "' constraint '" + constraintType + "' has an invalid numeric predicate '" + text + "': " + error2; return false; } list.Add(predicate); } predicates = list.ToArray(); return true; } private static bool TryParseNumericPredicate(string clause, out NumericPredicate predicate, out string error) { predicate = default(NumericPredicate); error = null; NumericComparisonOperator comparisonOperator; string text; if (clause.StartsWith(">=", StringComparison.Ordinal)) { comparisonOperator = NumericComparisonOperator.GreaterThanOrEqual; text = clause.Substring(2).Trim(); } else if (clause.StartsWith("<=", StringComparison.Ordinal)) { comparisonOperator = NumericComparisonOperator.LessThanOrEqual; text = clause.Substring(2).Trim(); } else if (clause.StartsWith("!=", StringComparison.Ordinal)) { comparisonOperator = NumericComparisonOperator.NotEqual; text = clause.Substring(2).Trim(); } else if (clause.StartsWith("==", StringComparison.Ordinal)) { comparisonOperator = NumericComparisonOperator.Equal; text = clause.Substring(2).Trim(); } else if (clause.StartsWith(">", StringComparison.Ordinal)) { comparisonOperator = NumericComparisonOperator.GreaterThan; text = clause.Substring(1).Trim(); } else { if (!clause.StartsWith("<", StringComparison.Ordinal)) { error = "predicate must start with one of <, <=, >, >=, ==, or !=."; return false; } comparisonOperator = NumericComparisonOperator.LessThan; text = clause.Substring(1).Trim(); } if (!decimal.TryParse(text, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result)) { error = "'" + text + "' is not a valid numeric value."; return false; } predicate = new NumericPredicate(comparisonOperator, result); return true; } private static bool IsValidPlaceholderStartCharacter(char character) { if (!char.IsLetter(character)) { return character == '_'; } return true; } private static bool IsValidPlaceholderCharacter(char character) { if (!char.IsLetterOrDigit(character)) { return character == '_'; } return true; } private static bool IsWordCharacter(char character) { if (!char.IsLetterOrDigit(character) && character != '_' && character != '-') { return character == '\''; } return true; } } internal static class GameTranslationResolver { private enum CacheEvictionKind { Resolve, ResolveMiss, Localize, LocalizeMiss, ItemNameToken, ItemNameTokenMiss } private const int MaxResolveCacheEntries = 4096; private const int MaxResolveMissCacheEntries = 2048; private const int MaxLocalizeCacheEntries = 4096; private const int MaxLocalizeMissCacheEntries = 2048; private const int MaxItemNameTokenCacheEntries = 4096; private const int MaxItemNameTokenMissCacheEntries = 2048; private static readonly Type ObjectDbType = AccessTools.TypeByName("ObjectDB"); private static readonly Type ZNetSceneType = AccessTools.TypeByName("ZNetScene"); private static readonly Type ItemDropType = AccessTools.TypeByName("ItemDrop"); private static readonly Type LocalizationType = AccessTools.TypeByName("Localization"); private static readonly FieldInfo ObjectDbInstanceField = AccessTools.Field(ObjectDbType, "instance"); private static readonly PropertyInfo ObjectDbInstanceProperty = AccessTools.Property(ObjectDbType, "instance"); private static readonly MethodInfo GetItemPrefabMethod = AccessTools.Method(ObjectDbType, "GetItemPrefab", new Type[1] { typeof(string) }, (Type[])null); private static readonly FieldInfo ZNetSceneInstanceField = AccessTools.Field(ZNetSceneType, "instance"); private static readonly PropertyInfo ZNetSceneInstanceProperty = AccessTools.Property(ZNetSceneType, "instance"); private static readonly MethodInfo GetPrefabMethod = AccessTools.Method(ZNetSceneType, "GetPrefab", new Type[1] { typeof(string) }, (Type[])null); private static readonly FieldInfo ItemDataField = AccessTools.Field(ItemDropType, "m_itemData"); private static readonly FieldInfo SharedDataField = AccessTools.Field(AccessTools.Inner(ItemDropType, "ItemData") ?? AccessTools.TypeByName("ItemDrop+ItemData"), "m_shared"); private static readonly FieldInfo SharedNameField = AccessTools.Field(AccessTools.Inner(ItemDropType, "ItemData+SharedData") ?? AccessTools.TypeByName("ItemDrop+ItemData+SharedData"), "m_name"); private static readonly FieldInfo LocalizationInstanceField = AccessTools.Field(LocalizationType, "instance"); private static readonly PropertyInfo LocalizationInstanceProperty = AccessTools.Property(LocalizationType, "instance"); private static readonly MethodInfo LocalizeMethod = AccessTools.Method(LocalizationType, "Localize", new Type[1] { typeof(string) }, (Type[])null); private static readonly BoundedStringDictionary resolveCache = new BoundedStringDictionary(4096, StringComparer.Ordinal); private static readonly BoundedStringSet resolveMissCache = new BoundedStringSet(2048, StringComparer.Ordinal); private static readonly BoundedStringDictionary localizeCache = new BoundedStringDictionary(4096, StringComparer.Ordinal); private static readonly BoundedStringSet localizeMissCache = new BoundedStringSet(2048, StringComparer.Ordinal); private static readonly BoundedStringDictionary itemNameTokenCache = new BoundedStringDictionary(4096, StringComparer.Ordinal); private static readonly BoundedStringSet itemNameTokenMissCache = new BoundedStringSet(2048, StringComparer.Ordinal); public static bool TryResolve(string input, out string resolved) { resolved = null; if (string.IsNullOrWhiteSpace(input)) { return false; } if (resolveCache.TryGetValue(input, out resolved)) { return true; } if (resolveMissCache.Contains(input)) { return false; } if (!TryResolveCore(input, out resolved)) { AddMiss(resolveMissCache, CacheEvictionKind.ResolveMiss, input); return false; } AddValue(resolveCache, CacheEvictionKind.Resolve, input, resolved); return true; } internal static void ClearCaches() { resolveCache.Clear(); resolveMissCache.Clear(); localizeCache.Clear(); localizeMissCache.Clear(); itemNameTokenCache.Clear(); itemNameTokenMissCache.Clear(); } private static bool TryResolveCore(string input, out string resolved) { resolved = null; if (TryLocalize(input, out resolved)) { return true; } if (!TryResolveItemNameToken(input, out var itemNameToken)) { return false; } if (TryLocalize(itemNameToken, out resolved)) { return true; } if (!string.Equals(itemNameToken, input, StringComparison.Ordinal)) { resolved = itemNameToken; return true; } return false; } private static bool TryResolveItemNameToken(string prefabName, out string itemNameToken) { itemNameToken = null; if (itemNameTokenCache.TryGetValue(prefabName, out itemNameToken)) { return true; } if (itemNameTokenMissCache.Contains(prefabName)) { return false; } GameObject val = TryGetItemPrefab(prefabName) ?? TryGetScenePrefab(prefabName); if ((Object)(object)val == (Object)null || ItemDropType == null) { AddMiss(itemNameTokenMissCache, CacheEvictionKind.ItemNameTokenMiss, prefabName); return false; } Component component = val.GetComponent(ItemDropType); if ((Object)(object)component == (Object)null || ItemDataField == null || SharedDataField == null || SharedNameField == null) { AddMiss(itemNameTokenMissCache, CacheEvictionKind.ItemNameTokenMiss, prefabName); return false; } object value = ItemDataField.GetValue(component); if (value == null) { AddMiss(itemNameTokenMissCache, CacheEvictionKind.ItemNameTokenMiss, prefabName); return false; } object value2 = SharedDataField.GetValue(value); if (value2 == null) { AddMiss(itemNameTokenMissCache, CacheEvictionKind.ItemNameTokenMiss, prefabName); return false; } itemNameToken = SharedNameField.GetValue(value2) as string; if (string.IsNullOrWhiteSpace(itemNameToken)) { AddMiss(itemNameTokenMissCache, CacheEvictionKind.ItemNameTokenMiss, prefabName); return false; } AddValue(itemNameTokenCache, CacheEvictionKind.ItemNameToken, prefabName, itemNameToken); return true; } private static GameObject TryGetItemPrefab(string prefabName) { object obj = ObjectDbInstanceField?.GetValue(null) ?? ObjectDbInstanceProperty?.GetValue(null, null); if (obj == null || GetItemPrefabMethod == null) { return null; } object? obj2 = GetItemPrefabMethod.Invoke(obj, new object[1] { prefabName }); return (GameObject)((obj2 is GameObject) ? obj2 : null); } private static GameObject TryGetScenePrefab(string prefabName) { object obj = ZNetSceneInstanceField?.GetValue(null) ?? ZNetSceneInstanceProperty?.GetValue(null, null); if (obj == null || GetPrefabMethod == null) { return null; } object? obj2 = GetPrefabMethod.Invoke(obj, new object[1] { prefabName }); return (GameObject)((obj2 is GameObject) ? obj2 : null); } private static bool TryLocalize(string input, out string localized) { localized = null; if (localizeCache.TryGetValue(input, out localized)) { return true; } if (localizeMissCache.Contains(input)) { return false; } object obj = LocalizationInstanceField?.GetValue(null) ?? LocalizationInstanceProperty?.GetValue(null, null); if (obj == null || LocalizeMethod == null) { return false; } try { localized = LocalizeMethod.Invoke(obj, new object[1] { input }) as string; } catch (Exception ex) { Exception exception = ex; PluginMain.DebugLog(() => "Game localization lookup failed for '" + PluginMain.FormatTextForDebug(input) + "': " + exception.Message); return false; } if (string.IsNullOrWhiteSpace(localized) || string.Equals(localized, input, StringComparison.Ordinal)) { AddMiss(localizeMissCache, CacheEvictionKind.LocalizeMiss, input); localized = null; return false; } AddValue(localizeCache, CacheEvictionKind.Localize, input, localized); return true; } private static void AddValue(BoundedStringDictionary cache, CacheEvictionKind evictionKind, string key, string value) { if (cache.Set(key, value, out var _)) { RecordEviction(evictionKind); } } private static void AddMiss(BoundedStringSet cache, CacheEvictionKind evictionKind, string key) { if (cache.Add(key, out var evictedKey) && evictedKey != null) { RecordEviction(evictionKind); } } private static void RecordEviction(CacheEvictionKind evictionKind) { switch (evictionKind) { case CacheEvictionKind.Resolve: TranslationPerformanceMetrics.RecordGameResolveCacheEviction(); break; case CacheEvictionKind.ResolveMiss: TranslationPerformanceMetrics.RecordGameResolveMissCacheEviction(); break; case CacheEvictionKind.Localize: TranslationPerformanceMetrics.RecordGameLocalizeCacheEviction(); break; case CacheEvictionKind.LocalizeMiss: TranslationPerformanceMetrics.RecordGameLocalizeMissCacheEviction(); break; case CacheEvictionKind.ItemNameToken: TranslationPerformanceMetrics.RecordGameItemNameTokenCacheEviction(); break; case CacheEvictionKind.ItemNameTokenMiss: TranslationPerformanceMetrics.RecordGameItemNameTokenMissCacheEviction(); break; } } } internal static class PluginLocalization { private const string EnglishLanguage = "English"; private const string TraditionalChineseLanguage = "Chinese_Trad"; private const string FileNamePrefix = "Localization-"; private const string FileExtension = ".json"; private const double ReloadDebounceMilliseconds = 250.0; private static readonly IReadOnlyList> DefaultEntries = BuildDefaultEntries(); private static readonly Dictionary DefaultValuesByKey = BuildDefaultValuesByKey(); private static readonly Dictionary DefaultLegacyValuesByKey = BuildDefaultLegacyValuesByKey(); private static readonly Dictionary TraditionalChineseValuesByKey = BuildTraditionalChineseValuesByKey(); private static readonly Dictionary TraditionalChineseLegacyValuesByKey = BuildTraditionalChineseLegacyValuesByKey(); private static readonly Dictionary DefaultKeysByFallback = BuildDefaultKeysByFallback(); private static Dictionary localizedValues = new Dictionary(StringComparer.Ordinal); private static FileSystemWatcher watcher; private static System.Timers.Timer reloadDebounceTimer; private static bool initialized; private static string currentLanguage = "English"; internal static event Action Reloaded; internal static void Initialize() { if (!initialized) { initialized = true; currentLanguage = ResolveLanguage(null); Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); ReloadCurrentLanguage(); SetupWatcher(); } } internal static void Shutdown() { if (initialized) { if (watcher != null) { watcher.EnableRaisingEvents = false; watcher.Changed -= OnLocalizationFileChanged; watcher.Created -= OnLocalizationFileChanged; watcher.Deleted -= OnLocalizationFileChanged; watcher.Renamed -= OnLocalizationFileRenamed; watcher.Dispose(); watcher = null; } if (reloadDebounceTimer != null) { reloadDebounceTimer.Stop(); reloadDebounceTimer.Elapsed -= OnReloadDebounceElapsed; reloadDebounceTimer.Dispose(); reloadDebounceTimer = null; } localizedValues = new Dictionary(StringComparer.Ordinal); initialized = false; currentLanguage = "English"; } } internal static void NotifyLanguageChanged(string languageName) { currentLanguage = ResolveLanguage(languageName); ReloadCurrentLanguage(); } internal static void NotifyLanguageReset() { NotifyLanguageChanged("English"); } internal static string Get(string key, string fallback) { if (!string.IsNullOrWhiteSpace(key) && localizedValues.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { return value; } if (!string.IsNullOrWhiteSpace(fallback)) { return fallback; } return key ?? string.Empty; } internal static string Format(string key, string fallback, params object[] args) { string text = Get(key, fallback); if (args != null && args.Length != 0) { return string.Format(CultureInfo.InvariantCulture, text, args); } return text; } internal static string TranslateFallback(string fallback) { if (string.IsNullOrEmpty(fallback)) { return fallback ?? string.Empty; } if (!DefaultKeysByFallback.TryGetValue(fallback, out var value)) { return fallback; } return Get(value, fallback); } private static void SetupWatcher() { watcher = new FileSystemWatcher(TranslationRuleLoader.ConfigRootPath, "Localization-*.json") { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime), SynchronizingObject = ThreadingHelper.SynchronizingObject }; watcher.Changed += OnLocalizationFileChanged; watcher.Created += OnLocalizationFileChanged; watcher.Deleted += OnLocalizationFileChanged; watcher.Renamed += OnLocalizationFileRenamed; watcher.EnableRaisingEvents = true; reloadDebounceTimer = new System.Timers.Timer(250.0) { AutoReset = false, SynchronizingObject = ThreadingHelper.SynchronizingObject }; reloadDebounceTimer.Elapsed += OnReloadDebounceElapsed; } private static void OnLocalizationFileChanged(object sender, FileSystemEventArgs e) { ScheduleReload(); } private static void OnLocalizationFileRenamed(object sender, RenamedEventArgs e) { ScheduleReload(); } private static void ScheduleReload() { if (reloadDebounceTimer == null) { ReloadCurrentLanguage(); return; } reloadDebounceTimer.Stop(); reloadDebounceTimer.Start(); } private static void OnReloadDebounceElapsed(object sender, ElapsedEventArgs e) { ReloadCurrentLanguage(); } private static void ReloadCurrentLanguage() { string text = (currentLanguage = ResolveLanguage(currentLanguage)); try { Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); Dictionary dictionary = EnsureFileAndLoad("English", GetFilePath("English"), DefaultValuesByKey); EnsureFileAndLoad("Chinese_Trad", GetFilePath("Chinese_Trad"), TraditionalChineseValuesByKey); Dictionary dictionary2 = new Dictionary(dictionary, StringComparer.Ordinal); if (!string.Equals(text, "English", StringComparison.Ordinal)) { foreach (KeyValuePair item in EnsureFileAndLoad(text, GetFilePath(text), GetSeedValuesForLanguage(text))) { if (!string.IsNullOrWhiteSpace(item.Value)) { dictionary2[item.Key] = item.Value; } } } localizedValues = dictionary2; } catch (Exception ex) { localizedValues = new Dictionary(DefaultValuesByKey, StringComparer.Ordinal); ManualLogSource log = PluginMain.Log; if (log != null) { log.LogError((object)("Failed to reload plugin localization files: " + ex.Message)); } } PluginLocalization.Reloaded?.Invoke(); } private static Dictionary EnsureFileAndLoad(string languageName, string filePath, IReadOnlyDictionary seedValues) { bool exists; Dictionary dictionary = TryLoadFile(filePath, out exists) ?? new Dictionary(StringComparer.Ordinal); IReadOnlyDictionary legacyValuesForLanguage = GetLegacyValuesForLanguage(languageName); bool flag = !exists; foreach (KeyValuePair defaultEntry in DefaultEntries) { string value; string text = ((seedValues != null && seedValues.TryGetValue(defaultEntry.Key, out value)) ? value : defaultEntry.Value); if (!dictionary.ContainsKey(defaultEntry.Key)) { dictionary[defaultEntry.Key] = text; flag = true; } else if (ShouldUpdateSeedValue(defaultEntry.Key, dictionary[defaultEntry.Key], defaultEntry.Value, text, legacyValuesForLanguage)) { dictionary[defaultEntry.Key] = text; flag = true; } } if (flag) { File.WriteAllText(filePath, JsonConvert.SerializeObject((object)dictionary, (Formatting)1)); } return dictionary; } private static Dictionary TryLoadFile(string filePath, out bool exists) { exists = File.Exists(filePath); if (!exists) { return null; } try { Dictionary dictionary = JsonConvert.DeserializeObject>(File.ReadAllText(filePath)); return (dictionary != null) ? new Dictionary(dictionary, StringComparer.Ordinal) : new Dictionary(StringComparer.Ordinal); } catch (Exception ex) { ManualLogSource log = PluginMain.Log; if (log != null) { log.LogError((object)("Failed to read plugin localization file '" + filePath + "': " + ex.Message)); } return new Dictionary(StringComparer.Ordinal); } } private static IReadOnlyDictionary GetSeedValuesForLanguage(string languageName) { if (!string.Equals(languageName, "Chinese_Trad", StringComparison.Ordinal)) { return DefaultValuesByKey; } return TraditionalChineseValuesByKey; } private static IReadOnlyDictionary GetLegacyValuesForLanguage(string languageName) { if (!string.Equals(languageName, "Chinese_Trad", StringComparison.Ordinal)) { return DefaultLegacyValuesByKey; } return TraditionalChineseLegacyValuesByKey; } private static bool ShouldUpdateSeedValue(string key, string currentValue, string defaultValue, string seedValue, IReadOnlyDictionary legacyValues) { if (currentValue == null) { return true; } if (string.Equals(currentValue, seedValue, StringComparison.Ordinal)) { return false; } if (!string.Equals(seedValue, defaultValue, StringComparison.Ordinal) && string.Equals(currentValue, defaultValue, StringComparison.Ordinal)) { return true; } if (legacyValues != null && legacyValues.TryGetValue(key, out var value)) { return string.Equals(currentValue, value, StringComparison.Ordinal); } return false; } private static string ResolveLanguage(string languageName) { if (!string.IsNullOrWhiteSpace(languageName)) { return languageName; } string @string = PlayerPrefs.GetString("language", "English"); if (!string.IsNullOrWhiteSpace(@string)) { return @string; } return "English"; } private static string GetFilePath(string languageName) { return Path.Combine(TranslationRuleLoader.ConfigRootPath, "Localization-" + ResolveLanguage(languageName) + ".json"); } private static IReadOnlyList> BuildDefaultEntries() { return new KeyValuePair[252] { new KeyValuePair("common.no", "No"), new KeyValuePair("common.off", "Off"), new KeyValuePair("common.on", "On"), new KeyValuePair("common.yes", "Yes"), new KeyValuePair("common.clear", "Clear"), new KeyValuePair("common.search", "Search"), new KeyValuePair("common.debug.null", ""), new KeyValuePair("common.debug.truncated_suffix", "... [truncated]"), new KeyValuePair("common.unknown", ""), new KeyValuePair("plugin.loaded", "{0} loaded."), new KeyValuePair("plugin.loaded_dedicated", "{0} loaded in dedicated server mode. ServerSync remains active; translation runtime and UI patches are skipped."), new KeyValuePair("plugin.log.devtools_not_ready", "Localization devtools UI is not ready yet."), new KeyValuePair("plugin.log.devtools_update_failed", "Failed to update {0} from the devtools UI: {1}"), new KeyValuePair("runtime.analysis.final_output", "Final output: '{0}'"), new KeyValuePair("runtime.analysis.pass_result_loop", "Pass 2 result: a translation loop back to the original input was detected, so the pass 1 output was kept."), new KeyValuePair("runtime.analysis.pass_result_no_change", "Pass 1 result: no translation rule changed the input."), new KeyValuePair("runtime.analysis.pass_result_no_further_change", "Pass 2 result: no further translation rule changed the first pass output."), new KeyValuePair("runtime.analysis.pass_result_updated", "Pass 2 result: output updated to '{0}'."), new KeyValuePair("runtime.analysis.runtime_disabled", "Translation is currently disabled; analysis is using the current in-memory rule state."), new KeyValuePair("runtime.analysis.runtime_empty", "Analysis stopped because the selected string is empty."), new KeyValuePair("runtime.analysis.runtime_not_initialized", "Analysis stopped because the translation runtime is not initialized."), new KeyValuePair("runtime.analysis.runtime_pass_chain", "Top-level pass chain: fixed 2 passes. Additional whole string longest match rewrites are shown as step N inside a pass."), new KeyValuePair("runtime.analysis.runtime_state", "Runtime State"), new KeyValuePair("runtime.analysis.runtime_initialized", "Runtime initialized: {0}"), new KeyValuePair("runtime.analysis.translation_enabled", "Translation enabled: {0}"), new KeyValuePair("runtime.analysis.pass_title_one", "Pass 1"), new KeyValuePair("runtime.analysis.pass_title_two", "Pass 2"), new KeyValuePair("runtime.analysis.final_title", "Final"), new KeyValuePair("runtime.analysis.stage.exact", "Exact"), new KeyValuePair("runtime.analysis.stage.normalized_exact", "Normalized Exact"), new KeyValuePair("runtime.analysis.stage.template", "Template"), new KeyValuePair("runtime.analysis.stage.whole_string_longest_match", "Whole String Longest Match"), new KeyValuePair("runtime.analysis.pass_stage_not_executed", "{0} {1}: not executed ({2} already produced output)"), new KeyValuePair("runtime.analysis.stage_summary_output", "Output: '{0}'"), new KeyValuePair("runtime.analysis.stage_summary_miss", "No rule matched."), new KeyValuePair("runtime.analysis.stage_summary_not_executed", "Skipped because {0} already produced output."), new KeyValuePair("runtime.analysis.pass_input", "{0} input: '{1}'"), new KeyValuePair("runtime.analysis.pass_exact_hit", "{0} exact: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'"), new KeyValuePair("runtime.analysis.pass_exact_miss", "{0} exact: miss"), new KeyValuePair("runtime.analysis.pass_normalized_input", "{0} normalized input: '{1}'"), new KeyValuePair("runtime.analysis.pass_normalized_exact_hit", "{0} normalized exact: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'"), new KeyValuePair("runtime.analysis.pass_normalized_exact_miss", "{0} normalized exact: miss"), new KeyValuePair("runtime.analysis.pass_template_hit", "{0} template: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'"), new KeyValuePair("runtime.analysis.pass_template_miss", "{0} template: miss"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_step_input", "{0} whole string longest match step {1} input: '{2}'"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_step_hit", "{0} whole string longest match step {1} hit [{2}..{3}): target='{4}', match='{5}', replace='{6}', file='{7}'"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_step_output", "{0} whole string longest match step {1} output: '{2}'"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_loop", "{0} whole string longest match: detected a loop and kept the last non-loop output."), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_final", "{0} whole string longest match: final -> '{1}'"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_matched", "{0} whole string longest match: matched -> '{1}'"), new KeyValuePair("runtime.analysis.pass_whole_string_longest_match_miss", "{0} whole string longest match: miss"), new KeyValuePair("runtime.log.config_root", "Translation config root: {0}"), new KeyValuePair("runtime.log.disabled_after_reason", "Translation runtime disabled; skipped rule loading after {0}."), new KeyValuePair("runtime.log.load_result", "{0} translation rules for '{1}'. English='{2}' (local={3}, synced={4}), Active='{5}' (local={6}, synced={7}), exact={8}, templates={9}."), new KeyValuePair("runtime.settings.read_failed", "Failed to read runtime settings from '{0}': {1}"), new KeyValuePair("runtime.settings.backfill_failed", "Failed to backfill missing runtime settings in '{0}': {1}"), new KeyValuePair("runtime.settings.summary", "Runtime settings via '{0}' ({1}): translation_enabled={2}, debug={3}, whole_string_longest_match_mode={4}, whole_string_longest_match_min_length={5}, trace_feed_max_entries={6}."), new KeyValuePair("serversync.log.source_of_truth", "ServerSync source-of-truth enabled; local config hot reload is active."), new KeyValuePair("serversync.log.snapshot_published", "Published local translation snapshot after {0}: {1} language folder(s), {2} file(s)."), new KeyValuePair("devtools.app.title", "Localization Devtools"), new KeyValuePair("devtools.page.browser", "Browser"), new KeyValuePair("devtools.page.performance", "Performance"), new KeyValuePair("devtools.page.playground", "Playground"), new KeyValuePair("devtools.page.plugin", "Plugin"), new KeyValuePair("devtools.page.trace", "Trace"), new KeyValuePair("devtools.plugin.section.overview", "Overview"), new KeyValuePair("devtools.plugin.section.devtools", "Devtools"), new KeyValuePair("devtools.plugin.section.runtime", "Runtime"), new KeyValuePair("devtools.plugin.section.status", "Status"), new KeyValuePair("devtools.plugin.row.plugin_version", "Plugin Version"), new KeyValuePair("devtools.plugin.row.current_language", "Current Language"), new KeyValuePair("devtools.plugin.row.remote_snapshot", "Remote Snapshot"), new KeyValuePair("devtools.plugin.row.overlay_enabled", "Overlay Enabled"), new KeyValuePair("devtools.plugin.row.toggle_shortcut", "Toggle Shortcut"), new KeyValuePair("devtools.plugin.row.close_actions", "Close Actions"), new KeyValuePair("devtools.plugin.row.translation_enabled", "Translation Enabled"), new KeyValuePair("devtools.plugin.row.debug_enabled", "Debug Enabled"), new KeyValuePair("devtools.plugin.row.whole_string_longest_match_mode", "Whole String Longest Match Mode"), new KeyValuePair("devtools.plugin.row.minimum_match_length", "Minimum Match Length"), new KeyValuePair("devtools.plugin.row.trace_feed_capacity", "Trace Feed Capacity"), new KeyValuePair("devtools.plugin.row.hot_update", "Hot Update"), new KeyValuePair("devtools.plugin.row.shortcut_capture", "Shortcut Capture"), new KeyValuePair("devtools.plugin.row.last_config_action", "Last Config Action"), new KeyValuePair("devtools.plugin.action.capture", "Capture"), new KeyValuePair("devtools.plugin.action.next", "Next"), new KeyValuePair("devtools.plugin.action.prev", "Prev"), new KeyValuePair("devtools.plugin.action.reset", "Reset"), new KeyValuePair("devtools.plugin.action.toggle", "Toggle"), new KeyValuePair("devtools.plugin.close_actions_value", "ESC"), new KeyValuePair("devtools.plugin.hot_update_value", "Immediate"), new KeyValuePair("devtools.plugin.shortcut.capture_prompt", "Press a new shortcut..."), new KeyValuePair("devtools.plugin.shortcut.listening", "Listening (ESC cancels)"), new KeyValuePair("devtools.plugin.shortcut.idle", "Idle"), new KeyValuePair("devtools.plugin.status.ready", "Ready"), new KeyValuePair("devtools.plugin.status.capture_cancelled", "Shortcut capture cancelled."), new KeyValuePair("devtools.plugin.status.capture_started", "Press the new toggle shortcut. ESC cancels."), new KeyValuePair("devtools.plugin.status.toggle_shortcut_updated", "Toggle Shortcut -> {0}"), new KeyValuePair("devtools.plugin.status.overlay_enabled_updated", "Overlay Enabled -> {0}"), new KeyValuePair("devtools.plugin.status.translation_enabled_updated", "Translation Enabled -> {0}"), new KeyValuePair("devtools.plugin.status.debug_enabled_updated", "Debug Enabled -> {0}"), new KeyValuePair("devtools.plugin.status.whole_string_longest_match_mode_updated", "Whole String Longest Match Mode -> {0}"), new KeyValuePair("devtools.plugin.status.min_length_updated", "Minimum Match Length -> {0}"), new KeyValuePair("devtools.plugin.status.trace_feed_capacity_updated", "Trace Feed Capacity -> {0}"), new KeyValuePair("devtools.plugin.status.setting_failed", "{0} update failed: {1}"), new KeyValuePair("devtools.plugin.mode.off", "Off"), new KeyValuePair("devtools.plugin.mode.min_length", "Min Length"), new KeyValuePair("devtools.plugin.mode.per_word", "Per Word"), new KeyValuePair("devtools.performance.section.loaded_content", "Loaded Content"), new KeyValuePair("devtools.performance.section.hot_path", "Hot Path"), new KeyValuePair("devtools.performance.section.caches", "Caches"), new KeyValuePair("devtools.performance.section.timings", "Timings"), new KeyValuePair("devtools.performance.row.exact_rules", "Exact Rules"), new KeyValuePair("devtools.performance.row.template_rules", "Template Rules"), new KeyValuePair("devtools.performance.row.local_english_files", "Local English Files"), new KeyValuePair("devtools.performance.row.synced_english_files", "Synced English Files"), new KeyValuePair("devtools.performance.row.local_active_files", "Local Active Files"), new KeyValuePair("devtools.performance.row.synced_active_files", "Synced Active Files"), new KeyValuePair("devtools.performance.row.intercepts", "Intercepts"), new KeyValuePair("devtools.performance.row.reuse_hits", "Reuse Hits"), new KeyValuePair("devtools.performance.row.reuse_hit_rate", "Reuse Hit Rate"), new KeyValuePair("devtools.performance.row.known_output_skips", "Known Output Skips"), new KeyValuePair("devtools.performance.row.known_output_skip_rate", "Known Output Skip Rate"), new KeyValuePair("devtools.performance.row.translation_cache_hits", "Translation Cache Hits"), new KeyValuePair("devtools.performance.row.translation_cache_calls", "Translation Cache Calls"), new KeyValuePair("devtools.performance.row.translation_cache_hit_rate", "Translation Cache Rate"), new KeyValuePair("devtools.performance.row.translation_miss_cache_hits", "Miss Cache Hits"), new KeyValuePair("devtools.performance.row.translation_miss_cache_calls", "Miss Cache Calls"), new KeyValuePair("devtools.performance.row.miss_cache_hit_rate", "Miss Cache Rate"), new KeyValuePair("devtools.performance.row.normalizer_cache_hits", "Normalizer Cache Hits"), new KeyValuePair("devtools.performance.row.normalizer_cache_calls", "Normalizer Cache Calls"), new KeyValuePair("devtools.performance.row.normalizer_cache_hit_rate", "Normalizer Cache Rate"), new KeyValuePair("devtools.performance.row.avg_template_candidates", "Avg Template Candidates"), new KeyValuePair("devtools.performance.row.translation_cache", "Translation Cache"), new KeyValuePair("devtools.performance.row.translation_miss_cache", "Translation Miss Cache"), new KeyValuePair("devtools.performance.row.placeholder_cache", "Placeholder Cache"), new KeyValuePair("devtools.performance.row.placeholder_miss_cache", "Placeholder Miss Cache"), new KeyValuePair("devtools.performance.row.runtime_evictions", "Runtime Evictions"), new KeyValuePair("devtools.performance.row.resolver_evictions", "Resolver Evictions"), new KeyValuePair("devtools.performance.row.avg_intercept", "Avg Intercept"), new KeyValuePair("devtools.performance.row.avg_translate", "Avg Translate"), new KeyValuePair("devtools.performance.row.avg_normalize", "Avg Normalize"), new KeyValuePair("devtools.performance.row.avg_template", "Avg Template"), new KeyValuePair("devtools.performance.row.avg_whole_string_longest_match", "Avg Whole String Longest Match"), new KeyValuePair("devtools.performance.row.template_attempts", "Template Attempts"), new KeyValuePair("devtools.performance.row.template_matches", "Template Hits"), new KeyValuePair("devtools.performance.row.template_constrained", "Template Constrained"), new KeyValuePair("devtools.performance.row.whole_string_longest_match_attempts", "Whole String Longest Match Attempts"), new KeyValuePair("devtools.performance.row.whole_string_longest_match_matches", "Whole String Longest Match Hits"), new KeyValuePair("devtools.performance.row.metrics", "Metrics"), new KeyValuePair("devtools.performance.status.cumulative", "Cumulative since startup"), new KeyValuePair("devtools.performance.status.reset_now", "Reset just now"), new KeyValuePair("devtools.performance.value.count", "Count"), new KeyValuePair("devtools.performance.value.hit", "Hit"), new KeyValuePair("devtools.performance.value.total", "Total"), new KeyValuePair("devtools.performance.value.rate", "Rate"), new KeyValuePair("devtools.performance.value.attempts", "Attempts"), new KeyValuePair("devtools.performance.value.matches", "Matches"), new KeyValuePair("devtools.performance.value.constrained", "Constrained"), new KeyValuePair("devtools.trace.section.how_to_use", "How To Use"), new KeyValuePair("devtools.trace.section.overview", "Overview"), new KeyValuePair("devtools.trace.header.feed", "Trace Feed"), new KeyValuePair("devtools.trace.header.analysis", "Translation Analysis"), new KeyValuePair("devtools.trace.header.selected_source", "Selected Source"), new KeyValuePair("devtools.trace.analysis.status.hit", "Hit"), new KeyValuePair("devtools.trace.analysis.status.miss", "Miss"), new KeyValuePair("devtools.trace.analysis.status.not_executed", "Not Executed"), new KeyValuePair("devtools.trace.button.start_feed", "Start Feed"), new KeyValuePair("devtools.trace.button.stop_feed", "Stop Feed"), new KeyValuePair("devtools.trace.button.copy_input", "Copy Input"), new KeyValuePair("devtools.trace.button.copy_report", "Copy Report"), new KeyValuePair("devtools.trace.report.title", "Translation Analysis Report"), new KeyValuePair("devtools.trace.feed.status", "Feed: {0} | Unique: {1} / {2}"), new KeyValuePair("devtools.trace.feed.unique_count", "Unique Strings: {0} / {1}"), new KeyValuePair("devtools.trace.search.placeholder", "Search captured strings or sources..."), new KeyValuePair("devtools.trace.overview.input", "Input: {0}"), new KeyValuePair("devtools.trace.overview.source", "Source: {0}"), new KeyValuePair("devtools.trace.overview.output", "Current Output: {0}"), new KeyValuePair("devtools.trace.usage.select", "Click a captured string on the left to run a fresh translation analysis."), new KeyValuePair("devtools.trace.usage.replay", "The analysis always replays the translation flow live for the selected input."), new KeyValuePair("devtools.trace.usage.capacity", "Feed only stores unique strings and stops at {0} entries."), new KeyValuePair("devtools.trace.source.empty", "Select a captured string to inspect its source."), new KeyValuePair("devtools.trace.ui.truncated_suffix", "... [truncated in UI]"), new KeyValuePair("devtools.browser.header.navigator", "Translation Browser"), new KeyValuePair("devtools.browser.header.details", "File Details"), new KeyValuePair("devtools.browser.search.placeholder", "Search file path, match, or replace..."), new KeyValuePair("devtools.browser.status.results", "Showing {0} / {1} files"), new KeyValuePair("devtools.browser.status.selection", "{0} | {1} exact / {2} template"), new KeyValuePair("devtools.browser.source.local", "Local"), new KeyValuePair("devtools.browser.source.server", "Server"), new KeyValuePair("devtools.browser.empty.no_results", "No translation files matched the current search."), new KeyValuePair("devtools.browser.empty.no_selection", "Select a translation file on the left to inspect its rules."), new KeyValuePair("devtools.browser.empty.no_rules", "This file does not contain any rules."), new KeyValuePair("devtools.browser.file.summary", "{0} exact / {1} template"), new KeyValuePair("devtools.browser.rule.title", "Rule #{0}"), new KeyValuePair("devtools.playground.section.scenario", "Scenario"), new KeyValuePair("devtools.playground.section.rule_editor", "Rule Editor"), new KeyValuePair("devtools.playground.section.enabled_rules", "Enabled Rules"), new KeyValuePair("devtools.playground.section.preview", "Preview"), new KeyValuePair("devtools.playground.section.generated_test_set", "Generated Test Set"), new KeyValuePair("devtools.playground.label.include_external_rules", "Include translation files loaded outside the Playground in the preview."), new KeyValuePair("devtools.playground.label.copy_json_help", "Copy only the saved playground rules as a bare Localization rule file."), new KeyValuePair("devtools.playground.field.test_input", "Test Input"), new KeyValuePair("devtools.playground.field.match", "Match"), new KeyValuePair("devtools.playground.field.replace", "Replace"), new KeyValuePair("devtools.playground.field.summary", "Summary"), new KeyValuePair("devtools.playground.field.matched_input", "Matched Input"), new KeyValuePair("devtools.playground.field.rule_editor_match_preview", "Rule Editor Match Preview"), new KeyValuePair("devtools.playground.field.output", "Output"), new KeyValuePair("devtools.playground.field.json_preview", "JSON Preview"), new KeyValuePair("devtools.playground.placeholder.test_input", "Enter text to evaluate against the active playground rule set."), new KeyValuePair("devtools.playground.placeholder.match", "Exact text or template match."), new KeyValuePair("devtools.playground.placeholder.replace", "Translated output for the match above."), new KeyValuePair("devtools.playground.button.add_rule", "Add Rule"), new KeyValuePair("devtools.playground.button.clear_rules", "Clear Rules"), new KeyValuePair("devtools.playground.button.copy_json", "Copy JSON"), new KeyValuePair("devtools.playground.button.remove", "Remove"), new KeyValuePair("devtools.playground.button.external_rules_on", "External Rules: On"), new KeyValuePair("devtools.playground.button.external_rules_off", "External Rules: Off"), new KeyValuePair("devtools.playground.button.rule_type", "Rule Type: {0}"), new KeyValuePair("devtools.playground.empty_rules", "No saved playground rules yet."), new KeyValuePair("devtools.playground.status.ready", "Ready."), new KeyValuePair("devtools.playground.status.include_external_rules_enabled", "Translation files loaded outside the Playground are now included in preview evaluation."), new KeyValuePair("devtools.playground.status.include_external_rules_disabled", "Preview evaluation now uses only Playground rules."), new KeyValuePair("devtools.playground.status.draft_mode_switched", "Draft mode switched to {0}."), new KeyValuePair("devtools.playground.status.rule_saved", "Saved {0} rule #{1}."), new KeyValuePair("devtools.playground.status.rules_cleared", "Cleared all saved playground rules."), new KeyValuePair("devtools.playground.status.rule_removed", "Removed {0} rule #{1}."), new KeyValuePair("devtools.playground.status.json_copied", "Copied playground JSON to clipboard."), new KeyValuePair("devtools.playground.status.output_empty", "Enter input to see the translated output."), new KeyValuePair("devtools.playground.status.draft_empty", "Draft empty."), new KeyValuePair("devtools.playground.status.draft_incomplete", "Draft incomplete. Both match and replace are required."), new KeyValuePair("devtools.playground.status.draft_invalid", "Template invalid: {0}"), new KeyValuePair("devtools.playground.status.draft_ready", "Draft ready as {0}."), new KeyValuePair("devtools.playground.status.no_input", "No input"), new KeyValuePair("devtools.playground.status.no_match", "No Match"), new KeyValuePair("devtools.playground.status.normalized_exact", "Normalized Exact"), new KeyValuePair("devtools.playground.status.whole_string_longest_match", "Whole String Longest Match"), new KeyValuePair("devtools.playground.status.highlight_prompt", "Enter input to see highlighted matches."), new KeyValuePair("devtools.playground.summary.saved_rules", "Saved rules: {0} ({1} exact / {2} template)"), new KeyValuePair("devtools.playground.summary.preview_rules_with_external", "Preview rule set: {0} exact / {1} template + external rules {2}"), new KeyValuePair("devtools.playground.summary.winning_stage", "Winning stage: {0}"), new KeyValuePair("devtools.playground.summary.stacked_result_winning_stage", "Stacked result winning stage: {0}"), new KeyValuePair("devtools.playground.summary.match_preview_scope", "Rule editor match preview uses only the current Match field. Output remains the full stacked rule result."), new KeyValuePair("devtools.playground.summary.draft", "Draft: {0}"), new KeyValuePair("devtools.playground.preview.no_input", "Enter test input to preview the current rule editor match."), new KeyValuePair("devtools.playground.preview.no_draft", "Fill Match and Replace to preview the current rule editor match."), new KeyValuePair("devtools.playground.preview.current_match", "Current Match field: {0}"), new KeyValuePair("devtools.playground.preview.applies", "Applies to current test input: {0}"), new KeyValuePair("devtools.playground.preview.stage", "Draft preview stage: {0}"), new KeyValuePair("devtools.playground.rule_row", "#{0} [{1}] {2} => {3}"), new KeyValuePair("devtools.playground.kind.exact", "Exact"), new KeyValuePair("devtools.playground.kind.template", "Template") }; } private static Dictionary BuildDefaultValuesByKey() { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int i = 0; i < DefaultEntries.Count; i++) { KeyValuePair keyValuePair = DefaultEntries[i]; dictionary[keyValuePair.Key] = keyValuePair.Value; } return dictionary; } private static Dictionary BuildDefaultLegacyValuesByKey() { return new Dictionary(StringComparer.Ordinal) { ["runtime.settings.summary"] = "Runtime settings via '{0}' ({1}): translation_enabled={2}, debug={3}, whole_string_longest_match_mode={4}, whole_string_longest_match_min_length={5}.", ["devtools.trace.usage.capacity"] = "Feed only stores unique strings and stops at 1024 entries.", ["devtools.browser.status.selection"] = "{0} / {1} / {2} | {3} exact / {4} template" }; } private static Dictionary BuildDefaultKeysByFallback() { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int i = 0; i < DefaultEntries.Count; i++) { KeyValuePair keyValuePair = DefaultEntries[i]; if (!string.IsNullOrWhiteSpace(keyValuePair.Value) && !dictionary.ContainsKey(keyValuePair.Value)) { dictionary.Add(keyValuePair.Value, keyValuePair.Key); } } return dictionary; } private static Dictionary BuildTraditionalChineseValuesByKey() { return new Dictionary(DefaultValuesByKey, StringComparer.Ordinal) { ["common.no"] = "否", ["common.off"] = "關", ["common.on"] = "開", ["common.yes"] = "是", ["common.clear"] = "清除", ["common.search"] = "搜尋", ["common.debug.truncated_suffix"] = "... [已截斷]", ["common.unknown"] = "<未知>", ["plugin.loaded"] = "{0} 已載入。", ["plugin.loaded_dedicated"] = "{0} 以專用伺服器模式載入。伺服器同步仍保持啟用;已略過翻譯執行階段與介面更新。", ["plugin.log.devtools_not_ready"] = "本地化開發工具介面尚未就緒。", ["plugin.log.devtools_update_failed"] = "無法從開發工具介面更新 {0}:{1}", ["runtime.analysis.final_output"] = "最終輸出:'{0}'", ["runtime.analysis.pass_result_loop"] = "第二輪結果:偵測到回到原始輸入的翻譯迴圈,因此保留第一輪輸出。", ["runtime.analysis.pass_result_no_change"] = "第一輪結果:沒有任何翻譯規則改變輸入。", ["runtime.analysis.pass_result_no_further_change"] = "第二輪結果:第一輪輸出未再被任何翻譯規則改變。", ["runtime.analysis.pass_result_updated"] = "第二輪結果:輸出更新為 '{0}'。", ["runtime.analysis.runtime_disabled"] = "翻譯目前已停用;分析仍會使用目前記憶體中的規則狀態。", ["runtime.analysis.runtime_empty"] = "分析已停止,因為選取的字串是空的。", ["runtime.analysis.runtime_not_initialized"] = "分析已停止,因為翻譯執行階段尚未初始化。", ["runtime.analysis.runtime_pass_chain"] = "最上層流程固定跑 2 輪。額外的整句最長匹配重寫會在每輪內顯示為步驟 N。", ["runtime.analysis.runtime_state"] = "執行階段狀態", ["runtime.analysis.runtime_initialized"] = "執行階段已初始化:{0}", ["runtime.analysis.translation_enabled"] = "翻譯已啟用:{0}", ["runtime.analysis.pass_title_one"] = "第一輪", ["runtime.analysis.pass_title_two"] = "第二輪", ["runtime.analysis.final_title"] = "最終結果", ["runtime.analysis.stage.exact"] = "精確匹配", ["runtime.analysis.stage.normalized_exact"] = "正規化精確匹配", ["runtime.analysis.stage.template"] = "樣板匹配", ["runtime.analysis.stage.whole_string_longest_match"] = "整句最長匹配", ["runtime.analysis.pass_stage_not_executed"] = "{0} {1}:未執行({2} 已先產生輸出)", ["runtime.analysis.stage_summary_output"] = "輸出:'{0}'", ["runtime.analysis.stage_summary_miss"] = "沒有規則命中。", ["runtime.analysis.stage_summary_not_executed"] = "已略過,因為 {0} 已先產生輸出。", ["runtime.analysis.pass_input"] = "{0} 輸入:'{1}'", ["runtime.analysis.pass_exact_hit"] = "{0} 精確匹配:命中 目標='{1}', 匹配='{2}', 替換='{3}', 檔案='{4}' -> '{5}'", ["runtime.analysis.pass_exact_miss"] = "{0} 精確匹配:未命中", ["runtime.analysis.pass_normalized_input"] = "{0} 正規化輸入:'{1}'", ["runtime.analysis.pass_normalized_exact_hit"] = "{0} 正規化精確匹配:命中 目標='{1}', 匹配='{2}', 替換='{3}', 檔案='{4}' -> '{5}'", ["runtime.analysis.pass_normalized_exact_miss"] = "{0} 正規化精確匹配:未命中", ["runtime.analysis.pass_template_hit"] = "{0} 樣板匹配:命中 目標='{1}', 匹配='{2}', 替換='{3}', 檔案='{4}' -> '{5}'", ["runtime.analysis.pass_template_miss"] = "{0} 樣板匹配:未命中", ["runtime.analysis.pass_whole_string_longest_match_step_input"] = "{0} 整句最長匹配 step {1} 輸入:'{2}'", ["runtime.analysis.pass_whole_string_longest_match_step_hit"] = "{0} 整句最長匹配 step {1} 命中 [{2}..{3}):目標='{4}', 匹配='{5}', 替換='{6}', 檔案='{7}'", ["runtime.analysis.pass_whole_string_longest_match_step_output"] = "{0} 整句最長匹配 step {1} 輸出:'{2}'", ["runtime.analysis.pass_whole_string_longest_match_loop"] = "{0} 整句最長匹配:偵測到迴圈,已保留最後一個非迴圈輸出。", ["runtime.analysis.pass_whole_string_longest_match_final"] = "{0} 整句最長匹配:最終 -> '{1}'", ["runtime.analysis.pass_whole_string_longest_match_matched"] = "{0} 整句最長匹配:命中 -> '{1}'", ["runtime.analysis.pass_whole_string_longest_match_miss"] = "{0} 整句最長匹配:未命中", ["runtime.log.config_root"] = "翻譯設定根目錄:{0}", ["runtime.log.disabled_after_reason"] = "翻譯執行階段已停用;在 {0} 後略過規則載入。", ["runtime.log.load_result"] = "{0} '{1}' 的翻譯規則。英文='{2}'(本地={3}, 已同步={4}),啟用='{5}'(本地={6}, 已同步={7}),精確={8},樣板={9}。", ["runtime.settings.read_failed"] = "無法讀取 '{0}' 的執行階段設定:{1}", ["runtime.settings.backfill_failed"] = "無法補齊 '{0}' 中缺少的執行階段設定:{1}", ["runtime.settings.summary"] = "'{0}' 的執行階段設定({1}):翻譯啟用={2}, 除錯啟用={3}, 整句最長匹配模式={4}, 最小匹配長度={5}, 追蹤資料流容量={6}。", ["serversync.log.source_of_truth"] = "伺服器同步唯一資料來源已啟用;本機設定的熱更新目前生效。", ["serversync.log.snapshot_published"] = "已在 {0} 後發佈本機翻譯快照:{1} 個語言資料夾、{2} 個檔案。", ["devtools.app.title"] = "本地化開發工具", ["devtools.page.browser"] = "翻譯瀏覽", ["devtools.page.performance"] = "效能", ["devtools.page.playground"] = "沙盒", ["devtools.page.plugin"] = "插件", ["devtools.page.trace"] = "追蹤", ["devtools.plugin.section.overview"] = "總覽", ["devtools.plugin.section.devtools"] = "開發工具", ["devtools.plugin.section.runtime"] = "執行階段", ["devtools.plugin.section.status"] = "狀態", ["devtools.plugin.row.plugin_version"] = "插件版本", ["devtools.plugin.row.current_language"] = "目前語言", ["devtools.plugin.row.remote_snapshot"] = "遠端快照", ["devtools.plugin.row.overlay_enabled"] = "覆蓋層啟用", ["devtools.plugin.row.toggle_shortcut"] = "切換快捷鍵", ["devtools.plugin.row.close_actions"] = "關閉方式", ["devtools.plugin.row.translation_enabled"] = "啟用翻譯", ["devtools.plugin.row.debug_enabled"] = "啟用除錯", ["devtools.plugin.row.whole_string_longest_match_mode"] = "整句最長匹配模式", ["devtools.plugin.row.minimum_match_length"] = "最小匹配長度", ["devtools.plugin.row.trace_feed_capacity"] = "追蹤資料流容量", ["devtools.plugin.row.hot_update"] = "熱更新", ["devtools.plugin.row.shortcut_capture"] = "快捷鍵擷取", ["devtools.plugin.row.last_config_action"] = "最近設定動作", ["devtools.plugin.action.capture"] = "擷取", ["devtools.plugin.action.next"] = "下一個", ["devtools.plugin.action.prev"] = "上一個", ["devtools.plugin.action.reset"] = "重設", ["devtools.plugin.action.toggle"] = "切換", ["devtools.plugin.hot_update_value"] = "即時", ["devtools.plugin.shortcut.capture_prompt"] = "請按下新的快捷鍵...", ["devtools.plugin.shortcut.listening"] = "監聽中(ESC 取消)", ["devtools.plugin.shortcut.idle"] = "閒置", ["devtools.plugin.status.ready"] = "就緒", ["devtools.plugin.status.capture_cancelled"] = "快捷鍵擷取已取消。", ["devtools.plugin.status.capture_started"] = "請按下新的切換快捷鍵。ESC 可取消。", ["devtools.plugin.status.toggle_shortcut_updated"] = "切換快捷鍵 -> {0}", ["devtools.plugin.status.overlay_enabled_updated"] = "覆蓋層啟用 -> {0}", ["devtools.plugin.status.translation_enabled_updated"] = "翻譯啟用 -> {0}", ["devtools.plugin.status.debug_enabled_updated"] = "除錯啟用 -> {0}", ["devtools.plugin.status.whole_string_longest_match_mode_updated"] = "整句最長匹配模式 -> {0}", ["devtools.plugin.status.min_length_updated"] = "最小匹配長度 -> {0}", ["devtools.plugin.status.trace_feed_capacity_updated"] = "追蹤資料流容量 -> {0}", ["devtools.plugin.status.setting_failed"] = "{0} 更新失敗:{1}", ["devtools.plugin.mode.off"] = "關閉", ["devtools.plugin.mode.min_length"] = "最小長度", ["devtools.plugin.mode.per_word"] = "逐詞", ["devtools.performance.section.loaded_content"] = "已載入內容", ["devtools.performance.section.hot_path"] = "熱路徑", ["devtools.performance.section.caches"] = "快取", ["devtools.performance.section.timings"] = "耗時", ["devtools.performance.row.exact_rules"] = "精確規則", ["devtools.performance.row.template_rules"] = "樣板規則", ["devtools.performance.row.local_english_files"] = "本機英文翻譯檔案", ["devtools.performance.row.synced_english_files"] = "同步英文翻譯檔案", ["devtools.performance.row.local_active_files"] = "本機目前翻譯語言檔案", ["devtools.performance.row.synced_active_files"] = "同步目前翻譯語言檔案", ["devtools.performance.row.intercepts"] = "攔截次數", ["devtools.performance.row.reuse_hits"] = "重用命中", ["devtools.performance.row.reuse_hit_rate"] = "重用命中比率", ["devtools.performance.row.known_output_skips"] = "已知輸出略過", ["devtools.performance.row.known_output_skip_rate"] = "已知輸出略過比率", ["devtools.performance.row.translation_cache_hits"] = "翻譯快取命中", ["devtools.performance.row.translation_cache_calls"] = "翻譯快取查詢", ["devtools.performance.row.translation_cache_hit_rate"] = "翻譯快取比率", ["devtools.performance.row.translation_miss_cache_hits"] = "未命中快取命中", ["devtools.performance.row.translation_miss_cache_calls"] = "未命中快取查詢", ["devtools.performance.row.miss_cache_hit_rate"] = "未命中快取比率", ["devtools.performance.row.normalizer_cache_hits"] = "正規化快取命中", ["devtools.performance.row.normalizer_cache_calls"] = "正規化快取查詢", ["devtools.performance.row.normalizer_cache_hit_rate"] = "正規化快取比率", ["devtools.performance.row.avg_template_candidates"] = "平均樣板候選數", ["devtools.performance.row.translation_cache"] = "翻譯快取", ["devtools.performance.row.translation_miss_cache"] = "翻譯為命中的快取", ["devtools.performance.row.placeholder_cache"] = "佔位符快取", ["devtools.performance.row.placeholder_miss_cache"] = "佔位符未命中的快取", ["devtools.performance.row.runtime_evictions"] = "執行階段捨棄次數", ["devtools.performance.row.resolver_evictions"] = "解析器捨棄次數", ["devtools.performance.row.avg_intercept"] = "平均攔截耗時", ["devtools.performance.row.avg_translate"] = "平均翻譯耗時", ["devtools.performance.row.avg_normalize"] = "平均正規化耗時", ["devtools.performance.row.avg_template"] = "平均樣板耗時", ["devtools.performance.row.avg_whole_string_longest_match"] = "平均整句最長匹配耗時", ["devtools.performance.row.template_attempts"] = "樣板嘗試次數", ["devtools.performance.row.template_matches"] = "樣板命中次數", ["devtools.performance.row.template_constrained"] = "樣板受限次數", ["devtools.performance.row.whole_string_longest_match_attempts"] = "整句最長匹配嘗試次數", ["devtools.performance.row.whole_string_longest_match_matches"] = "整句最長匹配命中次數", ["devtools.performance.row.metrics"] = "指標", ["devtools.performance.status.cumulative"] = "自啟動後累積", ["devtools.performance.status.reset_now"] = "剛剛已重設", ["devtools.performance.value.count"] = "次數", ["devtools.performance.value.hit"] = "命中", ["devtools.performance.value.total"] = "總數", ["devtools.performance.value.rate"] = "比率", ["devtools.performance.value.attempts"] = "嘗試", ["devtools.performance.value.matches"] = "命中", ["devtools.performance.value.constrained"] = "受限", ["devtools.trace.section.how_to_use"] = "使用方式", ["devtools.trace.section.overview"] = "概覽", ["devtools.trace.header.feed"] = "追蹤紀錄", ["devtools.trace.header.analysis"] = "翻譯分析", ["devtools.trace.header.selected_source"] = "已選來源", ["devtools.trace.analysis.status.hit"] = "命中", ["devtools.trace.analysis.status.miss"] = "未命中", ["devtools.trace.analysis.status.not_executed"] = "未執行", ["devtools.trace.button.start_feed"] = "開始串流", ["devtools.trace.button.stop_feed"] = "停止串流", ["devtools.trace.button.copy_input"] = "複製輸入", ["devtools.trace.button.copy_report"] = "複製報告", ["devtools.trace.report.title"] = "翻譯分析報告", ["devtools.trace.feed.status"] = "資料流:{0} | 唯一字串:{1} / {2}", ["devtools.trace.feed.unique_count"] = "唯一字串:{0} / {1}", ["devtools.trace.search.placeholder"] = "搜尋已擷取字串或來源...", ["devtools.trace.overview.input"] = "輸入:{0}", ["devtools.trace.overview.source"] = "來源:{0}", ["devtools.trace.overview.output"] = "目前輸出:{0}", ["devtools.trace.usage.select"] = "點擊左側已擷取的字串,可重新執行該字串的翻譯分析。", ["devtools.trace.usage.replay"] = "分析會即時重播所選輸入的翻譯流程。", ["devtools.trace.usage.capacity"] = "資料流只會儲存唯一字串,最多 {0} 筆。", ["devtools.trace.source.empty"] = "請先選取一筆已擷取字串,以檢視其來源。", ["devtools.trace.ui.truncated_suffix"] = "... [UI 已截斷]", ["devtools.browser.header.navigator"] = "翻譯瀏覽", ["devtools.browser.header.details"] = "檔案內容", ["devtools.browser.search.placeholder"] = "搜尋檔案路徑、匹配或替換...", ["devtools.browser.status.results"] = "顯示 {0} / {1} 個檔案", ["devtools.browser.status.selection"] = "{0} | {1} 筆精確 / {2} 筆樣板", ["devtools.browser.source.local"] = "本機", ["devtools.browser.source.server"] = "伺服器", ["devtools.browser.empty.no_results"] = "目前搜尋條件沒有命中的翻譯檔。", ["devtools.browser.empty.no_selection"] = "請在左側選擇翻譯檔以檢視規則內容。", ["devtools.browser.empty.no_rules"] = "這個檔案目前沒有任何規則。", ["devtools.browser.file.summary"] = "{0} 筆精確 / {1} 筆樣板", ["devtools.browser.rule.title"] = "規則 #{0}", ["devtools.playground.section.scenario"] = "情境", ["devtools.playground.section.rule_editor"] = "規則編輯器", ["devtools.playground.section.enabled_rules"] = "啟用規則", ["devtools.playground.section.preview"] = "預覽", ["devtools.playground.section.generated_test_set"] = "產生的測試集", ["devtools.playground.label.include_external_rules"] = "在預覽中包含沙盒以外已載入的翻譯檔。", ["devtools.playground.label.copy_json_help"] = "只複製已儲存的沙盒規則,作為純粹的本地化規則檔。", ["devtools.playground.field.test_input"] = "測試輸入", ["devtools.playground.field.match"] = "匹配", ["devtools.playground.field.replace"] = "替換", ["devtools.playground.field.summary"] = "摘要", ["devtools.playground.field.matched_input"] = "命中的輸入", ["devtools.playground.field.rule_editor_match_preview"] = "規則編輯器匹配預覽", ["devtools.playground.field.output"] = "輸出", ["devtools.playground.field.json_preview"] = "JSON 預覽", ["devtools.playground.placeholder.test_input"] = "輸入要用目前沙盒規則集評估的文字。", ["devtools.playground.placeholder.match"] = "精確文字或樣板匹配。", ["devtools.playground.placeholder.replace"] = "上方匹配對應的翻譯輸出。", ["devtools.playground.button.add_rule"] = "新增規則", ["devtools.playground.button.clear_rules"] = "清空規則", ["devtools.playground.button.copy_json"] = "複製 JSON", ["devtools.playground.button.remove"] = "移除", ["devtools.playground.button.external_rules_on"] = "外部規則:開", ["devtools.playground.button.external_rules_off"] = "外部規則:關", ["devtools.playground.button.rule_type"] = "規則類型:{0}", ["devtools.playground.empty_rules"] = "目前還沒有已儲存的沙盒規則。", ["devtools.playground.status.ready"] = "就緒。", ["devtools.playground.status.include_external_rules_enabled"] = "預覽評估現在會包含沙盒以外已載入的翻譯檔。", ["devtools.playground.status.include_external_rules_disabled"] = "預覽評估現在只使用沙盒規則。", ["devtools.playground.status.draft_mode_switched"] = "草稿模式已切換為 {0}。", ["devtools.playground.status.rule_saved"] = "已儲存第 #{1} 條 {0} 規則。", ["devtools.playground.status.rules_cleared"] = "已清空所有已儲存的沙盒規則。", ["devtools.playground.status.rule_removed"] = "已移除第 #{1} 條 {0} 規則。", ["devtools.playground.status.json_copied"] = "已將沙盒 JSON 複製到剪貼簿。", ["devtools.playground.status.output_empty"] = "輸入文字後即可查看翻譯輸出。", ["devtools.playground.status.draft_empty"] = "草稿為空。", ["devtools.playground.status.draft_incomplete"] = "草稿不完整。匹配與替換都必須填寫。", ["devtools.playground.status.draft_invalid"] = "樣板無效:{0}", ["devtools.playground.status.draft_ready"] = "草稿已就緒,類型為 {0}。", ["devtools.playground.status.no_input"] = "沒有輸入", ["devtools.playground.status.no_match"] = "未命中", ["devtools.playground.status.normalized_exact"] = "正規化精確匹配", ["devtools.playground.status.whole_string_longest_match"] = "整句最長匹配", ["devtools.playground.status.highlight_prompt"] = "輸入文字後即可查看高亮命中結果。", ["devtools.playground.summary.saved_rules"] = "已儲存規則:{0}({1} 筆精確 / {2} 筆樣板)", ["devtools.playground.summary.preview_rules_with_external"] = "預覽規則集:{0} 筆精確 / {1} 筆樣板 + 外部規則 {2}", ["devtools.playground.summary.winning_stage"] = "命中階段:{0}", ["devtools.playground.summary.stacked_result_winning_stage"] = "疊加結果命中階段:{0}", ["devtools.playground.summary.match_preview_scope"] = "規則編輯器匹配預覽只會使用目前的 Match 欄位;Output 仍然是完整疊加規則後的結果。", ["devtools.playground.summary.draft"] = "草稿:{0}", ["devtools.playground.preview.no_input"] = "輸入測試文字後,即可預覽目前規則編輯器的匹配結果。", ["devtools.playground.preview.no_draft"] = "填寫 Match 與 Replace 後,即可預覽目前規則編輯器的匹配結果。", ["devtools.playground.preview.current_match"] = "目前 Match 欄位:{0}", ["devtools.playground.preview.applies"] = "是否套用到目前測試輸入:{0}", ["devtools.playground.preview.stage"] = "草稿預覽階段:{0}", ["devtools.playground.rule_row"] = "#{0} [{1}] {2} => {3}", ["devtools.playground.kind.exact"] = "精確", ["devtools.playground.kind.template"] = "樣板" }; } private static Dictionary BuildTraditionalChineseLegacyValuesByKey() { return new Dictionary(StringComparer.Ordinal) { ["plugin.log.devtools_not_ready"] = "本地化開發工具介面尚未就緒。", ["devtools.app.title"] = "本地化開發工具", ["devtools.playground.label.include_external_rules"] = "在預覽中包含沙盒以外已載入的翻譯檔。", ["devtools.playground.label.copy_json_help"] = "只複製已儲存的沙盒規則,作為純粹的本地化規則檔。", ["devtools.playground.placeholder.match"] = "精確文字或樣板匹配。", ["devtools.playground.placeholder.replace"] = "上方匹配對應的翻譯輸出。", ["devtools.playground.empty_rules"] = "目前還沒有已儲存的沙盒規則。", ["devtools.playground.status.include_external_rules_enabled"] = "預覽評估現在會包含沙盒以外已載入的翻譯檔。", ["devtools.playground.status.include_external_rules_disabled"] = "預覽評估現在只使用沙盒規則。", ["devtools.playground.summary.saved_rules"] = "已儲存規則:{0}({1} 條精確 / {2} 條樣板", ["devtools.playground.summary.preview_rules_with_external"] = "預覽規則集:{0} 條精確 / {1} 條樣板ˋ + 外部規則 {2}", ["runtime.settings.summary"] = "'{0}' 的執行階段設定({1}):翻譯啟用={2}, 除錯啟用={3}, 整句最長匹配模式={4}, 最小匹配長度={5}。", ["devtools.trace.usage.capacity"] = "資料流只會儲存唯一字串,最多 1024 筆。", ["devtools.browser.status.selection"] = "{0} / {1} / {2} | {3} 筆精確 / {4} 筆樣板" }; } } [BepInPlugin("com.paxton0505.localization", "Localization", "2.0.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class PluginMain : BaseUnityPlugin { private const int MaxDebugTextLength = 4096; public const string PluginGuid = "com.paxton0505.localization"; public const string PluginName = "Localization"; public const string PluginVersion = "2.0.1"; private Harmony harmony; internal static ManualLogSource Log { get; private set; } internal static bool IsDedicatedServer { get; private set; } internal static bool DebugModeEnabled => RuntimeDebugConfig.Enabled; internal static void DebugLog(string message) { if (DebugModeEnabled && Log != null) { Log.LogInfo((object)("[Debug] " + LocalizeFallback(message))); } } internal static void DebugLog(Func messageFactory) { if (DebugModeEnabled && Log != null && messageFactory != null) { Log.LogInfo((object)("[Debug] " + LocalizeFallback(messageFactory()))); } } internal static string Localize(string key, string fallback) { return PluginLocalization.Get(key, fallback); } internal static string LocalizeFallback(string fallback) { return PluginLocalization.TranslateFallback(fallback); } internal static string LocalizeFormat(string key, string fallback, params object[] args) { return PluginLocalization.Format(key, fallback, args); } internal static string FormatTextForDebug(string value) { if (value == null) { return Localize("common.debug.null", ""); } string text = value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n"); if (text.Length <= 4096) { return text; } return text.Substring(0, 4096) + Localize("common.debug.truncated_suffix", "... [truncated]"); } private void Awake() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; IsDedicatedServer = Application.isBatchMode || (int)SystemInfo.graphicsDeviceType == 4; PluginLocalization.Initialize(); RuntimeDebugConfig.Initialize(); TranslationServerSyncBridge.Initialize(); if (IsDedicatedServer) { ((BaseUnityPlugin)this).Logger.LogInfo((object)LocalizeFormat("plugin.loaded_dedicated", "{0} loaded in dedicated server mode. ServerSync remains active; translation runtime and UI patches are skipped.", "Localization")); return; } TranslationRuntime.Initialize(); harmony = new Harmony("com.paxton0505.localization"); harmony.PatchAll(); DevToolsBootstrap.Initialize(this); ((BaseUnityPlugin)this).Logger.LogInfo((object)LocalizeFormat("plugin.loaded", "{0} loaded.", "Localization")); } private void OnDestroy() { DevToolsBootstrap.Shutdown(); TranslationServerSyncBridge.Shutdown(); RuntimeDebugConfig.Shutdown(); PluginLocalization.Shutdown(); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } } [Serializable] internal sealed class RuleFileModel { public ExactRuleDefinition[] exact = Array.Empty(); public TemplateRuleDefinition[] templates = Array.Empty(); } [Serializable] internal sealed class ExactRuleDefinition { public string match = string.Empty; public string replace = string.Empty; } [Serializable] internal sealed class TemplateRuleDefinition { public string match = string.Empty; public string replace = string.Empty; } internal sealed class CompiledRuleSet { private readonly struct GreedyExactRule { public string Match { get; } public string OriginalMatch { get; } public string Replace { get; } public string SourcePath { get; } public int WordCharacterCount { get; } public GreedyExactRule(string match, string originalMatch, string replace, string sourcePath) { Match = match; OriginalMatch = (string.IsNullOrEmpty(originalMatch) ? match : originalMatch); Replace = replace; SourcePath = sourcePath ?? string.Empty; WordCharacterCount = CountWordCharacters(match); } } private readonly struct GreedyExactMatch { public int StartIndex { get; } public int EndIndex => StartIndex + Rule.Match.Length; public GreedyExactRule Rule { get; } public GreedyExactMatch(int startIndex, GreedyExactRule rule) { StartIndex = startIndex; Rule = rule; } } private const int MinimumDispatchLiteralLength = 2; [ThreadStatic] private static HashSet anchorVisitedCharactersScratch; [ThreadStatic] private static List greedyAllMatchesScratch; [ThreadStatic] private static List greedySelectedMatchesScratch; [ThreadStatic] private static bool[] greedyOccupiedScratch; [ThreadStatic] private static List templateCandidateScratch; [ThreadStatic] private static HashSet templateCandidateSeenScratch; private readonly Dictionary> prefixKeysByFirstCharacter = new Dictionary>(); private readonly Dictionary> anchorKeysByFirstCharacter = new Dictionary>(); private readonly Dictionary> greedyExactRulesByFirstCharacter = new Dictionary>(); public static readonly CompiledRuleSet Empty = new CompiledRuleSet(new Dictionary(0, StringComparer.Ordinal), new Dictionary(0, StringComparer.OrdinalIgnoreCase), new List(0)); public Dictionary ExactRules { get; } public Dictionary MatchNormalizedExactRules { get; } public Dictionary MatchNormalizedExactOriginalMatches { get; } public Dictionary ExactRuleMetadata { get; } public Dictionary NormalizedExactRuleMetadata { get; } public List TemplateRules { get; } public Dictionary> TemplateRulesByPrefix { get; } public Dictionary> TemplateRulesByAnchor { get; } public List FallbackTemplateRules { get; } public CompiledRuleSet(Dictionary exactRules, Dictionary englishExactRulesIgnoreCase, List templateRules) : this(exactRules, englishExactRulesIgnoreCase, BuildOriginalNormalizedExactMatches(englishExactRulesIgnoreCase), BuildExactRuleMetadata(exactRules), BuildNormalizedExactRuleMetadata(englishExactRulesIgnoreCase, null), templateRules) { } public CompiledRuleSet(Dictionary exactRules, Dictionary englishExactRulesIgnoreCase, Dictionary originalNormalizedExactMatches, List templateRules) : this(exactRules, englishExactRulesIgnoreCase, originalNormalizedExactMatches, BuildExactRuleMetadata(exactRules), BuildNormalizedExactRuleMetadata(englishExactRulesIgnoreCase, originalNormalizedExactMatches), templateRules) { } public CompiledRuleSet(Dictionary exactRules, Dictionary englishExactRulesIgnoreCase, Dictionary originalNormalizedExactMatches, Dictionary exactRuleMetadata, Dictionary normalizedExactRuleMetadata, List templateRules) { ExactRules = exactRules; MatchNormalizedExactRules = englishExactRulesIgnoreCase; MatchNormalizedExactOriginalMatches = originalNormalizedExactMatches ?? BuildOriginalNormalizedExactMatches(englishExactRulesIgnoreCase); ExactRuleMetadata = exactRuleMetadata ?? BuildExactRuleMetadata(exactRules); NormalizedExactRuleMetadata = normalizedExactRuleMetadata ?? BuildNormalizedExactRuleMetadata(englishExactRulesIgnoreCase, MatchNormalizedExactOriginalMatches); TemplateRules = templateRules; TemplateRulesByPrefix = new Dictionary>(StringComparer.OrdinalIgnoreCase); TemplateRulesByAnchor = new Dictionary>(StringComparer.OrdinalIgnoreCase); FallbackTemplateRules = new List(); BuildGreedyExactIndexes(); BuildDispatchIndexes(); } public static CompiledRuleSet CreateAdHoc(IEnumerable exactRules, IEnumerable templateRules) { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); Dictionary dictionary3 = new Dictionary(StringComparer.OrdinalIgnoreCase); Dictionary dictionary4 = new Dictionary(StringComparer.Ordinal); Dictionary dictionary5 = new Dictionary(StringComparer.OrdinalIgnoreCase); if (exactRules != null) { foreach (RuleTraceMetadata exactRule in exactRules) { if (exactRule != null && !string.IsNullOrWhiteSpace(exactRule.MatchText)) { dictionary[exactRule.MatchText] = exactRule.ReplaceText; dictionary4[exactRule.MatchText] = exactRule; string text = TextMatchNormalizer.Normalize(exactRule.MatchText); if (!string.IsNullOrWhiteSpace(text)) { dictionary2[text] = exactRule.ReplaceText; dictionary3[text] = exactRule.MatchText; dictionary5[text] = exactRule; } } } } List templateRules2 = ((templateRules != null) ? new List(templateRules) : new List()); return new CompiledRuleSet(dictionary, dictionary2, dictionary3, dictionary4, dictionary5, templateRules2); } public bool TryGetOriginalNormalizedExactMatch(string normalizedMatch, out string originalMatch) { originalMatch = null; if (string.IsNullOrWhiteSpace(normalizedMatch) || MatchNormalizedExactOriginalMatches == null) { return false; } return MatchNormalizedExactOriginalMatches.TryGetValue(normalizedMatch, out originalMatch); } public bool TryGetExactRuleMetadata(string match, out RuleTraceMetadata metadata) { metadata = null; if (!string.IsNullOrWhiteSpace(match) && ExactRuleMetadata != null) { return ExactRuleMetadata.TryGetValue(match, out metadata); } return false; } public bool TryGetNormalizedExactRuleMetadata(string normalizedMatch, out RuleTraceMetadata metadata) { metadata = null; if (!string.IsNullOrWhiteSpace(normalizedMatch) && NormalizedExactRuleMetadata != null) { return NormalizedExactRuleMetadata.TryGetValue(normalizedMatch, out metadata); } return false; } public bool TryTranslateGreedyExact(string input, out string translated) { return TryTranslateGreedyExact(TextMatchNormalizer.Analyze(input), out translated); } public bool TryTranslateGreedyExact(TextMatchNormalizer.NormalizedText normalizedInput, out string translated) { List analysisSteps; bool loopDetected; return TryTranslateGreedyExactCore(normalizedInput, captureAnalysis: false, out translated, out analysisSteps, out loopDetected); } public bool TryAnalyzeGreedyExact(TextMatchNormalizer.NormalizedText normalizedInput, out GreedyExactAnalysisResult analysis) { string translated; List analysisSteps; bool loopDetected; bool result = TryTranslateGreedyExactCore(normalizedInput, captureAnalysis: true, out translated, out analysisSteps, out loopDetected); analysis = new GreedyExactAnalysisResult(translated, loopDetected, (analysisSteps != null) ? analysisSteps.ToArray() : Array.Empty()); return result; } private bool TryTranslateGreedyExactCore(TextMatchNormalizer.NormalizedText normalizedInput, bool captureAnalysis, out string translated, out List analysisSteps, out bool loopDetected) { string current = normalizedInput.RawText; translated = current; loopDetected = false; analysisSteps = (captureAnalysis ? new List() : null); if (greedyExactRulesByFirstCharacter.Count == 0) { return false; } bool flag = false; string initialState = current; HashSet hashSet = null; TextMatchNormalizer.NormalizedText normalizedInput2 = normalizedInput; List selectedMatches; while (!string.IsNullOrEmpty(normalizedInput2.Value) && TrySelectGreedyExactMatches(normalizedInput2.Value, out selectedMatches)) { string text = ApplyGreedyExactMatches(normalizedInput2, selectedMatches); if (string.Equals(text, current, StringComparison.Ordinal)) { break; } if (captureAnalysis && analysisSteps != null) { analysisSteps.Add(CreateGreedyExactAnalysisStep(normalizedInput2, current, text, selectedMatches)); } if (string.Equals(text, initialState, StringComparison.Ordinal)) { loopDetected = true; PluginMain.DebugLog(() => "Detected whole string longest match translation loop while translating '" + PluginMain.FormatTextForDebug(initialState) + "'. Returning '" + PluginMain.FormatTextForDebug(current) + "'."); break; } if (hashSet != null) { if (!hashSet.Add(text)) { loopDetected = true; PluginMain.DebugLog(() => "Detected whole string longest match translation loop while translating '" + PluginMain.FormatTextForDebug(initialState) + "'. Returning '" + PluginMain.FormatTextForDebug(current) + "'."); break; } } else if (flag) { hashSet = new HashSet(StringComparer.Ordinal) { initialState, current }; if (!hashSet.Add(text)) { loopDetected = true; PluginMain.DebugLog(() => "Detected whole string longest match translation loop while translating '" + PluginMain.FormatTextForDebug(initialState) + "'. Returning '" + PluginMain.FormatTextForDebug(current) + "'."); break; } } current = text; normalizedInput2 = TextMatchNormalizer.Analyze(current); flag = true; } if (!flag) { if (analysisSteps != null) { analysisSteps.Clear(); } return false; } translated = current; return true; } public IReadOnlyList GetTemplateCandidates(string normalizedInput) { if (TemplateRules.Count == 0) { return TemplateRules; } if (TemplateRulesByPrefix.Count == 0 && TemplateRulesByAnchor.Count == 0) { return TemplateRules; } List singleCandidateSource = null; List mergedCandidates = null; HashSet seenCandidates = null; AddPrefixCandidates(normalizedInput, ref singleCandidateSource, ref mergedCandidates, ref seenCandidates); AddAnchorCandidates(normalizedInput, ref singleCandidateSource, ref mergedCandidates, ref seenCandidates); if (FallbackTemplateRules.Count > 0) { AppendCandidateSource(FallbackTemplateRules, ref singleCandidateSource, ref mergedCandidates, ref seenCandidates); } if (mergedCandidates != null) { if (mergedCandidates.Count > 1) { mergedCandidates.Sort(CompareByDispatchOrder); } return mergedCandidates; } IReadOnlyList readOnlyList = singleCandidateSource; return readOnlyList ?? Array.Empty(); } private void BuildDispatchIndexes() { for (int i = 0; i < TemplateRules.Count; i++) { CompiledTemplateRule compiledTemplateRule = TemplateRules[i]; compiledTemplateRule.SetDispatchOrder(i); if (!TryAddPrefixRule(compiledTemplateRule) && !TryAddAnchorRule(compiledTemplateRule)) { FallbackTemplateRules.Add(compiledTemplateRule); } } } private void BuildGreedyExactIndexes() { foreach (KeyValuePair matchNormalizedExactRule in MatchNormalizedExactRules) { if (!string.IsNullOrEmpty(matchNormalizedExactRule.Key)) { char key = char.ToUpperInvariant(matchNormalizedExactRule.Key[0]); if (!greedyExactRulesByFirstCharacter.TryGetValue(key, out var value)) { value = new List(); greedyExactRulesByFirstCharacter[key] = value; } RuleTraceMetadata metadata; RuleTraceMetadata ruleTraceMetadata = (TryGetNormalizedExactRuleMetadata(matchNormalizedExactRule.Key, out metadata) ? metadata : new RuleTraceMetadata(matchNormalizedExactRule.Key, matchNormalizedExactRule.Value, null)); value.Add(new GreedyExactRule(matchNormalizedExactRule.Key, ruleTraceMetadata.MatchText, ruleTraceMetadata.ReplaceText, ruleTraceMetadata.SourcePath)); } } foreach (List value2 in greedyExactRulesByFirstCharacter.Values) { value2.Sort(CompareGreedyExactRules); } } private bool TrySelectGreedyExactMatches(string input, out List selectedMatches) { selectedMatches = null; List list = GetGreedyAllMatchesScratch(); CollectGreedyExactMatches(input, list); if (list.Count == 0) { return false; } list.Sort(CompareGreedyExactMatches); bool[] occupied = GetGreedyOccupiedScratch(input.Length); List selected = GetGreedySelectedMatchesScratch(list.Count); foreach (GreedyExactMatch item in list) { if (!OverlapsOccupiedRange(occupied, item.StartIndex, item.EndIndex)) { selected.Add(item); MarkOccupiedRange(occupied, item.StartIndex, item.EndIndex); } } if (selected.Count == 0) { return false; } selected.Sort(CompareGreedyExactMatchesByStartIndex); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(delegate { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Whole string longest match selected "); stringBuilder.Append(selected.Count); stringBuilder.Append(" match(es) for normalized input '"); stringBuilder.Append(PluginMain.FormatTextForDebug(input)); stringBuilder.Append("': "); for (int i = 0; i < selected.Count; i++) { if (i > 0) { stringBuilder.Append(" | "); } GreedyExactMatch greedyExactMatch = selected[i]; stringBuilder.Append('['); stringBuilder.Append(greedyExactMatch.StartIndex); stringBuilder.Append(".."); stringBuilder.Append(greedyExactMatch.EndIndex); stringBuilder.Append("] '"); stringBuilder.Append(PluginMain.FormatTextForDebug(input.Substring(greedyExactMatch.StartIndex, greedyExactMatch.Rule.Match.Length))); stringBuilder.Append("' -> '"); stringBuilder.Append(PluginMain.FormatTextForDebug(greedyExactMatch.Rule.Replace)); stringBuilder.Append('\''); } return stringBuilder.ToString(); }); } selectedMatches = selected; return true; } private void CollectGreedyExactMatches(string input, List matches) { for (int i = 0; i < input.Length; i++) { char key = char.ToUpperInvariant(input[i]); if (!greedyExactRulesByFirstCharacter.TryGetValue(key, out var value)) { continue; } foreach (GreedyExactRule item in value) { if (i + item.Match.Length <= input.Length && string.Compare(input, i, item.Match, 0, item.Match.Length, StringComparison.OrdinalIgnoreCase) == 0 && IsGreedyExactMatchAllowed(input, i, item)) { matches.Add(new GreedyExactMatch(i, item)); } } } } private static bool IsGreedyExactMatchAllowed(string input, int startIndex, GreedyExactRule rule) { return RuntimeDebugConfig.GreedyExactMatchMode switch { WholeStringGreedyExactMatchMode.Off => true, WholeStringGreedyExactMatchMode.MinLength => rule.WordCharacterCount >= RuntimeDebugConfig.GreedyExactMinimumMatchLength, _ => IsWholeWordMatch(input, startIndex, rule), }; } private static bool IsWholeWordMatch(string input, int startIndex, GreedyExactRule rule) { if (rule.WordCharacterCount <= 0) { return false; } int num = startIndex + rule.Match.Length; if (startIndex > 0 && IsWordCharacter(input[startIndex - 1]) && IsWordCharacter(input[startIndex])) { return false; } if (num < input.Length && IsWordCharacter(input[num - 1]) && IsWordCharacter(input[num])) { return false; } return true; } private static int CountWordCharacters(string input) { if (string.IsNullOrEmpty(input)) { return 0; } int num = 0; for (int i = 0; i < input.Length; i++) { if (IsWordCharacter(input[i])) { num++; } } return num; } private static bool IsWordCharacter(char character) { return char.IsLetterOrDigit(character); } private static bool OverlapsOccupiedRange(bool[] occupied, int startIndex, int endIndex) { for (int i = startIndex; i < endIndex; i++) { if (occupied[i]) { return true; } } return false; } private static void MarkOccupiedRange(bool[] occupied, int startIndex, int endIndex) { for (int i = startIndex; i < endIndex; i++) { occupied[i] = true; } } private static string ApplyGreedyExactMatches(TextMatchNormalizer.NormalizedText normalizedInput, List selectedMatches) { TextMatchNormalizer.ReplacementSegment[] array = new TextMatchNormalizer.ReplacementSegment[selectedMatches.Count]; for (int i = 0; i < selectedMatches.Count; i++) { GreedyExactMatch greedyExactMatch = selectedMatches[i]; array[i] = new TextMatchNormalizer.ReplacementSegment(greedyExactMatch.StartIndex, greedyExactMatch.Rule.Match.Length, greedyExactMatch.Rule.Replace); } return normalizedInput.ReplaceNormalizedRanges(array); } private static GreedyExactAnalysisStep CreateGreedyExactAnalysisStep(TextMatchNormalizer.NormalizedText normalizedInput, string input, string output, List selectedMatches) { GreedyExactAnalysisMatch[] array = new GreedyExactAnalysisMatch[selectedMatches.Count]; for (int i = 0; i < selectedMatches.Count; i++) { GreedyExactMatch greedyExactMatch = selectedMatches[i]; string inputSegment = normalizedInput.Value.Substring(greedyExactMatch.StartIndex, greedyExactMatch.Rule.Match.Length); array[i] = new GreedyExactAnalysisMatch(greedyExactMatch.StartIndex, greedyExactMatch.EndIndex, inputSegment, greedyExactMatch.Rule.OriginalMatch, greedyExactMatch.Rule.Replace, greedyExactMatch.Rule.SourcePath); } return new GreedyExactAnalysisStep(input, output, array); } private bool TryAddPrefixRule(CompiledTemplateRule rule) { string leadingLiteralPrefix = rule.LeadingLiteralPrefix; if (string.IsNullOrEmpty(leadingLiteralPrefix) || leadingLiteralPrefix.Length < 2) { return false; } AddBucketRule(TemplateRulesByPrefix, prefixKeysByFirstCharacter, leadingLiteralPrefix, rule); return true; } private bool TryAddAnchorRule(CompiledTemplateRule rule) { string longestLiteralAnchor = rule.LongestLiteralAnchor; if (string.IsNullOrEmpty(longestLiteralAnchor) || longestLiteralAnchor.Length < 2) { return false; } AddBucketRule(TemplateRulesByAnchor, anchorKeysByFirstCharacter, longestLiteralAnchor, rule); return true; } private void AddPrefixCandidates(string input, ref List singleCandidateSource, ref List mergedCandidates, ref HashSet seenCandidates) { if (string.IsNullOrEmpty(input) || !prefixKeysByFirstCharacter.TryGetValue(char.ToUpperInvariant(input[0]), out var value)) { return; } foreach (string item in value) { if (input.StartsWith(item, StringComparison.OrdinalIgnoreCase)) { AppendCandidateSource(TemplateRulesByPrefix[item], ref singleCandidateSource, ref mergedCandidates, ref seenCandidates); } } } private void AddAnchorCandidates(string input, ref List singleCandidateSource, ref List mergedCandidates, ref HashSet seenCandidates) { if (string.IsNullOrEmpty(input) || TemplateRulesByAnchor.Count == 0) { return; } HashSet hashSet = GetAnchorVisitedCharactersScratch(); for (int i = 0; i < input.Length; i++) { char c = char.ToUpperInvariant(input[i]); if (!hashSet.Add(c) || !anchorKeysByFirstCharacter.TryGetValue(c, out var value)) { continue; } foreach (string item in value) { if (input.IndexOf(item, StringComparison.OrdinalIgnoreCase) >= 0) { AppendCandidateSource(TemplateRulesByAnchor[item], ref singleCandidateSource, ref mergedCandidates, ref seenCandidates); } } } } private static void AppendCandidateSource(List source, ref List singleCandidateSource, ref List mergedCandidates, ref HashSet seenCandidates) { if (source != null && source.Count != 0) { if (mergedCandidates != null) { AddUniqueCandidates(source, mergedCandidates, seenCandidates); } else if (singleCandidateSource == null) { singleCandidateSource = source; } else if (singleCandidateSource != source) { mergedCandidates = GetTemplateCandidateScratch(singleCandidateSource.Count + source.Count); seenCandidates = GetTemplateCandidateSeenScratch(); AddUniqueCandidates(singleCandidateSource, mergedCandidates, seenCandidates); AddUniqueCandidates(source, mergedCandidates, seenCandidates); singleCandidateSource = null; } } } private static void AddUniqueCandidates(List source, List destination, HashSet seenCandidates) { for (int i = 0; i < source.Count; i++) { CompiledTemplateRule item = source[i]; if (seenCandidates.Add(item)) { destination.Add(item); } } } private static HashSet GetAnchorVisitedCharactersScratch() { HashSet hashSet = anchorVisitedCharactersScratch; if (hashSet == null) { return anchorVisitedCharactersScratch = new HashSet(); } hashSet.Clear(); return hashSet; } private static List GetGreedyAllMatchesScratch() { List list = greedyAllMatchesScratch; if (list == null) { return greedyAllMatchesScratch = new List(); } list.Clear(); return list; } private static List GetTemplateCandidateScratch(int capacity) { List list = templateCandidateScratch; if (list == null) { return templateCandidateScratch = new List(capacity); } list.Clear(); if (list.Capacity < capacity) { list.Capacity = capacity; } return list; } private static HashSet GetTemplateCandidateSeenScratch() { HashSet hashSet = templateCandidateSeenScratch; if (hashSet == null) { return templateCandidateSeenScratch = new HashSet(); } hashSet.Clear(); return hashSet; } private static List GetGreedySelectedMatchesScratch(int capacity) { List list = greedySelectedMatchesScratch; if (list == null) { return greedySelectedMatchesScratch = new List(capacity); } list.Clear(); if (list.Capacity < capacity) { list.Capacity = capacity; } return list; } private static Dictionary BuildOriginalNormalizedExactMatches(Dictionary normalizedExactRules) { if (normalizedExactRules == null || normalizedExactRules.Count == 0) { return new Dictionary(0, StringComparer.OrdinalIgnoreCase); } Dictionary dictionary = new Dictionary(normalizedExactRules.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair normalizedExactRule in normalizedExactRules) { if (!dictionary.ContainsKey(normalizedExactRule.Key)) { dictionary.Add(normalizedExactRule.Key, normalizedExactRule.Key); } } return dictionary; } private static Dictionary BuildExactRuleMetadata(Dictionary exactRules) { Dictionary dictionary = new Dictionary(exactRules?.Count ?? 0, StringComparer.Ordinal); if (exactRules == null) { return dictionary; } foreach (KeyValuePair exactRule in exactRules) { dictionary[exactRule.Key] = new RuleTraceMetadata(exactRule.Key, exactRule.Value, null); } return dictionary; } private static Dictionary BuildNormalizedExactRuleMetadata(Dictionary normalizedExactRules, Dictionary originalMatches) { Dictionary dictionary = new Dictionary(normalizedExactRules?.Count ?? 0, StringComparer.OrdinalIgnoreCase); if (normalizedExactRules == null) { return dictionary; } foreach (KeyValuePair normalizedExactRule in normalizedExactRules) { string value; string matchText = ((originalMatches != null && originalMatches.TryGetValue(normalizedExactRule.Key, out value)) ? value : normalizedExactRule.Key); dictionary[normalizedExactRule.Key] = new RuleTraceMetadata(matchText, normalizedExactRule.Value, null); } return dictionary; } private static bool[] GetGreedyOccupiedScratch(int length) { bool[] array = greedyOccupiedScratch; if (array == null || array.Length < length) { return greedyOccupiedScratch = new bool[length]; } Array.Clear(array, 0, length); return array; } private static void AddBucketRule(Dictionary> buckets, Dictionary> keysByFirstCharacter, string key, CompiledTemplateRule rule) { if (!buckets.TryGetValue(key, out var value)) { value = (buckets[key] = new List()); char key2 = char.ToUpperInvariant(key[0]); if (!keysByFirstCharacter.TryGetValue(key2, out var value2)) { value2 = (keysByFirstCharacter[key2] = new List()); } value2.Add(key); } value.Add(rule); } private static int CompareByDispatchOrder(CompiledTemplateRule left, CompiledTemplateRule right) { return left.DispatchOrder.CompareTo(right.DispatchOrder); } private static int CompareGreedyExactRules(GreedyExactRule left, GreedyExactRule right) { int num = right.Match.Length.CompareTo(left.Match.Length); if (num != 0) { return num; } return string.Compare(left.Match, right.Match, StringComparison.OrdinalIgnoreCase); } private static int CompareGreedyExactMatches(GreedyExactMatch left, GreedyExactMatch right) { int num = right.Rule.Match.Length.CompareTo(left.Rule.Match.Length); if (num != 0) { return num; } int num2 = left.StartIndex.CompareTo(right.StartIndex); if (num2 != 0) { return num2; } int num3 = string.Compare(left.Rule.Match, right.Rule.Match, StringComparison.OrdinalIgnoreCase); if (num3 != 0) { return num3; } return string.Compare(left.Rule.Replace, right.Rule.Replace, StringComparison.Ordinal); } private static int CompareGreedyExactMatchesByStartIndex(GreedyExactMatch left, GreedyExactMatch right) { return left.StartIndex.CompareTo(right.StartIndex); } } internal sealed class TranslationLoadResult { public string LanguageName { get; } public string EnglishFolderPath { get; } public int LocalEnglishFileCount { get; } public int SyncedEnglishFileCount { get; } public string ActiveFolderPath { get; } public int LocalActiveFileCount { get; } public int SyncedActiveFileCount { get; } public int ExactRuleCount { get; } public int TemplateRuleCount { get; } public CompiledRuleSet RuleSet { get; } public TranslationLoadResult(string languageName, string englishFolderPath, int localEnglishFileCount, int syncedEnglishFileCount, string activeFolderPath, int localActiveFileCount, int syncedActiveFileCount, int exactRuleCount, int templateRuleCount, CompiledRuleSet ruleSet) { LanguageName = languageName; EnglishFolderPath = englishFolderPath; LocalEnglishFileCount = localEnglishFileCount; SyncedEnglishFileCount = syncedEnglishFileCount; ActiveFolderPath = activeFolderPath; LocalActiveFileCount = localActiveFileCount; SyncedActiveFileCount = syncedActiveFileCount; ExactRuleCount = exactRuleCount; TemplateRuleCount = templateRuleCount; RuleSet = ruleSet; } } internal sealed class TranslationRuntimeDiagnostics { public bool Initialized { get; } public string LanguageName { get; } public bool TranslationEnabled { get; } public bool DebugEnabled { get; } public string ConfigRootPath { get; } public int ExactRuleCount { get; } public int TemplateRuleCount { get; } public int LocalEnglishFileCount { get; } public int SyncedEnglishFileCount { get; } public int LocalActiveFileCount { get; } public int SyncedActiveFileCount { get; } public int TranslationCacheCount { get; } public int TranslationMissCacheCount { get; } public int PlaceholderCacheCount { get; } public int PlaceholderMissCacheCount { get; } public bool HasRemoteSnapshot { get; } public TranslationPerformanceSnapshot Performance { get; } public TranslationRuntimeDiagnostics(bool initialized, string languageName, bool translationEnabled, bool debugEnabled, string configRootPath, int exactRuleCount, int templateRuleCount, int localEnglishFileCount, int syncedEnglishFileCount, int localActiveFileCount, int syncedActiveFileCount, int translationCacheCount, int translationMissCacheCount, int placeholderCacheCount, int placeholderMissCacheCount, bool hasRemoteSnapshot, TranslationPerformanceSnapshot performance) { Initialized = initialized; LanguageName = languageName; TranslationEnabled = translationEnabled; DebugEnabled = debugEnabled; ConfigRootPath = configRootPath; ExactRuleCount = exactRuleCount; TemplateRuleCount = templateRuleCount; LocalEnglishFileCount = localEnglishFileCount; SyncedEnglishFileCount = syncedEnglishFileCount; LocalActiveFileCount = localActiveFileCount; SyncedActiveFileCount = syncedActiveFileCount; TranslationCacheCount = translationCacheCount; TranslationMissCacheCount = translationMissCacheCount; PlaceholderCacheCount = placeholderCacheCount; PlaceholderMissCacheCount = placeholderMissCacheCount; HasRemoteSnapshot = hasRemoteSnapshot; Performance = performance; } } internal sealed class TranslationAnalysisSection { public string Title { get; } public string[] Lines { get; } public TranslationAnalysisPassView PassView { get; } public TranslationAnalysisSection(string title, string[] lines, TranslationAnalysisPassView passView = null) { Title = title ?? string.Empty; Lines = lines ?? Array.Empty(); PassView = passView; } } internal enum TranslationAnalysisStageStatus { Hit, Miss, NotExecuted } internal sealed class TranslationAnalysisPassStage { public string Title { get; } public TranslationAnalysisStageStatus Status { get; } public string Summary { get; } public string[] Details { get; } public TranslationAnalysisPassStage(string title, TranslationAnalysisStageStatus status, string summary, string[] details) { Title = title ?? string.Empty; Status = status; Summary = summary ?? string.Empty; Details = details ?? Array.Empty(); } } internal sealed class TranslationAnalysisPassView { public string Input { get; } public string Output { get; } public TranslationAnalysisPassStage[] Stages { get; } public TranslationAnalysisPassView(string input, string output, TranslationAnalysisPassStage[] stages) { Input = input ?? string.Empty; Output = output ?? string.Empty; Stages = stages ?? Array.Empty(); } } internal sealed class TranslationAnalysisResult { public string Input { get; } public string Output { get; } public TranslationAnalysisSection[] Sections { get; } public TranslationAnalysisResult(string input, string output, TranslationAnalysisSection[] sections) { Input = input ?? string.Empty; Output = output ?? string.Empty; Sections = sections ?? Array.Empty(); } } internal sealed class GreedyExactAnalysisResult { public string Output { get; } public bool LoopDetected { get; } public GreedyExactAnalysisStep[] Steps { get; } public GreedyExactAnalysisResult(string output, bool loopDetected, GreedyExactAnalysisStep[] steps) { Output = output ?? string.Empty; LoopDetected = loopDetected; Steps = steps ?? Array.Empty(); } } internal sealed class GreedyExactAnalysisStep { public string Input { get; } public string Output { get; } public GreedyExactAnalysisMatch[] Matches { get; } public GreedyExactAnalysisStep(string input, string output, GreedyExactAnalysisMatch[] matches) { Input = input ?? string.Empty; Output = output ?? string.Empty; Matches = matches ?? Array.Empty(); } } internal sealed class GreedyExactAnalysisMatch { public int StartIndex { get; } public int EndIndex { get; } public string InputSegment { get; } public string Match { get; } public string Replace { get; } public string SourcePath { get; } public GreedyExactAnalysisMatch(int startIndex, int endIndex, string inputSegment, string match, string replace, string sourcePath) { StartIndex = startIndex; EndIndex = endIndex; InputSegment = inputSegment ?? string.Empty; Match = match ?? string.Empty; Replace = replace ?? string.Empty; SourcePath = sourcePath ?? string.Empty; } } internal sealed class RuleTraceMetadata { public string MatchText { get; } public string ReplaceText { get; } public string SourcePath { get; } public RuleTraceMetadata(string matchText, string replaceText, string sourcePath) { MatchText = matchText ?? string.Empty; ReplaceText = replaceText ?? string.Empty; SourcePath = sourcePath ?? string.Empty; } } internal enum WholeStringGreedyExactMatchMode { Off, PerWord, MinLength } internal static class RuntimeDebugConfig { private const string ConfigFileName = "config.yaml"; private const double ReloadDebounceMilliseconds = 250.0; private const bool DefaultTranslationEnabled = true; private const bool DefaultDevToolsEnabled = true; private const WholeStringGreedyExactMatchMode DefaultGreedyExactMatchMode = WholeStringGreedyExactMatchMode.PerWord; private const int DefaultGreedyExactMinimumMatchLength = 5; internal const int DefaultTraceFeedMaxEntries = 1024; private static readonly KeyboardShortcut DefaultDevToolsToggleShortcut = new KeyboardShortcut((KeyCode)282, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }); private static FileSystemWatcher configWatcher; private static System.Timers.Timer reloadDebounceTimer; private static bool initialized; private static bool localTranslationEnabled = true; public static bool Enabled { get; private set; } public static bool TranslationEnabled => localTranslationEnabled; public static WholeStringGreedyExactMatchMode GreedyExactMatchMode { get; private set; } = WholeStringGreedyExactMatchMode.PerWord; public static int GreedyExactMinimumMatchLength { get; private set; } = 5; public static int TraceFeedMaxEntries { get; private set; } = 1024; public static bool DevToolsEnabled { get; private set; } = true; public static KeyboardShortcut DevToolsToggleShortcut { get; private set; } = DefaultDevToolsToggleShortcut; public static string ConfigFilePath => Path.Combine(TranslationRuleLoader.ConfigRootPath, "config.yaml"); internal static KeyboardShortcut DefaultDevToolsShortcut => DefaultDevToolsToggleShortcut; public static bool TrySetTranslationEnabled(bool value, out string error) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) return TryWriteSettings(value, Enabled, GreedyExactMatchMode, GreedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled, DevToolsToggleShortcut, "translation_enabled updated from devtools", out error); } public static bool TrySetDebugEnabled(bool value, out string error) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) return TryWriteSettings(TranslationEnabled, value, GreedyExactMatchMode, GreedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled, DevToolsToggleShortcut, "debug updated from devtools", out error); } public static bool TrySetGreedyExactMatchMode(WholeStringGreedyExactMatchMode value, out string error) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) return TryWriteSettings(TranslationEnabled, Enabled, value, GreedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled, DevToolsToggleShortcut, "whole_string_longest_match_mode updated from devtools", out error); } public static bool TrySetGreedyExactMinimumMatchLength(int value, out string error) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) int greedyExactMinimumMatchLength = ((value <= 0) ? 1 : value); return TryWriteSettings(TranslationEnabled, Enabled, GreedyExactMatchMode, greedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled, DevToolsToggleShortcut, "whole_string_longest_match_min_length updated from devtools", out error); } public static bool TrySetTraceFeedMaxEntries(int value, out string error) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) int traceFeedMaxEntries = ((value <= 0) ? 1 : value); return TryWriteSettings(TranslationEnabled, Enabled, GreedyExactMatchMode, GreedyExactMinimumMatchLength, traceFeedMaxEntries, DevToolsEnabled, DevToolsToggleShortcut, "trace_feed_max_entries updated from devtools", out error); } public static bool TrySetDevToolsEnabled(bool value, out string error) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) return TryWriteSettings(TranslationEnabled, Enabled, GreedyExactMatchMode, GreedyExactMinimumMatchLength, TraceFeedMaxEntries, value, DevToolsToggleShortcut, "devtools_enabled updated from devtools", out error); } public static bool TrySetDevToolsToggleShortcut(KeyboardShortcut shortcut, out string error) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if ((int)((KeyboardShortcut)(ref shortcut)).MainKey == 0) { error = "Shortcut must include a main key."; return false; } return TryWriteSettings(TranslationEnabled, Enabled, GreedyExactMatchMode, GreedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled, shortcut, "devtools_toggle_shortcut updated from devtools", out error); } public static bool ResetDevToolsToggleShortcut(out string error) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return TrySetDevToolsToggleShortcut(DefaultDevToolsToggleShortcut, out error); } public static void Initialize() { if (!initialized) { initialized = true; Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); EnsureConfigFileExists(); Reload("startup", logWhenUnchanged: true); SetupWatcher(); } } public static void Shutdown() { if (initialized) { if (configWatcher != null) { configWatcher.EnableRaisingEvents = false; configWatcher.Changed -= OnConfigChanged; configWatcher.Created -= OnConfigChanged; configWatcher.Deleted -= OnConfigChanged; configWatcher.Renamed -= OnConfigRenamed; configWatcher.Dispose(); configWatcher = null; } if (reloadDebounceTimer != null) { reloadDebounceTimer.Stop(); reloadDebounceTimer.Elapsed -= OnReloadDebounceElapsed; reloadDebounceTimer.Dispose(); reloadDebounceTimer = null; } initialized = false; } } private static void SetupWatcher() { configWatcher = new FileSystemWatcher(TranslationRuleLoader.ConfigRootPath, "config.yaml") { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime), SynchronizingObject = ThreadingHelper.SynchronizingObject }; configWatcher.Changed += OnConfigChanged; configWatcher.Created += OnConfigChanged; configWatcher.Deleted += OnConfigChanged; configWatcher.Renamed += OnConfigRenamed; configWatcher.EnableRaisingEvents = true; reloadDebounceTimer = new System.Timers.Timer(250.0) { AutoReset = false, SynchronizingObject = ThreadingHelper.SynchronizingObject }; reloadDebounceTimer.Elapsed += OnReloadDebounceElapsed; } private static void EnsureConfigFileExists() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (!File.Exists(ConfigFilePath)) { File.WriteAllText(ConfigFilePath, BuildConfigContents(translationEnabled: true, debugEnabled: false, WholeStringGreedyExactMatchMode.PerWord, 5, 1024, devToolsEnabled: true, DefaultDevToolsToggleShortcut)); } } private static void OnConfigChanged(object sender, FileSystemEventArgs e) { ScheduleReload(); } private static void OnConfigRenamed(object sender, RenamedEventArgs e) { ScheduleReload(); } private static void ScheduleReload() { if (reloadDebounceTimer == null) { Reload("config.yaml change"); return; } reloadDebounceTimer.Stop(); reloadDebounceTimer.Start(); } private static void OnReloadDebounceElapsed(object sender, ElapsedEventArgs e) { Reload("config.yaml change"); } private static void Reload(string reason, bool logWhenUnchanged = false) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) if (!TryReadSettings(out var translationEnabled, out var enabled, out var greedyExactMatchMode, out var greedyExactMinimumMatchLength, out var traceFeedMaxEntries, out var devToolsEnabled, out var devToolsToggleShortcut, out var error)) { ManualLogSource log = PluginMain.Log; if (log != null) { log.LogError((object)PluginMain.LocalizeFormat("runtime.settings.read_failed", "Failed to read runtime settings from '{0}': {1}", ConfigFilePath, error)); } return; } BackfillMissingSettingsIfNeeded(translationEnabled, enabled, greedyExactMatchMode, greedyExactMinimumMatchLength, traceFeedMaxEntries, devToolsEnabled, devToolsToggleShortcut); bool num = translationEnabled != localTranslationEnabled; bool flag = enabled != Enabled; bool flag2 = greedyExactMatchMode != GreedyExactMatchMode || greedyExactMinimumMatchLength != GreedyExactMinimumMatchLength; bool flag3 = traceFeedMaxEntries != TraceFeedMaxEntries; bool flag4 = devToolsEnabled != DevToolsEnabled || !KeyboardShortcutsEqual(devToolsToggleShortcut, DevToolsToggleShortcut); localTranslationEnabled = translationEnabled; Enabled = enabled; GreedyExactMatchMode = greedyExactMatchMode; GreedyExactMinimumMatchLength = greedyExactMinimumMatchLength; TraceFeedMaxEntries = traceFeedMaxEntries; DevToolsEnabled = devToolsEnabled; DevToolsToggleShortcut = devToolsToggleShortcut; TranslationRuntime.NotifyRuntimeSettingsChanged(num); if (num || flag || flag2 || flag3 || flag4 || logWhenUnchanged) { ManualLogSource log2 = PluginMain.Log; if (log2 != null) { log2.LogInfo((object)PluginMain.LocalizeFormat("runtime.settings.summary", "Runtime settings via '{0}' ({1}): translation_enabled={2}, debug={3}, whole_string_longest_match_mode={4}, whole_string_longest_match_min_length={5}, trace_feed_max_entries={6}, devtools_enabled={7}, devtools_toggle_shortcut={8}.", ConfigFilePath, reason, TranslationEnabled ? "true" : "false", Enabled ? "enabled" : "disabled", FormatGreedyExactMatchMode(GreedyExactMatchMode), GreedyExactMinimumMatchLength, TraceFeedMaxEntries, DevToolsEnabled ? "true" : "false", FormatKeyboardShortcutValue(DevToolsToggleShortcut))); } } } private static void BackfillMissingSettingsIfNeeded(bool translationEnabled, bool debugEnabled, WholeStringGreedyExactMatchMode greedyExactMatchMode, int greedyExactMinimumMatchLength, int traceFeedMaxEntries, bool devToolsEnabled, KeyboardShortcut devToolsToggleShortcut) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) try { if (!ConfigHasAllKnownSettings()) { Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); File.WriteAllText(ConfigFilePath, BuildConfigContents(translationEnabled, debugEnabled, greedyExactMatchMode, greedyExactMinimumMatchLength, traceFeedMaxEntries, devToolsEnabled, devToolsToggleShortcut)); } } catch (Exception ex) { ManualLogSource log = PluginMain.Log; if (log != null) { log.LogWarning((object)PluginMain.LocalizeFormat("runtime.settings.backfill_failed", "Failed to backfill missing runtime settings in '{0}': {1}", ConfigFilePath, ex.Message)); } } } private static bool TryWriteSettings(bool translationEnabled, bool debugEnabled, WholeStringGreedyExactMatchMode greedyExactMatchMode, int greedyExactMinimumMatchLength, int traceFeedMaxEntries, bool devToolsEnabled, KeyboardShortcut devToolsToggleShortcut, string reason, out string error) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) error = null; try { Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); File.WriteAllText(ConfigFilePath, BuildConfigContents(translationEnabled, debugEnabled, greedyExactMatchMode, greedyExactMinimumMatchLength, traceFeedMaxEntries, devToolsEnabled, devToolsToggleShortcut)); Reload(reason, logWhenUnchanged: true); return true; } catch (Exception ex) { error = ex.Message; return false; } } private static bool TryReadSettings(out bool translationEnabled, out bool enabled, out WholeStringGreedyExactMatchMode greedyExactMatchMode, out int greedyExactMinimumMatchLength, out int traceFeedMaxEntries, out bool devToolsEnabled, out KeyboardShortcut devToolsToggleShortcut, out string error) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) translationEnabled = true; enabled = false; greedyExactMatchMode = WholeStringGreedyExactMatchMode.PerWord; greedyExactMinimumMatchLength = 5; traceFeedMaxEntries = 1024; devToolsEnabled = true; devToolsToggleShortcut = DefaultDevToolsToggleShortcut; error = null; try { EnsureConfigFileExists(); string[] array = File.ReadAllLines(ConfigFilePath); for (int i = 0; i < array.Length; i++) { string text = StripComment(array[i]).Trim(); if (text.Length == 0) { continue; } int num = text.IndexOf(':'); if (num < 0) { continue; } string a = text.Substring(0, num).Trim(); string text2 = text.Substring(num + 1).Trim(); if (string.Equals(a, "translation_enabled", StringComparison.OrdinalIgnoreCase)) { if (!TryParseBoolean(text2, out translationEnabled)) { error = "Unsupported boolean value '" + text2 + "' for 'translation_enabled'."; return false; } } else if (string.Equals(a, "debug", StringComparison.OrdinalIgnoreCase)) { if (!TryParseBoolean(text2, out enabled)) { error = "Unsupported boolean value '" + text2 + "' for 'debug'."; return false; } } else if (string.Equals(a, "whole_string_longest_match_mode", StringComparison.OrdinalIgnoreCase)) { if (!TryParseGreedyExactMatchMode(text2, out greedyExactMatchMode)) { error = "Unsupported value '" + text2 + "' for 'whole_string_longest_match_mode'. Expected off, per_word, or min_length."; return false; } } else if (string.Equals(a, "whole_string_longest_match_min_length", StringComparison.OrdinalIgnoreCase)) { if (!TryParsePositiveInteger(text2, out greedyExactMinimumMatchLength)) { error = "Unsupported positive integer value '" + text2 + "' for 'whole_string_longest_match_min_length'."; return false; } } else if (string.Equals(a, "trace_feed_max_entries", StringComparison.OrdinalIgnoreCase)) { if (!TryParsePositiveInteger(text2, out traceFeedMaxEntries)) { error = "Unsupported positive integer value '" + text2 + "' for 'trace_feed_max_entries'."; return false; } } else if (string.Equals(a, "devtools_enabled", StringComparison.OrdinalIgnoreCase)) { if (!TryParseBoolean(text2, out devToolsEnabled)) { error = "Unsupported boolean value '" + text2 + "' for 'devtools_enabled'."; return false; } } else if (string.Equals(a, "devtools_toggle_shortcut", StringComparison.OrdinalIgnoreCase) && !TryParseKeyboardShortcut(text2, out devToolsToggleShortcut)) { error = "Unsupported value '" + text2 + "' for 'devtools_toggle_shortcut'."; return false; } } return true; } catch (Exception ex) { error = ex.Message; return false; } } private static string StripComment(string value) { int num = value.IndexOf('#'); if (num < 0) { return value; } return value.Substring(0, num); } private static bool ConfigHasAllKnownSettings() { if (ConfigContainsKey("translation_enabled") && ConfigContainsKey("debug") && ConfigContainsKey("whole_string_longest_match_mode") && ConfigContainsKey("whole_string_longest_match_min_length") && ConfigContainsKey("trace_feed_max_entries") && ConfigContainsKey("devtools_enabled")) { return ConfigContainsKey("devtools_toggle_shortcut"); } return false; } private static bool ConfigContainsKey(string expectedKey) { if (string.IsNullOrWhiteSpace(expectedKey) || !File.Exists(ConfigFilePath)) { return false; } string[] array = File.ReadAllLines(ConfigFilePath); for (int i = 0; i < array.Length; i++) { string text = StripComment(array[i]).Trim(); if (text.Length != 0) { int num = text.IndexOf(':'); if (num >= 0 && string.Equals(text.Substring(0, num).Trim(), expectedKey, StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } private static string BuildConfigContents(bool translationEnabled, bool debugEnabled, WholeStringGreedyExactMatchMode greedyExactMatchMode, int greedyExactMinimumMatchLength, int traceFeedMaxEntries, bool devToolsEnabled, KeyboardShortcut devToolsToggleShortcut) { //IL_0092: Unknown result type (might be due to invalid IL or missing references) return "# Localization runtime settings\n# Set to false to bypass the entire translation pipeline.\ntranslation_enabled: " + FormatBooleanValue(translationEnabled) + "\n\n# Set to true to log intercepted UI text and translation decisions.\ndebug: " + FormatBooleanValue(debugEnabled) + "\n\n# Controls whole-string longest-match exact replacement.\n# Values: off, per_word, min_length\nwhole_string_longest_match_mode: " + FormatGreedyExactMatchMode(greedyExactMatchMode) + "\n\n# Minimum letter/digit count for a whole string longest match when mode is min_length.\n" + $"whole_string_longest_match_min_length: {greedyExactMinimumMatchLength}\n" + "\n# Maximum unique entries kept in the Trace feed before the oldest entries are evicted.\n" + $"trace_feed_max_entries: {traceFeedMaxEntries}\n" + "\n# Set to false to disable the in-game Localization devtools overlay.\ndevtools_enabled: " + FormatBooleanValue(devToolsEnabled) + "\n\n# Shortcut used to open or close the in-game Localization devtools overlay.\n# Default: Ctrl + F1 in the UI (LeftControl + F1 in this file).\n# Format example: LeftControl + F1\ndevtools_toggle_shortcut: " + FormatKeyboardShortcutValue(devToolsToggleShortcut) + "\n"; } private static bool TryParseBoolean(string value, out bool result) { switch ((value ?? string.Empty).Trim().ToLowerInvariant()) { case "1": case "on": case "yes": case "true": result = true; return true; case "0": case "no": case "off": case "false": result = false; return true; default: result = false; return false; } } private static string FormatBooleanValue(bool value) { if (!value) { return "false"; } return "true"; } private static bool TryParseKeyboardShortcut(string value, out KeyboardShortcut shortcut) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) shortcut = DefaultDevToolsToggleShortcut; string text = NormalizeScalarValue(value); if (string.IsNullOrWhiteSpace(text)) { return false; } if (string.Equals(text, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "unset", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "", StringComparison.OrdinalIgnoreCase)) { shortcut = new KeyboardShortcut((KeyCode)0, Array.Empty()); return true; } string[] array = text.Split(new char[1] { '+' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { return false; } List list = new List(); KeyCode val = (KeyCode)0; for (int i = 0; i < array.Length; i++) { if (!TryParseShortcutKey(array[i], out var key)) { return false; } key = NormalizeModifierKey(key); if (IsModifierKey(key)) { AddUniqueModifier(list, key); continue; } if ((int)val != 0) { return false; } val = key; } if ((int)val == 0) { return false; } shortcut = ((list.Count == 0) ? new KeyboardShortcut(val, Array.Empty()) : new KeyboardShortcut(val, list.ToArray())); return true; } private static string FormatKeyboardShortcutValue(KeyboardShortcut shortcut) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) if ((int)((KeyboardShortcut)(ref shortcut)).MainKey == 0) { return "None"; } HashSet hashSet = new HashSet(); foreach (KeyCode modifier in ((KeyboardShortcut)(ref shortcut)).Modifiers) { hashSet.Add(NormalizeModifierKey(modifier)); } List list = new List(); AppendModifierIfPresent(list, hashSet, (KeyCode)306); AppendModifierIfPresent(list, hashSet, (KeyCode)304); AppendModifierIfPresent(list, hashSet, (KeyCode)308); AppendModifierIfPresent(list, hashSet, (KeyCode)310); KeyCode mainKey = ((KeyboardShortcut)(ref shortcut)).MainKey; list.Add(((object)(KeyCode)(ref mainKey)).ToString()); return string.Join(" + ", list.ToArray()); } private static bool KeyboardShortcutsEqual(KeyboardShortcut left, KeyboardShortcut right) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) return string.Equals(FormatKeyboardShortcutValue(left), FormatKeyboardShortcutValue(right), StringComparison.OrdinalIgnoreCase); } private static string NormalizeScalarValue(string value) { string text = (value ?? string.Empty).Trim(); if (text.Length >= 2) { char c = text[0]; char c2 = text[text.Length - 1]; if ((c == '"' && c2 == '"') || (c == '\'' && c2 == '\'')) { return text.Substring(1, text.Length - 2).Trim(); } } return text; } private static bool TryParseShortcutKey(string value, out KeyCode key) { string text = NormalizeScalarValue(value).Replace(" ", string.Empty); switch (text.ToLowerInvariant()) { case "ctrl": case "control": key = (KeyCode)306; return true; case "shift": key = (KeyCode)304; return true; case "alt": key = (KeyCode)308; return true; case "cmd": case "command": key = (KeyCode)310; return true; default: return Enum.TryParse(text, ignoreCase: true, out key); } } private static bool IsModifierKey(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 if (key - 303 <= 7) { return true; } return false; } private static KeyCode NormalizeModifierKey(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected I4, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) return (KeyCode)((key - 303) switch { 2 => 306, 0 => 304, 4 => 308, 6 => 310, _ => key, }); } private static void AddUniqueModifier(List modifiers, KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) KeyCode val = NormalizeModifierKey(key); for (int i = 0; i < modifiers.Count; i++) { if (modifiers[i] == val) { return; } } modifiers.Add(val); } private static void AppendModifierIfPresent(List parts, HashSet modifierSet, KeyCode modifier) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (modifierSet.Contains(modifier)) { parts.Add(((object)(KeyCode)(ref modifier)).ToString()); } } private static bool TryParseGreedyExactMatchMode(string value, out WholeStringGreedyExactMatchMode result) { switch ((value ?? string.Empty).Trim().ToLowerInvariant()) { case "none": case "disabled": case "off": case "unrestricted": result = WholeStringGreedyExactMatchMode.Off; return true; case "word": case "per_word": case "word_boundary": result = WholeStringGreedyExactMatchMode.PerWord; return true; case "min_length": case "minimum_length": case "minimum_match_length": result = WholeStringGreedyExactMatchMode.MinLength; return true; default: result = WholeStringGreedyExactMatchMode.PerWord; return false; } } private static bool TryParsePositiveInteger(string value, out int result) { if (int.TryParse((value ?? string.Empty).Trim(), out result) && result > 0) { return true; } result = 0; return false; } private static string FormatGreedyExactMatchMode(WholeStringGreedyExactMatchMode mode) { return mode switch { WholeStringGreedyExactMatchMode.Off => "off", WholeStringGreedyExactMatchMode.MinLength => "min_length", _ => "per_word", }; } } [HarmonyPatch] internal static class TextInterceptionPatches { private sealed class InterceptedTextState { public bool IsDevToolsComponent; public int Version; public string Input; public string Output; } private static readonly ConditionalWeakTable interceptedTextStates = new ConditionalWeakTable(); private static IEnumerable TargetMethods() { List list = new List(); MethodInfo methodInfo = AccessTools.PropertySetter(typeof(TMP_Text), "text"); if (methodInfo != null) { list.Add(methodInfo); } MethodInfo methodInfo2 = AccessTools.PropertySetter(typeof(Text), "text"); if (methodInfo2 != null) { list.Add(methodInfo2); } MethodInfo methodInfo3 = AccessTools.Method(typeof(TMP_Text), "SetText", new Type[2] { typeof(string), typeof(bool) }, (Type[])null); if (methodInfo3 != null) { list.Add(methodInfo3); } return list; } private static void Prefix(Component __instance, ref string __0, MethodBase __originalMethod) { long startTimestamp = TranslationPerformanceMetrics.StartTiming(); TranslationPerformanceMetrics.RecordInterceptionCall(); try { if (ShouldBypassDevToolsComponent(__instance)) { return; } int interceptionStateVersion = TranslationRuntime.InterceptionStateVersion; string text = __0; if (TryReuseInterceptedText(__instance, text, interceptionStateVersion, out var output)) { TranslationPerformanceMetrics.RecordInterceptionStateReuse(); __0 = output; return; } if (TranslationRuntime.IsKnownTranslatedOutput(text)) { TranslationPerformanceMetrics.RecordInterceptionKnownTranslatedOutput(); RememberInterceptedText(__instance, text, text, interceptionStateVersion); return; } bool num = !string.IsNullOrEmpty(__0); bool flag = num && TranslationTraceFeed.IsEnabled; bool flag2 = num && PluginMain.DebugModeEnabled; string sourceName = null; if (flag || flag2) { sourceName = ((__originalMethod == null) ? "" : (__originalMethod.DeclaringType?.FullName + "." + __originalMethod.Name)); } if (flag && TranslationTraceFeed.Capture(__0, sourceName)) { TranslationPerformanceMetrics.RecordTraceCapture(); } if (!RuntimeDebugConfig.TranslationEnabled) { RememberInterceptedText(__instance, text, text, interceptionStateVersion); return; } if (flag2) { string interceptedText = PluginMain.FormatTextForDebug(__0); PluginMain.DebugLog(() => "Captured UI text via " + sourceName + ": '" + interceptedText + "'"); } __0 = TranslationRuntime.Translate(__0); RememberInterceptedText(__instance, text, __0, interceptionStateVersion); } finally { TranslationPerformanceMetrics.AddInterceptionDuration(startTimestamp); } } private static bool ShouldBypassDevToolsComponent(Component instance) { if ((Object)(object)instance == (Object)null) { return false; } InterceptedTextState orCreateValue = interceptedTextStates.GetOrCreateValue(instance); if (!orCreateValue.IsDevToolsComponent) { orCreateValue.IsDevToolsComponent = (Object)(object)instance.GetComponentInParent() != (Object)null; } return orCreateValue.IsDevToolsComponent; } private static bool TryReuseInterceptedText(Component instance, string input, int version, out string output) { output = null; if ((Object)(object)instance == (Object)null) { return false; } if (!interceptedTextStates.TryGetValue(instance, out var value)) { return false; } if (value.Version != version || !string.Equals(value.Input, input, StringComparison.Ordinal)) { return false; } output = value.Output; return true; } private static void RememberInterceptedText(Component instance, string input, string output, int version) { if (!((Object)(object)instance == (Object)null)) { InterceptedTextState orCreateValue = interceptedTextStates.GetOrCreateValue(instance); orCreateValue.Version = version; orCreateValue.Input = input; orCreateValue.Output = output; } } } [HarmonyPatch(typeof(PlayerPrefs), "SetString", new Type[] { typeof(string), typeof(string) })] internal static class LanguagePreferencePatches { private static void Postfix(string key, string value) { if (string.Equals(key, "language", StringComparison.Ordinal)) { PluginLocalization.NotifyLanguageChanged(value); TranslationRuntime.NotifyLanguageChanged(value); } } } [HarmonyPatch(typeof(PlayerPrefs), "DeleteKey", new Type[] { typeof(string) })] internal static class LanguagePreferenceResetPatch { private static void Postfix(string key) { if (string.Equals(key, "language", StringComparison.Ordinal)) { PluginLocalization.NotifyLanguageReset(); TranslationRuntime.NotifyLanguageReset(); } } } internal static class TextMatchNormalizer { internal readonly struct ReplacementSegment { public int NormalizedStartIndex { get; } public int NormalizedLength { get; } public string ReplacementText { get; } public ReplacementSegment(int normalizedStartIndex, int normalizedLength, string replacementText) { NormalizedStartIndex = normalizedStartIndex; NormalizedLength = normalizedLength; ReplacementText = replacementText ?? string.Empty; } } internal readonly struct NormalizedText { private readonly int[] rawStartsByNormalizedIndex; private readonly int[] rawEndsByNormalizedIndex; private readonly TagToken[] tags; public string RawText { get; } public string Value { get; } internal NormalizedText(string rawText, string value, int[] rawStartsByNormalizedIndex, int[] rawEndsByNormalizedIndex, TagToken[] tags) { RawText = rawText; Value = value; this.rawStartsByNormalizedIndex = rawStartsByNormalizedIndex; this.rawEndsByNormalizedIndex = rawEndsByNormalizedIndex; this.tags = tags; } public string WrapWholeMatch(string replacement) { if (string.IsNullOrEmpty(Value)) { return replacement; } if (tags.Length == 0) { return replacement; } return CreateWrappedMatch(0, Value.Length, replacement).ReplacementText; } public string ReplaceNormalizedRanges(IReadOnlyList replacements) { if (replacements == null || replacements.Count == 0) { return RawText; } StringBuilder stringBuilder = new StringBuilder((RawText?.Length ?? 0) + 16); int num = 0; for (int i = 0; i < replacements.Count; i++) { ReplacementSegment replacementSegment = replacements[i]; if (replacementSegment.NormalizedLength > 0 && replacementSegment.NormalizedStartIndex >= 0 && replacementSegment.NormalizedStartIndex + replacementSegment.NormalizedLength <= Value.Length) { int num2; int num3; if (rawStartsByNormalizedIndex == null || rawEndsByNormalizedIndex == null) { num2 = replacementSegment.NormalizedStartIndex; num3 = replacementSegment.NormalizedStartIndex + replacementSegment.NormalizedLength; } else { num2 = rawStartsByNormalizedIndex[replacementSegment.NormalizedStartIndex]; num3 = rawEndsByNormalizedIndex[replacementSegment.NormalizedStartIndex + replacementSegment.NormalizedLength - 1]; } if (num2 > num) { stringBuilder.Append(RawText, num, num2 - num); } stringBuilder.Append(replacementSegment.ReplacementText); num = num3; } } if (num < RawText.Length) { stringBuilder.Append(RawText, num, RawText.Length - num); } return stringBuilder.ToString(); } public WrappedMatch CreateWrappedMatch(int normalizedStartIndex, int normalizedLength, string replacement) { if (normalizedLength <= 0 || normalizedStartIndex < 0 || normalizedStartIndex + normalizedLength > Value.Length) { return new WrappedMatch(0, 0, replacement); } int rawInnerStart; int rawInnerEnd; if (rawStartsByNormalizedIndex == null || rawEndsByNormalizedIndex == null) { rawInnerStart = normalizedStartIndex; rawInnerEnd = normalizedStartIndex + normalizedLength; } else { rawInnerStart = rawStartsByNormalizedIndex[normalizedStartIndex]; rawInnerEnd = rawEndsByNormalizedIndex[normalizedStartIndex + normalizedLength - 1]; } int rawStart; List prefixTags = CollectAdjacentOpeningTags(rawInnerStart, out rawStart); int rawEnd; List suffixTags = CollectAdjacentClosingTags(rawInnerEnd, out rawEnd); return new WrappedMatch(rawStart, rawEnd, BuildBalancedReplacement(prefixTags, replacement, suffixTags)); } private List CollectAdjacentOpeningTags(int rawInnerStart, out int rawStart) { List list = new List(); int num = rawInnerStart; TagToken matchedTag; while (TryFindOpeningTagEndingAt(num, out matchedTag)) { list.Insert(0, matchedTag); num = matchedTag.RawStart; } rawStart = num; return list; } private List CollectAdjacentClosingTags(int rawInnerEnd, out int rawEnd) { List list = new List(); int num = rawInnerEnd; TagToken matchedTag; while (TryFindClosingTagStartingAt(num, out matchedTag)) { list.Add(matchedTag); num = matchedTag.RawEnd; } rawEnd = num; return list; } private string BuildBalancedReplacement(List prefixTags, string replacement, List suffixTags) { if ((prefixTags == null || prefixTags.Count == 0) && (suffixTags == null || suffixTags.Count == 0)) { return replacement; } StringBuilder stringBuilder = new StringBuilder((replacement?.Length ?? 0) + 32); List list = new List(prefixTags?.Count ?? 0); if (prefixTags != null) { for (int i = 0; i < prefixTags.Count; i++) { TagToken tagToken = prefixTags[i]; AppendRawTag(stringBuilder, tagToken); if (!tagToken.IsSelfClosing) { list.Add(tagToken); } } } stringBuilder.Append(replacement); if (suffixTags != null) { for (int j = 0; j < suffixTags.Count; j++) { TagToken tag = suffixTags[j]; int num = FindLastMatchingOpenTagIndex(list, tag.Name); if (num >= 0) { for (int num2 = list.Count - 1; num2 > num; num2--) { stringBuilder.Append(CreateClosingTagText(list[num2])); list.RemoveAt(num2); } AppendRawTag(stringBuilder, tag); list.RemoveAt(list.Count - 1); } } } for (int num3 = list.Count - 1; num3 >= 0; num3--) { stringBuilder.Append(CreateClosingTagText(list[num3])); } return stringBuilder.ToString(); } private void AppendRawTag(StringBuilder builder, TagToken tag) { builder.Append(RawText, tag.RawStart, tag.RawEnd - tag.RawStart); } private static int FindLastMatchingOpenTagIndex(List openStack, string closingTagName) { for (int num = openStack.Count - 1; num >= 0; num--) { if (string.Equals(openStack[num].Name, closingTagName, StringComparison.OrdinalIgnoreCase)) { return num; } } return -1; } private static string CreateClosingTagText(TagToken openingTag) { if (!string.IsNullOrEmpty(openingTag.Name)) { return ""; } return string.Empty; } private bool TryFindOpeningTagEndingAt(int rawEnd, out TagToken matchedTag) { for (int i = 0; i < tags.Length; i++) { TagToken tagToken = tags[i]; if (!tagToken.IsClosing && !tagToken.IsSelfClosing && tagToken.RawEnd == rawEnd) { matchedTag = tagToken; return true; } } matchedTag = default(TagToken); return false; } private bool TryFindClosingTagStartingAt(int rawStart, out TagToken matchedTag) { for (int i = 0; i < tags.Length; i++) { TagToken tagToken = tags[i]; if (tagToken.IsClosing && tagToken.RawStart == rawStart) { matchedTag = tagToken; return true; } } matchedTag = default(TagToken); return false; } } internal readonly struct WrappedMatch { public int RawStart { get; } public int RawEnd { get; } public string ReplacementText { get; } public WrappedMatch(int rawStart, int rawEnd, string replacementText) { RawStart = rawStart; RawEnd = rawEnd; ReplacementText = replacementText; } } internal readonly struct TagToken { public int RawStart { get; } public int RawEnd { get; } public bool IsClosing { get; } public bool IsSelfClosing { get; } public string Name { get; } public TagToken(int rawStart, int rawEnd, bool isClosing, bool isSelfClosing, string name) { RawStart = rawStart; RawEnd = rawEnd; IsClosing = isClosing; IsSelfClosing = isSelfClosing; Name = name; } } private const int MaxAnalysisCacheEntries = 4096; private static readonly BoundedStringDictionary analysisCache = new BoundedStringDictionary(4096, StringComparer.Ordinal); private static readonly string[] SupportedTagNames = new string[39] { "align", "allcaps", "alpha", "b", "br", "color", "cspace", "font", "font-weight", "gradient", "i", "indent", "line-height", "line-indent", "link", "lowercase", "margin", "mark", "material", "mspace", "nobr", "noparse", "page", "pos", "quad", "rotate", "s", "size", "smallcaps", "space", "sprite", "strikethrough", "style", "sub", "sup", "u", "uppercase", "voffset", "width" }; public static string Normalize(string input) { return Analyze(input).Value; } public static NormalizedText Analyze(string input) { string text = input ?? string.Empty; long startTimestamp = TranslationPerformanceMetrics.StartTiming(); TranslationPerformanceMetrics.RecordNormalizationCall(); try { if (analysisCache.TryGetValue(text, out var value)) { TranslationPerformanceMetrics.RecordNormalizationCacheHit(); return value; } NormalizedText normalizedText = AnalyzeCore(text); if (analysisCache.Set(text, normalizedText, out var _)) { TranslationPerformanceMetrics.RecordNormalizationCacheEviction(); } return normalizedText; } finally { TranslationPerformanceMetrics.AddNormalizationDuration(startTimestamp); } } private static NormalizedText AnalyzeCore(string rawText) { if (rawText.Length == 0) { return new NormalizedText(rawText, string.Empty, Array.Empty(), Array.Empty(), Array.Empty()); } if (rawText.IndexOf('<') < 0) { return new NormalizedText(rawText, rawText, null, null, Array.Empty()); } StringBuilder stringBuilder = new StringBuilder(rawText.Length); List list = new List(rawText.Length); List list2 = new List(rawText.Length); List list3 = new List(); int num = 0; while (num < rawText.Length) { if (rawText[num] == '<' && TryParseSupportedTag(rawText, num, out var tag)) { list3.Add(tag); num = tag.RawEnd; continue; } list.Add(num); stringBuilder.Append(rawText[num]); num++; list2.Add(num); } return new NormalizedText(rawText, stringBuilder.ToString(), list.ToArray(), list2.ToArray(), list3.ToArray()); } private static bool TryParseSupportedTag(string input, int startIndex, out TagToken tag) { tag = default(TagToken); int num = input.IndexOf('>', startIndex + 1); if (num < 0) { return false; } int num2 = num - startIndex - 1; if (num2 <= 0) { return false; } if (!LooksLikeSupportedRichTextTag(input.Substring(startIndex + 1, num2), out var isClosing, out var isSelfClosing, out var tagName)) { return false; } tag = new TagToken(startIndex, num + 1, isClosing, isSelfClosing, tagName); return true; } private static bool LooksLikeSupportedRichTextTag(string content, out bool isClosing, out bool isSelfClosing, out string tagName) { isClosing = false; isSelfClosing = false; tagName = null; if (string.IsNullOrEmpty(content) || char.IsWhiteSpace(content[0])) { return false; } int i = 0; if (content[i] == '/') { isClosing = true; i++; if (i >= content.Length || char.IsWhiteSpace(content[i])) { return false; } } if (content[i] == '#') { if (isClosing || !IsSupportedHexColorTag(content, i)) { return false; } tagName = "color"; return true; } int num = i; for (; i < content.Length && IsTagNameCharacter(content[i]); i++) { } if (i == num) { return false; } string text = content.Substring(num, i - num); if (!IsSupportedTagName(text)) { return false; } tagName = text.ToLowerInvariant(); if (isClosing) { return HasOnlyTrailingWhitespace(content, i); } if (SupportsParameterlessTag(tagName)) { if (!HasOnlyOptionalSelfClosingSuffix(content, i, out var hasSelfClosingSlash)) { return false; } isSelfClosing = IsSelfClosingTag(tagName) || hasSelfClosingSlash; return true; } if (!HasAttributeAssignment(content, i)) { return false; } isSelfClosing = IsSelfClosingTag(tagName); return true; } private static bool IsSupportedHexColorTag(string content, int startIndex) { int num = content.Length - startIndex - 1; if (num != 3 && num != 4 && num != 6 && num != 8) { return false; } for (int i = startIndex + 1; i < content.Length; i++) { if (!IsHexDigit(content[i])) { return false; } } return true; } private static bool IsHexDigit(char character) { if ((character < '0' || character > '9') && (character < 'a' || character > 'f')) { if (character >= 'A') { return character <= 'F'; } return false; } return true; } private static bool IsTagNameCharacter(char character) { if (!char.IsLetter(character)) { return character == '-'; } return true; } private static bool IsSupportedTagName(string tagName) { for (int i = 0; i < SupportedTagNames.Length; i++) { if (string.Equals(SupportedTagNames[i], tagName, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static bool SupportsParameterlessTag(string tagName) { string text = tagName.ToLowerInvariant(); if (text != null) { switch (text.Length) { case 7: { char c = text[0]; if (c != 'a') { if (c != 'n' || !(text == "noparse")) { break; } } else if (!(text == "allcaps")) { break; } goto IL_0179; } case 1: { char c = text[0]; if ((uint)c <= 105u) { if (c != 'b' && c != 'i') { break; } } else if (c != 's' && c != 'u') { break; } goto IL_0179; } case 9: { char c = text[0]; if (c != 'l') { if (c != 's') { if (c != 'u' || !(text == "uppercase")) { break; } } else if (!(text == "smallcaps")) { break; } } else if (!(text == "lowercase")) { break; } goto IL_0179; } case 3: { char c = text[2]; if (c != 'b') { if (c != 'p' || !(text == "sup")) { break; } } else if (!(text == "sub")) { break; } goto IL_0179; } case 2: if (!(text == "br")) { break; } goto IL_0179; case 4: if (!(text == "nobr")) { break; } goto IL_0179; case 13: { if (!(text == "strikethrough")) { break; } goto IL_0179; } IL_0179: return true; } } return false; } private static bool IsSelfClosingTag(string tagName) { switch (tagName.ToLowerInvariant()) { case "br": case "page": case "quad": case "space": case "sprite": return true; default: return false; } } private static bool HasOnlyTrailingWhitespace(string content, int startIndex) { for (int i = startIndex; i < content.Length; i++) { if (!char.IsWhiteSpace(content[i])) { return false; } } return true; } private static bool HasOnlyOptionalSelfClosingSuffix(string content, int startIndex, out bool hasSelfClosingSlash) { hasSelfClosingSlash = false; int i; for (i = startIndex; i < content.Length && char.IsWhiteSpace(content[i]); i++) { } if (i == content.Length) { return true; } if (content[i] != '/') { return false; } hasSelfClosingSlash = true; i++; return HasOnlyTrailingWhitespace(content, i); } private static bool HasAttributeAssignment(string content, int startIndex) { int i; for (i = startIndex; i < content.Length && char.IsWhiteSpace(content[i]); i++) { } if (i >= content.Length) { return false; } if (content[i] == '=') { return true; } int num = i; for (; i < content.Length && IsAttributeNameCharacter(content[i]); i++) { } if (i == num) { return false; } for (; i < content.Length && char.IsWhiteSpace(content[i]); i++) { } if (i < content.Length) { return content[i] == '='; } return false; } private static bool IsAttributeNameCharacter(char character) { if (!char.IsLetterOrDigit(character) && character != '-') { return character == '_'; } return true; } } internal sealed class TranslationPerformanceSnapshot { public long InterceptionCallCount { get; } public long InterceptionStateReuseCount { get; } public long InterceptionKnownTranslatedOutputCount { get; } public long InterceptionTraceCaptureCount { get; } public long TranslationCallCount { get; } public long TranslationCacheHitCount { get; } public long TranslationMissCacheHitCount { get; } public long PlaceholderCacheHitCount { get; } public long PlaceholderMissCacheHitCount { get; } public long NormalizationCallCount { get; } public long NormalizationCacheHitCount { get; } public long TemplateDispatchCallCount { get; } public long TemplateCandidateCount { get; } public long TemplateRuleAttemptCount { get; } public long TemplateConstrainedAttemptCount { get; } public long TemplateMatchCount { get; } public long GreedyAttemptCount { get; } public long GreedyMatchCount { get; } public long TranslationCacheEvictionCount { get; } public long TranslationMissCacheEvictionCount { get; } public long PlaceholderCacheEvictionCount { get; } public long PlaceholderMissCacheEvictionCount { get; } public long NormalizationCacheEvictionCount { get; } public long GameResolveCacheEvictionCount { get; } public long GameResolveMissCacheEvictionCount { get; } public long GameLocalizeCacheEvictionCount { get; } public long GameLocalizeMissCacheEvictionCount { get; } public long GameItemNameTokenCacheEvictionCount { get; } public long GameItemNameTokenMissCacheEvictionCount { get; } public double InterceptionTotalMilliseconds { get; } public double TranslationTotalMilliseconds { get; } public double NormalizationTotalMilliseconds { get; } public double TemplateTotalMilliseconds { get; } public double GreedyTotalMilliseconds { get; } public double InterceptionAverageMilliseconds => CalculateAverage(InterceptionTotalMilliseconds, InterceptionCallCount); public double TranslationAverageMilliseconds => CalculateAverage(TranslationTotalMilliseconds, TranslationCallCount); public double NormalizationAverageMilliseconds => CalculateAverage(NormalizationTotalMilliseconds, NormalizationCallCount); public double TemplateAverageMilliseconds => CalculateAverage(TemplateTotalMilliseconds, TemplateDispatchCallCount); public double GreedyAverageMilliseconds => CalculateAverage(GreedyTotalMilliseconds, GreedyAttemptCount); public double TranslationCacheHitRate => CalculateRate(TranslationCacheHitCount, TranslationCallCount); public double TranslationMissCacheHitRate => CalculateRate(TranslationMissCacheHitCount, TranslationCallCount); public double NormalizationCacheHitRate => CalculateRate(NormalizationCacheHitCount, NormalizationCallCount); public double AverageTemplateCandidateCount => CalculateAverage(TemplateCandidateCount, TemplateDispatchCallCount); public long TotalRuntimeCacheEvictions => TranslationCacheEvictionCount + TranslationMissCacheEvictionCount + PlaceholderCacheEvictionCount + PlaceholderMissCacheEvictionCount + NormalizationCacheEvictionCount; public long TotalResolverCacheEvictions => GameResolveCacheEvictionCount + GameResolveMissCacheEvictionCount + GameLocalizeCacheEvictionCount + GameLocalizeMissCacheEvictionCount + GameItemNameTokenCacheEvictionCount + GameItemNameTokenMissCacheEvictionCount; public TranslationPerformanceSnapshot(long interceptionCallCount, long interceptionStateReuseCount, long interceptionKnownTranslatedOutputCount, long interceptionTraceCaptureCount, long translationCallCount, long translationCacheHitCount, long translationMissCacheHitCount, long placeholderCacheHitCount, long placeholderMissCacheHitCount, long normalizationCallCount, long normalizationCacheHitCount, long templateDispatchCallCount, long templateCandidateCount, long templateRuleAttemptCount, long templateConstrainedAttemptCount, long templateMatchCount, long greedyAttemptCount, long greedyMatchCount, long translationCacheEvictionCount, long translationMissCacheEvictionCount, long placeholderCacheEvictionCount, long placeholderMissCacheEvictionCount, long normalizationCacheEvictionCount, long gameResolveCacheEvictionCount, long gameResolveMissCacheEvictionCount, long gameLocalizeCacheEvictionCount, long gameLocalizeMissCacheEvictionCount, long gameItemNameTokenCacheEvictionCount, long gameItemNameTokenMissCacheEvictionCount, long interceptionTicks, long translationTicks, long normalizationTicks, long templateTicks, long greedyTicks) { InterceptionCallCount = interceptionCallCount; InterceptionStateReuseCount = interceptionStateReuseCount; InterceptionKnownTranslatedOutputCount = interceptionKnownTranslatedOutputCount; InterceptionTraceCaptureCount = interceptionTraceCaptureCount; TranslationCallCount = translationCallCount; TranslationCacheHitCount = translationCacheHitCount; TranslationMissCacheHitCount = translationMissCacheHitCount; PlaceholderCacheHitCount = placeholderCacheHitCount; PlaceholderMissCacheHitCount = placeholderMissCacheHitCount; NormalizationCallCount = normalizationCallCount; NormalizationCacheHitCount = normalizationCacheHitCount; TemplateDispatchCallCount = templateDispatchCallCount; TemplateCandidateCount = templateCandidateCount; TemplateRuleAttemptCount = templateRuleAttemptCount; TemplateConstrainedAttemptCount = templateConstrainedAttemptCount; TemplateMatchCount = templateMatchCount; GreedyAttemptCount = greedyAttemptCount; GreedyMatchCount = greedyMatchCount; TranslationCacheEvictionCount = translationCacheEvictionCount; TranslationMissCacheEvictionCount = translationMissCacheEvictionCount; PlaceholderCacheEvictionCount = placeholderCacheEvictionCount; PlaceholderMissCacheEvictionCount = placeholderMissCacheEvictionCount; NormalizationCacheEvictionCount = normalizationCacheEvictionCount; GameResolveCacheEvictionCount = gameResolveCacheEvictionCount; GameResolveMissCacheEvictionCount = gameResolveMissCacheEvictionCount; GameLocalizeCacheEvictionCount = gameLocalizeCacheEvictionCount; GameLocalizeMissCacheEvictionCount = gameLocalizeMissCacheEvictionCount; GameItemNameTokenCacheEvictionCount = gameItemNameTokenCacheEvictionCount; GameItemNameTokenMissCacheEvictionCount = gameItemNameTokenMissCacheEvictionCount; InterceptionTotalMilliseconds = TicksToMilliseconds(interceptionTicks); TranslationTotalMilliseconds = TicksToMilliseconds(translationTicks); NormalizationTotalMilliseconds = TicksToMilliseconds(normalizationTicks); TemplateTotalMilliseconds = TicksToMilliseconds(templateTicks); GreedyTotalMilliseconds = TicksToMilliseconds(greedyTicks); } private static double TicksToMilliseconds(long ticks) { if (ticks > 0) { return (double)ticks * 1000.0 / (double)Stopwatch.Frequency; } return 0.0; } private static double CalculateAverage(double total, long count) { if (count > 0) { return total / (double)count; } return 0.0; } private static double CalculateAverage(long total, long count) { if (count > 0) { return (double)total / (double)count; } return 0.0; } private static double CalculateRate(long hits, long total) { if (total > 0) { return (double)hits / (double)total; } return 0.0; } } internal static class TranslationPerformanceMetrics { private static long interceptionCallCount; private static long interceptionStateReuseCount; private static long interceptionKnownTranslatedOutputCount; private static long interceptionTraceCaptureCount; private static long translationCallCount; private static long translationCacheHitCount; private static long translationMissCacheHitCount; private static long placeholderCacheHitCount; private static long placeholderMissCacheHitCount; private static long normalizationCallCount; private static long normalizationCacheHitCount; private static long templateDispatchCallCount; private static long templateCandidateCount; private static long templateRuleAttemptCount; private static long templateConstrainedAttemptCount; private static long templateMatchCount; private static long greedyAttemptCount; private static long greedyMatchCount; private static long translationCacheEvictionCount; private static long translationMissCacheEvictionCount; private static long placeholderCacheEvictionCount; private static long placeholderMissCacheEvictionCount; private static long normalizationCacheEvictionCount; private static long gameResolveCacheEvictionCount; private static long gameResolveMissCacheEvictionCount; private static long gameLocalizeCacheEvictionCount; private static long gameLocalizeMissCacheEvictionCount; private static long gameItemNameTokenCacheEvictionCount; private static long gameItemNameTokenMissCacheEvictionCount; private static long interceptionTicks; private static long translationTicks; private static long normalizationTicks; private static long templateTicks; private static long greedyTicks; private static bool CollectionEnabled => RuntimeDebugConfig.TranslationEnabled; internal static long StartTiming() { if (!CollectionEnabled) { return 0L; } return Stopwatch.GetTimestamp(); } internal static TranslationPerformanceSnapshot GetSnapshot() { return new TranslationPerformanceSnapshot(Interlocked.Read(ref interceptionCallCount), Interlocked.Read(ref interceptionStateReuseCount), Interlocked.Read(ref interceptionKnownTranslatedOutputCount), Interlocked.Read(ref interceptionTraceCaptureCount), Interlocked.Read(ref translationCallCount), Interlocked.Read(ref translationCacheHitCount), Interlocked.Read(ref translationMissCacheHitCount), Interlocked.Read(ref placeholderCacheHitCount), Interlocked.Read(ref placeholderMissCacheHitCount), Interlocked.Read(ref normalizationCallCount), Interlocked.Read(ref normalizationCacheHitCount), Interlocked.Read(ref templateDispatchCallCount), Interlocked.Read(ref templateCandidateCount), Interlocked.Read(ref templateRuleAttemptCount), Interlocked.Read(ref templateConstrainedAttemptCount), Interlocked.Read(ref templateMatchCount), Interlocked.Read(ref greedyAttemptCount), Interlocked.Read(ref greedyMatchCount), Interlocked.Read(ref translationCacheEvictionCount), Interlocked.Read(ref translationMissCacheEvictionCount), Interlocked.Read(ref placeholderCacheEvictionCount), Interlocked.Read(ref placeholderMissCacheEvictionCount), Interlocked.Read(ref normalizationCacheEvictionCount), Interlocked.Read(ref gameResolveCacheEvictionCount), Interlocked.Read(ref gameResolveMissCacheEvictionCount), Interlocked.Read(ref gameLocalizeCacheEvictionCount), Interlocked.Read(ref gameLocalizeMissCacheEvictionCount), Interlocked.Read(ref gameItemNameTokenCacheEvictionCount), Interlocked.Read(ref gameItemNameTokenMissCacheEvictionCount), Interlocked.Read(ref interceptionTicks), Interlocked.Read(ref translationTicks), Interlocked.Read(ref normalizationTicks), Interlocked.Read(ref templateTicks), Interlocked.Read(ref greedyTicks)); } internal static void Reset() { Interlocked.Exchange(ref interceptionCallCount, 0L); Interlocked.Exchange(ref interceptionStateReuseCount, 0L); Interlocked.Exchange(ref interceptionKnownTranslatedOutputCount, 0L); Interlocked.Exchange(ref interceptionTraceCaptureCount, 0L); Interlocked.Exchange(ref translationCallCount, 0L); Interlocked.Exchange(ref translationCacheHitCount, 0L); Interlocked.Exchange(ref translationMissCacheHitCount, 0L); Interlocked.Exchange(ref placeholderCacheHitCount, 0L); Interlocked.Exchange(ref placeholderMissCacheHitCount, 0L); Interlocked.Exchange(ref normalizationCallCount, 0L); Interlocked.Exchange(ref normalizationCacheHitCount, 0L); Interlocked.Exchange(ref templateDispatchCallCount, 0L); Interlocked.Exchange(ref templateCandidateCount, 0L); Interlocked.Exchange(ref templateRuleAttemptCount, 0L); Interlocked.Exchange(ref templateConstrainedAttemptCount, 0L); Interlocked.Exchange(ref templateMatchCount, 0L); Interlocked.Exchange(ref greedyAttemptCount, 0L); Interlocked.Exchange(ref greedyMatchCount, 0L); Interlocked.Exchange(ref translationCacheEvictionCount, 0L); Interlocked.Exchange(ref translationMissCacheEvictionCount, 0L); Interlocked.Exchange(ref placeholderCacheEvictionCount, 0L); Interlocked.Exchange(ref placeholderMissCacheEvictionCount, 0L); Interlocked.Exchange(ref normalizationCacheEvictionCount, 0L); Interlocked.Exchange(ref gameResolveCacheEvictionCount, 0L); Interlocked.Exchange(ref gameResolveMissCacheEvictionCount, 0L); Interlocked.Exchange(ref gameLocalizeCacheEvictionCount, 0L); Interlocked.Exchange(ref gameLocalizeMissCacheEvictionCount, 0L); Interlocked.Exchange(ref gameItemNameTokenCacheEvictionCount, 0L); Interlocked.Exchange(ref gameItemNameTokenMissCacheEvictionCount, 0L); Interlocked.Exchange(ref interceptionTicks, 0L); Interlocked.Exchange(ref translationTicks, 0L); Interlocked.Exchange(ref normalizationTicks, 0L); Interlocked.Exchange(ref templateTicks, 0L); Interlocked.Exchange(ref greedyTicks, 0L); } internal static void RecordInterceptionCall() { IncrementIfEnabled(ref interceptionCallCount); } internal static void RecordInterceptionStateReuse() { IncrementIfEnabled(ref interceptionStateReuseCount); } internal static void RecordInterceptionKnownTranslatedOutput() { IncrementIfEnabled(ref interceptionKnownTranslatedOutputCount); } internal static void RecordTraceCapture() { IncrementIfEnabled(ref interceptionTraceCaptureCount); } internal static void RecordTranslationCall() { IncrementIfEnabled(ref translationCallCount); } internal static void RecordTranslationCacheHit() { IncrementIfEnabled(ref translationCacheHitCount); } internal static void RecordTranslationMissCacheHit() { IncrementIfEnabled(ref translationMissCacheHitCount); } internal static void RecordPlaceholderCacheHit() { IncrementIfEnabled(ref placeholderCacheHitCount); } internal static void RecordPlaceholderMissCacheHit() { IncrementIfEnabled(ref placeholderMissCacheHitCount); } internal static void RecordNormalizationCall() { IncrementIfEnabled(ref normalizationCallCount); } internal static void RecordNormalizationCacheHit() { IncrementIfEnabled(ref normalizationCacheHitCount); } internal static void RecordTemplateDispatch(int candidateCount) { if (CollectionEnabled) { Interlocked.Increment(ref templateDispatchCallCount); Interlocked.Add(ref templateCandidateCount, candidateCount); } } internal static void RecordTemplateRuleAttempt(bool constrained) { if (CollectionEnabled) { Interlocked.Increment(ref templateRuleAttemptCount); if (constrained) { Interlocked.Increment(ref templateConstrainedAttemptCount); } } } internal static void RecordTemplateMatch() { IncrementIfEnabled(ref templateMatchCount); } internal static void RecordGreedyAttempt() { IncrementIfEnabled(ref greedyAttemptCount); } internal static void RecordGreedyMatch() { IncrementIfEnabled(ref greedyMatchCount); } internal static void RecordTranslationCacheEviction() { IncrementIfEnabled(ref translationCacheEvictionCount); } internal static void RecordTranslationMissCacheEviction() { IncrementIfEnabled(ref translationMissCacheEvictionCount); } internal static void RecordPlaceholderCacheEviction() { IncrementIfEnabled(ref placeholderCacheEvictionCount); } internal static void RecordPlaceholderMissCacheEviction() { IncrementIfEnabled(ref placeholderMissCacheEvictionCount); } internal static void RecordNormalizationCacheEviction() { IncrementIfEnabled(ref normalizationCacheEvictionCount); } internal static void RecordGameResolveCacheEviction() { IncrementIfEnabled(ref gameResolveCacheEvictionCount); } internal static void RecordGameResolveMissCacheEviction() { IncrementIfEnabled(ref gameResolveMissCacheEvictionCount); } internal static void RecordGameLocalizeCacheEviction() { IncrementIfEnabled(ref gameLocalizeCacheEvictionCount); } internal static void RecordGameLocalizeMissCacheEviction() { IncrementIfEnabled(ref gameLocalizeMissCacheEvictionCount); } internal static void RecordGameItemNameTokenCacheEviction() { IncrementIfEnabled(ref gameItemNameTokenCacheEvictionCount); } internal static void RecordGameItemNameTokenMissCacheEviction() { IncrementIfEnabled(ref gameItemNameTokenMissCacheEvictionCount); } internal static void AddInterceptionDuration(long startTimestamp) { AddDuration(ref interceptionTicks, startTimestamp); } internal static void AddTranslationDuration(long startTimestamp) { AddDuration(ref translationTicks, startTimestamp); } internal static void AddNormalizationDuration(long startTimestamp) { AddDuration(ref normalizationTicks, startTimestamp); } internal static void AddTemplateDuration(long startTimestamp) { AddDuration(ref templateTicks, startTimestamp); } internal static void AddGreedyDuration(long startTimestamp) { AddDuration(ref greedyTicks, startTimestamp); } private static void AddDuration(ref long destinationTicks, long startTimestamp) { if (CollectionEnabled && startTimestamp > 0) { long num = Stopwatch.GetTimestamp() - startTimestamp; if (num > 0) { Interlocked.Add(ref destinationTicks, num); } } } private static void IncrementIfEnabled(ref long counter) { if (CollectionEnabled) { Interlocked.Increment(ref counter); } } } internal static class TranslationRuleBrowserSnapshotProvider { private static readonly object Sync = new object(); private static int nextVersion = 1; private static TranslationRuleBrowserSnapshot snapshot; internal static TranslationRuleBrowserSnapshot GetSnapshot() { lock (Sync) { if (snapshot != null) { return snapshot; } snapshot = new TranslationRuleBrowserSnapshot(nextVersion++, new TranslationRuleBrowserSourceSnapshot[2] { BuildSource("local", TranslationServerSyncBridge.GetLocalSnapshotForBrowsing()), BuildSource("server", TranslationServerSyncBridge.GetServerSnapshotForBrowsing()) }); return snapshot; } } internal static void Invalidate() { lock (Sync) { snapshot = null; } } private static TranslationRuleBrowserSourceSnapshot BuildSource(string sourceKey, TranslationSyncSnapshot sourceSnapshot) { List list = new List(); foreach (SyncedLanguageRules item in (sourceSnapshot?.languages ?? Array.Empty()).Where((SyncedLanguageRules candidate) => candidate != null && !string.IsNullOrWhiteSpace(candidate.name)).OrderBy((SyncedLanguageRules candidate) => candidate.name, StringComparer.OrdinalIgnoreCase)) { List list2 = new List(); foreach (SyncedRuleFile item2 in (item.files ?? Array.Empty()).Where((SyncedRuleFile candidate) => candidate != null && !string.IsNullOrWhiteSpace(candidate.fileName)).OrderBy((SyncedRuleFile candidate) => candidate.fileName, StringComparer.OrdinalIgnoreCase)) { RuleFileModel ruleFileModel = item2.rules ?? new RuleFileModel(); TranslationRuleBrowserRuleSnapshot[] exactRules = BuildRules(TranslationRuleBrowserRuleKind.Exact, ruleFileModel.exact); TranslationRuleBrowserRuleSnapshot[] templateRules = BuildRules(TranslationRuleBrowserRuleKind.Template, ruleFileModel.templates); list2.Add(new TranslationRuleBrowserFileSnapshot(sourceKey, item.name, item2.fileName, exactRules, templateRules)); } list.Add(new TranslationRuleBrowserLanguageSnapshot(item.name, list2.ToArray())); } return new TranslationRuleBrowserSourceSnapshot(sourceKey, list.ToArray()); } private static TranslationRuleBrowserRuleSnapshot[] BuildRules(TranslationRuleBrowserRuleKind kind, ExactRuleDefinition[] rules) { if (rules == null || rules.Length == 0) { return Array.Empty(); } List list = new List(rules.Length); foreach (ExactRuleDefinition exactRuleDefinition in rules) { if (exactRuleDefinition != null) { list.Add(new TranslationRuleBrowserRuleSnapshot(kind, exactRuleDefinition.match, exactRuleDefinition.replace)); } } return list.ToArray(); } private static TranslationRuleBrowserRuleSnapshot[] BuildRules(TranslationRuleBrowserRuleKind kind, TemplateRuleDefinition[] rules) { if (rules == null || rules.Length == 0) { return Array.Empty(); } List list = new List(rules.Length); foreach (TemplateRuleDefinition templateRuleDefinition in rules) { if (templateRuleDefinition != null) { list.Add(new TranslationRuleBrowserRuleSnapshot(kind, templateRuleDefinition.match, templateRuleDefinition.replace)); } } return list.ToArray(); } } internal sealed class TranslationRuleBrowserSnapshot { public int Version { get; } public TranslationRuleBrowserSourceSnapshot[] Sources { get; } public TranslationRuleBrowserSnapshot(int version, TranslationRuleBrowserSourceSnapshot[] sources) { Version = version; Sources = sources ?? Array.Empty(); } } internal sealed class TranslationRuleBrowserSourceSnapshot { public string SourceKey { get; } public TranslationRuleBrowserLanguageSnapshot[] Languages { get; } public TranslationRuleBrowserSourceSnapshot(string sourceKey, TranslationRuleBrowserLanguageSnapshot[] languages) { SourceKey = sourceKey ?? string.Empty; Languages = languages ?? Array.Empty(); } } internal sealed class TranslationRuleBrowserLanguageSnapshot { public string LanguageCode { get; } public TranslationRuleBrowserFileSnapshot[] Files { get; } public TranslationRuleBrowserLanguageSnapshot(string languageCode, TranslationRuleBrowserFileSnapshot[] files) { LanguageCode = languageCode ?? string.Empty; Files = files ?? Array.Empty(); } } internal sealed class TranslationRuleBrowserFileSnapshot { public string Id { get; } public string SourceKey { get; } public string LanguageCode { get; } public string FileName { get; } public TranslationRuleBrowserRuleSnapshot[] ExactRules { get; } public TranslationRuleBrowserRuleSnapshot[] TemplateRules { get; } public int TotalRuleCount => ExactRules.Length + TemplateRules.Length; public TranslationRuleBrowserFileSnapshot(string sourceKey, string languageCode, string fileName, TranslationRuleBrowserRuleSnapshot[] exactRules, TranslationRuleBrowserRuleSnapshot[] templateRules) { SourceKey = sourceKey ?? string.Empty; LanguageCode = languageCode ?? string.Empty; FileName = fileName ?? string.Empty; ExactRules = exactRules ?? Array.Empty(); TemplateRules = templateRules ?? Array.Empty(); Id = SourceKey + "|" + LanguageCode + "|" + FileName; } } internal sealed class TranslationRuleBrowserRuleSnapshot { public TranslationRuleBrowserRuleKind Kind { get; } public string Match { get; } public string Replace { get; } public TranslationRuleBrowserRuleSnapshot(TranslationRuleBrowserRuleKind kind, string match, string replace) { Kind = kind; Match = match ?? string.Empty; Replace = replace ?? string.Empty; } } internal enum TranslationRuleBrowserRuleKind { Exact, Template } internal static class TranslationRuleLoader { private sealed class RuleAccumulator { public string LanguageName { get; } public Dictionary ExactRules { get; } = new Dictionary(StringComparer.Ordinal); public Dictionary TemplateRules { get; } = new Dictionary(StringComparer.Ordinal); public List> OrderedTemplateRules { get; } = new List>(); public bool HasRules { get { if (ExactRules.Count <= 0) { return OrderedTemplateRules.Count > 0; } return true; } } public RuleAccumulator(string languageName) { LanguageName = languageName; } } private sealed class TrackedExactRule { public ExactRuleDefinition Rule { get; } public string SourcePath { get; } public TrackedExactRule(ExactRuleDefinition rule, string sourcePath) { Rule = rule; SourcePath = sourcePath ?? string.Empty; } } private sealed class TrackedTemplateRule { public TemplateRuleDefinition Rule { get; } public string SourcePath { get; } public TrackedTemplateRule(TemplateRuleDefinition rule, string sourcePath) { Rule = rule; SourcePath = sourcePath ?? string.Empty; } } private const string EnglishLanguage = "English"; private const string ConfigFolderName = "com.paxton0505.localization"; public static string ConfigRootPath => Path.Combine(Paths.ConfigPath, "com.paxton0505.localization"); public static string EnglishFolderPath => GetLanguagePath("English"); public static TranslationLoadResult Load(string languageName, TranslationSyncSnapshot remoteSnapshot = null) { string text = (string.IsNullOrWhiteSpace(languageName) ? "English" : languageName); TranslationSyncSnapshot remoteSnapshot2 = remoteSnapshot ?? TranslationSyncSnapshot.Empty; Directory.CreateDirectory(ConfigRootPath); Directory.CreateDirectory(EnglishFolderPath); string languagePath = GetLanguagePath(text); Directory.CreateDirectory(languagePath); int fileCount; RuleAccumulator ruleAccumulator = LoadLanguageRules("English", out fileCount); int fileCount2; RuleAccumulator ruleAccumulator2 = LoadSyncedLanguageRules(remoteSnapshot2, "English", out fileCount2); RuleAccumulator localActiveRules; RuleAccumulator syncedActiveRules; int fileCount3; int fileCount4; if (string.Equals(text, "English", StringComparison.Ordinal)) { localActiveRules = ruleAccumulator; syncedActiveRules = ruleAccumulator2; fileCount3 = 0; fileCount4 = 0; } else { localActiveRules = LoadLanguageRules(text, out fileCount3); syncedActiveRules = LoadSyncedLanguageRules(remoteSnapshot2, text, out fileCount4); } List precedenceOrderedLayers = BuildRuleLayers(text, ruleAccumulator, ruleAccumulator2, localActiveRules, syncedActiveRules); Dictionary exactRuleMetadata; Dictionary exactRules = BuildExactRules(precedenceOrderedLayers, out exactRuleMetadata); Dictionary originalNormalizedExactMatches; Dictionary normalizedExactRuleMetadata; Dictionary englishExactRulesIgnoreCase = BuildNormalizedExactRules(precedenceOrderedLayers, out originalNormalizedExactMatches, out normalizedExactRuleMetadata); List templateRules = BuildCompiledTemplates(precedenceOrderedLayers); CompiledRuleSet compiledRuleSet = new CompiledRuleSet(exactRules, englishExactRulesIgnoreCase, originalNormalizedExactMatches, exactRuleMetadata, normalizedExactRuleMetadata, templateRules); return new TranslationLoadResult(text, EnglishFolderPath, fileCount, fileCount2, languagePath, fileCount3, fileCount4, compiledRuleSet.ExactRules.Count, compiledRuleSet.TemplateRules.Count, compiledRuleSet); } private static List BuildRuleLayers(string resolvedLanguage, RuleAccumulator localEnglishRules, RuleAccumulator syncedEnglishRules, RuleAccumulator localActiveRules, RuleAccumulator syncedActiveRules) { List list = new List(4); if (!string.Equals(resolvedLanguage, "English", StringComparison.Ordinal)) { AddRuleLayer(list, syncedActiveRules); AddRuleLayer(list, localActiveRules); } AddRuleLayer(list, syncedEnglishRules); AddRuleLayer(list, localEnglishRules); return list; } private static void AddRuleLayer(List layers, RuleAccumulator accumulator) { if (accumulator.HasRules) { layers.Add(accumulator); } } private static Dictionary BuildExactRules(IReadOnlyList precedenceOrderedLayers, out Dictionary exactRuleMetadata) { int capacity = precedenceOrderedLayers.Sum((RuleAccumulator layer) => layer.ExactRules.Count); Dictionary dictionary = new Dictionary(capacity, StringComparer.Ordinal); exactRuleMetadata = new Dictionary(capacity, StringComparer.Ordinal); foreach (RuleAccumulator precedenceOrderedLayer in precedenceOrderedLayers) { foreach (KeyValuePair exactRule in precedenceOrderedLayer.ExactRules) { if (!dictionary.ContainsKey(exactRule.Key)) { dictionary.Add(exactRule.Key, exactRule.Value.Rule.replace); exactRuleMetadata.Add(exactRule.Key, new RuleTraceMetadata(exactRule.Key, exactRule.Value.Rule.replace, exactRule.Value.SourcePath)); } } } return dictionary; } private static Dictionary BuildNormalizedExactRules(IReadOnlyList precedenceOrderedLayers, out Dictionary originalNormalizedExactMatches, out Dictionary normalizedExactRuleMetadata) { int capacity = precedenceOrderedLayers.Sum((RuleAccumulator layer) => layer.ExactRules.Count); Dictionary dictionary = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); originalNormalizedExactMatches = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); normalizedExactRuleMetadata = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); foreach (RuleAccumulator precedenceOrderedLayer in precedenceOrderedLayers) { MergeNormalizedExactRules(dictionary, originalNormalizedExactMatches, normalizedExactRuleMetadata, precedenceOrderedLayer); } return dictionary; } private static void MergeNormalizedExactRules(Dictionary ignoreCaseRules, Dictionary originalNormalizedExactMatches, Dictionary normalizedExactRuleMetadata, RuleAccumulator accumulator) { foreach (KeyValuePair rule in accumulator.ExactRules) { string text = TextMatchNormalizer.Normalize(rule.Key); if (string.IsNullOrWhiteSpace(text)) { continue; } if (ignoreCaseRules.ContainsKey(text)) { PluginMain.DebugLog(() => "Duplicate normalized exact fallback rule '" + rule.Key + "' in language '" + accumulator.LanguageName + "'. Later definition was ignored."); } else { ignoreCaseRules.Add(text, rule.Value.Rule.replace); originalNormalizedExactMatches.Add(text, rule.Key); normalizedExactRuleMetadata.Add(text, new RuleTraceMetadata(rule.Key, rule.Value.Rule.replace, rule.Value.SourcePath)); } } } private static List BuildCompiledTemplates(IReadOnlyList precedenceOrderedLayers) { List list = new List(precedenceOrderedLayers.Sum((RuleAccumulator layer) => layer.OrderedTemplateRules.Count)); HashSet seenTemplateMatches = new HashSet(StringComparer.Ordinal); foreach (RuleAccumulator precedenceOrderedLayer in precedenceOrderedLayers) { LoadCompiledTemplates(precedenceOrderedLayer.OrderedTemplateRules, precedenceOrderedLayer.LanguageName, list, seenTemplateMatches); } return list; } private static void LoadCompiledTemplates(IReadOnlyList> sourceRules, string languageName, List orderedTemplates, HashSet seenTemplateMatches) { foreach (KeyValuePair sourceRule in sourceRules) { if (seenTemplateMatches.Add(sourceRule.Key)) { if (!CompiledTemplateRule.TryCreate(sourceRule.Value.Rule.match, sourceRule.Value.Rule.replace, sourceRule.Value.SourcePath, out var rule, out var error)) { PluginMain.Log.LogError((object)("Failed to compile template rule '" + sourceRule.Key + "' from language '" + languageName + "': " + error)); } else { orderedTemplates.Add(rule); } } } } private static RuleAccumulator LoadLanguageRules(string languageName, out int fileCount) { string languagePath = GetLanguagePath(languageName); RuleAccumulator ruleAccumulator = new RuleAccumulator(languageName); string[] array = Directory.GetFiles(languagePath, "*.json", SearchOption.TopDirectoryOnly).OrderBy((string path) => Path.GetFileName(path), StringComparer.OrdinalIgnoreCase).ToArray(); fileCount = array.Length; string[] array2 = array; foreach (string filePath in array2) { try { string text = File.ReadAllText(filePath); RuleFileModel model = JsonConvert.DeserializeObject(text) ?? new RuleFileModel(); PluginMain.DebugLog(delegate { object[] obj = new object[4] { filePath, languageName, null, null }; ExactRuleDefinition[] exact = model.exact; obj[2] = ((exact != null) ? exact.Length : 0); TemplateRuleDefinition[] templates = model.templates; obj[3] = ((templates != null) ? templates.Length : 0); return string.Format("Loaded local translation file '{0}' for language '{1}': exact={2}, templates={3}.", obj); }); MergeExactRules(ruleAccumulator, model.exact, filePath); MergeTemplateRules(ruleAccumulator, model.templates, filePath); } catch (Exception ex) { PluginMain.Log.LogError((object)("Failed to load translation file '" + filePath + "': " + ex.Message)); } } return ruleAccumulator; } private static RuleAccumulator LoadSyncedLanguageRules(TranslationSyncSnapshot remoteSnapshot, string languageName, out int fileCount) { RuleAccumulator ruleAccumulator = new RuleAccumulator(languageName); fileCount = 0; if (remoteSnapshot == null || !remoteSnapshot.TryGetLanguage(languageName, out var languageRules) || languageRules.files == null) { return ruleAccumulator; } SyncedRuleFile[] array = languageRules.files.Where((SyncedRuleFile file) => file != null && !string.IsNullOrWhiteSpace(file.fileName)).OrderBy((SyncedRuleFile file) => file.fileName, StringComparer.OrdinalIgnoreCase).ToArray(); fileCount = array.Length; SyncedRuleFile[] array2 = array; foreach (SyncedRuleFile syncedRuleFile in array2) { RuleFileModel model = syncedRuleFile.rules ?? new RuleFileModel(); string snapshotPath = "serversync://" + languageName + "/" + syncedRuleFile.fileName; PluginMain.DebugLog(delegate { string arg = snapshotPath; ExactRuleDefinition[] exact = model.exact; object arg2 = ((exact != null) ? exact.Length : 0); TemplateRuleDefinition[] templates = model.templates; return $"Loaded synced translation file '{arg}': exact={arg2}, templates={((templates != null) ? templates.Length : 0)}."; }); MergeExactRules(ruleAccumulator, model.exact, snapshotPath); MergeTemplateRules(ruleAccumulator, model.templates, snapshotPath); } return ruleAccumulator; } private static void MergeExactRules(RuleAccumulator accumulator, ExactRuleDefinition[] rules, string filePath) { if (rules == null) { return; } foreach (ExactRuleDefinition rule in rules) { if (rule == null || string.IsNullOrWhiteSpace(rule.match) || rule.replace == null) { PluginMain.Log.LogWarning((object)("Ignoring invalid exact rule in '" + filePath + "'.")); } else if (accumulator.ExactRules.ContainsKey(rule.match)) { PluginMain.DebugLog(() => "Duplicate exact rule '" + rule.match + "' in language '" + accumulator.LanguageName + "'. Later definition from '" + filePath + "' was ignored."); } else { accumulator.ExactRules.Add(rule.match, new TrackedExactRule(rule, filePath)); } } } private static void MergeTemplateRules(RuleAccumulator accumulator, TemplateRuleDefinition[] rules, string filePath) { if (rules == null) { return; } foreach (TemplateRuleDefinition rule in rules) { if (rule == null || string.IsNullOrWhiteSpace(rule.match) || rule.replace == null) { PluginMain.Log.LogWarning((object)("Ignoring invalid template rule in '" + filePath + "'.")); } else if (accumulator.TemplateRules.ContainsKey(rule.match)) { PluginMain.DebugLog(() => "Duplicate template rule '" + rule.match + "' in language '" + accumulator.LanguageName + "'. Later definition from '" + filePath + "' was ignored."); } else { TrackedTemplateRule value = new TrackedTemplateRule(rule, filePath); accumulator.TemplateRules.Add(rule.match, value); accumulator.OrderedTemplateRules.Add(new KeyValuePair(rule.match, value)); } } } private static string GetLanguagePath(string languageName) { return Path.Combine(ConfigRootPath, languageName); } } internal static class TranslationRuntime { private const string EnglishLanguage = "English"; private const int MaxTranslationCacheEntries = 16384; private const int MaxMissCacheEntries = 4096; private const int MaxPlaceholderCacheEntries = 4096; private const int MaxPlaceholderMissCacheEntries = 2048; private static readonly BoundedStringDictionary translationCache = new BoundedStringDictionary(16384, StringComparer.Ordinal); private static readonly BoundedStringSet missCache = new BoundedStringSet(4096, StringComparer.Ordinal); private static readonly Dictionary knownTranslatedOutputCounts = new Dictionary(StringComparer.Ordinal); private static readonly BoundedStringDictionary placeholderCache = new BoundedStringDictionary(4096, StringComparer.Ordinal); private static readonly BoundedStringSet placeholderMissCache = new BoundedStringSet(2048, StringComparer.Ordinal); private static CompiledRuleSet compiledRuleSet = CompiledRuleSet.Empty; private static TranslationSyncSnapshot remoteSnapshot = TranslationSyncSnapshot.Empty; private static string currentLanguage = "English"; private static TranslationLoadResult lastLoadResult; private static bool initialized; private static int interceptionStateVersion; internal static int InterceptionStateVersion => Volatile.Read(ref interceptionStateVersion); public static void Initialize() { if (!initialized) { initialized = true; currentLanguage = ResolveCurrentLanguage(); PluginMain.Log.LogInfo((object)PluginMain.LocalizeFormat("runtime.log.config_root", "Translation config root: {0}", TranslationRuleLoader.ConfigRootPath)); if (!RuntimeDebugConfig.TranslationEnabled) { DisableRuntime("startup", logInfo: true); return; } TranslationLoadResult loadResult = ReloadCompiledRules(); LogLoadResult("Loaded", loadResult); } } internal static TranslationRuntimeDiagnostics GetDiagnostics() { TranslationLoadResult translationLoadResult = lastLoadResult; return new TranslationRuntimeDiagnostics(initialized, translationLoadResult?.LanguageName ?? currentLanguage, RuntimeDebugConfig.TranslationEnabled, RuntimeDebugConfig.Enabled, TranslationRuleLoader.ConfigRootPath, translationLoadResult?.ExactRuleCount ?? compiledRuleSet.ExactRules.Count, translationLoadResult?.TemplateRuleCount ?? compiledRuleSet.TemplateRules.Count, translationLoadResult?.LocalEnglishFileCount ?? 0, translationLoadResult?.SyncedEnglishFileCount ?? 0, translationLoadResult?.LocalActiveFileCount ?? 0, translationLoadResult?.SyncedActiveFileCount ?? 0, translationCache.Count, missCache.Count, placeholderCache.Count, placeholderMissCache.Count, remoteSnapshot != TranslationSyncSnapshot.Empty, TranslationPerformanceMetrics.GetSnapshot()); } internal static CompiledRuleSet GetCompiledRuleSetSnapshot() { return compiledRuleSet ?? CompiledRuleSet.Empty; } internal static TranslationAnalysisResult AnalyzeTranslation(string input) { return AnalyzeTranslation(input, compiledRuleSet); } internal static TranslationAnalysisResult AnalyzeTranslation(string input, CompiledRuleSet rules) { CompiledRuleSet activeRuleSet = rules ?? CompiledRuleSet.Empty; List list = new List(); string text = input ?? string.Empty; List list2 = new List(); string text2 = PluginMain.Localize("runtime.analysis.pass_title_one", "Pass 1"); string text3 = PluginMain.Localize("runtime.analysis.pass_title_two", "Pass 2"); list2.Add(PluginMain.LocalizeFormat("runtime.analysis.runtime_initialized", "Runtime initialized: {0}", initialized ? PluginMain.Localize("common.yes", "Yes") : PluginMain.Localize("common.no", "No"))); list2.Add(PluginMain.LocalizeFormat("runtime.analysis.translation_enabled", "Translation enabled: {0}", RuntimeDebugConfig.TranslationEnabled ? PluginMain.Localize("common.yes", "Yes") : PluginMain.Localize("common.no", "No"))); list2.Add(PluginMain.Localize("runtime.analysis.runtime_pass_chain", "Top-level pass chain: fixed 2 passes. Additional whole string longest match rewrites are shown as step N inside a pass.")); if (!initialized) { list2.Add(PluginMain.Localize("runtime.analysis.runtime_not_initialized", "Analysis stopped because the translation runtime is not initialized.")); list.Add(new TranslationAnalysisSection(PluginMain.Localize("runtime.analysis.runtime_state", "Runtime State"), list2.ToArray())); return new TranslationAnalysisResult(text, text, list.ToArray()); } if (text.Length == 0) { list2.Add(PluginMain.Localize("runtime.analysis.runtime_empty", "Analysis stopped because the selected string is empty.")); list.Add(new TranslationAnalysisSection(PluginMain.Localize("runtime.analysis.runtime_state", "Runtime State"), list2.ToArray())); return new TranslationAnalysisResult(text, text, list.ToArray()); } if (!RuntimeDebugConfig.TranslationEnabled) { list2.Add(PluginMain.Localize("runtime.analysis.runtime_disabled", "Translation is currently disabled; analysis is using the current in-memory rule state.")); } list.Add(new TranslationAnalysisSection(PluginMain.Localize("runtime.analysis.runtime_state", "Runtime State"), list2.ToArray())); List list3 = new List(); TranslationAnalysisPassView passView; string text4 = AnalyzeTranslationPass(activeRuleSet, text, text2, list3, out passView); list.Add(new TranslationAnalysisSection(text2, list3.ToArray(), passView)); if (string.Equals(text4, text, StringComparison.Ordinal)) { list.Add(new TranslationAnalysisSection(PluginMain.Localize("runtime.analysis.final_title", "Final"), new string[2] { PluginMain.Localize("runtime.analysis.pass_result_no_change", "Pass 1 result: no translation rule changed the input."), PluginMain.LocalizeFormat("runtime.analysis.final_output", "Final output: '{0}'", PluginMain.FormatTextForDebug(text)) })); return new TranslationAnalysisResult(text, text, list.ToArray()); } List list4 = new List(); TranslationAnalysisPassView passView2; string text5 = AnalyzeTranslationPass(activeRuleSet, text4, text3, list4, out passView2); list.Add(new TranslationAnalysisSection(text3, list4.ToArray(), passView2)); string text6 = text4; List list5 = new List(); if (string.Equals(text5, text4, StringComparison.Ordinal)) { list5.Add(PluginMain.Localize("runtime.analysis.pass_result_no_further_change", "Pass 2 result: no further translation rule changed the first pass output.")); } else if (string.Equals(text5, text, StringComparison.Ordinal)) { list5.Add(PluginMain.Localize("runtime.analysis.pass_result_loop", "Pass 2 result: a translation loop back to the original input was detected, so the pass 1 output was kept.")); } else { text6 = text5; list5.Add(PluginMain.LocalizeFormat("runtime.analysis.pass_result_updated", "Pass 2 result: output updated to '{0}'.", PluginMain.FormatTextForDebug(text6))); } list5.Add(PluginMain.LocalizeFormat("runtime.analysis.final_output", "Final output: '{0}'", PluginMain.FormatTextForDebug(text6))); list.Add(new TranslationAnalysisSection(PluginMain.Localize("runtime.analysis.final_title", "Final"), list5.ToArray())); return new TranslationAnalysisResult(text, text6, list.ToArray()); } public static string Translate(string input) { if (!initialized || !RuntimeDebugConfig.TranslationEnabled || string.IsNullOrEmpty(input)) { return input; } long startTimestamp = TranslationPerformanceMetrics.StartTiming(); TranslationPerformanceMetrics.RecordTranslationCall(); try { if (translationCache.TryGetValue(input, out var cachedValue)) { TranslationPerformanceMetrics.RecordTranslationCacheHit(); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Translation cache hit: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(cachedValue) + "'."); string replayedValue = TranslateCore(input); if (!string.Equals(replayedValue, cachedValue, StringComparison.Ordinal)) { PluginMain.DebugLog(() => "Cached translation replay diverged from the stored value: cached='" + PluginMain.FormatTextForDebug(cachedValue) + "', replayed='" + PluginMain.FormatTextForDebug(replayedValue) + "'."); } } return cachedValue; } if (missCache.Contains(input)) { TranslationPerformanceMetrics.RecordTranslationMissCacheHit(); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Translation miss cache hit: '" + PluginMain.FormatTextForDebug(input) + "'."); } return input; } string translated = TranslateCore(input); if (string.Equals(translated, input, StringComparison.Ordinal)) { AddTranslationMiss(input); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "No translation rule matched: '" + PluginMain.FormatTextForDebug(input) + "'."); } return input; } AddTranslationHit(input, translated); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Translation result: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(translated) + "'."); } return translated; } finally { TranslationPerformanceMetrics.AddTranslationDuration(startTimestamp); } } internal static string ResolveTemplatePlaceholder(string input) { if (string.IsNullOrWhiteSpace(input)) { return input; } if (placeholderCache.TryGetValue(input, out var cachedValue)) { TranslationPerformanceMetrics.RecordPlaceholderCacheHit(); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Template placeholder cache hit: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(cachedValue) + "'."); } return cachedValue; } if (placeholderMissCache.Contains(input)) { TranslationPerformanceMetrics.RecordPlaceholderMissCacheHit(); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Template placeholder miss cache hit: '" + PluginMain.FormatTextForDebug(input) + "'."); } return input; } string text = ResolveTemplatePlaceholderCore(input); if (string.Equals(text, input, StringComparison.Ordinal)) { AddPlaceholderMiss(input); return input; } AddPlaceholderHit(input, text); return text; } private static string ResolveTemplatePlaceholderCore(string input) { string translated = TranslatePlaceholderChain(input); if (!string.Equals(translated, input, StringComparison.Ordinal)) { if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Template placeholder resolved through exact/whole-string chain: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(translated) + "'."); } return translated; } if (GameTranslationResolver.TryResolve(input, out var gameResolvedValue)) { string resolved = TranslatePlaceholderChain(gameResolvedValue); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog(() => "Template placeholder resolved through game localization + exact/whole-string chain: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(gameResolvedValue) + "' -> '" + PluginMain.FormatTextForDebug(resolved) + "'."); } return resolved; } return input; } private static string TranslateCore(string input) { return TranslateChain(input, TranslateSinglePass); } private static string AnalyzeTranslationPass(CompiledRuleSet activeRuleSet, string input, string passLabel, List lines, out TranslationAnalysisPassView passView) { List list = new List(); lines.Add(PluginMain.LocalizeFormat("runtime.analysis.pass_input", "{0} input: '{1}'", passLabel, PluginMain.FormatTextForDebug(input))); string text = PluginMain.Localize("runtime.analysis.stage.exact", "Exact"); string text2 = PluginMain.Localize("runtime.analysis.stage.normalized_exact", "Normalized Exact"); string text3 = PluginMain.Localize("runtime.analysis.stage.template", "Template"); string text4 = PluginMain.Localize("runtime.analysis.stage.whole_string_longest_match", "Whole String Longest Match"); if (activeRuleSet.ExactRules.TryGetValue(input, out var value)) { RuleTraceMetadata metadata; RuleTraceMetadata ruleTraceMetadata = (activeRuleSet.TryGetExactRuleMetadata(input, out metadata) ? metadata : new RuleTraceMetadata(input, value, null)); string text5 = PluginMain.LocalizeFormat("runtime.analysis.pass_exact_hit", "{0} exact: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'", passLabel, PluginMain.FormatTextForDebug(input), PluginMain.FormatTextForDebug(ruleTraceMetadata.MatchText), PluginMain.FormatTextForDebug(ruleTraceMetadata.ReplaceText), PluginMain.FormatTextForDebug(FormatTraceSourcePath(ruleTraceMetadata.SourcePath)), PluginMain.FormatTextForDebug(value)); lines.Add(text5); list.Add(CreateAnalysisStage(text, TranslationAnalysisStageStatus.Hit, value, text5)); list.Add(CreateNotExecutedStage(passLabel, text2, text, lines)); list.Add(CreateNotExecutedStage(passLabel, text3, text, lines)); list.Add(CreateNotExecutedStage(passLabel, text4, text, lines)); passView = new TranslationAnalysisPassView(input, value, list.ToArray()); return value; } string text6 = PluginMain.LocalizeFormat("runtime.analysis.pass_exact_miss", "{0} exact: miss", passLabel); lines.Add(text6); list.Add(CreateAnalysisStage(text, TranslationAnalysisStageStatus.Miss, input, text6)); TextMatchNormalizer.NormalizedText normalizedInput = TextMatchNormalizer.Analyze(input); List list2 = new List(); if (!string.Equals(normalizedInput.Value, input, StringComparison.Ordinal)) { string item = PluginMain.LocalizeFormat("runtime.analysis.pass_normalized_input", "{0} normalized input: '{1}'", passLabel, PluginMain.FormatTextForDebug(normalizedInput.Value)); lines.Add(item); list2.Add(item); } if (activeRuleSet.MatchNormalizedExactRules.TryGetValue(normalizedInput.Value, out var value2)) { string text7 = normalizedInput.WrapWholeMatch(value2); RuleTraceMetadata metadata2; RuleTraceMetadata ruleTraceMetadata2 = (activeRuleSet.TryGetNormalizedExactRuleMetadata(normalizedInput.Value, out metadata2) ? metadata2 : new RuleTraceMetadata(normalizedInput.Value, value2, null)); string item2 = PluginMain.LocalizeFormat("runtime.analysis.pass_normalized_exact_hit", "{0} normalized exact: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'", passLabel, PluginMain.FormatTextForDebug(normalizedInput.Value), PluginMain.FormatTextForDebug(ruleTraceMetadata2.MatchText), PluginMain.FormatTextForDebug(ruleTraceMetadata2.ReplaceText), PluginMain.FormatTextForDebug(FormatTraceSourcePath(ruleTraceMetadata2.SourcePath)), PluginMain.FormatTextForDebug(text7)); lines.Add(item2); list2.Add(item2); list.Add(CreateAnalysisStage(text2, TranslationAnalysisStageStatus.Hit, text7, list2.ToArray())); list.Add(CreateNotExecutedStage(passLabel, text3, text2, lines)); list.Add(CreateNotExecutedStage(passLabel, text4, text2, lines)); passView = new TranslationAnalysisPassView(input, text7, list.ToArray()); return text7; } string item3 = PluginMain.LocalizeFormat("runtime.analysis.pass_normalized_exact_miss", "{0} normalized exact: miss", passLabel); lines.Add(item3); list2.Add(item3); list.Add(CreateAnalysisStage(text2, TranslationAnalysisStageStatus.Miss, null, list2.ToArray())); List list3 = new List(); foreach (CompiledTemplateRule templateCandidate in activeRuleSet.GetTemplateCandidates(normalizedInput.Value)) { if (templateCandidate.TryTranslate(normalizedInput, out var translated)) { string item4 = PluginMain.LocalizeFormat("runtime.analysis.pass_template_hit", "{0} template: hit target='{1}', match='{2}', replace='{3}', file='{4}' -> '{5}'", passLabel, PluginMain.FormatTextForDebug(normalizedInput.Value), PluginMain.FormatTextForDebug(templateCandidate.Signature), PluginMain.FormatTextForDebug(templateCandidate.ReplaceTemplate), PluginMain.FormatTextForDebug(FormatTraceSourcePath(templateCandidate.SourcePath)), PluginMain.FormatTextForDebug(translated)); lines.Add(item4); list3.Add(item4); list.Add(CreateAnalysisStage(text3, TranslationAnalysisStageStatus.Hit, translated, list3.ToArray())); list.Add(CreateNotExecutedStage(passLabel, text4, text3, lines)); passView = new TranslationAnalysisPassView(input, translated, list.ToArray()); return translated; } } string item5 = PluginMain.LocalizeFormat("runtime.analysis.pass_template_miss", "{0} template: miss", passLabel); lines.Add(item5); list3.Add(item5); list.Add(CreateAnalysisStage(text3, TranslationAnalysisStageStatus.Miss, null, list3.ToArray())); List list4 = new List(); if (activeRuleSet.TryTranslateGreedyExact(normalizedInput, out var translated2)) { if (activeRuleSet.TryAnalyzeGreedyExact(normalizedInput, out var analysis)) { for (int i = 0; i < analysis.Steps.Length; i++) { GreedyExactAnalysisStep greedyExactAnalysisStep = analysis.Steps[i]; int num = i + 1; string item6 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_step_input", "{0} whole string longest match step {1} input: '{2}'", passLabel, num, PluginMain.FormatTextForDebug(greedyExactAnalysisStep.Input)); lines.Add(item6); list4.Add(item6); for (int j = 0; j < greedyExactAnalysisStep.Matches.Length; j++) { GreedyExactAnalysisMatch greedyExactAnalysisMatch = greedyExactAnalysisStep.Matches[j]; string item7 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_step_hit", "{0} whole string longest match step {1} hit [{2}..{3}): target='{4}', match='{5}', replace='{6}', file='{7}'", passLabel, num, greedyExactAnalysisMatch.StartIndex, greedyExactAnalysisMatch.EndIndex, PluginMain.FormatTextForDebug(greedyExactAnalysisMatch.InputSegment), PluginMain.FormatTextForDebug(greedyExactAnalysisMatch.Match), PluginMain.FormatTextForDebug(greedyExactAnalysisMatch.Replace), PluginMain.FormatTextForDebug(FormatTraceSourcePath(greedyExactAnalysisMatch.SourcePath))); lines.Add(item7); list4.Add(item7); } string item8 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_step_output", "{0} whole string longest match step {1} output: '{2}'", passLabel, num, PluginMain.FormatTextForDebug(greedyExactAnalysisStep.Output)); lines.Add(item8); list4.Add(item8); } if (analysis.LoopDetected) { string item9 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_loop", "{0} whole string longest match: detected a loop and kept the last non-loop output.", passLabel); lines.Add(item9); list4.Add(item9); } string item10 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_final", "{0} whole string longest match: final -> '{1}'", passLabel, PluginMain.FormatTextForDebug(analysis.Output)); lines.Add(item10); list4.Add(item10); list.Add(CreateAnalysisStage(text4, TranslationAnalysisStageStatus.Hit, analysis.Output, list4.ToArray())); passView = new TranslationAnalysisPassView(input, analysis.Output, list.ToArray()); return analysis.Output; } string item11 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_matched", "{0} whole string longest match: matched -> '{1}'", passLabel, PluginMain.FormatTextForDebug(translated2)); lines.Add(item11); list4.Add(item11); list.Add(CreateAnalysisStage(text4, TranslationAnalysisStageStatus.Hit, translated2, list4.ToArray())); passView = new TranslationAnalysisPassView(input, translated2, list.ToArray()); return translated2; } string item12 = PluginMain.LocalizeFormat("runtime.analysis.pass_whole_string_longest_match_miss", "{0} whole string longest match: miss", passLabel); lines.Add(item12); list4.Add(item12); list.Add(CreateAnalysisStage(text4, TranslationAnalysisStageStatus.Miss, null, list4.ToArray())); passView = new TranslationAnalysisPassView(input, input, list.ToArray()); return input; } private static TranslationAnalysisPassStage CreateAnalysisStage(string title, TranslationAnalysisStageStatus status, string output, params string[] details) { return new TranslationAnalysisPassStage(title, status, (status == TranslationAnalysisStageStatus.Hit) ? PluginMain.LocalizeFormat("runtime.analysis.stage_summary_output", "Output: '{0}'", PluginMain.FormatTextForDebug(output)) : PluginMain.Localize("runtime.analysis.stage_summary_miss", "No rule matched."), details ?? Array.Empty()); } private static TranslationAnalysisPassStage CreateNotExecutedStage(string passLabel, string stageTitle, string previousStageTitle, List lines) { string text = PluginMain.LocalizeFormat("runtime.analysis.pass_stage_not_executed", "{0} {1}: not executed ({2} already produced output)", passLabel, stageTitle, previousStageTitle); lines.Add(text); return new TranslationAnalysisPassStage(stageTitle, TranslationAnalysisStageStatus.NotExecuted, PluginMain.LocalizeFormat("runtime.analysis.stage_summary_not_executed", "Skipped because {0} already produced output.", previousStageTitle), new string[1] { text }); } private static string FormatTraceSourcePath(string sourcePath) { if (!string.IsNullOrWhiteSpace(sourcePath)) { return sourcePath; } return PluginMain.Localize("common.unknown", ""); } private static string TranslateSinglePass(string input) { if (compiledRuleSet.ExactRules.TryGetValue(input, out var value)) { LogMatch("exact", input, value); return value; } TextMatchNormalizer.NormalizedText normalizedInput = TextMatchNormalizer.Analyze(input); if (compiledRuleSet.MatchNormalizedExactRules.TryGetValue(normalizedInput.Value, out var value2)) { string text = normalizedInput.WrapWholeMatch(value2); string originalMatch; string detail = (compiledRuleSet.TryGetOriginalNormalizedExactMatch(normalizedInput.Value, out originalMatch) ? originalMatch : normalizedInput.Value); LogMatch("normalized exact", input, text, normalizedInput.Value, detail); return text; } long startTimestamp = TranslationPerformanceMetrics.StartTiming(); IReadOnlyList templateCandidates = compiledRuleSet.GetTemplateCandidates(normalizedInput.Value); TranslationPerformanceMetrics.RecordTemplateDispatch(templateCandidates.Count); try { for (int i = 0; i < templateCandidates.Count; i++) { CompiledTemplateRule compiledTemplateRule = templateCandidates[i]; TranslationPerformanceMetrics.RecordTemplateRuleAttempt(compiledTemplateRule.UsesConstrainedPlaceholders); if (compiledTemplateRule.TryTranslate(normalizedInput, out var translated)) { TranslationPerformanceMetrics.RecordTemplateMatch(); LogMatch("template", input, translated, normalizedInput.Value, compiledTemplateRule.Signature); return translated; } } } finally { TranslationPerformanceMetrics.AddTemplateDuration(startTimestamp); } long startTimestamp2 = TranslationPerformanceMetrics.StartTiming(); TranslationPerformanceMetrics.RecordGreedyAttempt(); try { if (compiledRuleSet.TryTranslateGreedyExact(normalizedInput, out var translated2)) { TranslationPerformanceMetrics.RecordGreedyMatch(); LogMatch("whole-string longest-match exact replacement", input, translated2, normalizedInput.Value); return translated2; } return input; } finally { TranslationPerformanceMetrics.AddGreedyDuration(startTimestamp2); } } private static string TranslateExactChain(string input) { return TranslateChain(input, TranslateExactSinglePass); } private static string TranslatePlaceholderChain(string input) { return TranslateChain(input, TranslatePlaceholderSinglePass); } private static string TranslateExactSinglePass(string input) { return TranslateExactSinglePass(input, TextMatchNormalizer.Analyze(input)); } private static string TranslateExactSinglePass(string input, TextMatchNormalizer.NormalizedText normalizedInput) { if (compiledRuleSet.ExactRules.TryGetValue(input, out var value)) { LogMatch("exact", input, value); return value; } if (compiledRuleSet.MatchNormalizedExactRules.TryGetValue(normalizedInput.Value, out var value2)) { string text = normalizedInput.WrapWholeMatch(value2); string originalMatch; string detail = (compiledRuleSet.TryGetOriginalNormalizedExactMatch(normalizedInput.Value, out originalMatch) ? originalMatch : normalizedInput.Value); LogMatch("normalized exact", input, text, normalizedInput.Value, detail); return text; } return input; } private static string TranslatePlaceholderSinglePass(string input) { TextMatchNormalizer.NormalizedText normalizedInput = TextMatchNormalizer.Analyze(input); string text = TranslateExactSinglePass(input, normalizedInput); if (!string.Equals(text, input, StringComparison.Ordinal)) { return text; } long startTimestamp = TranslationPerformanceMetrics.StartTiming(); TranslationPerformanceMetrics.RecordGreedyAttempt(); try { if (compiledRuleSet.TryTranslateGreedyExact(normalizedInput, out var translated)) { TranslationPerformanceMetrics.RecordGreedyMatch(); LogMatch("whole-string longest-match exact replacement", input, translated, normalizedInput.Value); return translated; } return input; } finally { TranslationPerformanceMetrics.AddGreedyDuration(startTimestamp); } } private static string TranslateChain(string input, Func translatePass) { string firstPass = translatePass(input); if (string.Equals(firstPass, input, StringComparison.Ordinal)) { return input; } string text = translatePass(firstPass); if (string.Equals(text, firstPass, StringComparison.Ordinal)) { return firstPass; } if (string.Equals(text, input, StringComparison.Ordinal)) { PluginMain.DebugLog(() => "Detected translation loop while translating '" + PluginMain.FormatTextForDebug(input) + "'. Returning '" + PluginMain.FormatTextForDebug(firstPass) + "'."); return firstPass; } return text; } public static void NotifyLanguageChanged(string languageName) { if (!initialized) { return; } string a = (string.IsNullOrWhiteSpace(languageName) ? "English" : languageName); if (!string.Equals(a, currentLanguage, StringComparison.Ordinal)) { currentLanguage = a; if (!RuntimeDebugConfig.TranslationEnabled) { DisableRuntime("language change", logInfo: false); return; } TranslationLoadResult loadResult = ReloadCompiledRules(); LogLoadResult("Reloaded", loadResult); } } public static void NotifyLanguageReset() { NotifyLanguageChanged("English"); } internal static void NotifyRuntimeSettingsChanged(bool translationEnabledChanged) { if (!initialized) { return; } if (translationEnabledChanged) { if (!RuntimeDebugConfig.TranslationEnabled) { DisableRuntime("runtime settings change", logInfo: true); return; } TranslationLoadResult loadResult = ReloadCompiledRules(); LogLoadResult("Reloaded", loadResult); return; } translationCache.Clear(); missCache.Clear(); knownTranslatedOutputCounts.Clear(); placeholderCache.Clear(); placeholderMissCache.Clear(); GameTranslationResolver.ClearCaches(); InvalidateInterceptionState(); if (PluginMain.DebugModeEnabled) { PluginMain.DebugLog("Cleared translation caches after runtime settings change."); } } internal static void NotifyLocalRulesChanged() { if (initialized) { if (!RuntimeDebugConfig.TranslationEnabled) { DisableRuntime("local rule change", logInfo: false); return; } TranslationLoadResult loadResult = ReloadCompiledRules(); LogLoadResult("Reloaded", loadResult); } } internal static void NotifySyncedRulesChanged(TranslationSyncSnapshot snapshot) { remoteSnapshot = snapshot ?? TranslationSyncSnapshot.Empty; if (initialized) { if (!RuntimeDebugConfig.TranslationEnabled) { DisableRuntime("synced rule change", logInfo: false); return; } TranslationLoadResult loadResult = ReloadCompiledRules(); LogLoadResult("Reloaded", loadResult); } } private static string ResolveCurrentLanguage() { string @string = PlayerPrefs.GetString("language", "English"); if (!string.IsNullOrWhiteSpace(@string)) { return @string; } return "English"; } private static TranslationLoadResult ReloadCompiledRules() { TranslationLoadResult translationLoadResult = TranslationRuleLoader.Load(currentLanguage, remoteSnapshot); compiledRuleSet = translationLoadResult.RuleSet; lastLoadResult = translationLoadResult; TranslationRuleBrowserSnapshotProvider.Invalidate(); ClearRuntimeCaches(); GameTranslationResolver.ClearCaches(); InvalidateInterceptionState(); return translationLoadResult; } private static void DisableRuntime(string reason, bool logInfo) { compiledRuleSet = CompiledRuleSet.Empty; lastLoadResult = null; TranslationRuleBrowserSnapshotProvider.Invalidate(); ClearRuntimeCaches(); GameTranslationResolver.ClearCaches(); InvalidateInterceptionState(); if (logInfo) { ManualLogSource log = PluginMain.Log; if (log != null) { log.LogInfo((object)PluginMain.LocalizeFormat("runtime.log.disabled_after_reason", "Translation runtime disabled; skipped rule loading after {0}.", reason)); } } } private static void AddPlaceholderHit(string input, string translated) { if (placeholderCache.Set(input, translated, out var _)) { TranslationPerformanceMetrics.RecordPlaceholderCacheEviction(); } } private static void AddPlaceholderMiss(string input) { if (placeholderMissCache.Add(input, out var evictedKey) && evictedKey != null) { TranslationPerformanceMetrics.RecordPlaceholderMissCacheEviction(); } } private static void LogMatch(string stage, string input, string output, string normalizedInput = null, string detail = null) { if (!PluginMain.DebugModeEnabled) { return; } PluginMain.DebugLog(delegate { string text = stage + " match: '" + PluginMain.FormatTextForDebug(input) + "' -> '" + PluginMain.FormatTextForDebug(output) + "'"; if (!string.IsNullOrWhiteSpace(detail)) { text = text + " using '" + PluginMain.FormatTextForDebug(detail) + "'"; } if (!string.IsNullOrEmpty(normalizedInput) && !string.Equals(normalizedInput, input, StringComparison.Ordinal)) { text = text + " (normalized='" + PluginMain.FormatTextForDebug(normalizedInput) + "')"; } return text; }); } private static void LogLoadResult(string verb, TranslationLoadResult loadResult) { PluginMain.Log.LogInfo((object)PluginMain.LocalizeFormat("runtime.log.load_result", "{0} translation rules for '{1}'. English='{2}' (local={3}, synced={4}), Active='{5}' (local={6}, synced={7}), exact={8}, templates={9}.", verb, loadResult.LanguageName, loadResult.EnglishFolderPath, loadResult.LocalEnglishFileCount, loadResult.SyncedEnglishFileCount, loadResult.ActiveFolderPath, loadResult.LocalActiveFileCount, loadResult.SyncedActiveFileCount, loadResult.ExactRuleCount, loadResult.TemplateRuleCount)); } internal static bool IsKnownTranslatedOutput(string input) { if (!string.IsNullOrEmpty(input)) { return knownTranslatedOutputCounts.ContainsKey(input); } return false; } private static void InvalidateInterceptionState() { Interlocked.Increment(ref interceptionStateVersion); } private static void AddTranslationHit(string input, string translated) { if (translationCache.Set(input, translated, out var evictedEntry)) { TranslationPerformanceMetrics.RecordTranslationCacheEviction(); UntrackKnownTranslatedOutput(evictedEntry.Value); } TrackKnownTranslatedOutput(translated); } private static void AddTranslationMiss(string input) { if (missCache.Add(input, out var evictedKey) && evictedKey != null) { TranslationPerformanceMetrics.RecordTranslationMissCacheEviction(); } } private static void ClearRuntimeCaches() { translationCache.Clear(); missCache.Clear(); knownTranslatedOutputCounts.Clear(); placeholderCache.Clear(); placeholderMissCache.Clear(); } private static void TrackKnownTranslatedOutput(string translated) { if (!string.IsNullOrEmpty(translated)) { if (knownTranslatedOutputCounts.TryGetValue(translated, out var value)) { knownTranslatedOutputCounts[translated] = value + 1; } else { knownTranslatedOutputCounts.Add(translated, 1); } } } private static void UntrackKnownTranslatedOutput(string translated) { if (!string.IsNullOrEmpty(translated) && knownTranslatedOutputCounts.TryGetValue(translated, out var value)) { if (value <= 1) { knownTranslatedOutputCounts.Remove(translated); } else { knownTranslatedOutputCounts[translated] = value - 1; } } } } internal static class TranslationServerSyncBridge { private const string SnapshotIdentifier = "translation-rules-snapshot"; private const double ConfigSnapshotDebounceMilliseconds = 500.0; private static readonly ConfigSync configSync = new ConfigSync("com.paxton0505.localization") { DisplayName = "Localization", CurrentVersion = "2.0.1", MinimumRequiredVersion = "2.0.1" }; private static readonly CustomSyncedValue syncedSnapshotJson = new CustomSyncedValue(configSync, "translation-rules-snapshot", string.Empty); private static FileSystemWatcher configWatcher; private static System.Timers.Timer configSnapshotDebounceTimer; private static TranslationSyncSnapshot lastAppliedRemoteSnapshot = TranslationSyncSnapshot.Empty; private static bool initialized; public static void Initialize() { if (!initialized) { initialized = true; Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); syncedSnapshotJson.ValueChanged += OnSnapshotValueChanged; configSync.SourceOfTruthChanged += OnSourceOfTruthChanged; SetupWatcher(); if (ShouldPublishServerSnapshot()) { PublishLocalSnapshot("startup"); } else { syncedSnapshotJson.Value = string.Empty; } ApplyRemoteSnapshot(); } } public static void Shutdown() { if (initialized) { if (configWatcher != null) { configWatcher.EnableRaisingEvents = false; configWatcher.Changed -= OnConfigFilesChanged; configWatcher.Created -= OnConfigFilesChanged; configWatcher.Deleted -= OnConfigFilesChanged; configWatcher.Renamed -= OnConfigFilesChanged; configWatcher.Dispose(); configWatcher = null; } if (configSnapshotDebounceTimer != null) { configSnapshotDebounceTimer.Stop(); configSnapshotDebounceTimer.Elapsed -= OnConfigSnapshotDebounceElapsed; configSnapshotDebounceTimer.Dispose(); configSnapshotDebounceTimer = null; } syncedSnapshotJson.ValueChanged -= OnSnapshotValueChanged; configSync.SourceOfTruthChanged -= OnSourceOfTruthChanged; lastAppliedRemoteSnapshot = TranslationSyncSnapshot.Empty; initialized = false; } } internal static TranslationSyncSnapshot GetLocalSnapshotForBrowsing() { return BuildLocalSnapshot(); } internal static TranslationSyncSnapshot GetServerSnapshotForBrowsing() { return lastAppliedRemoteSnapshot ?? TranslationSyncSnapshot.Empty; } private static void SetupWatcher() { configWatcher = new FileSystemWatcher(TranslationRuleLoader.ConfigRootPath, "*.json") { IncludeSubdirectories = true, SynchronizingObject = ThreadingHelper.SynchronizingObject, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite | NotifyFilters.CreationTime) }; configWatcher.Changed += OnConfigFilesChanged; configWatcher.Created += OnConfigFilesChanged; configWatcher.Deleted += OnConfigFilesChanged; configWatcher.Renamed += OnConfigFilesChanged; configWatcher.EnableRaisingEvents = true; configSnapshotDebounceTimer = new System.Timers.Timer(500.0) { AutoReset = false, SynchronizingObject = ThreadingHelper.SynchronizingObject }; configSnapshotDebounceTimer.Elapsed += OnConfigSnapshotDebounceElapsed; } private static void OnConfigFilesChanged(object sender, FileSystemEventArgs e) { ScheduleLocalSnapshotPublish(); } private static void OnConfigSnapshotDebounceElapsed(object sender, ElapsedEventArgs e) { if (ShouldPublishServerSnapshot()) { PublishLocalSnapshot("debounced local config change"); } TranslationRuntime.NotifyLocalRulesChanged(); } private static void ScheduleLocalSnapshotPublish() { if (configSnapshotDebounceTimer == null) { if (ShouldPublishServerSnapshot()) { PublishLocalSnapshot("local config change"); } TranslationRuntime.NotifyLocalRulesChanged(); } else { configSnapshotDebounceTimer.Stop(); configSnapshotDebounceTimer.Start(); } } private static void OnSnapshotValueChanged() { if (!configSync.IsSourceOfTruth) { ApplyRemoteSnapshot(); } } private static void OnSourceOfTruthChanged(bool isSourceOfTruth) { if (isSourceOfTruth && PluginMain.IsDedicatedServer) { if (PluginMain.Log != null) { PluginMain.Log.LogInfo((object)PluginMain.Localize("serversync.log.source_of_truth", "ServerSync source-of-truth enabled; local config hot reload is active.")); } TranslationRuntime.NotifySyncedRulesChanged(TranslationSyncSnapshot.Empty); configSnapshotDebounceTimer?.Stop(); PublishLocalSnapshot("source-of-truth activation"); } else { configSnapshotDebounceTimer?.Stop(); if (isSourceOfTruth) { syncedSnapshotJson.Value = string.Empty; } TranslationRuntime.NotifySyncedRulesChanged(TranslationSyncSnapshot.Empty); } } private static void PublishLocalSnapshot(string reason) { TranslationSyncSnapshot translationSyncSnapshot = BuildLocalSnapshot(); string value = SerializeSnapshot(translationSyncSnapshot); syncedSnapshotJson.Value = value; if (!configSync.IsSourceOfTruth || PluginMain.Log == null) { return; } SyncedLanguageRules[] languages = translationSyncSnapshot.languages; int num = ((languages != null) ? languages.Length : 0); int num2 = 0; if (translationSyncSnapshot.languages != null) { SyncedLanguageRules[] languages2 = translationSyncSnapshot.languages; foreach (SyncedLanguageRules syncedLanguageRules in languages2) { if (syncedLanguageRules?.files != null) { num2 += syncedLanguageRules.files.Length; } } } PluginMain.Log.LogInfo((object)PluginMain.LocalizeFormat("serversync.log.snapshot_published", "Published local translation snapshot after {0}: {1} language folder(s), {2} file(s).", reason, num, num2)); } private static void ApplyRemoteSnapshot() { if (configSync.IsSourceOfTruth) { lastAppliedRemoteSnapshot = TranslationSyncSnapshot.Empty; TranslationRuntime.NotifySyncedRulesChanged(TranslationSyncSnapshot.Empty); } else { TranslationRuntime.NotifySyncedRulesChanged(lastAppliedRemoteSnapshot = DeserializeSnapshot(syncedSnapshotJson.Value)); } } private static bool ShouldPublishServerSnapshot() { if (PluginMain.IsDedicatedServer) { return configSync.IsSourceOfTruth; } return false; } private static TranslationSyncSnapshot BuildLocalSnapshot() { Directory.CreateDirectory(TranslationRuleLoader.ConfigRootPath); List list = new List(); foreach (string item in Directory.GetDirectories(TranslationRuleLoader.ConfigRootPath).OrderBy((string path) => Path.GetFileName(path), StringComparer.OrdinalIgnoreCase)) { string fileName = Path.GetFileName(item); if (string.IsNullOrWhiteSpace(fileName)) { continue; } List list2 = new List(); foreach (string item2 in Directory.GetFiles(item, "*.json", SearchOption.TopDirectoryOnly).OrderBy((string path) => Path.GetFileName(path), StringComparer.OrdinalIgnoreCase)) { try { RuleFileModel rules = JsonConvert.DeserializeObject(File.ReadAllText(item2)) ?? new RuleFileModel(); list2.Add(new SyncedRuleFile { fileName = Path.GetFileName(item2), rules = rules }); } catch (Exception ex) { PluginMain.Log.LogError((object)("Failed to serialize translation file '" + item2 + "' for ServerSync: " + ex.Message)); } } list.Add(new SyncedLanguageRules { name = fileName, files = list2.ToArray() }); } return new TranslationSyncSnapshot { languages = list.ToArray() }; } private static string SerializeSnapshot(TranslationSyncSnapshot snapshot) { return JsonConvert.SerializeObject((object)snapshot); } private static TranslationSyncSnapshot DeserializeSnapshot(string serializedSnapshot) { if (string.IsNullOrWhiteSpace(serializedSnapshot)) { return TranslationSyncSnapshot.Empty; } try { return JsonConvert.DeserializeObject(serializedSnapshot) ?? TranslationSyncSnapshot.Empty; } catch (Exception ex) { PluginMain.Log.LogError((object)("Failed to deserialize ServerSync translation snapshot: " + ex.Message)); return TranslationSyncSnapshot.Empty; } } } [Serializable] internal sealed class TranslationSyncSnapshot { public static readonly TranslationSyncSnapshot Empty = new TranslationSyncSnapshot(); public SyncedLanguageRules[] languages = Array.Empty(); public bool TryGetLanguage(string languageName, out SyncedLanguageRules languageRules) { if (languages != null) { SyncedLanguageRules[] array = languages; foreach (SyncedLanguageRules syncedLanguageRules in array) { if (syncedLanguageRules != null && !string.IsNullOrWhiteSpace(syncedLanguageRules.name) && string.Equals(syncedLanguageRules.name, languageName, StringComparison.Ordinal)) { languageRules = syncedLanguageRules; return true; } } } languageRules = null; return false; } } [Serializable] internal sealed class SyncedLanguageRules { public string name = string.Empty; public SyncedRuleFile[] files = Array.Empty(); } [Serializable] internal sealed class SyncedRuleFile { public string fileName = string.Empty; public RuleFileModel rules = new RuleFileModel(); } } namespace Localization.DevTools { internal sealed class DevToolsAutoLayoutContainer : MonoBehaviour { private RectTransform rectTransform; private HorizontalOrVerticalLayoutGroup layoutGroup; private LayoutElement layoutElement; private ContentSizeFitter contentSizeFitter; internal RectTransform RectTransform => rectTransform ?? (rectTransform = ((Component)this).GetComponent()); internal void Bind(HorizontalOrVerticalLayoutGroup group, LayoutElement element) { rectTransform = ((Component)this).GetComponent(); layoutGroup = group; layoutElement = element; contentSizeFitter = ((Component)this).GetComponent(); } internal void Apply(DevToolsSurfaceOptions options) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) HorizontalOrVerticalLayoutGroup val = ResolveLayoutGroup(); if (!((Object)(object)val == (Object)null)) { ((LayoutGroup)val).childAlignment = options.ChildAlignment; val.childControlWidth = options.ChildControlWidth; val.childControlHeight = options.ChildControlHeight; val.childForceExpandWidth = options.ChildForceExpandWidth; val.childForceExpandHeight = options.ChildForceExpandHeight; val.spacing = options.Spacing; ((LayoutGroup)val).padding = ClonePadding(options.Padding); DevToolsUiFactory.ApplyLayoutElement(ResolveLayoutElement(), options.PreferredWidth, options.PreferredHeight, options.MinWidth, options.MinHeight, options.FlexibleWidth, options.FlexibleHeight); } } internal void ConfigureContentFitter(FitMode horizontalFit, FitMode verticalFit) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) ContentSizeFitter val = ResolveContentSizeFitter(); if ((Object)(object)val == (Object)null) { val = (contentSizeFitter = ((Component)this).gameObject.AddComponent()); } val.horizontalFit = horizontalFit; val.verticalFit = verticalFit; } internal void UseTopAnchoredStretchContent() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) RectTransform obj = RectTransform; obj.anchorMin = new Vector2(0f, 1f); obj.anchorMax = new Vector2(1f, 1f); obj.pivot = new Vector2(0.5f, 1f); obj.offsetMin = Vector2.zero; obj.offsetMax = Vector2.zero; } internal void UseStretchFill() { DevToolsUiFactory.StretchToFill(RectTransform); } internal void Rebuild() { LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform); } private HorizontalOrVerticalLayoutGroup ResolveLayoutGroup() { if ((Object)(object)layoutGroup != (Object)null) { return layoutGroup; } layoutGroup = (HorizontalOrVerticalLayoutGroup)(object)((Component)this).GetComponent(); if ((Object)(object)layoutGroup == (Object)null) { layoutGroup = (HorizontalOrVerticalLayoutGroup)(object)((Component)this).GetComponent(); } return layoutGroup; } private LayoutElement ResolveLayoutElement() { return layoutElement ?? (layoutElement = ((Component)this).GetComponent()); } private ContentSizeFitter ResolveContentSizeFitter() { return contentSizeFitter ?? (contentSizeFitter = ((Component)this).GetComponent()); } private static RectOffset ClonePadding(RectOffset padding) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown if (padding != null) { return new RectOffset(padding.left, padding.right, padding.top, padding.bottom); } return new RectOffset(0, 0, 0, 0); } } internal readonly struct DevToolsTextOptions { public int FontSize { get; } public TextAnchor Alignment { get; } public HorizontalWrapMode HorizontalOverflow { get; } public VerticalWrapMode VerticalOverflow { get; } public bool SupportRichText { get; } public bool UseLocalizationFallback { get; } public float LineSpacing { get; } public Vector2 OffsetMin { get; } public Vector2 OffsetMax { get; } public DevToolsTextOptions(int fontSize, TextAnchor alignment, HorizontalWrapMode horizontalOverflow, VerticalWrapMode verticalOverflow, bool supportRichText, bool useLocalizationFallback, float lineSpacing, Vector2 offsetMin, Vector2 offsetMax) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) FontSize = Mathf.Max(1, fontSize); Alignment = alignment; HorizontalOverflow = horizontalOverflow; VerticalOverflow = verticalOverflow; SupportRichText = supportRichText; UseLocalizationFallback = useLocalizationFallback; LineSpacing = ((lineSpacing <= 0f) ? 1f : lineSpacing); OffsetMin = offsetMin; OffsetMax = offsetMax; } public DevToolsTextOptions WithFontSize(int fontSize) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) return new DevToolsTextOptions(fontSize, Alignment, HorizontalOverflow, VerticalOverflow, SupportRichText, UseLocalizationFallback, LineSpacing, OffsetMin, OffsetMax); } public DevToolsTextOptions WithAlignment(TextAnchor alignment) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) return new DevToolsTextOptions(FontSize, alignment, HorizontalOverflow, VerticalOverflow, SupportRichText, UseLocalizationFallback, LineSpacing, OffsetMin, OffsetMax); } public DevToolsTextOptions WithOverflow(HorizontalWrapMode horizontalOverflow, VerticalWrapMode verticalOverflow) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) return new DevToolsTextOptions(FontSize, Alignment, horizontalOverflow, verticalOverflow, SupportRichText, UseLocalizationFallback, LineSpacing, OffsetMin, OffsetMax); } public DevToolsTextOptions WithInsets(Vector2 offsetMin, Vector2 offsetMax) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) return new DevToolsTextOptions(FontSize, Alignment, HorizontalOverflow, VerticalOverflow, SupportRichText, UseLocalizationFallback, LineSpacing, offsetMin, offsetMax); } public DevToolsTextOptions WithRichText(bool supportRichText, bool useLocalizationFallback) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) return new DevToolsTextOptions(FontSize, Alignment, HorizontalOverflow, VerticalOverflow, supportRichText, useLocalizationFallback, LineSpacing, OffsetMin, OffsetMax); } } internal readonly struct DevToolsButtonOptions { public float Width { get; } public float Height { get; } public bool FlexibleWidth { get; } public int FontSize { get; } public TextAnchor TextAlignment { get; } public bool Wrap { get; } public bool SupportRichText { get; } public bool UseLocalizationFallback { get; } public Vector2 TextOffsetMin { get; } public Vector2 TextOffsetMax { get; } public DevToolsButtonOptions(float width, float height, bool flexibleWidth, int fontSize, TextAnchor textAlignment, bool wrap, bool supportRichText, bool useLocalizationFallback, Vector2 textOffsetMin, Vector2 textOffsetMax) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) Width = Mathf.Max(0f, width); Height = Mathf.Max(0f, height); FlexibleWidth = flexibleWidth; FontSize = Mathf.Max(1, fontSize); TextAlignment = textAlignment; Wrap = wrap; SupportRichText = supportRichText; UseLocalizationFallback = useLocalizationFallback; TextOffsetMin = textOffsetMin; TextOffsetMax = textOffsetMax; } public DevToolsButtonOptions WithSize(float width, float height, bool? flexibleWidth = null) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) return new DevToolsButtonOptions(width, height, flexibleWidth ?? FlexibleWidth, FontSize, TextAlignment, Wrap, SupportRichText, UseLocalizationFallback, TextOffsetMin, TextOffsetMax); } public DevToolsButtonOptions WithFontSize(int fontSize) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) return new DevToolsButtonOptions(Width, Height, FlexibleWidth, fontSize, TextAlignment, Wrap, SupportRichText, UseLocalizationFallback, TextOffsetMin, TextOffsetMax); } public DevToolsButtonOptions WithContent(TextAnchor textAlignment, bool wrap, bool supportRichText, bool useLocalizationFallback) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) return new DevToolsButtonOptions(Width, Height, FlexibleWidth, FontSize, textAlignment, wrap, supportRichText, useLocalizationFallback, TextOffsetMin, TextOffsetMax); } public DevToolsButtonOptions WithInsets(Vector2 textOffsetMin, Vector2 textOffsetMax) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) return new DevToolsButtonOptions(Width, Height, FlexibleWidth, FontSize, TextAlignment, Wrap, SupportRichText, UseLocalizationFallback, textOffsetMin, textOffsetMax); } } internal readonly struct DevToolsSurfaceOptions { public string SpriteName { get; } public Type ImageType { get; } public Color Color { get; } public TextAnchor ChildAlignment { get; } public bool ChildControlWidth { get; } public bool ChildControlHeight { get; } public bool ChildForceExpandWidth { get; } public bool ChildForceExpandHeight { get; } public float Spacing { get; } public RectOffset Padding { get; } public float PreferredWidth { get; } public float PreferredHeight { get; } public float MinWidth { get; } public float MinHeight { get; } public float FlexibleWidth { get; } public float FlexibleHeight { get; } public DevToolsSurfaceOptions(string spriteName, Type imageType, Color color, TextAnchor childAlignment, bool childControlWidth, bool childControlHeight, bool childForceExpandWidth, bool childForceExpandHeight, float spacing, RectOffset padding, float preferredWidth, float preferredHeight, float minWidth, float minHeight, float flexibleWidth, float flexibleHeight) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) SpriteName = spriteName ?? string.Empty; ImageType = imageType; Color = color; ChildAlignment = childAlignment; ChildControlWidth = childControlWidth; ChildControlHeight = childControlHeight; ChildForceExpandWidth = childForceExpandWidth; ChildForceExpandHeight = childForceExpandHeight; Spacing = spacing; Padding = (RectOffset)(((object)padding) ?? ((object)new RectOffset(0, 0, 0, 0))); PreferredWidth = preferredWidth; PreferredHeight = preferredHeight; MinWidth = minWidth; MinHeight = minHeight; FlexibleWidth = flexibleWidth; FlexibleHeight = flexibleHeight; } public DevToolsSurfaceOptions WithSize(float preferredWidth, float preferredHeight, float minWidth, float minHeight, float flexibleWidth, float flexibleHeight) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) return new DevToolsSurfaceOptions(SpriteName, ImageType, Color, ChildAlignment, ChildControlWidth, ChildControlHeight, ChildForceExpandWidth, ChildForceExpandHeight, Spacing, Padding, preferredWidth, preferredHeight, minWidth, minHeight, flexibleWidth, flexibleHeight); } public DevToolsSurfaceOptions WithLayout(float spacing, RectOffset padding) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) return new DevToolsSurfaceOptions(SpriteName, ImageType, Color, ChildAlignment, ChildControlWidth, ChildControlHeight, ChildForceExpandWidth, ChildForceExpandHeight, spacing, padding, PreferredWidth, PreferredHeight, MinWidth, MinHeight, FlexibleWidth, FlexibleHeight); } } internal readonly struct DevToolsScrollViewOptions { public float ViewportAlpha { get; } public bool ShowViewportGraphic { get; } public float ScrollSensitivity { get; } public bool Horizontal { get; } public bool Vertical { get; } public MovementType MovementType { get; } public float FlexibleWidth { get; } public float FlexibleHeight { get; } public float MinWidth { get; } public float MinHeight { get; } public DevToolsScrollViewOptions(float viewportAlpha, bool showViewportGraphic, float scrollSensitivity, bool horizontal, bool vertical, MovementType movementType, float flexibleWidth, float flexibleHeight, float minWidth, float minHeight) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) ViewportAlpha = Mathf.Clamp01(viewportAlpha); ShowViewportGraphic = showViewportGraphic; ScrollSensitivity = Mathf.Max(1f, scrollSensitivity); Horizontal = horizontal; Vertical = vertical; MovementType = movementType; FlexibleWidth = flexibleWidth; FlexibleHeight = flexibleHeight; MinWidth = minWidth; MinHeight = minHeight; } } internal readonly struct DevToolsInputFieldOptions { public float PreferredHeight { get; } public float MinHeight { get; } public bool ReadOnly { get; } public LineType LineType { get; } public Transition Transition { get; } public Vector2 ViewportOffsetMin { get; } public Vector2 ViewportOffsetMax { get; } public Vector2 TextOffsetMin { get; } public Vector2 TextOffsetMax { get; } public float ViewportAlpha { get; } public bool ShowViewportGraphic { get; } public DevToolsInputFieldOptions(float preferredHeight, float minHeight, bool readOnly, LineType lineType, Transition transition, Vector2 viewportOffsetMin, Vector2 viewportOffsetMax, Vector2 textOffsetMin, Vector2 textOffsetMax, float viewportAlpha, bool showViewportGraphic) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) PreferredHeight = Mathf.Max(0f, preferredHeight); MinHeight = Mathf.Max(0f, minHeight); ReadOnly = readOnly; LineType = lineType; Transition = transition; ViewportOffsetMin = viewportOffsetMin; ViewportOffsetMax = viewportOffsetMax; TextOffsetMin = textOffsetMin; TextOffsetMax = textOffsetMax; ViewportAlpha = Mathf.Clamp01(viewportAlpha); ShowViewportGraphic = showViewportGraphic; } public DevToolsInputFieldOptions WithHeight(float preferredHeight, float minHeight) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) return new DevToolsInputFieldOptions(preferredHeight, minHeight, ReadOnly, LineType, Transition, ViewportOffsetMin, ViewportOffsetMax, TextOffsetMin, TextOffsetMax, ViewportAlpha, ShowViewportGraphic); } } internal static class DevToolsComponentDefaults { internal static readonly DevToolsTextOptions PlainText = new DevToolsTextOptions(15, (TextAnchor)3, (HorizontalWrapMode)1, (VerticalWrapMode)1, supportRichText: false, useLocalizationFallback: true, 1f, Vector2.zero, Vector2.zero); internal static readonly DevToolsTextOptions WrappedBodyText = PlainText.WithOverflow((HorizontalWrapMode)0, (VerticalWrapMode)1); internal static readonly DevToolsTextOptions HeaderTitleText = new DevToolsTextOptions(18, (TextAnchor)3, (HorizontalWrapMode)1, (VerticalWrapMode)1, supportRichText: false, useLocalizationFallback: true, 1f, new Vector2(14f, 0f), new Vector2(-14f, 0f)); internal static readonly DevToolsTextOptions SearchPlaceholderText = new DevToolsTextOptions(15, (TextAnchor)3, (HorizontalWrapMode)1, (VerticalWrapMode)1, supportRichText: false, useLocalizationFallback: true, 1f, new Vector2(12f, 0f), new Vector2(-12f, 0f)); internal static readonly DevToolsTextOptions MultilineInputText = new DevToolsTextOptions(15, (TextAnchor)0, (HorizontalWrapMode)0, (VerticalWrapMode)1, supportRichText: false, useLocalizationFallback: true, 1f, new Vector2(4f, 4f), new Vector2(-4f, -4f)); internal static readonly DevToolsTextOptions RichWrappedText = WrappedBodyText.WithRichText(supportRichText: true, useLocalizationFallback: false); internal static readonly DevToolsButtonOptions StandardButton = new DevToolsButtonOptions(156f, 38f, flexibleWidth: false, 15, (TextAnchor)4, wrap: true, supportRichText: false, useLocalizationFallback: true, Vector2.zero, Vector2.zero); internal static readonly DevToolsButtonOptions RouterButton = new DevToolsButtonOptions(220f, 42f, flexibleWidth: false, 16, (TextAnchor)4, wrap: true, supportRichText: false, useLocalizationFallback: true, Vector2.zero, Vector2.zero); internal static readonly DevToolsButtonOptions CloseButton = new DevToolsButtonOptions(64f, 48f, flexibleWidth: false, 16, (TextAnchor)4, wrap: false, supportRichText: false, useLocalizationFallback: true, Vector2.zero, Vector2.zero); internal static readonly DevToolsButtonOptions ListEntryButton = new DevToolsButtonOptions(320f, 58f, flexibleWidth: false, 15, (TextAnchor)3, wrap: true, supportRichText: true, useLocalizationFallback: false, new Vector2(12f, 0f), new Vector2(-12f, 0f)); internal static readonly DevToolsSurfaceOptions CardSurface = new DevToolsSurfaceOptions("text_field", (Type)1, new Color(1f, 1f, 1f, 0.18f), (TextAnchor)0, childControlWidth: true, childControlHeight: true, childForceExpandWidth: true, childForceExpandHeight: false, 0f, new RectOffset(0, 0, 0, 0), 0f, 0f, 0f, 0f, 1f, 0f); internal static readonly DevToolsSurfaceOptions HeaderSurface = new DevToolsSurfaceOptions("woodpanel_settings", (Type)1, new Color(0.78f, 0.73f, 0.64f, 0.95f), (TextAnchor)3, childControlWidth: true, childControlHeight: true, childForceExpandWidth: false, childForceExpandHeight: false, 8f, new RectOffset(14, 14, 0, 0), 0f, 42f, 0f, 42f, 1f, 0f); internal static readonly DevToolsSurfaceOptions HorizontalRowSurface = new DevToolsSurfaceOptions("text_field", (Type)1, new Color(1f, 1f, 1f, 0.18f), (TextAnchor)3, childControlWidth: true, childControlHeight: true, childForceExpandWidth: false, childForceExpandHeight: false, 0f, new RectOffset(0, 0, 0, 0), 0f, 42f, 0f, 42f, 1f, 0f); internal static readonly DevToolsSurfaceOptions ContentStackSurface = new DevToolsSurfaceOptions(string.Empty, (Type)0, new Color(0f, 0f, 0f, 0f), (TextAnchor)0, childControlWidth: true, childControlHeight: true, childForceExpandWidth: true, childForceExpandHeight: false, 0f, new RectOffset(0, 0, 0, 0), 0f, 0f, 0f, 0f, 1f, 0f); internal static readonly DevToolsScrollViewOptions ScrollView = new DevToolsScrollViewOptions(0.08f, showViewportGraphic: true, 24f, horizontal: false, vertical: true, (MovementType)2, 1f, 1f, 0f, 0f); internal static readonly DevToolsInputFieldOptions SingleLineInput = new DevToolsInputFieldOptions(42f, 42f, readOnly: false, (LineType)0, (Transition)1, new Vector2(8f, 8f), new Vector2(-8f, -8f), new Vector2(4f, 4f), new Vector2(-4f, -4f), 0.02f, showViewportGraphic: false); internal static readonly DevToolsInputFieldOptions MultiLineInput = new DevToolsInputFieldOptions(96f, 96f, readOnly: false, (LineType)2, (Transition)1, new Vector2(8f, 8f), new Vector2(-8f, -8f), new Vector2(4f, 4f), new Vector2(-4f, -4f), 0.02f, showViewportGraphic: false); internal static readonly DevToolsInputFieldOptions ReadOnlyTextArea = new DevToolsInputFieldOptions(96f, 96f, readOnly: true, (LineType)2, (Transition)1, new Vector2(8f, 8f), new Vector2(-8f, -8f), new Vector2(4f, 4f), new Vector2(-4f, -4f), 0.02f, showViewportGraphic: false); } internal static class DevToolsInputFieldFactory { internal static InputField CreateField(Transform parent, string name, string placeholder, DevToolsInputFieldOptions fieldOptions, DevToolsTextOptions placeholderOptions, DevToolsTextOptions valueOptions, out LayoutElement fieldLayout, out Text placeholderText, out Text valueText) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Invalid comparison between Unknown and I4 //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Expected O, but got Unknown //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) //IL_0233: Unknown result type (might be due to invalid IL or missing references) //IL_023a: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[4] { typeof(RectTransform), typeof(Image), typeof(InputField), typeof(LayoutElement) }); val.transform.SetParent(parent, false); Image component = val.GetComponent(); DevToolsSurfaceFactory.ApplySurfaceStyle(component, DevToolsComponentDefaults.CardSurface); fieldLayout = val.GetComponent(); DevToolsUiFactory.ApplyLayoutElement(fieldLayout, 0f, fieldOptions.PreferredHeight, 0f, fieldOptions.MinHeight, 1f, 0f); InputField component2 = val.GetComponent(); component2.readOnly = fieldOptions.ReadOnly; component2.lineType = fieldOptions.LineType; ((Selectable)component2).transition = fieldOptions.Transition; ((Selectable)component2).targetGraphic = (Graphic)(object)component; component2.caretColor = Color.white; component2.customCaretColor = true; bool flag = (int)fieldOptions.LineType > 0; GameObject val2 = new GameObject("Viewport", new Type[3] { typeof(RectTransform), typeof(Image), typeof(Mask) }); val2.transform.SetParent(val.transform, false); ((Graphic)val2.GetComponent()).color = new Color(0f, 0f, 0f, fieldOptions.ViewportAlpha); val2.GetComponent().showMaskGraphic = fieldOptions.ShowViewportGraphic; RectTransform component3 = val2.GetComponent(); DevToolsUiFactory.StretchToFill(component3, fieldOptions.ViewportOffsetMin, fieldOptions.ViewportOffsetMax); if (placeholder != null) { placeholderText = DevToolsUiFactory.CreateTextBlock(val2.transform, "Placeholder", placeholder, placeholderOptions); ((Graphic)placeholderText).color = new Color(1f, 1f, 1f, 0.45f); DevToolsUiFactory.SetTextInsets(placeholderText, fieldOptions.TextOffsetMin, fieldOptions.TextOffsetMax); component2.placeholder = (Graphic)(object)placeholderText; } else { placeholderText = null; } valueText = DevToolsUiFactory.CreateTextBlock(val2.transform, "Text", string.Empty, valueOptions); component2.textComponent = valueText; if (flag) { Scrollbar scrollbar = DevToolsScrollbars.CreateVerticalScrollbar(val.GetComponent(), component3, "VerticalScrollbar"); val.AddComponent().Bind(component3, valueText, scrollbar, fieldOptions.TextOffsetMin, fieldOptions.TextOffsetMax, DevToolsComponentDefaults.ScrollView.ScrollSensitivity); } else { DevToolsUiFactory.SetTextInsets(valueText, fieldOptions.TextOffsetMin, fieldOptions.TextOffsetMax); } return component2; } } internal sealed class DevToolsScrollableInputField : MonoBehaviour, IScrollHandler, IEventSystemHandler, IPointerDownHandler, IPointerClickHandler { private const float DefaultScrollSensitivity = 24f; private InputField inputField; private RectTransform viewportRect; private RectTransform textRect; private Text valueText; private Scrollbar verticalScrollbar; private float scrollSensitivity = 24f; private float leftInset; private float rightInset; private float topInset; private float bottomInset; private float currentContentHeight; private float currentScrollOffset; private bool suppressScrollbarCallback; private Vector2 lastViewportSize = new Vector2(float.MinValue, float.MinValue); private string lastValue; private int lastFontSize = -1; internal void Bind(RectTransform viewport, Text textComponent, Scrollbar scrollbar, Vector2 textOffsetMin, Vector2 textOffsetMax, float sensitivity) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) inputField = ((Component)this).GetComponent(); viewportRect = viewport; valueText = textComponent; textRect = (((Object)(object)textComponent != (Object)null) ? ((Graphic)textComponent).rectTransform : null); verticalScrollbar = scrollbar; scrollSensitivity = ((sensitivity <= 0f) ? 24f : sensitivity); leftInset = textOffsetMin.x; bottomInset = textOffsetMin.y; rightInset = textOffsetMax.x; topInset = 0f - textOffsetMax.y; if ((Object)(object)verticalScrollbar != (Object)null) { ((UnityEvent)(object)verticalScrollbar.onValueChanged).RemoveListener((UnityAction)HandleScrollbarChanged); ((UnityEvent)(object)verticalScrollbar.onValueChanged).AddListener((UnityAction)HandleScrollbarChanged); } ConfigureTextRect(); Refresh(force: true); } public void OnScroll(PointerEventData eventData) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (eventData != null && CanScroll()) { SetScrollOffset(currentScrollOffset - eventData.scrollDelta.y * scrollSensitivity); ((AbstractEventData)eventData).Use(); } } public void OnPointerDown(PointerEventData eventData) { ActivateInputField(); } public void OnPointerClick(PointerEventData eventData) { ActivateInputField(); } private void LateUpdate() { Refresh(force: false); } private void OnDestroy() { if ((Object)(object)verticalScrollbar != (Object)null) { ((UnityEvent)(object)verticalScrollbar.onValueChanged).RemoveListener((UnityAction)HandleScrollbarChanged); } } private void ConfigureTextRect() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)textRect == (Object)null) && !((Object)(object)valueText == (Object)null)) { textRect.anchorMin = new Vector2(0f, 1f); textRect.anchorMax = new Vector2(1f, 1f); textRect.pivot = new Vector2(0.5f, 1f); DevToolsUiFactory.EnsureContentSizeFitter((Component)(object)valueText, (FitMode)0, (FitMode)2); } } private void Refresh(bool force) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)viewportRect == (Object)null) && !((Object)(object)textRect == (Object)null) && !((Object)(object)valueText == (Object)null)) { Rect rect = viewportRect.rect; Vector2 size = ((Rect)(ref rect)).size; string a = valueText.text ?? string.Empty; int fontSize = valueText.fontSize; if (force || !(size == lastViewportSize) || fontSize != lastFontSize || !string.Equals(a, lastValue, StringComparison.Ordinal)) { lastViewportSize = size; lastValue = a; lastFontSize = fontSize; LayoutRebuilder.ForceRebuildLayoutImmediate(textRect); rect = viewportRect.rect; float num = Mathf.Max(0f, ((Rect)(ref rect)).height); float contentHeight = Mathf.Max(Mathf.Max(1f, num - topInset - bottomInset), LayoutUtility.GetPreferredHeight(textRect)); ApplyContentRect(contentHeight); } } } private void HandleScrollbarChanged(float value) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) if (!suppressScrollbarCallback && !((Object)(object)viewportRect == (Object)null)) { float num = topInset + currentContentHeight + bottomInset; Rect rect = viewportRect.rect; float num2 = Mathf.Max(0f, num - ((Rect)(ref rect)).height); SetScrollOffset((num2 <= 0f) ? 0f : ((1f - value) * num2)); } } private void ApplyContentRect(float contentHeight) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)textRect == (Object)null) && !((Object)(object)viewportRect == (Object)null)) { currentContentHeight = Mathf.Max(1f, contentHeight); Rect rect = viewportRect.rect; float num = Mathf.Max(0f, ((Rect)(ref rect)).height); float num2 = topInset + currentContentHeight + bottomInset; float num3 = Mathf.Max(0f, num2 - num); currentScrollOffset = Mathf.Clamp(currentScrollOffset, 0f, num3); textRect.offsetMin = new Vector2(leftInset, 0f - (topInset + currentContentHeight + bottomInset) + currentScrollOffset); textRect.offsetMax = new Vector2(rightInset, 0f - topInset + currentScrollOffset); UpdateScrollbar(num, num2, num3); } } private void SetScrollOffset(float value) { currentScrollOffset = Mathf.Max(0f, value); ApplyContentRect((currentContentHeight <= 0f) ? 1f : currentContentHeight); } private void UpdateScrollbar(float viewportHeight, float totalContentHeight, float maxScrollOffset) { if (!((Object)(object)verticalScrollbar == (Object)null)) { suppressScrollbarCallback = true; verticalScrollbar.size = ((maxScrollOffset <= 0f) ? 1f : Mathf.Clamp01(viewportHeight / Mathf.Max(totalContentHeight, viewportHeight))); verticalScrollbar.value = ((maxScrollOffset <= 0f) ? 1f : (1f - currentScrollOffset / maxScrollOffset)); ((Selectable)verticalScrollbar).interactable = maxScrollOffset > 0f; suppressScrollbarCallback = false; } } private bool CanScroll() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)viewportRect == (Object)null) { return false; } float num = topInset + currentContentHeight + bottomInset; Rect rect = viewportRect.rect; return num - ((Rect)(ref rect)).height > 0.5f; } private void ActivateInputField() { if (!((Object)(object)inputField == (Object)null) && ((UIBehaviour)inputField).IsActive() && ((Selectable)inputField).IsInteractable()) { ((Selectable)inputField).Select(); inputField.ActivateInputField(); } } } internal static class DevToolsScrollViewFactory { internal static ScrollRect CreateViewportScrollView(Transform parent, string name, out RectTransform viewportRect, out LayoutElement layoutElement, DevToolsScrollViewOptions? optionsOverride = null) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) DevToolsScrollViewOptions devToolsScrollViewOptions = optionsOverride ?? DevToolsComponentDefaults.ScrollView; GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(ScrollRect), typeof(LayoutElement) }); val.transform.SetParent(parent, false); RectTransform component = val.GetComponent(); DevToolsUiFactory.StretchToFill(component); layoutElement = val.GetComponent(); DevToolsUiFactory.ApplyLayoutElement(layoutElement, 0f, 0f, devToolsScrollViewOptions.MinWidth, devToolsScrollViewOptions.MinHeight, devToolsScrollViewOptions.FlexibleWidth, devToolsScrollViewOptions.FlexibleHeight); GameObject val2 = new GameObject("Viewport", new Type[3] { typeof(RectTransform), typeof(Image), typeof(Mask) }); val2.transform.SetParent(val.transform, false); ((Graphic)val2.GetComponent()).color = new Color(0f, 0f, 0f, devToolsScrollViewOptions.ViewportAlpha); val2.GetComponent().showMaskGraphic = devToolsScrollViewOptions.ShowViewportGraphic; viewportRect = val2.GetComponent(); DevToolsUiFactory.StretchToFill(viewportRect); ScrollRect component2 = val.GetComponent(); component2.viewport = viewportRect; component2.horizontal = devToolsScrollViewOptions.Horizontal; component2.vertical = devToolsScrollViewOptions.Vertical; component2.movementType = devToolsScrollViewOptions.MovementType; component2.scrollSensitivity = devToolsScrollViewOptions.ScrollSensitivity; DevToolsScrollbars.AttachVerticalScrollbar(component, viewportRect, component2, "VerticalScrollbar"); return component2; } internal static ScrollRect CreateVerticalScrollView(Transform parent, string name, out RectTransform contentRect, out VerticalLayoutGroup contentLayout, out LayoutElement layoutElement, DevToolsScrollViewOptions? optionsOverride = null) { RectTransform viewportRect; ScrollRect obj = CreateViewportScrollView(parent, name, out viewportRect, out layoutElement, optionsOverride); contentRect = DevToolsSurfaceFactory.CreateVerticalContentContainer((Transform)(object)viewportRect, "Content", DevToolsComponentDefaults.ContentStackSurface, out contentLayout, out var autoLayout); autoLayout.ConfigureContentFitter((FitMode)0, (FitMode)2); autoLayout.UseTopAnchoredStretchContent(); obj.content = contentRect; return obj; } } internal abstract class DevToolsStructuredInfoPage : DevToolsPage { protected sealed class PageColumnDefinition { public string Name { get; } public float FlexWeight { get; } public PageSectionDefinition[] Sections { get; } public PageColumnDefinition(string name, float flexWeight, params PageSectionDefinition[] sections) { Name = (string.IsNullOrWhiteSpace(name) ? "Column" : name); FlexWeight = ((flexWeight <= 0f) ? 1f : flexWeight); Sections = sections ?? Array.Empty(); } } protected sealed class PageSectionDefinition { public string Title { get; } public int DisplayItemCount { get; } public PageRowDefinition[] Rows { get; } public PageSectionDefinition(string title, params PageRowDefinition[] rows) : this(title, 0, rows) { } public PageSectionDefinition(string title, int displayItemCount, params PageRowDefinition[] rows) { Title = title ?? string.Empty; DisplayItemCount = ((displayItemCount > 0) ? displayItemCount : 0); Rows = rows ?? Array.Empty(); } } protected sealed class PageRowActionDefinition { public string Label { get; } public Action Action { get; } public PageRowActionDefinition(string label, Action action) { Label = (string.IsNullOrWhiteSpace(label) ? "Action" : label); Action = action; } } protected sealed class PageRowDefinition { public string Label { get; } public string Key { get; } public PageRowActionDefinition[] Actions { get; } public PageRowDefinition(string label, string key, params PageRowActionDefinition[] actions) { Label = label ?? string.Empty; Key = (string.IsNullOrWhiteSpace(key) ? (label ?? string.Empty) : key); Actions = actions ?? Array.Empty(); } } private const float DefaultActionButtonWidth = 96f; private const float DefaultActionButtonHeight = 32f; private readonly List columnLayouts = new List(); private readonly List sectionLayouts = new List(); private readonly List sectionLayoutElements = new List(); private readonly List sectionHeaderLayouts = new List(); private readonly List sectionHeaderTexts = new List(); private readonly List builtSectionDefinitions = new List(); private readonly List rowLayouts = new List(); private readonly List rowLayoutElements = new List(); private readonly List rowHasActions = new List(); private readonly List spacerRowLayouts = new List(); private readonly List labelTexts = new List(); private readonly List labelLayouts = new List(); private readonly List rowValueTexts = new List(); private readonly List valueLayouts = new List(); private readonly List valueTextAlignments = new List(); private readonly List valueRowDefinitions = new List(); private readonly List