2019年3月27日水曜日

Offworld Trading Company MODの作り方

もはや日本国内ではプレイヤーはいなさそうですが、まずはこちらの映像をご覧ください。
上記の内容だけで表題の件は完了なのですが、その内容を含めて今回のVer1.21の日本語化に当たってOffworld Trading Company(以下OTC)のMOD作成でわかったことを、今後の備忘録もかねて記録しておきたいと思います。

・このゲームではMODといっても実装方法に二種類用意されている。

もともとあるシステム上のデータを改変する場合は、予め定義されているXMLファイルの値を変更したうえでMODとして配置します。ゲームシステムの改変というより、ゲームシステム上で動くパラメータの変更に適しています。
この場合は、どのXMLで何が定義されていて、その定義はどういう意味を持っていて、どういう場合に使用されて、何の追加が許されて何が許されないのかという、ゲーム開発元が決めたルールだけがすべてとなりますので、そのオレオレルールの知識が不可欠となります。このため、プログラム作成によるMODより敷居が高いと思います。最低限のルールとして、zType要素は全システムでユニークな識別子であること、です。これを基準に
日本語化するMODも、実際には単純に表示データの改変をするだけですから、こちらの範疇に入ります。

一方、ゲーム製造元会社様が用意したインタフェイスに従ってバイナリを書くと、それを読み込んで実行してくれるという機能も実装されています。
このゲームもその辺の掃いて捨てるほどあるUnityゲームの一つにすぎません。
ですので、こちらの方法では、いつも通りの手順でプログラムを作成すればゲームシステムの改変や新規機能の追加、OSとの連携など、かなりのことができます。むろん、その辺のMOD使用可能なUnityゲームと同様に、できないことは全くできません。
XMLの改変とバイナリプログラムは組み合わせることによって、同時に一つのMODとして統合して運用することができます。
たとえば、日本語化をした際に日本語表示用にもっときれいなフォントで表示させたい、といったような用途では、プログラムでゲーム中で使用されるフォントを変更する必要があります。そこへ日本語化データのXMLを組み合わせるとプログラム+XMLのMODが一つ出来上がるという寸法です。
本記事では、最終的にその具体例を示す所存です。

・MODは一種類しか同時にゲームシステムに適用できない。

この理由により、MODがアンロードされても改変結果が破棄されないようなMOD(この具体例は後でプログラム作成を伴うMODをご説明する際に触れる予定です)を除き、他のMODに影響を与えるMODを作成することが困難です。

・このゲームは何かというと本体とは別のMODとして実装されている。

たとえば、チュートリアルもMODですし、シナリオもMODですし、練習戦(Practice Challenges)もMODです。
従って、前項の理由により、日本語化MODと同時に適用できませんから、それぞれのMODを個別に日本語化しなければなりません。

・MODの配置先は内部的に二か所に分かれている。

これはユーザMODと公式MODの配置先を分けたかったものと見えます。
実際には自作MODをどちらに配置したところで動作は同一です。
大きな違いは、どちらに設置したかによってMODのrootディレクトリが変わることです。
このゲームはゲームプログラム中でMODのrootディレクトリをハードコーティングで決め打ちしています。
この際、Windowsではドライブレターという概念がありますので、ユーザMODと公式MODの配置先が別ドライブだった場合、基本はユーザMOD用ディレクトリにデータを置いておき、必要最低限部分だけを公式MODに反映したうえで絶対パスで別ドライブにあるユーザMODのデータを参照させようという芸当ができません。当然相対パスでも別ドライブを参照できません。

従って、日本語化MODのようにあちこちのMODに日本語化データをまき散らす必要がある場合、公式MOD用のディレクトリにおとなしくインストールしたほうが有利になります。

どちらに配置すれば何にアクセスできるのかを見極めて配置先を決定する必要があります。

・MODの開発に必要な元ネタの場所とMODの配置先

さて、上記まで説明したので、具体的に何がどこにあるか、またはどこへ配置すべきかに触れておきます。
  1. 元ネタのXML
    [user]\Documents\My Games\Offworld\Mods\Hidden\Reference\Data
  2. プログラムで使用されているライブラリのソース
    [user]\Documents\My Games\Offworld\Mods\Hidden\Reference\Source
  3. ユーザーMODの配置先
    [user]\Documents\My Games\Offworld\Mods\[任意のディレクトリ名]
  4. ユーザMODで変更したXMLファイルの配置先
    [user]\Documents\My Games\Offworld\Mods\[任意のディレクトリ名]\Data
  5. ユーザMODで作成したプログラム(クラスライブラリDLL)の配置先
    ユーザMODの配置先と同様。
  6. クラスライブラリから参照するファイルの配置先(MODの設定ファイルなど)
    特に固定化されてはいません。
    C#からアクセスできるところであれば世界中どこでも。
  7. 公式MODの配置先
    [steamインストールdir]\steamapps\common\Offworld Trading Company\Offworld_Data\StreamingAssets\Mods
  8. チュートリアルMODの配置先
    [steamインストールdir]\steamapps\common\Offworld Trading Company\Offworld_Data\StreamingAssets\Mods\Hidden\Tutorials
  9. Jupiter's Forge Expansion Packチュートリアルの配置先
    [steamインストールdir]\steamapps\common\Offworld Trading Company\Offworld_Data\StreamingAssets\Mods\Hidden\IoTutorial
  10. Practice Challenges(練習戦)の配置先
    なし。
    標準ではUnityのアセットを直接参照しています。但し、scenario.xmlでMODディレクトリの参照先を変更できますので、scenario.xmlを改変し、そこにMODディレクトリを明示したMODをまず生成(ややこしい)したうえで、そのMODディレクトリに改変したいデータを設置することで改変が可能となります。

・MODの異常はすべてoutput_log.txtに記録される。

これはOTCに限らずUnity上で動くゲームすべてに言えることですが、Japanese locale / 日本語化 (β)を作った作者さんへのコメント欄が「動かない」「ナントカしろ」挙句の果てに「対応しろ」ばっかりだったので本当に気の毒なのであえて特記します。
特に気の毒なのは、output_log.txtを見れば一瞬で分かることなうえに加えて、すぐに自分で直せるような内容なのにもかかわらずこの有様なのはもうまことに気の毒でしょうがない。
私も他ゲームになりますが、私がsteam上で公開しているMODでも変な要望や願望や斜め右上の妄想を持ち込んでくる人はいますが、こんな苦情しか言えないような低レベルな人が大量に湧き出てきてしまっている光景は気の毒で気の毒で仕方がありません。率直に言ってこんなひどいのが集まってるのを見たことがない。
本当に気の毒で、それで個人的にはやりもしない、実際に日本国内のプレイヤーもいなさそうなゲームの翻訳に手を付ける気になったわけで、本記事もそれがもととなって書かれていますので敢えて項目として挙げました。
上記はおくとしても、このことは大変重要です。釈迦に説法かもしれませんが、うまく動かない時はやみくもに手元のMODをいじらないで、このログファイルをまず確認して何が起きているのかを把握したうえで次の行動に移ることが肝要です。

・XML MODの改変方法は4通りあり、それぞれファイル名の命名規約が異なる。

以下の三通りがありました。
  1. オリジナルのXMLを丸々書き換える
    これはオリジナルのXMLの項目数やエントリ名をそのままに、中身だけ変更する場合に適用可能です。
    具体的には、日本語化を例にとると、ほかの内容はそのままに、Englishの項目の中身だけを全部日本語に置き換えてしまうといった場合に使用できます。
    この場合のファイル名の命名規則は「オリジナルと同じ」であること、です。
  2. オリジナルのXMLの特定項目だけを変更したXML
    これは必要なzType要素を持つ要素だけに対して変更を加えた内容のXMLファイルを作成したいときに使用します。
    たとえば、Practice Challenges MODの参照先ディレクトリだけを変更したファイルを作成する場合、ほかのzTypeは変更する必要がないのでMODとしては不要だからファイルに記載したくない、という場合に使用します。不要部分も一緒に持ってきちゃう場合は項番1の方法で構いません。
    この場合のファイル命名規則はオリジナルの拡張子を除いたファイル名に「-change」を追加します。
  3. 全く新しいzType要素を追加したXML
    これはオリジナルのXMLにない項目を追加したい場合に使用します。
    この場合のファイル命名規則は、オリジナルの拡張子を除いたファイル名に「-add」を追加します。
  4. 既存のzType要素を削除したい。
    これはまずremove.xmlというファイル名のxmlを用意し、Entry要素1つに対してType名を1記述します。削除対象が複数ある場合はEntry要素を削除したいType名の数だけ記述します。
    ファイル命名規則はremove.xmlという名前で決め打ちです。
  5. 全く新しいXMLファイル名を用意したい
    ゲームシステム上で読み込むファイル名が決め打ちなのでXMLファイルの変更だけでは困難です。

・プログラム(クラスライブラリ)MODの基本

Unity上で動作しているMono上で動作します。
Unityのバージョンが5.3のため、Monoの.NET相当バージョンは3.5です(古いゲームだから仕方がないけど・・・うんざりするんだよなあ)
従って、プロジェクト生成時やコンパイル時にそのように指定しないと、Monoが理解できないILを生成されたり、うっかりUnity上では用意されていないライブラリを使ってしまってあとから丸々作り直し、なんてことになります。
まあ、同じ3.5でもMonoとUnityと.NET間で微妙に使えなりクラスライブラリなどの差異があることはあえて触れるまでもないでしょう。XMLまみれの本ゲームにおいてはXmlDocmentはあるけどXDocumentがないヨ、とかね。

さて、どういう手続きを踏めばゲームから呼び出されてくれるのかというと、クラスにModEntryPointAdapterクラスを継承するだけで呼び出されることが可能な状態になります。

そのうえで例によってModEntryPointAdapterで定義されたInitialize()やOnGUI()、Update()などの仮想関数をオーバーライドすることによって、その関数がゲーム本体から呼ばれます。
いうまでもありませんが、クラスライブラリのDLLは最低限、UnityEngineおよびCSharp-Assembly-firstpassを参照している必要があります。
以上までが基本です。
あとはUnityで出来ることとCSharp-Assemblyに定義されたゲーム会社が用意するインタフェイスを通してできることは何でもできますので、この記事で説明が必要なことはもうありません。

・実際にOTC用のプログラムMODを作ってみましょう。

今回の日本語化MODですと、ゲームシステムで使用されているフォントが全部欧文用であり、日本語用のフォントが入っていません。
そのため、unityによってfallbackされて、巡り巡ってOSのフォントまでさかのぼり、最終的にWindowsの場合はMSゴシックあたりに日本語のデータがあるということで、そのフォントで表示されます。
フォールバックしてくれる仕組みが備わっているので日本語としてちゃんと表示されること自体は誠にご同慶の至りではありますが、固定ピッチなので若干間延びして見えてしまいます。

そこで、フォントを変更するMODを作ってみたいと思います。

しかしながら、そのままだとXMLファイルにはフォントの定義が一切ないので、changeもaddもremoveもできません。

そのため、プログラムを作り、フォントをどうにかしたいと思います。

まず、ゲームのアセットに入っているフォントを使用しないで新しいフォントを用意して、各GUIパーツに対してフォントを適用しなおす・・・なんてことをやりたければやればいいと思いますが、この際は折角フォールバックする仕組みがあるのですから、ゲーム内で使用されているフォントのフォールバック先を定義してしまえばお手軽です。

直接アセットバンドルを覗けばいいんですが、あんなのチマチマとひとつづつ確認してたら日が暮れちゃいますし、なんか問題もありそうなので、まずフォントを列挙するMODを作ります。これならだれもケチはつけられんでしょう。

というわけで、こういう形で作ってみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EnumFonts : ModEntryPointAdapter
{
  public override void Initialize()
  {
    base.Initialize();
    EnumFonts();
  }
  void EnumFonts()
  {
    Font[] fontarr = Resources.FindObjectsOfTypeAll<Font>();
    if (fontarr == null)
    {
      return;
    }
    int count = 0;
    foreach (Font font in fontarr)
    {
      count++;
      log(count.ToString() + ": " + "font.name: " + font.name);
      foreach (string fn in font.fontNames)
      {
        log(count.ToString() + ":   fallback to:" + count.ToString() + ": " + fn);
      }
    }
  }
}

説明するまでもありませんが、念のため何をしているのか説明します。
  1. ModEntryPointAdapterを継承したクラスを作る
  2. Initialize()関数を経由してゲーム内リソースに含まれるフォントを列挙する関数をcallする
  3. 結果をoutput_log.txtに出力する

ここまででこの項目の表題は完了してしまったわけですが、とりあえず進めます。
このMODをビルドして前項でご説明したMOD配置先に配置してOTCを一回起動します。
すると現今では次のフォントが採用されていることが分かります。
  • Arial
    皆さんおなじみ、Unityの標準フォント。
  • TitilliumWeb-SemiBold
    フォールバック先はTitillium Web
  • FiraSans-Bold
    フォールバック先はFira Sans
  • TitilliumWeb-Regular
    フォールバック先はTitillium Web
  • FiraMono-Bold
    フォールバック先はFira Mono
  • boston_traffic
    フォールバック先はBoston Traffic

以上のフォントのフォールバック先を片っ端から書き換えてしまえば目的のMODの完成です。
直接MS UI Gothicとか直接代入しても目的は果たせますが、それではややいかがなものかと思われますので、フォントの設定を外部ファイル化してみたいと思います。
それが以下になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class FontFallbackModifier : ModEntryPointAdapter
{
  const string MODNAME = "Avocado";
  Dictionary<string, List<string>> targetFontNamesDic = new Dictionary<string, List<string>>();
  public override void Initialize()
  {
    base.Initialize();
    ReadFontSettingXML();
    ModifyFallbackFontnames();
  }
  void ModifyFallbackFontnames()
  {
    Font[] fontarr = Resources.FindObjectsOfTypeAll<Font>();
    if ( fontarr == null )
    {
      return;
    }
    foreach ( Font font in fontarr )
    {
      if ( !targetFontNamesDic.ContainsKey( font.name ) )
      {
        continue;
      }
      if ( targetFontNamesDic[ font.name ].Count == 0 )
      {
        continue;
      }
      font.fontNames = targetFontNamesDic[ font.name ].ToArray();
    }
  }
  void ReadFontSettingXML()
  {
    targetFontNamesDic = new Dictionary<string, List<string>>();
    string fname = Application.streamingAssetsPath + "/Mods/" + MODNAME + "/font-setting.xml";
    if ( !File.Exists( fname ) )
    {
      log( fname + " is missing" );
      return;
    }
    var xml = new XmlDocument();
    xml.Load( fname );
    XmlElement root = xml.DocumentElement;
    XmlNodeList eles = root.GetElementsByTagName( "Font" );
    if ( eles == null )
    {
      log( "eles is null" );
      return;
    }
    foreach ( XmlElement ele in eles )
    {
      if ( ele == null )
      {
        log( "ele is null" );
        continue;
      }
      XmlNode s = ele.SelectSingleNode( "Source" );
      if ( s == null )
      {
        log( "s is null" );
        continue;
      }
      if ( s.InnerText.Trim().Length == 0 )
      {
        log( "s is empty" );
        continue;
      }
      XmlNodeList fts = ele.GetElementsByTagName( "FallbackTo" );
      if ( fts == null )
      {
        log( "fts is null" );
        continue;
      }
      string key = s.InnerText.Trim();
      foreach ( XmlNode d in fts )
      {
        if ( d.InnerText.Trim().Length == 0 )
        {
          log( "d is empty" );
          continue;
        }
        string value = d.InnerText.Trim();
        if ( targetFontNamesDic.ContainsKey( key ) )
        {
          targetFontNamesDic[ key ].Add( value );
          Debug.Log( s.InnerText + "+=" + value );
        }
        else
        {
          targetFontNamesDic.Add( key, new List<string>() { value } );
        }
      }
    }
  }
  void log( string s )
  {
    Debug.Log( "Avocado: DEBUG: " + DateTime.Now.ToString() + ": " + s );
  }
}
フォントを変更する箇所より設定ファイルを読みだすほうがコード的に長くなってしまいました。もっとも、ほとんどエラーチェックですが。

実は一つ問題があって、MODがロードされるタイミングによっては、MODを再ロードするまでフォントの変更が画面上に反映されないということがあります。

原因はいろいろ考えられますが、現状の動作を見る限りでは、今回作ったMODではInitialize()でMODがロードされたときに一回だけ実行する関数内でフォントの変更をしていますが、このInitialize()がcallされる前にOTC本体側の画面の構築が完了する場合があるようで、そのケースで発生する事象のようです。

試しに、Initialize()関数内で無理やり5秒ほどSleep()して止めてしまうと初回起動時からフォントが正しく変更されていました。まあ、この手法はあまりにもばかばかしいので数回テストしただけでやめちゃいましたから本当にそれでいいのかは疑問符つきです。
今度は逆にUpdate()関数内で1000フレームほど経過してからフォント変更処理を走らせてみると、文字全部がいったん崩れてしまいますが、マウスカーソルを当てるなどして色変更などの処理が走る(再描画処理が走る)パーツについてはフォントが切り替わることは確認しました。
だからと言って、各画面内パーツに片っ端からInvalidateをやらせる処理をゴリゴリ書くほどフォントを変えたいというわけでもないし、フォントはゲーム起動後アンロードされないので、別MODを起動するとか言語設定を他に変えてから戻すとかマップエディタを起動して戻るとかで対応可能ですからやりません。

これは日本語化XMLにも同様の事が言えます。
MODで用意したXMLがロードされる前に画面の構築処理が走る場合があるようで、再ロードしない限り画面上に翻訳文が反映されないという現象がありますが、おそらくこれと同根ではないかと思います。

要するに、(Unity用語の)シーンの切り替えによって画面の再生成が行われればいいんだろうと思います。

この問題は認識済みで、かつ対応する気はサラサラ、これっぽっちも、微塵もありませんので、予めご了承ください。

なお、このMODのソースファイルは、以前ご紹介したスプレッドシート上にある日本語翻訳文をXML化するツールに同梱してありますので、必要ならご参照ください。

さて、随分長くなってしまいました。成果物だけクレクレ様もおられるでしょうし次の記事でXMLとプログラムをまとめたMODとして実際にゲームに適用できるようにした現物と、ゲームへの適用方法についてご説明いたします。

ここまでお読みいただいてありがとうございました。
ま、世界で一人でもいたら奇跡ですわね。

0 件のコメント:

コメントを投稿