kengo700のダイアリー

誰の役にも立たないと思う情報を発信するブログ

『Renowned Explorers: International Society』の日本語化のための試行錯誤メモ

ローグライクゲーム『Renowned Explorers: International Society』の日本語化のために試行錯誤した内容をメモしておく記事です。まだ日本語化はできていません。

はじめに

ふとしたきっかけから『Renowned Explorers: International Society』(以下、RE:IS)というゲームを日本語化しようと思い立ったので、その試行錯誤の内容を記録しておきます。

今回は有志を募らずに一人で翻訳してみようかと思うのと、サプライズ的に日本語化パッチを公開したいなと思うので、この記事もパッチが完成するまで非公開で書き進めていきます。

もちろん、そもそも日本語化が実現できるかは分かりませんが…

というよりも、簡単に日本語化できるならこんな記録も必要ないのですが、ちょっと取り組んでみた感じでは「けっこう大変そうなので記録しておかないと忘れてしまいそう…」という状況です。

8/15追記:やっぱり一人で訳しきるのは難しそうなので、この記事を公開して有志翻訳者を募ります。ご協力いただける方は、下記ページをご覧ください。

試行錯誤

情報収集

7/20~

有志翻訳の第一歩は、公式が翻訳を募集していないか、海外の有志が翻訳していないかを調べるところから。

Steamコミュニティのスレッドで検索したところ、ユーザー「○○語に翻訳予定はない?」→開発者「テキスト量が多いからローカライズは難しいです。」みたいなやり取りはある。Fan Translationは行われてないっぽいか。

次に中文化MODを検索。中文化MODがあれば日本語化は実現できたようなもん。だけどなさそう。

ゲーム名でググると「ゲーム名 日本語化」のサジェスチョンが出るので、試しにググってみると、日本語のSteamガイドが見つかった。後で読もう。

さらに某掲示板で、RE:ISの日本語化を試してみたらしい人の書き込みが見つかる。その方の情報をまとめると下記の通り。

  • ゲームデータについて
  • フォントについて
    • アンパックしたフォルダのルートのttfファイルを日本語フォントで上書きすればOK
  • テキストについて
    • LuaJITでコンパイルされたluaファイルに含まれる
    • 大半のテキストは\lua\abbeybase\i18.luaにテーブル形式で格納
    • デコンパイラは存在しないらしいので、編集にはプログラマの協力が必要
    • 日本語化にてを出すかは、実際にテキストをみて判断したほうがいい

マジで何者なんだ… というか、ここまでできるのにプログラマじゃないのかよ… それとReisUnpackなるソフトを作った人の存在も謎過ぎる…

「LuaJIT」や「lua」は初めて見た単語だけど、とりあえず後回しで1つずつ進める。

ゲームテキストの確認

某掲示板の書き込みのリンクからダウンロードして解凍した中身は「i18n.csv」というファイル。Excelで開いてみるとこんな感じ。

f:id:kengo700:20170722025257p:plain

おそらく1列目がID、2列目がテキスト。テキスト内のカッコで囲まれた部分はプログラムが置き換えるテキストだと思われる。たぶん… あとでゲーム内の表示を見ればわかるはず。

テキスト数は17898行。分かっていたけど、なかなかヘビーだ…

ゲームデータのアンパック・リパック

「ReisUnpack」のGitHubのページからzip形式でダウンロードして解凍すると、中身は下記のような感じ。普通のVisual Studioのプロジェクトっぽい。

f:id:kengo700:20170722215258p:plain

GitHubのページでも見れる「README.md」には「.Net 4 is required to run. Built with Visual Studio 2015.」と書かれている。Visual Studio 2015はいつも使っているものなので、そのまま「ReisUnpack.csproj」をダブルクリックで起動。ビルドも問題なくできた。

f:id:kengo700:20170722215738p:plain

試しに実行してみると、使い方が表示された。

f:id:kengo700:20170722215837p:plain

引数に「unpack」や「repack」を指定すると、それの説明も表示された。これによると、アンパックとリパックは下記のような感じで引数を与えればいいっぽい。

アンパック時

ReisUnpack unpack 「リソースファイル(content.tim)」 「出力フォルダ」

リパック時

ReisUnpack repack 「入力フォルダ」 「出力ファイル(content.tim)」

いちいちコマンドラインで操作するのはめんどいので、各種ファイルをフォルダにまとめて下記のバッチファイルを作成

アンパック.bat

.\ReisUnpackBin\ReisUnpack.exe unpack ".\OriginalData_20170720\content.tim" ".\UnpackTest"
pause

リパック.bat

.\ReisUnpackBin\ReisUnpack.exe repack ".\Localize_20170720" ".\RepackTest\content.tim"
pause

「アンパック.bat」を実行してしばらく待つと、問題なくリパックできたっぽい。

f:id:kengo700:20170722220841p:plain

アンパックしたものをそのままリパックしてみる。「アンパック.bat」を実行してしばらく待つと「content.tim」が生成され、これをゲームフォルダのオリジナルのファイルに上書きしてゲームを起動してみると、問題なく起動できた。

f:id:kengo700:20170722225134p:plain

ゲームデータの改変テスト

まずはフォントを変えてみる。

日本語フォントの「NotoSansCJKjp-Medium.otf」を「WOFFコンバータ」でwoff形式を経由してttf形式に変換し、アンパックしたファイル内の下記のファイルに上書きしてリパック。

  • arial.ttf
  • arialb.ttf
  • arialbi.ttf
  • ariali.ttf
  • default.ttf
  • defaultb.ttf
  • defaultbi.ttf
  • defaulti.ttf
  • header.ttf
  • headerb.ttf
  • headerbi.ttf
  • headeri.ttf

ゲームを起動し、フォントが変わっていることを確認。

↓オリジナル
f:id:kengo700:20170722221538p:plain

↓フォントを変えたバージョン
f:id:kengo700:20170722221829p:plain

Luaについて情報収集

軽くググってみたところ、「Lua」はスクリプト言語らしい。 ゲーム向け寄りのRubyやPythonみたいなものか。

「LuaJIT」はコンパイルすることで処理が高速化した「Lua」らしい。 なので「Lua」のコードはメモ帳でも開けるけど、「LuaJIT」でコンパイルしたものはバイナリ形式。

使う予定のないプログラム言語を勉強することほど面倒くさいこともないので、Luaについてはとりあえずこれくらい分かっとけばいいか。 必要になったらまた調べよう。

i18n.luaについて調査

大半のテキストデータが入ってるらしいi18n.luaというファイルをバイナリエディタ「Stirling」で開いてみる。

f:id:kengo700:20170725063026p:plain

f:id:kengo700:20170725063205p:plain

確かにIDとテキストが収められている。それ以外の冒頭部分と末尾部分のデータの意味はよく分からんので後回し。

IDとテキスト部分をよく見てみると、それぞれのID・テキストの間に1または2バイトのデータが入っている。 順番に何個か抜き出してみると、下記のような感じ。

ID テキスト
15 FREYA_SPEAR_DESC 39 The sacred spear of the goddess of valor and beauty.
1F INCA_VILLAGE_BATTLE_1_TEXT 83 01 You arrive in a small Incan village. �ptain% is not sure whether this particular village is with or against [b|the Emperor].
1D INCA_ANGRY_REVIEW_2_TEXT 74 The man continues:.[{QUOTE}|Nothing you choose matters. Why are you here? Are you capable of making decisions?]

どこまでがID・テキストなのかは、某掲示板で解析されていた人の「i18n.csv」を元に判断している。

コンパイルされたファイルであるi18n.luaを読み込む側のプログラムの気持ちになって考えると、冒頭から1バイトずつ読み込んでいくはずで、その場合どこからどこまでがID・テキストで、データの区切りはどこなのかが分からないと、データを読み込みようがない。

変態的な実装になっていなければ、各ID・テキストのバイト数の情報があるはずで、これが前述の謎データの可能性がある。

前述の表の謎データ部分を16進数から10進数に変換し、IDとテキストは文字数をカウントして並べてみる。Googleスプレッドシートで「HEX2DEC」と「LEN」関数を使用。

謎(10進数) ID文字数 謎(10進数) テキスト文字数
21 16 57 52
31 26 33537 126
29 24 116 111

各ID・テキストの文字数とその直前のデータには、それっぽい関連がありそう。「83 01」以外のものでは、謎データ(10進数)から5を引いた値がID・テキストの文字数になっている。 やっぱりこの部分にはID・テキストのバイト数の情報が収納されてるっぽい。

よく分からない「83 01」については保留。

その他気付いたことメモ

  • 改行は「0A」
  • 文字の制御コマンドは[コマンド|テキスト]
    • 例えば[b|text]は「text」を太字で表示する
    • コマンドの詳細については後々調べる

f:id:kengo700:20170725065405p:plain

日本語の文字の表示テスト

7/21~

日本語の文字を表示できるか試してみる。

i18n.luaファイルのテキスト部分に、UTF-8形式で「あ」の文字を入れ込んでみる。UTF-8形式なのは、Luaでutf8うんぬんというページを見たから。 全体のバイト数が変わらないように、3バイト分入れ替え。

  • テキスト:「New Game」→「New あe」

f:id:kengo700:20170722222820p:plain

リパックしてゲームを起動してみると、「あ」の文字が表示できた!

f:id:kengo700:20170722222854p:plain

これができれば日本語化はできそうだと言えるので、ひと安心。

ゲームデータについて調査

いろいろ試しつつ、i18n.luaの中身を調べて行く。

試しに一つのテキストを2文字を減らしてみる。文字数を表してると思われる部分はそのまま。

  • テキスト:「New Game」 → 「New Ge」

f:id:kengo700:20170722214705p:plain

このファイルをリパックしてゲームを起動しようとすると、エラーが出て起動できない。

f:id:kengo700:20170722214919p:plain

これは想定通り。これで起動してしまったら「文字数を表してると思われる部分」がわけわからんくなる。

次は一つのテキストを2文字を減らし、文字数を表してると思われる部分も修正してみる。

  • テキスト:「New Game」 → 「New Ge」
  • 文字数データ:0D → 0B

f:id:kengo700:20170722213850p:plain

このファイルをリパックしてゲームを起動しようとすると、エラーが出て起動できない。

f:id:kengo700:20170722214356p:plain

これで起動してくれたら楽だったけど、そう簡単にはいかないか。

この結果から、テキストの情報サイズは、テキスト総数ではなくデータサイズで記録されていると思われる(文字数を変えただけでテキスト総数は変えていないのにエラーが出たため)。

7/22~

昨日の仮説を確かめるために、一つのテキストの文字を減らし、別のテキストの文字を同じだけ増やして、トータルの文字数が同じになるように改変してみる。文字数を表してると思われる部分も修正しておく。

  • テキスト:「New Game」 → 「New Ge」
  • 文字数データ:0D → 0B
  • テキスト:「Load Game」 → 「Load Gamaae」
  • 文字数データ:0E → 10

f:id:kengo700:20170722213218p:plain

f:id:kengo700:20170722213252p:plain

このファイルをリパックしてゲームを起動してみると、期待通り2箇所のテキストの改変に成功!

f:id:kengo700:20170722213321p:plain

昨日の結果と合わせて考えると、やはり「テキストデータ部分のサイズ」や「テキストデータ部分の末尾のバイト数」的な情報が、i18n.luaに含まれていると思われる。 そいつを特定する必要がある。 逆に言えば、そいつさえ特定できれば、解析作業は完了なはず。

i18n.luaの解析

7/24~

テキストデータのサイズ情報がi18n.luaのどこにあるのか調査する。

f:id:kengo700:20170725070657p:plain

一番最初のID「FREYA_SPEAR_DESC」が0x0000122~0x0000131で、そのひとつ前の0x00000121が文字数を表していると思われるので、テキストデータのサイズ情報はそれよりも前にあるはず。これくらいの量なら総当たり的に調べることもできそうか。

しかしそもそもこのファイル内で数値情報がどのような形式で書き込まれているのか分からないと探し出すのが難しい。

なのでこれまで出てきた「文字数を表していると思われる情報」についてしっかり考えてみる。

いくつかのテキスト情報を調べて文字数順に並べてみた結果が下記の通り。

文字数? テキスト テキストの文字数
7D Your crew proves too mighty for the hyenas! You take some hyenas to [{GOLD_TEXT}|sell] in London. Punching makes profit! 120
7E [{QUOTE}|You don’t have many traveling stories! That is fine! I hope we meet again when you have seen more of the world!] 121
7F The jungle seems bountiful but is hard to traverse. You could spend an extra [{SUPPLIES}] to [b|explore] this deep jungle. 122
80 01 With the Key of Heart, Rivaleux and his crew receive resistances against [{UNFRIENDLY}] emotions and a boost to [{SPEECH}]! 123
81 01 Long story short: you got attacked by jaguars, and %star% is now afraid of bugs that look for “humid, warm places” at night. 124
82 01 Scientists trust in the scientific method to gain knowledge about the world. Gain extra [{CURIO}] from completing challenges. 125

「FF」ではなく「7F」で切り替わっているのがよく分からん。

下記ページで色々試してみると、「7F」は2進数で「01111111」、「80」は「10000000」なので、1バイトの内7桁で切り替わってる? だとすると8桁目は何だろう?

バイナリデータについて勉強し直したほうが良いかと思い「16進ダンプ」とかをいろいろググってみているときに「符号付き2進数」の情報が目についた。

まさにこれは7桁で数を表し、8桁目で符号を表現している。しかし文字数でマイナスが必要なわけはない…

そもそも「7F」から「80 01」にバイト数が増えるのがおかしい。文字数すら分からない状態で、文字数を表す情報が何バイトか統一されてないなら、その情報はどうやって分かるのだろうか?

そこでふと思いつく。8桁目(1ビット目)を「次のバイトに続くかどうかのフラグ」として使ってるんじゃないか? つまり、数値が格納されてるバイトの1ビット目が1なら次のバイトの情報も読みに行く、という処理を繰り返してるんじゃないか、ということ。 そう考えて変換してみる。

16進数 2進数 各7桁のみ使用 10進数
7D 01111101 1111101 125
7E 01111110 1111110 126
7F 01111111 1111111 127
80 01 10000000 00000001 0000001 0000000 128
81 01 10000001 00000001 0000001 0000001 129
82 01 10000010 00000001 0000001 0000010 130

(゚∀゚)キタコレ!! これ以上なくぴったりはまってる!

もっと文字数の多い箇所の「81 03」でも確かめられたので、たぶんこれが正解だろう…

こんな感じでパズルが完璧に解けるような体験ができるから、データ解析は楽しい。

i18n.luaの解析2

テキストの文字数情報が前述の方法で記載されているとして、テキストデータのサイズ情報を探す。

まずはテキストデータの各種サイズ情報を変換してみて、その結果を検索してみることにする。 やみくもに探すよりも効率的だろう。

16進数 10進数 2進数 変換2進数 変換16進数
テキストデータの先頭 121 289 10 0100001 10100001 00000010 A1 02
テキストデータの末尾 1FD864 2087012 1111111 0110000 1100100 11100100 10110000 01111111 E4 B0 7F
テキストデータサイズ 2087012-288=2086724 1111111 0101110 1000100 11000100 10101110 01111111 C4 AE 7F
ファイルの末尾 1FD88E 2087054 1111111 0110001 0001110 10001110 10110001 01111111 8E B1 7F

これらの数値でi18n.luaファイル内を検索してもヒットせず。

細かい数値の間違いがあるかもしれないので、下の桁だけで検索したところ「B0 7F」がヒットした。

f:id:kengo700:20170815143827p:plain

その前後の数値を見ると、一つ左のバイトの1ビット目が1なので、これも含めた「9B B0 7F」で一つの数値を表している可能性がある。

「9B B0 7F」を変換すると「2086939」になる。

16進数 2進数 各7桁のみ使用 10進数
9B B0 7F 10011011 10110000 01111111 1111111 0110000 0011011 2086939

「9B B0 7F」の「7F」の位置が0x00000072(114)であり、2087054-114=2086940。 1だけずれてるのが気になるが、この部分(0x00000070~0x00000072)がデータサイズを表している可能性が高い。

ゲームデータの改変テスト

前節の考察結果を確認するため、一つのテキストを2文字を減らし、文字数を表してると思われる部分と、総データ数情報を表してると思われる部分も修正してみる。

  • テキスト:「New Game」 → 「New Ge」
  • 文字数情報:0D → 0B
  • データ数情報:9B B0 7F → 99 B0 7F

f:id:kengo700:20170724193011p:plain

リパックしてゲームを実行。

f:id:kengo700:20170724192737p:plain

元の文字数と変えたテキストを表示することに成功!

同じ要領で日本語テキストを表示してみる。

  • テキスト:「New Game」(4E 65 77 20 47 61 6D 65) → 「ニューゲーム」E3 83 8B E3 83 A5 E3 83 BC E3 82 B2 E3 83 BC E3 83 A0)
  • 文字数情報:0D → 17
  • データ数情報:9B B0 7F → A5 B0 7F

f:id:kengo700:20170724194532p:plain

f:id:kengo700:20170724194205p:plain

リパックしてゲームを実行。

f:id:kengo700:20170724203047p:plain

これは… 勝ち申した…!

あとはこれを自動で編集するプログラムを作って、そんでひたすら翻訳するだけだ。

i18n.luaの編集プログラムの作成

7/25~

C#でのバイナリデータのファイル入出力について調べる。 前も調べたけど、頻繁に使うものじゃないとすぐに忘れてしまうな…

「バイト型配列」なんてのがあるのか。まあそりゃあるだろうけど、バイナリデータを扱うことは少ないから知らんかった(単に忘れてただけかもしれない…)。

あんまり情報が出てこないけど、まあ普通の配列と同じ感じで使えるんかな?

7/26~

まずはi18n.luaを読み込んで表示するだけのプログラム。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(
                @"..\..\..\i18n.lua",
                System.IO.FileMode.Open,
                System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            for(int i=0; i< bs.Length; i++)
            {

                // バイト型配列の内容を表示
                if (i % 32 == 0) Console.WriteLine();
                Console.Write("{0:X2} ", bs[i]);

            }

        }
    }
}

f:id:kengo700:20170729201053p:plain

問題なく読み込み&表示できた。

あとプログラミング中に思い出したけど、「byte」型はただのサイズが小さい整数型だった。昨日はバイト型配列特有の処理が必要かと身構えていたけど、単に整数の処理なだけだった。

次に、データ数情報の部分を読み込み、2進数で表示してみる。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {
            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(
                @"..\..\..\i18n.lua",
                System.IO.FileMode.Open,
                System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            for(int i=0; i< bs.Length; i++)
            {

                if (i == address_size || i == address_size + 1 || i == address_size + 2)
                {
                    Console.Write("{0:X2} ", bs[i]);

                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[i] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    foreach (int bit in bits)
                    {
                        Console.Write("{0}", bit);
                    }

                    Console.WriteLine();

                }
            }

        }
    }
}

f:id:kengo700:20170729201150p:plain

問題なさげ。BitArrayからint配列への変換部分とかよく理解してないけど、まあ大丈夫じゃろ。

次に、このデータ数情報の部分を、前述の方式で整数に変換する。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {
            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(
                @"..\..\..\i18n.lua",
                System.IO.FileMode.Open,
                System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            // i18n.lua内のテキストサイズを表す数値を16進数から整数に変換する
            int num_byte = CountByte(bs, (int)address_size);
            int size = Byte2Int(bs, (int)address_size, num_byte);

            Console.WriteLine("{0}", size);

        }

        static int CountByte(byte[] bs, int address)
        {
            int num_byte = 0;
            for (int j = 0; ; j++)
            {
                // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                BitArray b = new BitArray(new byte[] { bs[address + j] });
                int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                Array.Reverse(bits);

                if (bits[0] == 1) num_byte++;
                else break;

            }
            return num_byte;
        }

        static int Byte2Int(byte[] bs, int address, int num_byte)
        {
            int[] result_bits = new int[7];
            for (int j = 0; j <= num_byte; j++)
            {
                if (j == 0)
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    for (int k = 0; k < 7; k++)
                    {
                        result_bits[k] = bits[k + 1];
                    }
                }
                else
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    int[] temp_bits = new int[7];
                    for (int k = 0; k < 7; k++)
                    {
                        temp_bits[k] = bits[k + 1];
                    }

                    // https://dobon.net/vb/dotnet/programing/arraymerge.html
                    result_bits = temp_bits.Concat(result_bits).ToArray();

                }

            }

            foreach (int bit in result_bits)
            {
                Console.Write("{0}", bit);
            }
            Console.WriteLine();

            return Convert.ToInt32(String.Join("", result_bits), 2);

        }

    }
}

f:id:kengo700:20170729201233p:plain

ちゃんと「9B B0 7F」を「2086939」に変換できた。この変換はID・テキストの読み込み部分でも使うので、関数にまとめてみた。関数名の適当さはつっこんではいけない。どうせ使い捨てのプログラムなので、変数名や関数名をちゃんと考えるのがめんどい…

次に、i18n.luaのIDとテキストのデータを読み込む。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {
            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
            long address_text_start = Convert.ToInt64("0x121", 16); // i18n.lua内のID・テキストデータの先頭位置
            //long address_text_end = Convert.ToInt64("0x1FD864", 16); // i18n.lua内のID・テキストデータの末尾位置
            long address_text_end = 1000;

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(
                @"..\..\..\i18n.lua",
                System.IO.FileMode.Open,
                System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            // i18n.lua内のテキストサイズを表す数値を16進数から整数に変換する
            int num_byte = CountByte(bs, (int)address_size);
            int size = Byte2Int(bs, (int)address_size, num_byte);

            Console.WriteLine("total text size: {0}", size);

            List<string> IDText = new List<string>();

            for(int i= (int)address_text_start; i< (int)address_text_end; i++)
            {
                int num_byte_text = CountByte(bs, i);
                int size_text = Byte2Int(bs, i, num_byte_text);
                size_text -= 5;

                Console.WriteLine("text({0}) size: {1} ",i, size_text);

                byte[] byte_text = new byte[size_text];

                for (int j=0; j < size_text; j++)
                {
                    byte_text[j] = bs[i + num_byte_text + 1 + j];
                }

                Console.WriteLine("{0}", Encoding.UTF8.GetString(byte_text));

                i += num_byte_text + size_text;

            }

        }

        static int CountByte(byte[] bs, int address)
        {
            int num_byte = 0;
            for (int j = 0; ; j++)
            {
                // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                BitArray b = new BitArray(new byte[] { bs[address + j] });
                int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                Array.Reverse(bits);

                if (bits[0] == 1) num_byte++;
                else break;

            }
            return num_byte;
        }

        static int Byte2Int(byte[] bs, int address, int num_byte)
        {
            int[] result_bits = new int[7];
            for (int j = 0; j <= num_byte; j++)
            {
                if (j == 0)
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    for (int k = 0; k < 7; k++)
                    {
                        result_bits[k] = bits[k + 1];
                    }
                }
                else
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    int[] temp_bits = new int[7];
                    for (int k = 0; k < 7; k++)
                    {
                        temp_bits[k] = bits[k + 1];
                    }

                    // https://dobon.net/vb/dotnet/programing/arraymerge.html
                    result_bits = temp_bits.Concat(result_bits).ToArray();

                }

            }

            return Convert.ToInt32(String.Join("", result_bits), 2);

        }

    }
}

address_text_endを1000にして実行したところ、うまくいってそう。

f:id:kengo700:20170729201326p:plain

しかし実際の値(0x1FD864)にしたところ、エラー終了した。

f:id:kengo700:20170729201418p:plain

見たまんま、サイズがマイナスになってる。そういえば確かに文字数との比較で-5を入れていたけれど、これでは場合によってマイナスになってしまうのは当たり前だ。変換した整数が小さい場合のことをちゃんと考えてなかった。

いろいろと試した結果、下記のようなコードに。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {
            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
            long address_text_start = Convert.ToInt64("0x121", 16); // i18n.lua内のID・テキストデータの先頭位置
            long address_text_end = Convert.ToInt64("0x1FD864", 16); // i18n.lua内のID・テキストデータの末尾位置

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(
                @"..\..\..\i18n.lua",
                System.IO.FileMode.Open,
                System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            // i18n.lua内のテキストサイズを表す数値を16進数から整数に変換する
            int num_byte = CountByte(bs, (int)address_size);
            int size = Byte2Int(bs, (int)address_size, num_byte);

            Console.WriteLine("total text size: {0}", size);

            List<string> IDText = new List<string>();

            for (int i= (int)address_text_start; i< (int)address_text_end; i++)
            {
                int num_byte_text = CountByte(bs, i);
                int size_text = Byte2Int(bs, i, num_byte_text);

                if (size_text >= 5) size_text -= 5;

                //Console.WriteLine("text({0}) size: {1} ", i, size_text);

                byte[] byte_text = new byte[size_text];

                for (int j = 0; j < size_text; j++)
                {
                    byte_text[j] = bs[i + num_byte_text + 1 + j];
                }

                // 「richTextMacros」だけはIDでもテキストでもないっぽいので例外的に処理する(その後の4バイト分も加えてしまう)
                if (Encoding.UTF8.GetString(byte_text) == "richTextMacros")
                {
                    size_text += 4;
                    byte[] byte_text2 = new byte[size_text];
                    for (int j = 0; j < size_text; j++)
                    {
                        byte_text2[j] = bs[i + num_byte_text + 1 + j];
                    }

                    //Console.WriteLine("{0}", Encoding.UTF8.GetString(byte_text2));

                    IDText.Add(Encoding.UTF8.GetString(byte_text2));
                    IDText.Add("");

                    i += num_byte_text + size_text;

                }
                else {

                    //Console.WriteLine("{0}", Encoding.UTF8.GetString(byte_text));

                    IDText.Add(Encoding.UTF8.GetString(byte_text));

                    i += num_byte_text + size_text;

                }

            }

            // 出力用ファイルを開く               
            using (StreamWriter writer = new StreamWriter(@"..\..\..\text.csv", false, Encoding.UTF8))
            {

                // 見出し行を出力
                writer.WriteLine("\"ID\",\"Text\"");

                // データを書き込み
                for (int i = 0; i < IDText.Count(); i++)
                {
                    if (i % 2 == 0 && i + 1 < IDText.Count()) {
                        string temp_ID = IDText[i].Replace("\"", "\"\"");
                        string temp_Text = IDText[i+1].Replace("\"", "\"\"");

                        writer.WriteLine("\"{0}\",\"{1}\"", temp_ID, temp_Text);
                    }
                }

            }

        }

        static int CountByte(byte[] bs, int address)
        {
            int num_byte = 0;
            for (int j = 0; ; j++)
            {
                // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                BitArray b = new BitArray(new byte[] { bs[address + j] });
                int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                Array.Reverse(bits);

                if (bits[0] == 1) num_byte++;
                else break;

            }
            return num_byte;
        }

        static int Byte2Int(byte[] bs, int address, int num_byte)
        {
            int[] result_bits = new int[7];
            for (int j = 0; j <= num_byte; j++)
            {
                if (j == 0)
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    for (int k = 0; k < 7; k++)
                    {
                        result_bits[k] = bits[k + 1];
                    }
                }
                else
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    int[] temp_bits = new int[7];
                    for (int k = 0; k < 7; k++)
                    {
                        temp_bits[k] = bits[k + 1];
                    }

                    // https://dobon.net/vb/dotnet/programing/arraymerge.html
                    result_bits = temp_bits.Concat(result_bits).ToArray();

                }

            }

            return Convert.ToInt32(String.Join("", result_bits), 2);

        }

    }
}

文字数がマイナスにならないようにチェックしているのと、エラーが出ていた「richTextMacros」についてはIDでもテキストでもないっぽいので例外処理している。

f:id:kengo700:20170805184028p:plain

出力したファイルをEXCELで開くと、ぱっと見は大丈夫っぽい。しかし掲示板で配布されていたファイルは17898行、今回出力したファイルは20930行になっているのが気になる。 ちゃんと確認する必要がありそう。

7/27~

ちょっとOneShotのベータテストに浮気…

7/30~

ちょっとBTTFのゲームの翻訳に浮気…

i18n.luaの編集プログラムの作成2

8/5~

BTTFの翻訳が一息ついたので、復帰。

先日作成したプログラムの出力を、掲示板で配布されていたものと比較する。

各ファイルをEXCELで開き、ソートしたものをテキストファイルにコピペし、WimMergeで比較してみる。

f:id:kengo700:20170805190911p:plain

ざっと見た感じは、テキスト抽出のミスというより、ゲームのアプデによってテキストが追加・修正された結果のように見える。

とりあえずはこのまま進めて、問題が出たら詳しくチェックすることにしよう。 あと、発売からずいぶん経っているのでそうそうアプデはないだろうけど、一応SteamストアのニュースのRSSを個人Slackに登録して、アプデがあれば気付けるようにしとく。

プログラムの作成を進める前に、前につくったプログラムを確認。一週間ちょいしかたってないのに、もう内容が分からねえ… ブログに書いといてよかった。まあホントはGitで管理するべきだけど、Visual StudioでGitを使う方法を調べるのがめんどい… いい加減調べないと…

次は、i18n.luaを翻訳データで書き換える時に必要になる、「任意の整数が与えられたときに、前述の方法で特殊な16進数に変換するプログラム」を作る。 例えば「2086939」を「9B B0 7F」に変換する。

バイト型やBitArrayについて復習しつつ、作成するものの、うまく動かない。

8/6~

しっかりと寝てから昨日うまく動かなかったプログラムを見たら、ミスが一瞬で分かった。やっぱり昨日はだいぶ疲れてたらしい。

作成したプログラムは下記の通り。変数名がだいぶ適当なのは、見て見ぬふりをしよう…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {

            int temp_num = 2086939;
            Console.WriteLine("{0}", temp_num);
            byte[] bytes1 = new byte[CountInt(temp_num)];
            bytes1 = Int2Byte(temp_num);

            for(int i =0; i< bytes1.Length; i++) Console.Write("{0:X2} ", bytes1[i]);
            Console.WriteLine("");
            Console.WriteLine("");

            temp_num = 127;
            Console.WriteLine("{0}", temp_num);
            byte[] bytes2 = new byte[CountInt(temp_num)];
            bytes2 = Int2Byte(temp_num);

            for (int i = 0; i < bytes2.Length; i++) Console.Write("{0:X2} ", bytes2[i]);
            Console.WriteLine("");
            Console.WriteLine("");

            temp_num = 128;
            Console.WriteLine("{0}", temp_num);
            byte[] bytes3 = new byte[CountInt(temp_num)];
            bytes3 = Int2Byte(temp_num);

            for (int i = 0; i < bytes3.Length; i++) Console.Write("{0:X2} ", bytes3[i]);
            Console.WriteLine("");
            Console.WriteLine("");

            temp_num = 289;
            Console.WriteLine("{0}", temp_num);
            byte[] bytes4 = new byte[CountInt(temp_num)];
            bytes4 = Int2Byte(temp_num);

            for (int i = 0; i < bytes4.Length; i++) Console.Write("{0:X2} ", bytes4[i]);
            Console.WriteLine("");
            Console.WriteLine("");

            temp_num = 2147483647;
            Console.WriteLine("{0}", temp_num);
            byte[] bytes5 = new byte[CountInt(temp_num)];
            bytes5 = Int2Byte(temp_num);

            for (int i = 0; i < bytes5.Length; i++) Console.Write("{0:X2} ", bytes5[i]);
            Console.WriteLine("");

        }

        static int CountInt(int num)
        {
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            for (int i = 0; i < bits.Length; i++)
            {
                if (bits[i] == 1)
                {
                    double count = Math.Ceiling(1.0 * (32 - i) / 7);
                    return (int)count;
                }
            }

            return 0;

        }

        static byte[] Int2Byte(int num)
        {
            // 整数を2進数に変換、num -> bits
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            // 2進数で表示
            for (int i = 0; i < bits.Length; i++)
            {
                Console.Write("{0} ", bits[i]);
            }
            Console.WriteLine("");

            int count = CountInt(num);
            
            byte[] bytes = new byte[count];
            byte[] temp_byte = new byte[1];

            for (int i = 0; i < count; i++)
            {
                // 右から7桁ずつBitArrayにコピー
                BitArray bit = new BitArray(8);
                for(int j = 0; j < 7; j++)
                {
                    if (31 - i * 7 - j < 0) break;
                    if (bits[31 - i * 7 - j] == 1) bit[j] = true;
                    else bit[j] = false;
                }

                // 一番大きな桁に、数値情報が次に続くかの情報を入れる
                if (i + 1 < count) bit[7] = true;
                else bit[7] = false;

                // BitArrayからバイト型配列に変換
                // https://stackoverflow.com/questions/560123/convert-from-bitarray-to-byte
                bit.CopyTo(temp_byte, 0);
                bytes[i] = temp_byte[0];

                // 2進数で表示
                for (int k = 0; k < 8; k++)
                {
                    if (bit[7-k]) Console.Write("1 ");
                    else Console.Write("0 ");
                }
                Console.WriteLine("");

            }

            return bytes;
        }

    }
}

結果の出力は下記の通り。まあ大丈夫っぽい。

f:id:kengo700:20170806112535p:plain

これで必要な部品はだいたいそろったかな。

翻訳作業の準備

次は実際にi18n.luaを書き換えるプログラムを作る前に、そのプログラムに読み込ませる翻訳ファイル(仮)を作るために、作業所を作ろう。

Googleスプレッドシートで、昨日作成したcsvファイルを読み込み、訳文の列を追加。最初は原文データを入れておく。

f:id:kengo700:20170806120028p:plain

翻訳は一人で進めるつもりなので、別にEXCELで進めてもいいのだけれど、Googleスプレッドシートの方が使い慣れているのと、バックアップを取るのが簡単なので、今回もスプレッドシートで作業することにする。

「形式を指定してダウンロード」でcsv形式でダウンロードし、EXCELで開いてみると、形式がおかしいという警告がでるものの、ぱっと見は問題なさそう。

f:id:kengo700:20170806120533p:plain

実際は記号の処理とか改行の処理を工夫しなくてはならないかもしれないけれど、これも問題が出てから対処することにしよう。

今後はプログラムの作成と共に、翻訳作業も少しずつ進めていく。

8/7~

ふたたびBTTFのゲームの翻訳に浮気。

やっぱり進捗を公表しながら進めると、モチベーションにつながるようだ。 RE:ISの翻訳が完遂できるのか心配になってきた…

8/11~

Dead Cellsの日本語(かな)MODの作成に浮気。

翻訳作業の準備2

8/12~

気分転換? にRE:ISをプレイ開始。 とりあえずチュートリアル終わりまで。 怒涛の説明ラッシュで大変だ。ちびちび進めて行こう。

f:id:kengo700:20170812213145p:plain

ちょいちょい分からない単語を調べつつなら理解できるので、だいぶ難しくない英文だとは思うけど、だったら最初から翻訳しながら進めたいところではある。

…と思って気付いたけど、すでに作業所は作ってあるんだった。翻訳しながらちまちま進めてこう。

f:id:kengo700:20170812213259p:plain

ゲーム中で「(アイコン)Encounter」となっているところが、テキストでは「[{ENCOUNTER}]」となっているのは、どうしたらいいんじゃろうか。 最悪の場合はアイコンなしになるかも…

…と思ったけど、別の場所で、アイコンと色と文字が定義されてるっぽい。

f:id:kengo700:20170812213725p:plain

ついでに用語の訳をまとめるページを作っとく。あんまりこだわると進まなくなるけど、「この単語はどう訳したっけ?」という時に見れると楽なので。

f:id:kengo700:20170812215157p:plain

原文ソートを利用して、「[{FRIENDLY}]」のような明らかに翻訳する必要がない部分をざっくり翻訳済みにしたところ、4.17%まで進んだ。

f:id:kengo700:20170813005748p:plain

i18n.luaの編集プログラムの作成3

作業所からダウンロードすることになるCSV形式のファイルを読み込む部分を作成する。

問題はテキストデータ内に改行が含まれることで、読み込みプログラムを自作するのは大変。

調べたところ「CsvHelper」というライブラリが使えそう。

8/13~

上記ページを参考に、csvファイルを読み込むプログラムを作成。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;
using CsvHelper;
using CsvHelper.Configuration;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {

            string path = @"..\..\..\Renowned Explorers- International Society 日本語化作業所 - text.csv";

            CsvParser parser = new CsvParser(new StreamReader(path));
            parser.Configuration.HasHeaderRecord = true;  // ヘッダ行あり
            parser.Configuration.RegisterClassMap<MyClassMap>();

            CsvReader reader = new CsvReader(parser);
            List<MyClass> texts = reader.GetRecords<MyClass>().ToList();

            foreach(MyClass text in texts)
            {
                Console.WriteLine("ID:{0}", text.Id);
                Console.WriteLine("Original:{0}", text.Original);
                Console.WriteLine("Translated:{0}", text.Translated);
                Console.WriteLine();
            }
        }
    }

    class MyClass
    {
        public string Id { get; set; }
        public string Original { get; set; }
        public string Translated { get; set; }
    }

    class MyClassMap : CsvClassMap<MyClass>
    {
        public MyClassMap()
        {
            Map(m => m.Id).Index(0);
            Map(m => m.Original).Index(1);
            Map(m => m.Translated).Index(2);
        }
    }

}

f:id:kengo700:20170813163020p:plain

正直、マッピングの部分とかはよく分かってないけど、まあ大丈夫じゃろ。

改行の処理を確認するため、下図のスプレッドシートから出力したcsvファイルを読み込んで、バイト型配列に変換して表示してみる。

f:id:kengo700:20170813181251p:plain

            foreach(MyClass text in texts)
            {
                Console.WriteLine("ID:{0}", text.Id);
                byte[] byte_id = Encoding.UTF8.GetBytes(text.Id);
                Console.Write("{0} :", byte_id.Length);
                for (int i = 0; i < byte_id.Length; i++)
                    Console.Write("{0:X2} ", byte_id[i]);
                Console.WriteLine();

                Console.WriteLine("Original:{0}", text.Original);
                byte[] byte_original = Encoding.UTF8.GetBytes(text.Original);
                Console.Write("{0} :", byte_original.Length);
                for (int i = 0; i < byte_original.Length; i++)
                    Console.Write("{0:X2} ", byte_original[i]);
                Console.WriteLine();

                Console.WriteLine("Translated:{0}", text.Translated);
                byte[] byte_translated = Encoding.UTF8.GetBytes(text.Translated);
                Console.Write("{0} :", byte_translated.Length);
                for (int i = 0; i < byte_translated.Length; i++)
                    Console.Write("{0:X2} ", byte_translated[i]);
                Console.WriteLine();

                Console.WriteLine();

                //count++;
                //if (count > 5) break;
            }

f:id:kengo700:20170813181310p:plain

スプレッドシート上の改行が、ちゃんと「0A」に変換されているので、追加で処理する必要はなさそう。

i18n.luaの編集プログラムの作成4

i18n.luaに翻訳データを埋め込むプログラムを作成する。

8/14~

ひとまずこんな感じのプログラムに。 こんだけなのに、えらく時間かかってしまった…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections;
using CsvHelper;
using CsvHelper.Configuration;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {

            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
            long address_text_start = Convert.ToInt64("0x121", 16); // i18n.lua内のID・テキストデータの先頭位置
            long address_text_end = Convert.ToInt64("0x1FD864", 16); // i18n.lua内のID・テキストデータの末尾位置

            
            //string path = @"..\..\..\Renowned Explorers- International Society 日本語化作業所 - text.csv";
            string path_csv = @"..\..\..\csvtest2.csv";
            string path_en_lua = @"..\..\..\i18n.lua";
            string path_ja_lua = @"..\..\..\ja_i18n.lua";



            // luaファイル読み込み

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(path_en_lua, System.IO.FileMode.Open, System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            // i18n.lua内のテキストサイズを表す数値を表す部分のバイト数
            int size_length = CountByte(bs, (int)address_size);

            // i18n.lua内のテキストサイズを表す数値
            int size = Byte2Int(bs, (int)address_size, size_length);

            // ファイルの先頭部分を保存する配列を作成
            byte[] original_header1 = new byte[address_size];
            byte[] original_header2 = new byte[address_text_start - address_size - size_length];

            // ファイルの末尾部分を保存する配列を作成
            byte[] original_footer = new byte[bs.Length - address_text_end - 1];

            // ファイルの先頭部分を読み出し
            for (int i = 0; i < original_header1.Length; i++)
            {
                original_header1[i] = bs[i];
            }
            for (int i = 0; i < original_header2.Length; i++)
            {
                original_header2[i] = bs[address_size + size_length + i];
            }

            // ファイルの末尾部分を読み出し
            for (int i = 0; i < original_footer.Length; i++)
            {
                original_footer[i] = bs[address_text_end + 1 + i];
            }



            // テキスト部分を読み出し

            List<string> en_texts = new List<string>();
            List<int> en_texts_length = new List<int>();

            for (int i = (int)address_text_start; i < (int)address_text_end; i++)
            {

                int byte_text_length = CountByte(bs, i);
                int text_length = Byte2Int(bs, i, byte_text_length);

                if (text_length >= 5) text_length -= 5;
                en_texts_length.Add(text_length);
                
                byte[] byte_text = new byte[text_length];

                for (int j = 0; j < text_length; j++)
                {
                    byte_text[j] = bs[i + byte_text_length + j];
                }

                // 「richTextMacros」だけはIDでもテキストでもないっぽいので例外的に処理する(その後の4バイト分も加えてしまう)
                if (Encoding.UTF8.GetString(byte_text) == "richTextMacros")
                {
                    text_length += 4;
                    byte[] byte_text2 = new byte[text_length];
                    for (int j = 0; j < text_length; j++)
                    {
                        byte_text2[j] = bs[i + byte_text_length + j];
                    }
                    
                    en_texts.Add(Encoding.UTF8.GetString(byte_text2));
                    en_texts.Add("");
                    en_texts_length.Add(0);

                    i += byte_text_length - 1 + text_length;

                }
                else
                {
                    en_texts.Add(Encoding.UTF8.GetString(byte_text));

                    i += byte_text_length - 1 + text_length;

                }

            }


            
            // 翻訳データを読み込む

            CsvParser parser = new CsvParser(new StreamReader(path_csv));
            parser.Configuration.HasHeaderRecord = true;  // ヘッダ行あり
            parser.Configuration.RegisterClassMap<MyClassMap>();

            CsvReader reader = new CsvReader(parser);
            List<MyClass> ja_texts = reader.GetRecords<MyClass>().ToList();
            


            // テキスト部分を翻訳データに差し替えたデータを生成

            int size_change = 0;
            List<byte> output_texts = new List<byte>();

            for (int i = 0; i < en_texts.Count(); i++)
            {

                // IDのデータ:そのまま出力

                byte[] temp_text = System.Text.Encoding.UTF8.GetBytes(en_texts[i]);
                byte[] temp_size = Int2Byte(en_texts_length[i] + 5);

                // 「richTextMacros」の部分だけは例外処理、手動で元のデータを入れる
                if (en_texts[i].Contains("richTextMacros"))
                {
                    output_texts.AddRange(temp_size);
                    byte[] temp_text2 = { 0x72, 0x69, 0x63, 0x68, 0x54, 0x65, 0x78, 0x74, 0x4D, 0x61, 0x63, 0x72, 0x6F, 0x73, 0x01, 0x00, 0xF5, 0x04 };
                    output_texts.AddRange(temp_text2);
                }
                else
                {
                    output_texts.AddRange(temp_size);
                    output_texts.AddRange(temp_text);
                }

                i++;


                // テキストのデータ:翻訳データに差し替えて出力

                string output_text = en_texts[i];
                int output_text_length = en_texts_length[i];

                for (int j= 0; j<ja_texts.Count(); j++)
                {
                    if(en_texts[i] == ja_texts[j].Original && ja_texts[j].isUsed == false)
                    {
                        output_text = ja_texts[j].Translated;
                        output_text_length = System.Text.Encoding.UTF8.GetBytes(output_text).Count();
                        size_change = size_change + (output_text_length - en_texts_length[i]);

                        ja_texts[j].isUsed = true;

                        break;
                    }
                }

                byte[] temp_text3 = System.Text.Encoding.UTF8.GetBytes(output_text);
                byte[] temp_size3 = Int2Byte(output_text_length + 5);

                // 「richTextMacros」の部分だけは例外処理
                if (en_texts[i-1].Contains("richTextMacros")==false)
                {
                    output_texts.AddRange(temp_size3);
                    output_texts.AddRange(temp_text3);
                }

            }

            // サイズデータを更新
            int output_size = size + size_change;

            byte[] b_output_size = Int2Byte(output_size);
            byte[] b_output_texts = output_texts.ToArray();



            // ファイルに出力

            fs = new FileStream(path_ja_lua, FileMode.Create);

            fs.Write(original_header1, 0, original_header1.Length);
            fs.Write(b_output_size, 0, b_output_size.Length);
            fs.Write(original_header2, 0, original_header2.Length);
            fs.Write(b_output_texts, 0, b_output_texts.Length);
            fs.Write(original_footer, 0, original_footer.Length);

            fs.Close();

        }

        static int CountInt(int num)
        {
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            for (int i = 0; i < bits.Length; i++)
            {
                if (bits[i] == 1)
                {
                    double count = Math.Ceiling(1.0 * (32 - i) / 7);
                    return (int)count;
                }
            }

            return 0;

        }

        static byte[] Int2Byte(int num)
        {
            // 整数を2進数に変換、num -> bits
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            int count = CountInt(num);
            
            byte[] bytes = new byte[count];
            byte[] temp_byte = new byte[1];

            for (int i = 0; i < count; i++)
            {
                // 右から7桁ずつBitArrayにコピー
                BitArray bit = new BitArray(8);
                for(int j = 0; j < 7; j++)
                {
                    if (31 - i * 7 - j < 0) break;
                    if (bits[31 - i * 7 - j] == 1) bit[j] = true;
                    else bit[j] = false;
                }

                // 一番大きな桁に、数値情報が次に続くかの情報を入れる
                if (i + 1 < count) bit[7] = true;
                else bit[7] = false;

                // BitArrayからバイト型配列に変換
                // https://stackoverflow.com/questions/560123/convert-from-bitarray-to-byte
                bit.CopyTo(temp_byte, 0);
                bytes[i] = temp_byte[0];

            }

            return bytes;
        }

        static int CountByte(byte[] bs, int address)
        {
            int num_byte = 1;
            for (int j = 0; ; j++)
            {
                // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                BitArray b = new BitArray(new byte[] { bs[address + j] });
                int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                Array.Reverse(bits);

                if (bits[0] == 1) num_byte++;
                else break;

            }
            return num_byte;
        }

        static int Byte2Int(byte[] bs, int address, int num_byte)
        {
            int[] result_bits = new int[7];
            for (int j = 0; j < num_byte; j++)
            {
                if (j == 0)
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    for (int k = 0; k < 7; k++)
                    {
                        result_bits[k] = bits[k + 1];
                    }
                }
                else
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    int[] temp_bits = new int[7];
                    for (int k = 0; k < 7; k++)
                    {
                        temp_bits[k] = bits[k + 1];
                    }

                    // https://dobon.net/vb/dotnet/programing/arraymerge.html
                    result_bits = temp_bits.Concat(result_bits).ToArray();

                }

            }

            return Convert.ToInt32(String.Join("", result_bits), 2);

        }

    }

    class MyClass
    {
        public string Id { get; set; }
        public string Original { get; set; }
        public string Translated { get; set; }
        public bool isUsed { get; set; } = false;
    }

    class MyClassMap : CsvClassMap<MyClass>
    {
        public MyClassMap()
        {
            Map(m => m.Id).Index(0);
            Map(m => m.Original).Index(1);
            Map(m => m.Translated).Index(2);
        }
    }

}

動作確認のため、メインメニューの文字だけのスプレッドシートから出力したcsvファイルを使ってみる。

f:id:kengo700:20170814120726p:plain

このデータでi18n.luaファイルを修正し、リパックしてゲームを起動してみる。

f:id:kengo700:20170814173948p:plain

やったぜ!

今の6.7%まで進んだ作業所のデータを使ってみる。

f:id:kengo700:20170814174116p:plain

f:id:kengo700:20170814183315p:plain

エラーが出た… ぐぬぬ…

スプレッドシートをコピーし、原文の列を訳文の列にコピペしたデータを使ってi18n.luaを作ってみる。

f:id:kengo700:20170814184009p:plain

Stirlingの機能を使って元のi18n.luaと比較してみると、「違いはない」と表示されたので、スプレッドシートから出力したcsvファイルを使っていることに問題はなさそう。

すぐに確認しやすい、チュートリアルの最初の文章だけ翻訳したものでi18n.luaを作ってみる。

f:id:kengo700:20170814184307p:plain

f:id:kengo700:20170814193948p:plain

普通に表示できた。「メインメニューは日本語化できるけど、他の部分はできない」ということはなさそうだ。

次に、「FRIENDLY_TEXT_ONLY」というIDの「Friendly」だけ翻訳したものでi18n.luaを作ってみる。

f:id:kengo700:20170814194359p:plain

リパックにすごい時間かかるのがめんどい…

f:id:kengo700:20170814203227p:plain

変わらない…?

ファイルを見てみたら、同じテキストの「PLAYSTYLE_REQUIREMENT_REWARDS_FRIENDLY」の方が「友好的」に変換されてた。 今のところはIDのチェックなどはせずに、上から順番にマッチさせてるので、いたってプログラム通り。

今後は両方とも翻訳したものでi18n.luaを作ってみる。

f:id:kengo700:20170814203354p:plain

しかし変換されない…

f:id:kengo700:20170814204706p:plain

変換部分のデータを表示してみると、同じデータが2回使われている…

f:id:kengo700:20170814204742p:plain

つまり単純なプログラムのミス。breakが必要だった(間違いを防ぐため、前述のコードは修正後のもの)。

                for (int j= 0; j<ja_texts.Count(); j++)
                {
                    if(en_texts[i] == ja_texts[j].Original && ja_texts[j].isUsed == false)
                    {
                        output_text = ja_texts[j].Translated;
                        output_text_length = System.Text.Encoding.UTF8.GetBytes(output_text).Count();
                        size_change = size_change + (output_text_length - en_texts_length[i]);

                        ja_texts[j].isUsed = true;

                        break; // ←これ!
                    }
                }

f:id:kengo700:20170814205030p:plain

ちゃんと書き換わった。このミスがエラーの原因かは微妙なので、同じ検証を続ける。

「FRIENDLY_TEXT_ONLY」と「PLAYSTYLE_REQUIREMENT_REWARDS_FRIENDLY」というIDの「Friendly」だけ翻訳したものでi18n.luaを作ってみる。

f:id:kengo700:20170814205359p:plain

f:id:kengo700:20170814214009p:plain

Friendlyも日本語化できた! これで「中カッコで囲まれてる太字や色文字の部分は日本語化できない」ということもなさそうか。

エラーの原因がプログラムのミスの可能性が高まってきたので、もう一度作業所のデータでi18n.luaを作ってみる。 まあこれは、「プログラムのミスがエラーの原因だと考えられる」という予想ではなくて、「プログラムのミスがエラーの原因だと楽だなあ」という願望だけど…

f:id:kengo700:20170814222716p:plain

やったぜ! 普通に起動できたぜ!

f:id:kengo700:20170814224503p:plain

f:id:kengo700:20170814224516p:plain

当然ながら日本語対応の改行処理は入ってないので、適宜スペースを入れる必要がある。 前に聞いたことがある「ゼロ幅スペース」とやらを、そのうち試してみよう。

あとは、とりあえずひたすら翻訳するだけだ!

8/15~

なんか恥ずかしいセリフを吐いてる奴がいるぞ?

……私だ!

昨日は日本語化の目途が立ったことで舞い上がってた模様。恥ずい…

フォントの選定

そういえば、日本語フォントをとりあえずで全部同じものを入れていたので、太字も普通の字も同じに表示されてしまって、プレイ中ちょっとわかりにくい。

もともと使われているフォントをエクスプローラのサムネで見ると、arialとdefaultは同じものっぽく、結局2種類のフォントしか使われていないっぽいか。

f:id:kengo700:20170815104135p:plain

ゲーム中で見ると、ほとんどの部分ではサンセリフ体が使われていて、「The Saxon Kings」みたいな部分にセリフ体が使われている。それほど特徴のあるフォントでもないので、それぞれゴシック体と明朝体の無難な無難なフォントを使えばいいかな。

f:id:kengo700:20170815104315p:plain

まあ、とりあえずはいつも通り「Noto Font」でいいか。癖がなくて読みやすいし。

サムネ画像で比較した限りでは、文字の太さは下記のような感じだと思う。イタリックはフォントがなさそうなので、そのままで。

  • default → NotoSansCJKjp-Light
  • defaultb → NotoSansCJKjp-Medium
  • header → NotoSerifCJKjp-SemiBold
  • headerb → NotoSerifCJKjp-Black

それぞれ「WOFFコンバータ」でotf→woff→ttf形式に変換。 リパックしてゲーム起動。

f:id:kengo700:20170815122628p:plain

うむ。なかなか元の雰囲気に近い気がする。日本語にすると、ちょっと細過ぎる気もするけど、まあフォントの変更は後からでもできるので、とりあえずはこれで行こう。

f:id:kengo700:20170815122829p:plain

作業所の整備

一人で翻訳してサプライズ的に公開しようかと思ってたけど、バレテルー! (ノ∀`)

まあ、Steamのアカウントは非公開にしてないですし、最初からバレバレではあったのだけれど…

やっぱり2万行のテキストを一人で訳しきるのは難しいし、非公開で進めてる途中で私が死んだら全部無駄になってしまうし、いいかげんDivinity: Original Sin 2の翻訳を進めないといつまでたっても終わらないというのもあるので、公開して有志翻訳者を募ることにしよう。

Divinity: Original Sin 2の有志翻訳で折れた心を癒すための息抜き目的のRE:IS有志翻訳なのに、息抜きに熱中してしまうことになりかねない…(すでにそうなってる…)

まあどっちにしても、大半は自分で訳すことになるのだろうけど…

とりあえず、他の人が参加しやすいように作業所に説明ページを追加。

f:id:kengo700:20170815132312p:plain

次に自動バックアップを設定する。

作業所の「ツール」メニューの「スクリプトエディタ」を開き、下記を入力。

function Backup() {  
  var file = DriveApp.getFileById('「URLの英数字の文字列の部分」');//バックアップしたいファイル
  var folder = DriveApp.getFolderById('「URLの英数字の文字列の部分」');  // ここで指定したフォルダにファイルが入る
  file.makeCopy(file.getName()+'-'+Utilities.formatDate(new Date(), 'JST', 'yyyy-MM-dd-HH'),folder);   
}

実行してみたところエラーが出た。 どうやら承認システムが変わったらしく、下記ページの方法で設定。

時計アイコンからバックアップを毎日実行するように設定。

f:id:kengo700:20170815133239p:plain

作業所の共有設定を「リンクを知っている全員が編集可」に設定。

翻訳作業の準備3

あまりテキスト量が多すぎると早々に心が折れることは、Divinity: Original Sin 2の有志翻訳で嫌と言うほど学んでいる(現在進行形)ので、進捗グラフを表示するようにして、「このペースで進んだら、何日頃に完了する」というのが見れるようにする。

自動的に更新出来たら楽だろうけど、とりあえずは手動で情報を書き込んでいくことにする。

f:id:kengo700:20170815141536p:plain

どれくらいで終わるだろうか… できれば一年以内に終わらせたいところ…

翻訳作業

さて… それでは気合を入れて… 艦これのイベントを進めるか!

8/16~

現状の翻訳データ(進捗11.32%)を使ってリパックしてゲームを起動したところ、エラーが出た。

テキスト内のコマンド(文字を太字にしたりするやつ)の書き換えを失敗したと予想し、問題があるテキストを特定するために、まずは作業所のシートをコピーして10000行以降を削除したデータを使ってリパックしてみる。

→エラー。5000行まででリトライ。

→エラー。2500行まででリトライ。

→エラー。1250行まででリトライ。

f:id:kengo700:20170816231854p:plain

やっと起動できた。やっぱりどこかのテキストに問題があるっぽい。

1900行まででリトライ。

→エラー。あとは目視で探すか…

8/17~

確認しても、問題ないように見える。1500行まででリトライ。

→エラー。1325行まででリトライ。

→起動。1400行まででリトライ。

→エラー。1331行まででリトライ。

→エラー。1330行まででリトライ。

→エラー。1327行まででリトライ。

→起動。原因の一つは1328行だと確定。

f:id:kengo700:20170817171203p:plain

原文と見比べると、「[b|tooltip]」が抜けているのに気付く。 文中の太字箇所の数が違うだけでエラーが起きるとは考えにくいけれど、下図のように書き換えてみる。

f:id:kengo700:20170817180506p:plain

→起動した。まじか…

ついでなので、他の場合も試してみる。

まずは1324行のテキスト内の「[{AGGRESSIVE}]」を「[{AAGGRESSIVE}]」(存在しないID)に書き換えてみる。

f:id:kengo700:20170817180844p:plain

→起動した。表示がどうなってるのかは不明。

次に1324行のテキスト内の「[{AGGRESSIVE}]」を「[{AGGRESSIVE]」(「}」を書き損じ)に書き換えてみる。

f:id:kengo700:20170817185652p:plain

→リパック中にアプデが入った。

アプデ対応

f:id:kengo700:20170817202338p:plain

アプデが入ったゲームを起動してみると、右下の表記が「Build #458」になっている(前までは457だった)。

念のため「ゲームファイルの整合性を確認」を実行した後に、「content.tim」を取り出してアンパック。

Stirlingで「i18n.lua」を比較してみたところ、違いは無し。

アンパックしたデータのフォントファイルを日本語のものに差し替え、下記のテキストで作成した「i18n.lua」を入れてリパック。

f:id:kengo700:20170817203454p:plain

最新バージョンでも問題なく日本語化できた。

f:id:kengo700:20170817225047p:plain

翻訳作業2

アプデ対応に入る前の続きで、1324行のテキスト内の「[{AGGRESSIVE}]」を「[{AGGRESSIVE]」(「}」を書き損じ)に書き換えてみる。

→起動した。まじか…

まあ起動するにしても正しくは表示されないはずなので、変換プログラムの方で「[]」と「{}」の数が原文の一致してるかくらいはチェックすべきか。 そのうちプログラムを修正しよう(忘れそう)。

次は、1328行以外にも問題がないか確かめるため、元のシートの1328行を修正したデータを使ってリパックしてみる。

→エラー。ぐぬぬ…

i18n.luaの編集プログラムの修正

8/18~

毎回地道にミスを探すのはしんどいので、プログラムにチェック処理を入れることにする。

入れた。

            foreach(MyClass ja_text in ja_texts)
            {
                int count_sb_l_en = CountChar(ja_text.Original, '[');
                int count_sb_l_ja = CountChar(ja_text.Translated, '[');
                int count_sb_r_en = CountChar(ja_text.Original, ']');
                int count_sb_r_ja = CountChar(ja_text.Translated, ']');
                int count_cb_l_en = CountChar(ja_text.Original, '{');
                int count_cb_l_ja = CountChar(ja_text.Translated, '{');
                int count_cb_r_en = CountChar(ja_text.Original, '}');
                int count_cb_r_ja = CountChar(ja_text.Translated, '}');

                if( count_sb_l_en != count_sb_l_ja ||
                    count_sb_r_en != count_sb_r_ja ||
                    count_cb_l_en != count_cb_l_ja ||
                    count_cb_r_en != count_cb_r_ja)
                {
                    Console.WriteLine("{0}", ja_text.Original);
                    Console.WriteLine("{0}", ja_text.Translated);
                    Console.WriteLine("[:{0},{1}  ]:{2},{3}", count_sb_l_en, count_sb_l_ja, count_sb_r_en, count_sb_r_ja);
                    Console.WriteLine("{{:{0},{1}  }}:{2},{3}", count_cb_l_en, count_cb_l_ja, count_cb_r_en, count_cb_r_ja);
                }

            }
        // http://www.atmarkit.co.jp/fdotnet/dotnettips/911countchar/countchar.html
        public static int CountChar(string s, char c)
        {
            return s.Length - s.Replace(c.ToString(), "").Length;
        }

f:id:kengo700:20170818144027p:plain

17114行目の「[b|(中略)]」が抜けていたらしい。

修正したデータでリパックしてみる。

f:id:kengo700:20170818144309p:plain

→エラー。ぐぬぬ…

前と同じように、10000行以降を削除したデータを使ってリパックしてみる。

→起動した。15000行まででリトライ。

→エラー。12500行まででリトライ。

→起動。13750行まででリトライ。

→エラー。13000行まででリトライ。

→エラー。12750行まででリトライ。

→起動。12900行まででリトライ。

8/19~

→エラー。12825行まででリトライ。

→エラー。12785行まででリトライ。

→起動。12800行まででリトライ。

→起動。12823行まででリトライ。

→起動。問題なのは12824行目だと思われる。

f:id:kengo700:20170819211037p:plain

i18n.luaファイルを見ても、問題はないっぽい。

f:id:kengo700:20170819212340p:plain

難易度に関する表示なので、1行の表示文字数に制限があるかと思い、適当に半角スペースを入れてみる。

f:id:kengo700:20170819223309p:plain

→エラー。

バイト数に制限があるかと思い、原文が119バイトなので、117バイト(39文字)にしてみる。

f:id:kengo700:20170819223907p:plain

→起動した。

考えられるのは、個別のテキストに文字数制限があるか、テキスト全体の文字制限があるか、文字数の桁計算をミスっているか。

8/20~

「易度です。」の5文字を12825行目に加えてみる。

f:id:kengo700:20170820131140p:plain

→起動した。あれ…?

f:id:kengo700:20170820131311p:plain

全体の文字数はエラーが出た時と同じなので、個別のテキストに文字数制限がある可能性が高くなる。 そんな実装になっているとは考えにくいけれど…

i18n.luaファイルを確認してみると、エラーが出た時(上)と今回(下)で、テキストの総サイズを表す部分は同じになっている。

f:id:kengo700:20170820131521p:plain

しかしファイルの最後を見ると、1バイトだけずれている。 これはおかしい。 どこかの処理をミスっている可能性が高い。

f:id:kengo700:20170820131656p:plain

12824行目のテキストの部分を確認すると、テキストのサイズを表す部分が「89 01」と「7A」で違っている。

f:id:kengo700:20170820131944p:plain

つまりこの部分のバイト数の変化を、テキスト総サイズの数値に勘案していなかったのが原因か。

プログラムを下記のように修正(size_change = size_change + (temp_size3.Count() - Int2Byte(en_texts_length[i] + 5).Count());の部分を追加)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections;
using CsvHelper;
using CsvHelper.Configuration;

namespace TextImporterForRenownedExplorers
{
    class Program
    {
        static void Main(string[] args)
        {

            // http://gushwell.ldblog.jp/archives/52369856.html
            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
            long address_text_start = Convert.ToInt64("0x121", 16); // i18n.lua内のID・テキストデータの先頭位置
            long address_text_end = Convert.ToInt64("0x1FD864", 16); // i18n.lua内のID・テキストデータの末尾位置

            //string path_csv = @"..\..\..\ja.csv";
            string path_csv = @"..\..\..\ja_test.csv";
            string path_en_lua = @"..\..\..\i18n.lua";
            string path_ja_lua = @"..\..\..\ja_i18n.lua";
            
            // luaファイル読み込み

            // https://dobon.net/vb/dotnet/file/filestream.html
            FileStream fs = new FileStream(path_en_lua, System.IO.FileMode.Open, System.IO.FileAccess.Read);

            // ファイルを読み込むバイト型配列を作成する
            byte[] bs = new byte[fs.Length];

            // ファイルの内容をすべて読み込む
            fs.Read(bs, 0, bs.Length);

            // 閉じる
            fs.Close();

            // i18n.lua内のテキストサイズを表す数値を表す部分のバイト数
            int size_length = CountByte(bs, (int)address_size);

            // i18n.lua内のテキストサイズを表す数値
            int size = Byte2Int(bs, (int)address_size, size_length);

            // ファイルの先頭部分を保存する配列を作成
            byte[] original_header1 = new byte[address_size];
            byte[] original_header2 = new byte[address_text_start - address_size - size_length];

            // ファイルの末尾部分を保存する配列を作成
            byte[] original_footer = new byte[bs.Length - address_text_end - 1];

            // ファイルの先頭部分を読み出し
            for (int i = 0; i < original_header1.Length; i++)
            {
                original_header1[i] = bs[i];
            }
            for (int i = 0; i < original_header2.Length; i++)
            {
                original_header2[i] = bs[address_size + size_length + i];
            }

            // ファイルの末尾部分を読み出し
            for (int i = 0; i < original_footer.Length; i++)
            {
                original_footer[i] = bs[address_text_end + 1 + i];
            }



            // テキスト部分を読み出し

            List<string> en_texts = new List<string>();
            List<int> en_texts_length = new List<int>();

            for (int i = (int)address_text_start; i < (int)address_text_end; i++)
            {

                int byte_text_length = CountByte(bs, i);
                int text_length = Byte2Int(bs, i, byte_text_length);

                if (text_length >= 5) text_length -= 5;
                en_texts_length.Add(text_length);
                
                byte[] byte_text = new byte[text_length];

                for (int j = 0; j < text_length; j++)
                {
                    byte_text[j] = bs[i + byte_text_length + j];
                }

                // 「richTextMacros」だけはIDでもテキストでもないっぽいので例外的に処理する(その後の4バイト分も加えてしまう)
                if (Encoding.UTF8.GetString(byte_text) == "richTextMacros")
                {
                    text_length += 4;
                    byte[] byte_text2 = new byte[text_length];
                    for (int j = 0; j < text_length; j++)
                    {
                        byte_text2[j] = bs[i + byte_text_length + j];
                    }
                    
                    en_texts.Add(Encoding.UTF8.GetString(byte_text2));
                    en_texts.Add("");
                    en_texts_length.Add(0);

                    i += byte_text_length - 1 + text_length;

                }
                else
                {
                    en_texts.Add(Encoding.UTF8.GetString(byte_text));

                    i += byte_text_length - 1 + text_length;

                }

            }


            
            // 翻訳データを読み込む

            CsvParser parser = new CsvParser(new StreamReader(path_csv));
            parser.Configuration.HasHeaderRecord = true;  // ヘッダ行あり
            parser.Configuration.RegisterClassMap<MyClassMap>();

            CsvReader reader = new CsvReader(parser);
            List<MyClass> ja_texts = reader.GetRecords<MyClass>().ToList();
            

            // 訳文の記号に不備がないかチェック

            foreach(MyClass ja_text in ja_texts)
            {
                int count_sb_l_en = CountChar(ja_text.Original, '[');
                int count_sb_l_ja = CountChar(ja_text.Translated, '[');
                int count_sb_r_en = CountChar(ja_text.Original, ']');
                int count_sb_r_ja = CountChar(ja_text.Translated, ']');
                int count_cb_l_en = CountChar(ja_text.Original, '{');
                int count_cb_l_ja = CountChar(ja_text.Translated, '{');
                int count_cb_r_en = CountChar(ja_text.Original, '}');
                int count_cb_r_ja = CountChar(ja_text.Translated, '}');

                if( count_sb_l_en != count_sb_l_ja ||
                    count_sb_r_en != count_sb_r_ja ||
                    count_cb_l_en != count_cb_l_ja ||
                    count_cb_r_en != count_cb_r_ja)
                {
                    Console.WriteLine("{0}", ja_text.Original);
                    Console.WriteLine("{0}", ja_text.Translated);
                    Console.WriteLine("[:{0},{1}  ]:{2},{3}", count_sb_l_en, count_sb_l_ja, count_sb_r_en, count_sb_r_ja);
                    Console.WriteLine("{{:{0},{1}  }}:{2},{3}", count_cb_l_en, count_cb_l_ja, count_cb_r_en, count_cb_r_ja);
                }

            }


            // テキスト部分を翻訳データに差し替えたデータを生成

            int size_change = 0;
            List<byte> output_texts = new List<byte>();

            for (int i = 0; i < en_texts.Count(); i++)
            {

                // IDのデータ:そのまま出力

                byte[] temp_text = System.Text.Encoding.UTF8.GetBytes(en_texts[i]);
                byte[] temp_size = Int2Byte(en_texts_length[i] + 5);

                // 「richTextMacros」の部分だけは例外処理、手動で元のデータを入れる
                if (en_texts[i].Contains("richTextMacros"))
                {
                    output_texts.AddRange(temp_size);
                    byte[] temp_text2 = { 0x72, 0x69, 0x63, 0x68, 0x54, 0x65, 0x78, 0x74, 0x4D, 0x61, 0x63, 0x72, 0x6F, 0x73, 0x01, 0x00, 0xF5, 0x04 };
                    output_texts.AddRange(temp_text2);
                }
                else
                {
                    output_texts.AddRange(temp_size);
                    output_texts.AddRange(temp_text);
                }

                i++;


                // テキストのデータ:翻訳データに差し替えて出力

                string output_text = en_texts[i];
                int output_text_length = en_texts_length[i];

                for (int j= 0; j<ja_texts.Count(); j++)
                {
                    if(en_texts[i] == ja_texts[j].Original && ja_texts[j].isUsed == false)
                    {
                        output_text = ja_texts[j].Translated;
                        output_text_length = System.Text.Encoding.UTF8.GetBytes(output_text).Count();
                        size_change = size_change + (output_text_length - en_texts_length[i]);

                        ja_texts[j].isUsed = true;
                        
                        break;

                    }
                }

                byte[] temp_text3 = System.Text.Encoding.UTF8.GetBytes(output_text);
                byte[] temp_size3 = Int2Byte(output_text_length + 5);

                size_change = size_change + (temp_size3.Count() - Int2Byte(en_texts_length[i] + 5).Count());

                // 「richTextMacros」の部分だけは例外処理
                if (en_texts[i-1].Contains("richTextMacros")==false)
                {
                    output_texts.AddRange(temp_size3);
                    output_texts.AddRange(temp_text3);
                }

            }

            // サイズデータを更新
            int output_size = size + size_change;

            byte[] b_output_size = Int2Byte(output_size);
            byte[] b_output_texts = output_texts.ToArray();



            // ファイルに出力

            fs = new FileStream(path_ja_lua, FileMode.Create);

            fs.Write(original_header1, 0, original_header1.Length);
            fs.Write(b_output_size, 0, b_output_size.Length);
            fs.Write(original_header2, 0, original_header2.Length);
            fs.Write(b_output_texts, 0, b_output_texts.Length);
            fs.Write(original_footer, 0, original_footer.Length);

            fs.Close();

        }

        static int CountInt(int num)
        {
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            for (int i = 0; i < bits.Length; i++)
            {
                if (bits[i] == 1)
                {
                    double count = Math.Ceiling(1.0 * (32 - i) / 7);
                    return (int)count;
                }
            }

            return 0;

        }

        static byte[] Int2Byte(int num)
        {
            // 整数を2進数に変換、num -> bits
            // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
            BitArray b = new BitArray(new int[] { num });
            int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
            Array.Reverse(bits);

            int count = CountInt(num);
            
            byte[] bytes = new byte[count];
            byte[] temp_byte = new byte[1];

            for (int i = 0; i < count; i++)
            {
                // 右から7桁ずつBitArrayにコピー
                BitArray bit = new BitArray(8);
                for(int j = 0; j < 7; j++)
                {
                    if (31 - i * 7 - j < 0) break;
                    if (bits[31 - i * 7 - j] == 1) bit[j] = true;
                    else bit[j] = false;
                }

                // 一番大きな桁に、数値情報が次に続くかの情報を入れる
                if (i + 1 < count) bit[7] = true;
                else bit[7] = false;

                // BitArrayからバイト型配列に変換
                // https://stackoverflow.com/questions/560123/convert-from-bitarray-to-byte
                bit.CopyTo(temp_byte, 0);
                bytes[i] = temp_byte[0];

            }

            return bytes;
        }

        static int CountByte(byte[] bs, int address)
        {
            int num_byte = 1;
            for (int j = 0; ; j++)
            {
                // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                BitArray b = new BitArray(new byte[] { bs[address + j] });
                int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                Array.Reverse(bits);

                if (bits[0] == 1) num_byte++;
                else break;

            }
            return num_byte;
        }

        static int Byte2Int(byte[] bs, int address, int num_byte)
        {
            int[] result_bits = new int[7];
            for (int j = 0; j < num_byte; j++)
            {
                if (j == 0)
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    for (int k = 0; k < 7; k++)
                    {
                        result_bits[k] = bits[k + 1];
                    }
                }
                else
                {
                    // https://stackoverflow.com/questions/6758196/convert-int-to-a-bit-array-in-net
                    BitArray b = new BitArray(new byte[] { bs[address + j] });
                    int[] bits = b.Cast<bool>().Select(bit => bit ? 1 : 0).ToArray();
                    Array.Reverse(bits);

                    int[] temp_bits = new int[7];
                    for (int k = 0; k < 7; k++)
                    {
                        temp_bits[k] = bits[k + 1];
                    }

                    // https://dobon.net/vb/dotnet/programing/arraymerge.html
                    result_bits = temp_bits.Concat(result_bits).ToArray();

                }

            }

            return Convert.ToInt32(String.Join("", result_bits), 2);

        }

        // http://www.atmarkit.co.jp/fdotnet/dotnettips/911countchar/countchar.html
        public static int CountChar(string s, char c)
        {
            return s.Length - s.Replace(c.ToString(), "").Length;
        }

    }

    class MyClass
    {
        public string Id { get; set; }
        public string Original { get; set; }
        public string Translated { get; set; }
        public bool isUsed { get; set; } = false;
    }

    class MyClassMap : CsvClassMap<MyClass>
    {
        public MyClassMap()
        {
            Map(m => m.Id).Index(0);
            Map(m => m.Original).Index(1);
            Map(m => m.Translated).Index(2);
        }
    }

}

12824行目を元に戻し、luaファイルを作成してみる。

f:id:kengo700:20170820133709p:plain

テキスト総サイズの部分が正しく計算されていることを確認。

f:id:kengo700:20170820133805p:plain

少し話が変わるけれど、テキスト総サイズには総サイズを表す数値のバイト数も含まれているかもしれない。 つまり総サイズの桁数が変わったら、それも総サイズの数値に勘案する必要があるかも。ないかも。 これは問題が起きてから調べればいいか。

閑話休題。

→起動した。

元のシートの1328行を修正したデータを使ってリパックしてみる。

→起動した。

最新の進捗16.12%のデータを使ってリパックしてみる。

→起動できた。

f:id:kengo700:20170820184715p:plain

f:id:kengo700:20170820184722p:plain

f:id:kengo700:20170820184735p:plain

f:id:kengo700:20170820184748p:plain

フォントもなかなかいい感じ。 改行については、後で対処しよう。

そういえば今考えると、1328行でエラーが起きていたのも、「[b|tooltip]」が抜けていたからではなく、テキストサイズを表すバイト数の変化を考慮していなかったことが原因な可能性が高い。

エラーが起きていたテキストでluaファイルを作ってみると、テキストサイズは「79」、原文のテキストサイズは「9F 01」、修正後のテキストサイズは「9D 01」になっていた。

まあ両方が原因だという可能性も無くは無いので、そのうち試してみよう。

i18n.luaの編集プログラムの修正2

それぞれのテキストに手動で改行やスペースを入れるのがめんどくさいので、「ゼロ幅スペース」を試してみる。

試しに難易度のテキストの文字を「E2 80 8B」に書き換えてみる。

f:id:kengo700:20170820191104p:plain

→ダメだった。改行されず、文字化けが表示される。

f:id:kengo700:20170820195903p:plain

アプデ対応

8/21~

アプデが入ったのでゲームを起動してみると、右下の表記が「Build #459」になっている。

ゲームのオンライン要素とかウィークリー要素とかで、頻繁にアプデが入る仕様なんじゃろうか…

アプデ後の「content.tim」を取り出してアンパック。

Stirlingで「i18n.lua」を比較してみたところ、違いは無し。

アンパックしたデータのフォントファイルを日本語のものに差し替え、最新の作業所のデータで作成した「i18n.lua」を入れてリパック。

最新バージョンでも問題なく日本語化できた。

翻訳作業3

8/26~

艦これイベントに集中するため、しばらくペースが落ちる予定…

8/29~

進捗が25%突破!

f:id:kengo700:20170829222619p:plain

だいぶ早いペースに見えるけれど、最初はアイテム名や「%name% は %skill% を取得した。」みたいな定型文を訳していたので早いだけ。

やっぱり半年~1年程度はかかるんじゃないかと思われる。

9/5~

作業所の進捗グラフをちょっと改良してみた。

f:id:kengo700:20170905224054p:plain

9/6~

完成予想日の計算を変更してみる。

作業日全体の平均データを使っていたのを、直近7日間のデータの平均に変更。

f:id:kengo700:20170906201117p:plain

進捗30%突破!

9/14~

Divinity: Original Sin 2の有志翻訳に浮気中…

割と初動が大切なので(翻訳が順調に進んでいると、他の人が参加しやすい)、一週間くらいはDOS2の翻訳に集中する予定。

9/23~

グラフで進捗状況と完了予測を示すのは分かりやすくて良いのだけれど、さぼった量もダイレクトに可視化される諸刃の剣であった… (´ཀ`」 ∠)

まあDOS2の難解な英文を訳した後に、RE:ISの「財宝を見つけた!よくやった!」的なノリのテキストを訳すと癒されるので、交互に少しずつ進めていくことにしよう。

おわりに

<翻訳が終わったら何か書く>