fc2ブログ

記事一覧

Hearthlands日本語化パッチができるまで

タイトル通りです。
せっかく苦労したので後学のためにも整理して残しておこうかと。

わからない人置いてけぼりの技術的な話なのでご注意ください。

立ちはだかる壁
まず最初の問題は、Hearthlandsのプログラム自体が日本語の表示に対応していないことでした。
ゲーム自体は多言語対応で英仏独西波に対応しており、
翻訳ファイルもほぼプレーンテキスト(tsvファイル、タブ区切りcsv)で用意されているのですが、
独自のビットマップフォントを利用しているため、収録されていない文字は表示できません。
(ウムラウト文字など、英語以外のヨーロッパ言語で使われる特殊なアルファベットは収録されています)
そこで、まずは日本語を表示させる方法を考える必要がありました。

よくある方法 - 画像の置き換え
最初に思いついたのが、日本人がまず使わないアルファベットや記号の画像フォントをひらがなに置き換え、
それらの記号を使って翻訳ファイルを記述することで日本語を表示する方法でした。

換字暗号のような感じになります。
簡単のために英語のアルファベットを例に説明すると、

A = あ
B = い
..
Z = は
a = ひ
b = ふ
..
u = ん



と文字画像ファイルのA〜uを順番にひらがなに置き換えたとします。
「さんま」を表示するには、さ = K、ん = u、ま = eとなるので、

Kue


と翻訳ファイルに記します。するとプログラム側ではKueを表示しようとするので、
置き換えられた画像が代わりに表示されて、画面には「さんま」と表示される、という寸法です。
実際にはアルファベットはできれば残したいので、Áやらöやらの馴染みのない文字を置き換えることになります。

HearthlandsはJavaで作られており、
Javaの実行ファイルの本体であるjarファイルは簡単に画像や音声のファイルを弄れるので、画像の編集は容易です。

が、この方法には問題があり、

- ひらがなしか表示できない
- 濁点や半濁点を考えると文字数が足りないので、半角カナのように分割する必要がある
- 翻訳ファイルが暗号文みたいになって読みづらい

という感じでした。
まあ、それ以前の問題でうまくいかなかったのですが。

バグる!
さて、Hearthlandsには様々な特殊アルファベットが収録されていますが、その全てが使われているわけではありません。
特定の文字の大文字は使わないとか、あるいはどの言語ファイルにも登場しないとか、そういう"死んだ"文字がたくさんありました。

そして、それらの使われていない文字が言語ファイルに含まれていると、

何故かクラッシュするんです。

これには焦りました。クラッシュする文字を除くと、使える文字数が20くらいになってしまうんです。
さらに厄介なことに、使える文字の中にも、配置によってはクラッシュするものがありました。

原因を探るため、プログラムを逆コンパイルして解析しました。
クラッシュの真の原因はパッチが完成した今もわかっていないのですが※1、対症療法は発見しました。
どうやら、文字クラス(と思われるもの※1com/hearthlands/b/r)のstaticフィールドであるa(なんらかのArrayList)が初期化※2される前に、そのSize()メソッドが参照されてぬるぽっていることが原因のようでした。
少々強引ですが、これをクラスrのインスタンス化時に初期化するコードを埋め込むことで、エラーを回避できるようになりました。

補足: Javaプログラムの改造
Javaプログラムの改変は(その他の言語で書かれたプログラムと比較すれば)容易です。
中間言語のようなものに変換されるため、逆コンパイルが可能ですし、
クラスごとに独立しているので部分的な書き換えが容易です。

しかし、Javaの逆コンパイルコードは大抵ビルドが通りません※1
なので、逆コンパイルしたコードを改変して再ビルドして埋め込むということはまずできません。
そのため、ビルド済みのclassファイルのバイナリを直接編集する必要があります。
中身はJVMに読ませるCPU命令(バイトコード)なので、
専門のツールがあれば解読や書き換えは容易です※3

文字のマッピング
プログラムを解析していくうち、何かに気づきました。
「これ、文字のマッピングをいじれば日本語を直接扱えるし、文字数自体も拡張できるんじゃね?」と。

まず最初に怪しいと思ったのが、文字画像ファイルの文字の並び順です。
文字コード的にはアルファベットの大文字の方が番号が若く、小文字が後に来るはずなのですが、
文字画像ファイルの中の並びは小文字→大文字の順でした。
そこから、「文字番号と画像番号のマッピングは独自なのでは?」という疑いを抱きました。※4

テキストクラスらしきものは判明していて(com/hearthlands/b/c)、それっぽいコードを発見しました。
要素数65536※5short配列がマッピングを保存しているフィールドで、
添字に文字コードを与えることで、対応する文字画像の番号を返すという単純な実装です※6
この配列に直接値を放り込むコードが書かれていたので、
日本語が含まれる文字画像を用意して、その(画像内の)番号と文字コードを関連づけるよう書き換えてやれば、日本語を表示できるようになります。

さすがにそれを手動でやるのは日が暮れるので、日本語の翻訳ファイルから使われている文字を抽出して画像化し、
同時に文字コードと画像番号の対応付けを行うためのバイトコードを自動生成するプログラムを書きました※7

この方法ならば、日本語を表す文字コードから直接文字画像を参照させることができるので、
日本語を変な記号で換字した暗号のような翻訳ファイルを用意する必要がありません。
日本語をそのまま書くことができます。

Hearthlandsの元々の文字画像ファイルには32×6=192個の文字しか入らないのですが、

これはプログラムを書き換えてやれば簡単に拡張できました※8

最後の壁
さて、ここで最後の関門が立ちはだかります。



の動画でも少し触れていましたが、謎の文字化けが発生しました。

文字化けの法則が全くわからず、同じ文字でも化けたり化けなかったり……とかなり悩まされました。
コメントでもご指摘をいただいた通り、内容自体は文字が?(U+3F)に化けるという、よくある"ダメ文字問題"のような感じでした。

「文字列が正しい文字コードで扱われていない」ということはすぐに予想できましたので※9
翻訳ファイルのロードを行なっているクラスを探しました。

結果、それはcom/hearthlands/a/jだと判明。
実装としては、ご丁寧に翻訳ファイルをバイトストリームとして読み、それをUTF-8でデコードして
JavaのString※10に変換し、
訳文のKVS(Key-Value Store)を生成するというものでした。これなら問題は起こらないはずです。

ところがどっこい、その通りの実装にはなっていませんでした。
流れとしては、
1. 翻訳ファイルをByteArrayInputStreamとして開く
2. データの切り出しのためバイト列を加工する※11
3. String(byte[] bytes, int offset, int length)コンストラクタを使い、加工したバイト列を使って中間Stringを生成(KeyとValueはそれぞれ別)
4. 中間StringからString.getBytes()を使い、バイト列を抽出
5. 抽出したバイト列を使って、String(byte[] bytes, String charsetName)コンストラクタを使って最終Stringを生成。charsetNameには"UTF-8"を指定。

という処理になっていました。
バイト列から直接Stringを生成するのではなく、中間Stringを挟んでいるわけですね。

なぜこういう実装になっているのか明確な意図はわからないのですが※12
この中間Stringが文字化けの元凶でした。

実は3のStringコンストラクタと4のgetBytes()メソッドは、
プラットフォームのデフォルトエンコーディングを使って処理を行うようになっています。
つまり、UTF-8が使われていません。これが文字化けの原因だったと考えられます。

実際に3と4に引数を追加して、エンコーディングにUTF-8を指定してやれば、文字化けは治りました。
晴れてパッチ完成です。

おわりに
とまあ、あんなパッチですが、私の持てる技術の粋を集めて作られています。

作業していて、かなり勉強になることが多かったです。

心残りなのが、たぶん多くの日本語化パッチがそうしているように、
「通常のTTFを利用してテキストを動的に描画するプログラムを埋め込む」
という方法をとったほうがスマートではないかな、ということです。
流石にそこまでは技術が及びませんでした。
気が向いたら試みるかもしれませんが、
少し私生活に余裕がなくなってきていて、動画制作も遅れてしまっているので、優先度はかなり低いです。


脚注
※1 リリースビルドを逆コンパイルしたソースコードはたいてい難読化されており(クラス名/メソッド名/フィールド名がaとかbとかに置き換えられている)、処理の内容からほぼノーヒントで役割を推測しなければならないので、解析は困難を極めます。しかもJVM命令語に変換される関係で参照型と整数型の区別がつかないため、型が正しく再現されず、逆コンパイルしたソースはまずビルドが通りません。そういう事情もあり、完全に全貌を理解するのは現実的ではありません。
※2 要するにnew ArrayList()することです。余談ですが、ジェネリクスではなく型パラメータがないのは、おそらく本来かつてはそうだったのですが、Javaの仕様でコンパイル時に消えるからです。なんのArrayListなのか一目ではわからないということです。
※3 とはいえ、ツールにはバグが多く、あのツールではいじれるクラスがこちらのツールではエラーを吐く、ということが結構あります。そういう時は複数のツールを使い分けるしかありません。
※4 無知ゆえ知らなかったのですが、LWJGL(Java向けGLライブラリ)を利用する場合標準的な実装だそうで。
※5 つまりUnicodeU+FFFFまでは取り扱いが可能ということですね。標準的な漢字はこの範囲に収まります。
※6 数学的には「文字コードから画像番号への全単射」と表現したほうがわかりやすいかもしれません。
※7 英語は文字ごとに字幅が違うので、等幅フォントだと表示が汚くなります。そのため、各文字の字幅を設定するための別の配列がありました(こちらは「画像番号から字幅への全単射」ですね)。この配列もいじらなければならなかったので、日本語に関しては一律字幅を100%に設定するためのコードも埋め込まねばならず、同時に生成する必要がありました。
※8 デフォルトだと1024×1024ピクセルまでの画像しか扱えないので、その限界もプログラムの改変で拡張してやる必要がありました。(テクスチャクラスcom/hearthlands/a/t
※9 一応、内部的に文字列自体がおかしくなっているのか、文字列は正常だが表示がおかしくなっているのかは調べました。結果、文字列自体が腐っていました。
※10 UTF-16ライクなやつですね。詳しくはググっていただくとして、2バイトで1文字を表現します。
※11 翻訳ファイルがtsvなのでHT(U+9)で訳文のKeyとValue区切ったり、そういう処理です
※12 コメントもない難読化ソースコードでそんなのわかるわけないじゃないですか。
スポンサーサイト



コメント

コメントの投稿

非公開コメント