using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using REPOJapaneseTranslation.Localization; using REPOJapaneseTranslation.Patches; using TMPro; using UnityEngine; using UnityEngine.TextCore.LowLevel; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("saitogo")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Japanese localization mod for R.E.P.O.")] [assembly: AssemblyFileVersion("1.0.7.0")] [assembly: AssemblyInformationalVersion("1.0.7+1d7de09580ffc65836598e4cb836666db8a19682")] [assembly: AssemblyProduct("REPO Japanese Translation")] [assembly: AssemblyTitle("REPOJapaneseTranslation")] [assembly: AssemblyVersion("1.0.7.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.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 REPOJapaneseTranslation { [BepInPlugin("REPOJapaneseTranslation", "REPO Japanese Translation", "1.0.7")] public class Plugin : BaseUnityPlugin { private readonly Harmony _harmony = new Harmony("REPOJapaneseTranslation"); public static Plugin Instance { get; private set; } public static ManualLogSource Logger { get; private set; } internal static ConfigEntry EnableTranslation { get; private set; } internal static ConfigEntry EnableJapaneseFont { get; private set; } internal static ConfigEntry LogUntranslated { get; private set; } private void Awake() { Instance = this; Logger = ((BaseUnityPlugin)this).Logger; EnableTranslation = ((BaseUnityPlugin)this).Config.Bind("General", "EnableTranslation", true, "テキストを日本語に翻訳するかどうか / Whether to translate text to Japanese"); EnableJapaneseFont = ((BaseUnityPlugin)this).Config.Bind("General", "EnableJapaneseFont", true, "日本語フォントをフォールバックとして追加するかどうか / Whether to add a Japanese font as TMP fallback"); LogUntranslated = ((BaseUnityPlugin)this).Config.Bind("Debug", "LogUntranslated", false, "未翻訳のテキストをログに出力するかどうか (開発者向け) / Log untranslated strings (for developers)"); if (LogUntranslated.Value) { Logger.LogInfo((object)"デバッグモード有効: 未翻訳テキストのログ出力を有効化しました。"); } TranslationManager.Initialize(); FontManager.Initialize(((BaseUnityPlugin)this).Info.Location); _harmony.PatchAll(typeof(TMPTextTranslationPatch).Assembly); Logger.LogInfo((object)"REPO Japanese Translation v1.0.7 が読み込まれました!"); Logger.LogInfo((object)$"翻訳エントリ数: {TranslationManager.TranslationCount}"); } } internal static class PluginInfo { public const string PLUGIN_GUID = "REPOJapaneseTranslation"; public const string PLUGIN_NAME = "REPO Japanese Translation"; public const string PLUGIN_VERSION = "1.0.7"; } } namespace REPOJapaneseTranslation.Patches { internal static class MenuPageTranslator { internal static void TranslatePage(MenuPage? menuPage) { if (!((Object)(object)menuPage == (Object)null)) { menuPage.menuHeaderName = TranslationManager.Translate(menuPage.menuHeaderName, logUntranslated: false); TMP_Text[] componentsInChildren = ((Component)menuPage).GetComponentsInChildren(true); FontManager.ApplyFallbackToTextComponents(componentsInChildren); TMP_Text[] array = componentsInChildren; foreach (TMP_Text text in array) { TranslateTextComponent(text); } } } private static void TranslateTextComponent(TMP_Text text) { if (!((Object)(object)text == (Object)null) && !string.IsNullOrEmpty(text.text)) { string text2 = TranslationManager.Translate(text.text, logUntranslated: false); if (text2 != text.text) { text.text = text2; } } } } [HarmonyPatch(typeof(MenuButton), "Awake")] internal static class MenuButtonTranslationPatch { [HarmonyPrefix] private static void Prefix(MenuButton __instance) { if (!((Object)(object)__instance == (Object)null) && !string.IsNullOrEmpty(__instance.buttonTextString)) { __instance.buttonTextString = TranslationManager.Translate(__instance.buttonTextString); } } } [HarmonyPatch(typeof(MenuManager), "PageOpen", new Type[] { typeof(MenuPageIndex), typeof(bool) })] internal static class MenuPageOpenTranslationPatch { [HarmonyPostfix] private static void Postfix(MenuPage __result) { MenuPageTranslator.TranslatePage(__result); } } [HarmonyPatch(typeof(MenuPage), "Start")] internal static class MenuPageStartTranslationPatch { [HarmonyPostfix] private static void Postfix(MenuPage __instance) { MenuPageTranslator.TranslatePage(__instance); } } [HarmonyPatch(typeof(MenuManager), "PagePopUp")] internal static class MenuPopUpTranslationPatch { [HarmonyPrefix] private static void Prefix(ref string headerText, ref string bodyText, ref string buttonText) { headerText = TranslationManager.Translate(headerText); bodyText = TranslationManager.Translate(bodyText); buttonText = TranslationManager.Translate(buttonText); } } [HarmonyPatch(typeof(MenuManager), "PagePopUpTwoOptions")] internal static class MenuTwoOptionPopUpTranslationPatch { [HarmonyPrefix] private static void Prefix(ref string popUpHeader, ref string popUpText, ref string option1Text, ref string option2Text) { popUpHeader = TranslationManager.Translate(popUpHeader); popUpText = TranslationManager.Translate(popUpText); option1Text = TranslationManager.Translate(option1Text); option2Text = TranslationManager.Translate(option2Text); } } [HarmonyPatch(typeof(MenuPageSaves), "SaveFileSelected")] internal static class MenuPageSavesSaveFileSelectedPatch { [HarmonyPostfix] private static void Postfix(MenuPageSaves __instance) { if ((Object)(object)__instance.saveFileInfoRow2 == (Object)null) { return; } string text = ((TMP_Text)__instance.saveFileInfoRow2).text; if (text.Contains("Total Haul:")) { string text2 = TranslationManager.Translate("Total Haul:", logUntranslated: false); if (!(text2 == "Total Haul:")) { ((TMP_Text)__instance.saveFileInfoRow2).text = text.Replace("Total Haul:", text2); } } } } [HarmonyPatch] internal static class TMPFontPatch { [HarmonyPatch(typeof(RunManager), "Awake")] [HarmonyPostfix] private static void ApplyFontAfterSceneLoad() { FontManager.ApplyFallbackToAllTextComponents(); } } [HarmonyPatch] internal static class TMPTextTranslationPatch { [CompilerGenerated] private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable { private int <>1__state; private MethodBase <>2__current; private int <>l__initialThreadId; private List.Enumerator <>7__wrap1; MethodBase IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__0(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <>7__wrap1 = default(List.Enumerator); <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: { <>1__state = -1; MethodInfo methodInfo = AccessTools.PropertySetter(typeof(TMP_Text), "text"); if (methodInfo != null) { <>2__current = methodInfo; <>1__state = 1; return true; } goto IL_0061; } case 1: <>1__state = -1; goto IL_0061; case 2: { <>1__state = -3; break; } IL_0061: <>7__wrap1 = AccessTools.GetDeclaredMethods(typeof(TMP_Text)).GetEnumerator(); <>1__state = -3; break; } while (<>7__wrap1.MoveNext()) { MethodInfo current = <>7__wrap1.Current; if (!(current.Name != "SetText")) { ParameterInfo[] parameters = current.GetParameters(); if (parameters.Length != 0 && !(parameters[0].ParameterType != typeof(string))) { <>2__current = current; <>1__state = 2; return true; } } } <>m__Finally1(); <>7__wrap1 = default(List.Enumerator); return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; ((IDisposable)<>7__wrap1).Dispose(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new d__0(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } [IteratorStateMachine(typeof(d__0))] [HarmonyTargetMethods] private static IEnumerable TargetMethods() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__0(-2); } [HarmonyPrefix] private static void TranslateText(ref string __0) { __0 = TranslationManager.Translate(__0); } } [HarmonyPatch(typeof(TruckScreenText), "UpdateTaxmanNickname")] internal static class TruckScreenTextUpdateTaxmanNicknamePatch { [HarmonyPrefix] private static void Prefix(ref string newName) { newName = TranslationManager.Translate(newName, logUntranslated: false); } } [HarmonyPatch(typeof(TuckScreenLocked), "LockChatToggle")] internal static class TuckScreenLockedLockChatTogglePatch { [HarmonyPrefix] private static void Prefix(ref string _lockedText) { _lockedText = TranslationManager.Translate(_lockedText, logUntranslated: false); } } } namespace REPOJapaneseTranslation.Localization { internal static class FontManager { private static class FontEngineLoadFontFacePatch { private static bool Prefix(Font font, int pointSize, ref FontEngineError __result) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected I4, but got Unknown if ((Object)(object)font == (Object)null || !SystemFontMap.TryGetValue(font, out byte[] value)) { return true; } __result = (FontEngineError)(int)FontEngine.LoadFontFace(value, pointSize); return false; } } private const string TtfFileName = "NotoSansJP-Regular-subset.ttf"; private static readonly Dictionary SystemFontMap = new Dictionary(); private static TMP_FontAsset? s_japaneseFontAsset; private static readonly HashSet s_patchedFonts = new HashSet(); private static bool s_initialized; private static bool s_fontEnginePatched; internal static void Initialize(string pluginLocation) { if (s_initialized) { return; } s_initialized = true; if (!Plugin.EnableJapaneseFont.Value) { return; } try { string path = Path.GetDirectoryName(pluginLocation) ?? string.Empty; string text = Path.Combine(path, "NotoSansJP-Regular-subset.ttf"); if (!File.Exists(text)) { Plugin.Logger.LogWarning((object)("日本語フォントファイルが見つかりません: " + text)); Plugin.Logger.LogWarning((object)"plugins/REPOJapaneseTranslation/NotoSansJP-Regular-subset.ttf を配置してください。"); return; } byte[] array = File.ReadAllBytes(text); Plugin.Logger.LogInfo((object)string.Format("フォントファイルを読み込みました: {0} ({1:N0} bytes)", "NotoSansJP-Regular-subset.ttf", array.Length)); PatchFontEngine(); s_japaneseFontAsset = CreateFontAssetFromBytes(array); if ((Object)(object)s_japaneseFontAsset == (Object)null) { Plugin.Logger.LogWarning((object)"日本語フォントの作成に失敗しました。日本語文字が正しく表示されない可能性があります。"); return; } Plugin.Logger.LogInfo((object)("日本語フォントを作成しました: " + ((Object)s_japaneseFontAsset).name)); TryAddToTMPGlobalFallback(s_japaneseFontAsset); } catch (Exception ex) { Plugin.Logger.LogError((object)("フォントの初期化中にエラーが発生しました: " + ex.Message)); } } internal static void ApplyFallbackToAllTextComponents() { if (!((Object)(object)s_japaneseFontAsset == (Object)null)) { TMP_Text[] texts = Object.FindObjectsOfType(true); int num = ApplyFallbackToTextComponents(texts); if (num > 0) { Plugin.Logger.LogDebug((object)$"{num} 個のフォントアセットにフォールバックを適用しました。"); } } } internal static int ApplyFallbackToTextComponents(IEnumerable texts) { if ((Object)(object)s_japaneseFontAsset == (Object)null) { return 0; } int num = 0; foreach (TMP_Text text in texts) { if (!((Object)(object)text.font == (Object)null) && !s_patchedFonts.Contains(text.font) && !text.font.fallbackFontAssetTable.Contains(s_japaneseFontAsset)) { text.font.fallbackFontAssetTable.Add(s_japaneseFontAsset); s_patchedFonts.Add(text.font); num++; } } return num; } private static void PatchFontEngine() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown if (s_fontEnginePatched) { return; } try { Harmony val = new Harmony("REPOJapaneseTranslation.FontEngineHook"); MethodInfo method = typeof(FontEngine).GetMethod("LoadFontFace", BindingFlags.Static | BindingFlags.Public, null, new Type[2] { typeof(Font), typeof(int) }, null); MethodInfo method2 = typeof(FontEngineLoadFontFacePatch).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic); if (method != null && method2 != null) { val.Patch((MethodBase)method, new HarmonyMethod(method2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); s_fontEnginePatched = true; Plugin.Logger.LogDebug((object)"FontEngine.LoadFontFace(Font, int) をパッチしました。"); } else { Plugin.Logger.LogWarning((object)"FontEngine.LoadFontFace(Font, int) のメソッドが見つかりませんでした。"); } } catch (Exception ex) { Plugin.Logger.LogDebug((object)("FontEngine パッチ適用をスキップしました: " + ex.Message)); } } private static TMP_FontAsset? CreateFontAssetFromBytes(byte[] fontBytes) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //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_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) FontEngine.InitializeFontEngine(); FontEngineError val = FontEngine.LoadFontFace(fontBytes); if ((int)val != 0) { Plugin.Logger.LogWarning((object)$"FontEngine.LoadFontFace(byte[]) 失敗: {val}"); return null; } Font val2 = new Font("NotoSansJP-subset"); SystemFontMap[val2] = fontBytes; TMP_FontAsset val3; try { val3 = TMP_FontAsset.CreateFontAsset(val2); } catch (Exception ex) { Plugin.Logger.LogDebug((object)("TMP_FontAsset.CreateFontAsset 例外: " + ex.Message)); val3 = null; } if ((Object)(object)val3 == (Object)null) { CleanupFailedDummyFont(val2); Plugin.Logger.LogWarning((object)"TMP_FontAsset を作成できませんでした。"); return null; } ((Object)val3).name = "JapaneseFallback_NotoSansJP"; val3.atlasPopulationMode = (AtlasPopulationMode)1; return val3; } private static void CleanupFailedDummyFont(Font dummyFont) { SystemFontMap.Remove(dummyFont); Object.Destroy((Object)(object)dummyFont); } private static void TryAddToTMPGlobalFallback(TMP_FontAsset fontAsset) { try { if ((Object)(object)TMP_Settings.instance == (Object)null) { Plugin.Logger.LogDebug((object)"TMP_Settings のインスタンスがまだ生成されていません。"); } else if (typeof(TMP_Settings).GetField("m_FallbackFontAssets", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(TMP_Settings.instance) is List list) { if (!list.Contains(fontAsset)) { list.Add(fontAsset); Plugin.Logger.LogInfo((object)"TMP グローバルフォールバックリストに日本語フォントを追加しました。"); } } else { Plugin.Logger.LogDebug((object)"TMP_Settings.m_FallbackFontAssets が見つかりませんでした。個別適用で対応します。"); } } catch (Exception ex) { Plugin.Logger.LogDebug((object)("TMP グローバルフォールバック設定をスキップしました: " + ex.Message)); } } } internal static class TranslationManager { private sealed class TranslationTemplate { private readonly Regex _pattern; private readonly string _translatedTemplate; private readonly string[] _placeholderTokens; internal int SourceLength { get; } private TranslationTemplate(Regex pattern, string translatedTemplate, string[] placeholderTokens, int sourceLength) { _pattern = pattern; _translatedTemplate = translatedTemplate; _placeholderTokens = placeholderTokens; SourceLength = sourceLength; } internal static TranslationTemplate Create(string sourceTemplate, string translatedTemplate) { MatchCollection matchCollection = s_placeholderTokenRegex.Matches(sourceTemplate); StringBuilder stringBuilder = new StringBuilder(); string[] array = new string[matchCollection.Count]; stringBuilder.Append('^'); int num = 0; int num2; for (int i = 0; i < matchCollection.Count; i++) { Match match = matchCollection[i]; num2 = num; stringBuilder.Append(Regex.Escape(sourceTemplate.Substring(num2, match.Index - num2))); stringBuilder.Append($"(?.+?)"); array[i] = match.Value; num = match.Index + match.Length; } num2 = num; stringBuilder.Append(Regex.Escape(sourceTemplate.Substring(num2, sourceTemplate.Length - num2))); stringBuilder.Append('$'); Regex pattern = new Regex(stringBuilder.ToString(), RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); return new TranslationTemplate(pattern, translatedTemplate, array, sourceTemplate.Length); } internal bool TryTranslate(string text, out string result) { Match match = _pattern.Match(text); if (!match.Success) { result = text; return false; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int i = 0; i < _placeholderTokens.Length; i++) { string key = _placeholderTokens[i]; if (!dictionary.ContainsKey(key)) { dictionary[key] = match.Groups[$"p{i}"].Value; } } result = _translatedTemplate; foreach (KeyValuePair item in dictionary) { item.Deconstruct(out var key2, out var value); string oldValue = key2; string newValue = value; result = result.Replace(oldValue, newValue, StringComparison.Ordinal); } return true; } } private static readonly Regex s_placeholderTokenRegex = new Regex("\\{[^{}]+\\}|\\[[^\\[\\]]+\\]", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex s_actionSuffixRegex = new Regex("^(?.+?)(?\\s*(?:<[^>]+>)*\\[[^\\[\\]]+\\](?:]+>)*\\s*)$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex s_richTextSuffixRegex = new Regex("^(?.+?)(?\\s*(?:<[^>]+>)+\\s*)$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex s_labeledPrefixRegex = new Regex("^(?(?:<[^>]+>)*)(?