【詳細】Steam版 The Room 4: Old Sins 日本語化(自動翻訳化)

こちらは詳細内容の記事です。必要ファイルのダウンロードおよび手順を知りたい方は下記記事をご覧ください。
The Room Two, Three と日本語フォントへ強制変更するコードをXUnity.AutoTranslatorへ追加するというアヤしい方法ながらも何とか日本語化(自動翻訳化)に成功しました。 そしてThe Room 4: Old Sinsです。最新作なのできっとTextMeshProが使われていて、難なくXUnity.AutoTranslatorで翻訳できるんだろうなぁ…と思っていたのですがハマりました。
最終的にはOverrideFontTextMeshProで指定できるフォントを自作し日本語化(自動翻訳化)は出来たのですが、その過程でいろいろと試したことや分かったことがありますのでココに残したいと思います。

分かった事のまとめ

フォント(より正確にはTextMeshProのフォントを含むAssetBundle)のTMP_FontAssetにあるm_AssemblyNameはゲームのリソース(Assets)内にあるm_AssemblyNameに合わせないとOverrideFontTextMeshProに利用できない。以降、ほぼ時系列で試したことや分かったことを記載していきます。

TheRoom4は『OverrideFontTextMeshPro = arialuni_sdf_u2019』が効かない

OverrideFontTextMeshProというのはXUnity.AutoTranslatorでTextMeshProのフォントを置き換える時に指定する設定項目です。
arialuni_sdf_u2019というのはXUnity.AutoTranslatorのリリースページからダウンロードできるTMP_Font_AssetBundles.zipに入っているフォントの一つでUnity v2019用です。
この設定でTextMeshProのフォントが置き換われば、それで終わったのですが…。
なおTheRoom4にXUnity.AutoTranslatorを導入する際の基本情報は下記のとおりです。
  • TheRoom4はIL2CPPでコンパイルされている(il2cpp_dataフォルダがある)
  • TheRoom4へBepInEx 6.0.0-pre.1をデフォルト設定で導入できる。
  • TheRoom4へXUnity.AutoTranslator(BepInEx版IL2CPP用)を難なく導入できる。
  • TheRoom4はUnity [2019.4.5f1]で制作されている。
XUnity.AutoTranslatorを導入し最低限の設定を済ませてゲームを起動すると…日本語へ翻訳されたが日本語が表示できるフォントではないので文字のない(もう見慣れた)メインメニューが表示されます。
ここでOverrideFontTextMeshPro・FallbackFontTextMeshProにarialuni_sdf_u2019を指定して日本語表示されれば終了だったのですが…ダメでした。

とりあえずTextMeshProが使用されているかを確認します。下記デバック用の設定を行い再度ゲームを起動します。
  • コンソール画面の表示
    BepInEx.cfg
    
    [Logging.Console]
    Enabled = true
    
  • Unityのログもファイルに残す(falseのままだとコンソール画面への表示だけ)
    BepInEx.cfg
    
    [Logging.Disk]
    WriteUnityLog = true
    
  • テキストを設定したオブジェクトのクラス名やパスを出力(XUnity.AutoTranslator)
    AutoTranslatorConfig.ini
    
    [Behaviour]
    EnableTextPathLogging=True
    
ログ(またはコンソール画面)を確認すると下記出力がありTheRoom4がTextMeshProを使用しているのが分かります。

[Info   :XUnity.AutoTranslator] Setting text on 'TMPro.TextMeshProUGUI' to '現在のプロフィール'
[Info   :XUnity.AutoTranslator] Path : /UI Root Canvas (2D)/RootPanel/SelectGamePanel/CurrentProfile/Current Profile Text
[Info   :XUnity.AutoTranslator] Level: 1
ではなぜOverrideFontTextMeshProが効かないのか?フォント(AssetBundle)ロード時のログが残っていました。

[Info   :XUnity.AutoTranslator] Attempting to load TextMesh Pro font from asset bundle.
[Warning:     Unity] The referenced script (TMPro.TMP_FontAsset) on this Behaviour is missing!
[Warning:     Unity] The referenced script on this Behaviour (Game Object '<null>') is missing!
[Error  :XUnity.AutoTranslator] Could not find the TextMeshPro font asset: arialuni_sdf_u2019
上記ログを踏まえてXUnity.AutoTranslatorのソースを見ると下記のことが分かります。
  • "arialuni_sdf_u2019"というファイルは存在してる。(パス指定をミスったりしてない)
  • AssetBundle(ファイル名"arialuni_sdf_u2019")のロードには成功している。
  • ロードしたAssetBundleからTMP_FontAssetを取り出そうとしたら失敗した。※
    ※ソースだけではUnityTypes.TMP_FontAsset == nullかも知れなかったので、のちにdnSpyでも調査。LoadAllAssetsで0個の配列が返されFirstOrDefaultでfont=nullとなっていた。

      public static UnityEngine.Object GetTextMeshProFont( string assetBundle )
      {
         UnityEngine.Object font = null;

         var overrideFontPath = Path.Combine( Paths.GameRoot, assetBundle );
         if( File.Exists( overrideFontPath ) )
         {
            // ↓の出力があるのでファイルは見つかっている
            XuaLogger.AutoTranslator.Info( "Attempting to load TextMesh Pro font from asset bundle." );

            // ↓の出力がないのでLoadFromFile(またはCreateFromFile)は成功している
            AssetBundle bundle = null;
            if( UnityTypes.AssetBundle_Methods.LoadFromFile != null )
            {
               bundle = (AssetBundle)UnityTypes.AssetBundle_Methods.LoadFromFile.Invoke( null, new object[] { overrideFontPath } );
            }
            else if( UnityTypes.AssetBundle_Methods.CreateFromFile != null )
            {
               bundle = (AssetBundle)UnityTypes.AssetBundle_Methods.CreateFromFile.Invoke( null, new object[] { overrideFontPath } );
            }
            else
            {
               XuaLogger.AutoTranslator.Error( "Could not find an appropriate asset bundle load method while loading font: " + overrideFontPath );
               return null;
            }
            if( bundle == null )
            {
               XuaLogger.AutoTranslator.Warn( "Could not load asset bundle while loading font: " + overrideFontPath );
               return null;
            }

            // この後のエラー出力があるのでココでfont = null
            if( UnityTypes.TMP_FontAsset != null )
            {
               if( UnityTypes.AssetBundle_Methods.LoadAllAssets != null )
               {
#if MANAGED
                  var assets = (UnityEngine.Object[])UnityTypes.AssetBundle_Methods.LoadAllAssets.Invoke( bundle, new object[] { UnityTypes.TMP_FontAsset.UnityType } );
#else
                  var assets = (UnhollowerBaseLib.Il2CppReferenceArray<UnityEngine.Object>)UnityTypes.AssetBundle_Methods.LoadAllAssets.Invoke( bundle, new object[] { UnityTypes.TMP_FontAsset.UnityType } );
#endif
                  font = assets?.FirstOrDefault();	// ココでnullとなっていた(dnSpyで確認)
               }
               else if( UnityTypes.AssetBundle_Methods.LoadAll != null )
               {
#if MANAGED
                  var assets = (UnityEngine.Object[])UnityTypes.AssetBundle_Methods.LoadAll.Invoke( bundle, new object[] { UnityTypes.TMP_FontAsset.UnityType } );
#else
                  var assets = (UnhollowerBaseLib.Il2CppReferenceArray<Unityengine.Object>)UnityTypes.AssetBundle_Methods.LoadAll.Invoke( bundle, new object[] { UnityTypes.TMP_FontAsset.UnityType } );
#endif
                  font = assets?.FirstOrDefault();
               }
            }
         }
         else
         {
            XuaLogger.AutoTranslator.Info( "Attempting to load TextMesh Pro font from internal Resources API." );

            font = Resources.Load( assetBundle );
         }

          // ↓のエラー出力があるのでココまででfontはnullになっている
         if( font != null )
         {
            var versionProperty = UnityTypes.TMP_FontAsset_Properties.Version;
            var version = (string)versionProperty?.Get( font ) ?? "Unknown";
            XuaLogger.AutoTranslator.Info( $"Loaded TextMesh Pro font uses version: {version}" );

            if( versionProperty != null && Settings.TextMeshProVersion != null && version != Settings.TextMeshProVersion )
            {
               XuaLogger.AutoTranslator.Warn( $"TextMesh Pro version mismatch. Font asset version: {version}, TextMesh Pro version: {Settings.TextMeshProVersion}" );
            }

            GameObject.DontDestroyOnLoad( font );
         }
         else
         {
            XuaLogger.AutoTranslator.Error( "Could not find the TextMeshPro font asset: " + assetBundle );
         }

         return font;
      }

arialuni_sdf_u2019にTMP_FontAssetがあるのかUABEAで確認すると…下画面の通りにちゃんとあります。 コレでなんでTMP_FontAssetの取り出しに失敗するのか? 謎
しかしTextMeshProを使用していることは確定したのでTMP_FontAssetの取り出しに成功するファイルがないかarialuni_sdf_u2018(Unity v2018用)や手当たり次第に様々な場所からダウンロードしたファイルも試してみましたが全滅でした。

TheRoom4が使用しているTextMeshProのバージョンが古い(v1.0.23以前)

AssetBundleでのフォント置換が行き詰ってしまったので、The Room Two, Threeのように日本語フォントへ強制変更するコードが追加できないか調査を開始。
TextMeshProのドキュメントを見るとTMP_FontAssetにCreateFontAssetというメソッドを発見。これが使えないか試そうとBepInEx\unhollowed\TextMeshPro.dllを見るとCreateFontAssetというメソッドがない。il2cppだから見れないだけで本当はあるのかな?と思いXuaLogger.AutoTranslatorの既存コードをマネてメソッド取得を試みるも失敗。GetCharactersなど見えているメソッドは取得できるので追加したコードにミスがあるわけではなく、見えない=本当にない という事のようでした。
ちなみにCreateFontAssetがないTextMeshProがあり得るのかバージョンを遡るとv1.3以前ならCreateFontAssetがないようでした。

CreateFontAsset以外にコードでUnityEngine.FontからTMP_FontAssetを作成する方法が見つからず(思いつかず)また行き詰ってしまいました。 そのため大変そうですが下記記事を参考にゲームのリソース(Assets)を書き換えてフォントを日本語化させる方法を試してみる事にしました。 (※こちらの方法でもフォントの日本語化に成功しています。せっかくなので後日記事にするかも知れません。)
この中で、TextMeshProのMonoBehaviourファイルにm_CreationSettingsセクションがあり、その中のPointSizeおよびPaddingの数値を元にフォントを作成する…という内容が記載されていますが、m_CreationSettingsというセクションがTheRoom4にはありません。
PointSize, Paddingという数値はありますがm_fontInfoというセクション内だけで他に存在しません。またm_CreationSettingsに似た名前としてfontCreationSettingsというセクションがあります。
ドキュメントを確認するとv1.2以前はm_CreationSettingsではなくfontCreationSettingsだったようです。
さらにTheRoom4からエクスポートしたTextMeshProのMonoBehaviourファイルにはm_kerningPair内にAscII_Leftという項目がありますがコレもバージョンアップにより変更になった項目のようです。
しかし最も古いv1.0.26のドキュメントにも記載はありません。
仕方がないので対象バージョンをPackage Managerでダウンロードしソース(Library\PackageCache\com.unity.textmeshpro@1.0.2x\Scripts\Runtime\TMPro_FontUtilities.cs)を確認しました。
v1.0.26 : なし (FormerlySerializedAsの記載あり)
v1.0.25 : なし (FormerlySerializedAsの記載あり)
v1.0.23 : あり
v1.0.21 : あり
以上により、TheRoom4が使用しているTextMeshProはv1.0.23以前であることが分かりました。そのためTextMeshPro v1.0.23で日本語フォントを作成しました。
ちなみにTheRoom4が使用しているUnityはv2019.4.5f1で、このバージョンのUnityと同時にインストールされるTextMeshProはv2.0.1でした。

AssetBundleのTMP_FontAssetにあるm_AssemblyNameはゲーム内と同じにする必要がある

ゲームのAssetsを書き換えてフォントを日本語化させる方法を試すにあたってUnity Editorをインストールしました。
Package ManagerでAssetBundleBrowserもインストールし下記組み合わせのAssetBundleを作成してOverrideFontTextMeshProで使用できるか試しましたが全滅でした。
  • Unity v2019.4.5f1 + TextMeshPro v2.0.1
  • Unity v2019.4.5f1 + TextMeshPro v1.3
  • Unity v2019.4.5f1 + TextMeshPro v1.2
  • Unity v2019.4.5f1 + TextMeshPro v1.1
  • Unity v2019.4.5f1 + TextMeshPro v1.0.26
  • Unity v2019.4.5f1 + TextMeshPro v1.0.23
  • Unity v2019.4.5f1 + TextMeshPro v1.0.21
結構自信のあったv1.0.23、v1.0.21でもダメでした。

結局、ゲームのAssetsを書き換えてフォントを日本語化させる方法がうまくいったので、OverrideFontTextMeshProが利用できなかったのは謎のまま終わりにしようと思ったのですが、ブログを記載するためにメモを整理していたら下記を確認していないことに気づきました。
  • ① ゲームのAssets内に"TMP_FontAsset"という名前のオブジェクトが存在するか?(AssetBundleにはあった)
  • ② もしゲーム内にも存在した場合、AssetBundle内の内容とどこが違うのか?
  • ③ もしゲーム内にも存在した場合、AssetBundle内のTMP_FontAssetをゲーム内のTMP_FontAssetで上書きすればOverrideFontTextMeshProで使用できないか?
①:globalgamemanagers.assets#218に"TMP_FontAsset"という名前のオブジェクトが1つだけありました。
こういうときはフォルダで読み込めるAssetStudioが楽です。StreamingAssetsがあると重いので一時避難が必要ですが…。また、下のようにあらかじめ全Assetのリストを保存しておくと便利です。
②:Unity v2019.4.5f1 + TextMeshPro v1.0.23で作成したAssetBundleとゲーム内のTMP_FontAssetの内容は下記の通りです。
異なるのはm_AssemblyNameだけです。どこのハッシュ値なのか分かりませんがm_PropertiesHashまで一致しています。なお途中までしか表示されていない…というわけではなくバイナリ(Raw)で比較してもm_AssemblyNameしか違いがありません。
③:Unity v2019.4.5f1 + TextMeshPro v1.0.23で作成したAssetBundleにゲーム内のTMP_FontAssetを上書きしたところOverrideFontTextMeshProで使用できるようなりました。
ちなみにarialuni_sdf_u2019にゲーム内のTMP_FontAssetを上書きしたところTMP_FontAssetの取得に成功しました。 下記ログの"Loaded TextMesh Pro font uses version:"が取得成功のメッセージです。 新バージョンのデータを旧バージョンのデータ構造で参照することになるため矛盾が生じ、例外が発生しまくりますが…。

[Info   :XUnity.AutoTranslator] Attempting to load TextMesh Pro font from asset bundle.
[Info   :XUnity.AutoTranslator] Loaded TextMesh Pro font uses version: Unknown
[Warning:XUnity.AutoTranslator] An error occurred while handling the UI discovery.
UnhollowerBaseLib.Il2CppException: System.NullReferenceException: Object reference not set to an instance of an object.
あとm_PropertiesHashが異なるとどうなるか気になったのでOverrideFontTextMeshProで利用できるようにしたUnity v2019.4.5f1 + TextMeshPro v1.0.23のAssetBundleのm_PropertiesHashをオールゼロに書き換えて試してみました。
普通に使えました。m_PropertiesHashによるチェックなどは(v2019.4.5f1では)無いようです。

最後に

ほぼ諦めたOverrideFontTextMeshProによるフォント指定で日本語化(自動翻訳化)することができました。はじめはOverrideFontTextMeshProが利用できない原因がil2cppにあるのではないかと思い、かなり見当違いなことも沢山試しました…。 最終的にはUnity v2019.4.5f1 + TextMeshPro v1.0.23で作成したAssetBundleのTMP_FontAssetに記載されたm_AssemblyNameを"com.unity.textmeshpro.Runtime.dll"から"TextMeshPro.dll"へ変更しただけでOverrideFontTextMeshProで利用可能となりました。
おそらく、このm_AssemblyNameがTMP_FontAssetクラスのあるdllの指定で、com.unity.textmeshpro.Runtime.dllがなかったのでTMP_FontAssetが取得できなかったのだと思います。 Unity・TextMeshPro両方のバージョンをゲームと合わせた環境でフォント(AssetBundle)を作成すれば自然にゲームと同じ値となるはずですが、TheRoom4の場合は何故か"com.unity.textmeshpro.Runtime.dll"を"TextMeshPro.dll"として実装しているためOverrideFontTextMeshProに利用できなかったのだと思います。
(Unityのバージョンに対し、妙に古いTextMeshProを使用していることから、Package Managerを使用せず手動でTextMeshProをセットアップしているような気がします。)
もし 他のゲームでXUnity.AutoTranslatorを使用しようとする時にUnity・TextMeshPro両バージョンをゲームと合わせた環境で作成したフォント(AssetBundle)でもOverrideFontTextMeshProに利用できない場合はAssetBundle内のTMP_FontAssetに記載されたm_AssemblyNameも確認して見てください。