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の「財宝を見つけた!よくやった!」的なノリのテキストを訳すと癒されるので、交互に少しずつ進めていくことにしよう。

2018/05/07~

息抜きに両方進めるとか考えが甘すぎた... むりちゃづけ...

そして久しぶりに作業所を覗いたら、その尋常じゃない進み具合に驚愕した... やばたにえん...

RE:IS 日本語化作業所 - Google スプレッドシート

f:id:kengo700:20180507225200p:plain

実際凄すぎる... 偉業を成し遂げかけている真の冒険家がいた...

これだけの頑張りを見せられて、放置したままにはできないので、日本語化パッチの作成を再開する。

日本語化方法の復習

日本語化の方法を思い出しつつ、現バージョンで日本語化できるか確認する。

「ゲームファイルの整合性を確認」を実行した後に、「content.tim」を取り出して、以前作った「アンパック.bat」でアンパック。「content.tim」の更新日時は2017/12/08になっていて、このときにアプデが入った模様。

アンパックした「lua\abbeybase\i18n.lua」を、2017/08/21にアンパックして取っておいたものとStirlingで比較してみたところ、いろいろ変わっている。 アプデでテキストの変更があった? f:id:kengo700:20180507225630p:plain

しかし確認は後回しで、とりあえず昔のプログラムと現行の作業所のデータで日本語化できるか試す。

「Localize_20180507」というフォルダを作り、アンパックしたデータをコピー。 以前用意していた12個のフォントファイルを、アンパックしたデータに上書き。 以前作成したプログラム「TextImporterForRenownedExplorers」のフォルダの中に、「i18n.lua」をコピペ。 作業所のデータをcsv形式でダウンロードし、同じフォルダにコピペして「ja.csv」にリネーム。

プログラムを実行したところ、エラーが発生。System.OverflowException

下記ページの

value は非 10 進法符号付き数値を表しますが、前に負の符号が付いています。 -または- value は Int32.MinValue 未満の数値か、Int32.MaxValue より大きい数値を表します。

かな?

i18n.luaの中身を確認すると、以前まで「9BB07F」だったデータサイズが、最新版では「DDFC8001」になっている。

これを変換すると 「DDFC8001」→「11011101 11111100 10000000 00000001」→「0000001 0000000 1111100 1011101」→「2113117」

データサイズは問題じゃなさそう。

もう一度プログラムを見直すと、データの位置をハードコーディングしていた...

           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・テキストデータの末尾位置

そして「i18n.lua」をもう一度Stirlingで見直すと、データの位置が「0x121」から「0x122」に変わってる(上が最新版、下が20170821版)。

f:id:kengo700:20180507225906p:plain

たぶんデータサイズが「9BB07F」から「DDFC8001」に桁数が増えたのでずれた?

データの末尾も比較してみると「0x203EA7」が末尾っぽい。これはアプデでテキストが増えたからか?

f:id:kengo700:20180507225929p:plain

プログラムを下記のように書き換え、実行。

           long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
           long address_text_start = Convert.ToInt64("0x122", 16); // i18n.lua内のID・テキストデータの先頭位置
           long address_text_end = Convert.ToInt64("0x203EA7", 16); // i18n.lua内のID・テキストデータの末尾位置

再びエラーが出た。今度はArgumentOutOfRangeException アプデでテキスト数が変わった影響っぽいか。

確認してみると、サイズ41229の配列の[41229]にアクセスしてしまっている。どこかにバグがある。 いやそもそもテキストデータは「ID」と「テキスト」がセットになっているので、サイズが奇数の41229なのはおかしい。

一度最新版のi18n.luaからテキストデータを抽出してみる。 このブログ記事を見返すと、どうやらプログラムの抽出したデータを出力する部分は消していまっているぽいので、以前の書き込みから下記コードをデータ読み込み後の部分にコピペして実行。

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

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

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

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

                       //Console.WriteLine("\"{0}\",\"{1}\"", temp_ID, temp_Text);
                       //return;
                   }
               }

           }

           return; 

出力ファイルを確認してみると、日本語がまざっていて明らかにおかしい。 プログラムで処理していたi18n.luaの更新日時を見ると、2017/09/06になってる! つまり処理するファイルを間違えるという凡ミスだった!

最新のi18n.luaに対してテキスト抽出プログラムを実行すると、実行できた!

Excelで開くとテキスト数は21165、アプデで236行だけ増えてるっぽい。アプデのデータを作業所に反映する作業は後でやるとして、もう一度最新の翻訳データを埋め込むプログラムを実行してみる。

エラーなく「ja_i18n.lua」ファイルが出力できた。 このファイルを「i18n.lua」にリネームし、Localize_20180507フォルダの「i18n.lua」に上書き。

以前作成した「リパック.bat」を実行。 このリパック処理に時間がかかる...

結局リパック処理に50分近くかかった...

リパックして生成した「content.tim」をゲームフォルダに上書きし、ゲームを実行してみる。

f:id:kengo700:20180507232344p:plain

日本語化されてない...?

元データを確認したところ、i18n.luaの更新日時が他のファイルと変わってない。変わりに「Localize_20170821」フォルダ内のi18n.luaの更新日時が2018/05/07になってる! また間違えた!

疲れで判断力がおかしくなってる...

「Localize_20180507」フォルダに入れ直して再び50分かけてリパック...

f:id:kengo700:20180508002102p:plain

f:id:kengo700:20180508002113p:plain

日本語化できた!

あとはアプデで追加されたテキストを作業所に反映し、テキストの改行を何とか自動化するだけだ。

改行の自動化

5/8~

以前試して駄目だったゼロ幅スペースをもう一度試してみる。

前回は直接文字コード「E2 80 8B」を書き込んだが、今回は作業所のテキストにゼロ幅スペースをコピペし、それをゲームデータに取り込んでみる。

下図の「→​←」にゼロ幅スペースが入っている。 f:id:kengo700:20180508211259p:plain

やっぱり駄目だった。 f:id:kengo700:20180508220740p:plain

しかたないので、半角スペースの幅を修正して代用する方法を試してみる。

5/13~

フォントを修正するために「FontForge」をインストールしてみる。

「FontForge」で修正したいフォントを開いたところ、「○○のグリフはフォントにありません」と表示され、フォントがすべて表示されない...

f:id:kengo700:20180513234451p:plain

下記ページにあるように、フォントに問題があるっぽい? ただし問題を修正するスクリプトがLinux用だ... 環境構築がめんどい...

他のフォントだと問題なく「FontForge」で読み込めた。「Noto Font」を「FontForge」で読み込むのを頑張るより、他のフォントを使った方がよさそうかも。

f:id:kengo700:20180514002428p:plain

5/23~

仕事がひと段落着いたので再開。

フォントを探してみて、下記のものを使ってみることにする。やっぱり明朝体のフリーフォントは少ない...

default → GenShinGothic-Light defaultb → GenShinGothic-Medium header → HanaMinA headerb → HanaMinA

それぞれ「FontForge」で開き、スペースの幅をゼロにしてフォント出力する(「Anchor」がどういう意味かは知らない...)。

f:id:kengo700:20180523203126p:plain

f:id:kengo700:20180523203138p:plain

f:id:kengo700:20180523203207p:plain

出力時にエラーが出たが、とりあえず無視して生成。

f:id:kengo700:20180523203214p:plain

改変したフォントをリネームし、オリジナルのものに上書き。 作業所のテキストに、試しに手動でスペースを入れて出力し、このデータを使って日本語化してみる。

f:id:kengo700:20180523203337p:plain

f:id:kengo700:20180523222227p:plain

自動で改行できた!

しかし英語のままの部分が読みにくくなってる...

f:id:kengo700:20180523222303p:plain

これは... とりあえずこのまま進めて、支障がありそうなら別の方法を考えよう...

5/26~

諦めきれずにまたゼロ幅スペースを試してみる。

源真ゴシックと花園明朝にはゼロ幅スペースが実装されていないっぽいので、FontForgeで追加してリパック。

f:id:kengo700:20180526142024p:plain

f:id:kengo700:20180526142030p:plain

やっぱり駄目だった。

f:id:kengo700:20180526142054p:plain

諦めて半角スペースをゼロ幅スペースに改変する方法でいこう。

改行の自動化2

下記ページを参考に、翻訳テキストの全角文字の間にスペースを挿入するプログラムを作成。

.NET TIPS 文字列の全角/半角をチェックするには? - C# - @IT

            // 改行処理のため、全角文字の間に半角スペースを挿入する
            foreach (MyClass ja_text in ja_texts)
            {
                string temp_ja_text = ja_text.Translated;

                for(int i = 0; i < temp_ja_text.Length; i++)
                {
                    if (StringChecker.isZenkaku(temp_ja_text[i].ToString()))
                    {
                        temp_ja_text = temp_ja_text.Insert(i, " ");
                        i++;
                    }
                }
                ja_text.Translated = temp_ja_text;
                //Console.WriteLine(ja_text.Translated);

            }
//http://www.atmarkit.co.jp/fdotnet/dotnettips/014strcheck/strcheck.html
public class StringChecker
{

    static Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");

    public static bool isZenkaku(string str)
    {
        int num = sjisEnc.GetByteCount(str);
        return num == str.Length * 2;
    }

    public static bool isHankaku(string str)
    {
        int num = sjisEnc.GetByteCount(str);
        return num == str.Length;
    }
}

禁則処理への対応や、もともとの半角スペースを全角スペースに書き換える処理などは後回し。

とりあえずこれでリパックしてみる。

f:id:kengo700:20180527010416p:plain

f:id:kengo700:20180527010424p:plain

自動的に改行できた!

ただし「ゲームオプション」の部分などは見切れてしまっている。まあこのあたりは「オプション」に変えるとか翻訳側で対処すればいいか。

あとはもともとの半角スペースを全角スペースに置き換える処理を入れ、アプデ対応し、日本語化パッチのβ版を作る

5/27~

禁則処理と半角スペースを全角スペースに置き換える処理を追加

            // 改行処理のため、半角スペースを全角スペースに置換
            foreach (MyClass ja_text in ja_texts)
            {
                ja_text.Translated = ja_text.Translated.Replace(" ", " ");
            }

            // 改行処理のため、全角文字の間に半角スペースを挿入する
            foreach (MyClass ja_text in ja_texts)
            {
                string temp_ja_text = ja_text.Translated;

                for(int i = 0; i < temp_ja_text.Length; i++)
                {
                    if (StringChecker.isZenkaku(temp_ja_text[i].ToString())
                        && temp_ja_text[i] != '。'
                        && temp_ja_text[i] != '、'
                        && temp_ja_text[i] != '!'
                        && temp_ja_text[i] != '?'
                        && temp_ja_text[i] != '」'
                        && temp_ja_text[i] != ')'
                        && temp_ja_text[i] != '・')
                    {
                        temp_ja_text = temp_ja_text.Insert(i, " ");
                        i++;
                    }
                }
                ja_text.Translated = temp_ja_text;
                //Console.WriteLine(ja_text.Translated);

            }

各フォントの全角スペース(U+3000)を、それぞれの半角スペースと同じ幅に改変

f:id:kengo700:20180527124627p:plain

f:id:kengo700:20180527124703p:plain

これで最新の翻訳データをリパックしてゲームをプレイ。キャンペーンのクリアまでプレイできることを確認

f:id:kengo700:20180527124751p:plain

いい感じ

ただし一部スペースが消えてしまっているのと、文字コマンドが正しく処理されなくなっている部分がある

f:id:kengo700:20180527124923p:plain

f:id:kengo700:20180527124936p:plain

スペースが消えている英文については、翻訳が進めば殆ど気にならなくなるはず

文字コマンドについては、間の半角スペースを置き換えてしまったのが原因か

文字コマンド内だけは置き換えない処理も可能だが、とりあえずは半角スペースの置き換えはしないことにする

と思ったけどやっぱり作戦変更で、「shadow」や「color」の文字が含まれていないテキストだけ半角スペースを置き換えてみる。多少場当たり的な対応だけど、これで大丈夫ならこれでいこう

            foreach (MyClass ja_text in ja_texts)
            {
                if(ja_text.Translated.Contains("shadow") == false && ja_text.Translated.Contains("color") == false)
                {
                    ja_text.Translated = ja_text.Translated.Replace(" ", " ");
                }
            }

リパックしてテスト。少なくとも遭遇戦の文字コマンドについては大丈夫っぽい

f:id:kengo700:20180527160127p:plain

アプデ対応

5月中に再びアプデがあったようなので、データを抽出し、以前のものと比較してみる

最新のデータをアンパックし、「i18n.lua」を「Stirling」で確認

プログラムを下記のように変更してデータ抽出

            long address_size = Convert.ToInt64("0x70", 16); // i18n.lua内のテキストサイズを表す数値の先頭位置
            long address_text_start = Convert.ToInt64("0x122", 16); // i18n.lua内のID・テキストデータの先頭位置
            long address_text_end = Convert.ToInt64("0x20439C", 16); // i18n.lua内のID・テキストデータの末尾位置

昨年のバックアップデータから抽出したものと「WinMerge」で比較すると、かなり変わっている

f:id:kengo700:20180527155516p:plain

ただしいくつかIDを検索して比べてみると、大部分は順番が入れ替わっただけっぽい? いずれにしてもこれを手作業でアップデートするのは無理なので、アプデ対応用プログラムを作る必要がある

6/1~

かなり試行錯誤しつつ、下記のようなプログラムに。

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

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

            string path_csv_old_ja = @"..\..\..\old_ja.csv";
            string path_csv_old_en = @"..\..\..\old_en.csv";
            string path_csv_new_en = @"..\..\..\new_en.csv";


            // 旧バージョンの翻訳データを読み込む

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

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


            // 旧バージョンの原文データを読み込む
            CsvParser parser2 = new CsvParser(new StreamReader(path_csv_old_en));
            parser2.Configuration.HasHeaderRecord = true;  // ヘッダ行あり
            parser2.Configuration.RegisterClassMap<MyClassMap2>();

            CsvReader reader2 = new CsvReader(parser2);
            List<MyClass> old_en_texts = reader2.GetRecords<MyClass>().ToList();


            // 新バージョンの原文データを読み込む
            CsvParser parser3 = new CsvParser(new StreamReader(path_csv_new_en));
            parser3.Configuration.HasHeaderRecord = true;  // ヘッダ行あり
            parser3.Configuration.RegisterClassMap<MyClassMap2>();

            CsvReader reader3 = new CsvReader(parser3);
            List<MyClass> new_en_texts = reader3.GetRecords<MyClass>().ToList();


            int count_delete = 0; // バージョンアップで削除されたテキストの数
            int count_add = 0; // バージョンアップで追加されたテキストの数
            int count_change = 0; // バージョンアップで変更されたテキストの数
            int count_conflict = 0;


            // 旧データにはあるけど新データには無い削除されたIDを検索
            List<MyClass> diff_delete = new List<MyClass>();
            Console.Write("削除されたテキスト: ");
            foreach (MyClass old_en_text in old_en_texts)
            {
                bool exists = false;
                foreach (MyClass new_en_text  in new_en_texts)
                {
                    if (old_en_text.Id == new_en_text.Id)
                    {
                        exists = true;
                        break;
                    }

                }

                if (!exists)
                {
                    diff_delete.Add(old_en_text);
                    count_delete++;
                }
            }

            Console.WriteLine(count_delete);


            // 新データにはあるけど旧データには無い追加されたIDを検索
            List<MyClass> diff_add = new List<MyClass>();
            Console.Write("追加されたテキスト: ");
            foreach (MyClass new_en_text in new_en_texts)
            {
                bool exists = false;
                foreach (MyClass old_en_text in old_en_texts)
                {
                    if (new_en_text.Id == old_en_text.Id)
                    {
                        exists = true;
                        break;
                    }

                }

                if (!exists)
                {
                    diff_add.Add(new_en_text);
                    count_add++;
                }
            }

            Console.WriteLine(count_add);



            // バージョンアップに対応する翻訳データを作成する

            // 新データをチェック
            List<MyClass> new_ja_texts = new List<MyClass>();

            foreach (MyClass new_en_text in new_en_texts)
            {
                MyClass new_ja_text = new MyClass();

                // デフォルトで新データを入れておく
                new_ja_text.Id = new_en_text.Id;
                new_ja_text.Original = new_en_text.Original;
                new_ja_text.Translated = new_en_text.Original;
                new_ja_text.Progress = "✕";


                // 「richTextMacros」だけはIDでもテキストでもないっぽいので例外的に処理する
                if (new_en_text.Id == "richTextMacros")
                {
                    continue;
                }

                // 旧データの中から一致するIDを検索
                //bool idExists = false;
                int i = 0;
                foreach (MyClass old_en_text in old_en_texts)
                {

                    if (new_en_text.Id == old_en_text.Id && old_en_texts[i].isUsed == false)
                    {
                        
                        old_en_texts[i].isUsed = true;

                        MyClass old_ja_text = old_ja_texts[i];

                        // 一致するIDが見つかった場合、テキストが一致するか確認
                        if (new_en_text.Original == old_en_text.Original)
                        {


                            // テキストが一致した(テキストが修正されていない)場合、旧翻訳データを検索してコピー
                            new_ja_text.Translated = old_ja_text.Translated;
                            new_ja_text.Progress = old_ja_text.Progress;
                                    
                            break;

                        }
                        else
                        {


                            if (old_ja_text.Progress != "✕")
                            {
                                // 既に翻訳していた場合、後から手動で修正しやすいように両方出力する
                                //   [要修正][旧版訳文:ほげほげ][旧版原文:hogehoge][新版原文:fugafuga]
                                new_ja_text.Translated = "[要修正]";
                                new_ja_text.Translated += "\r\n[旧版訳文:" + old_ja_text.Translated + "]";
                                new_ja_text.Translated += "\r\n[旧版原文:" + old_en_text.Original + "]";
                                new_ja_text.Translated += "\r\n[新版原文:" + new_en_text.Original + "]";
                                new_ja_text.Progress = old_ja_text.Progress;
                                count_conflict++;
                                count_change++;
                            }
                            else
                            {
                                // 翻訳していなかった場合、変更点を確認できるよう両方出力する
                                new_ja_text.Translated = "[テキスト変更]";
                                new_ja_text.Translated += "\r\n[旧版原文:" + old_en_text.Original + "]";
                                new_ja_text.Translated += "\r\n[新版原文:" + new_en_text.Original + "]";
                                count_change++;

                            }

                            break;


                        }

                    }

                    i++;

                }

                new_ja_texts.Add(new_ja_text);

            }

            Console.Write("変更されたテキスト: ");
            Console.WriteLine(count_change);

            Console.Write("翻訳済みだった変更されたテキスト: ");
            Console.WriteLine(count_conflict);


            // 結果を出力
            // https://qiita.com/ryo_naka/items/46bca218da94eb91b6fb
            using (var sw = new StreamWriter(@"..\..\..\new_ja.csv", false, Encoding.UTF8))
            using (var csv = new CsvHelper.CsvWriter(sw))
            {
                // ヘッダーあり
                csv.Configuration.HasHeaderRecord = true;
                // マッパーを登録
                csv.Configuration.RegisterClassMap<MyClassMap>();
                // データを読み出し
                csv.WriteRecords(new_ja_texts);
            }

        }
    }


    class MyClass
    {
        public string Id { get; set; }
        public string Original { get; set; }
        public string Translated { get; set; }
        public string Progress { 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);
            Map(m => m.Progress).Index(3);
        }
    }

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

}

「CsvHelper」が最新版だとプログラムの書き方が変わっているようなので、とりあえず旧バージョンを使う。

f:id:kengo700:20180601205301p:plain

最新データを作業所からダウンロードし、アップデート用プログラムを実行すると、下図のように追加テキスト258、変更テキスト41だった。

f:id:kengo700:20180601205348p:plain

出力された「new_ja.csv」ファイルをExcelで開いて問題なさそうなことを確認し、作業所へコピペする。

旧バージョンのデータは「翻訳テキスト(旧バージョン、#457)」というシートに残しておく。

入れ違いに編集されてしまうことを防ぐため、作業中はシートに保護設定を追加しておく。

バージョンアップ後の作業所で、「[テキスト変更]」と「[要修正]」の文字が入ったテキストを検索し、手動で修正する。 変更があったテキストは誤字の修正と、数値の変更などが主だった。

f:id:kengo700:20180601205643p:plain

また作業所の「進捗グラフ」シートで、計算に総テキスト数を使っている部分を修正。

作業後の作業所のデータをダウンロードし、リパックしてゲームを日本語化できるか試してみる。

ゲームは起動できたが、翻訳済みのはずのテキストが一部日本語化されていない...

f:id:kengo700:20180601214413p:plain

翻訳済みテキストの検索部分で、原文の一致チェックではなく、IDの一致チェックに変えてみる。

//                  if (en_texts[i] == ja_texts[j].Original && ja_texts[j].isUsed == false)
                    if (en_texts[i-1] == ja_texts[j].Id && ja_texts[j].isUsed == false)

問題なく日本語化できた!

f:id:kengo700:20180601231517p:plain

日本語化パッチ(アルファ版)作成

「ゲームファイルの整合性を確認」で英語に戻し、「udm差分ファイル作成ツール」で差分パッチを作成。

作成した差分パッチを実行し、日本語化できることを確認。

f:id:kengo700:20180601234603p:plain

差分パッチの容量が452MBもある... 差分をとっても全然容量が減ってない悲しみ...

README.txtファイルを作成し、「Lhaplus」で圧縮し、Googleドライブにアップロードして共有設定を「リンクを知っている全員が閲覧可能」に。

あと日本語化パッチの不具合などのフィードバックをもらうために、作業所に「問題報告掲示板」シートを作成。

Twitterで告知。

不具合の調査

7/14~

作業所にいただいたフィードバックを確認し、不具合をできる限り解消する。

現在いただいている不具合をまとめると下記の通り

  • 強制終了
    • DLC"More To Explore"で、Andean Adventureで街から別の町へ移動するの選択肢を選ぶと強制終了
    • タイトル画面のコレクションからキャンプファイアのタブを選択しても強制終了
    • Ultimate Athleteカードがブースターパックから排出された時点で強制終了
    • Show Advaneced Informationからでもドロレスかイワンを選択すると強制終了
  • プレイ上の不具合
    • たまに10秒ほど入力を受け付けないタイミングが生じる
  • テキストの不具合
    • マドリード王宮の専門家、B.G.ハンターの説明が逆?
    • MONSTRO_EMILIA_PASS_2_TEXTの英語テキストが空になっている?
    • Grappling Hookや専門家の説明で、切れ者の説明と実際のパーク名がズレている。
    • Carved Ace'(彫刻されたエースの固有の能力である毎遠征開始時に道具を獲得する云々のテキストサイズが取得時に過剰に小さく表示されて判読できない
    • 太字だった所が今は普通の文字に変わってる? 19421の[i|just]など

一つずつ状況を再現し、原因を探る

まずは「ファイルの整合性チェック」で英語版に戻し、アプデが入っていないか確認

「content.tim」を5/27にバックアップしておいたものと「stirling」を使って比較すると、違いはなかったので、その後アプデは入っていない。

DLCはまだインストールしていなかったので、Steamでインストール。

f:id:kengo700:20180714144543p:plain

すると「content.tim」と同じフォルダに「dlc_001.tim」と「dlc_002.tim」がインストールされた。

f:id:kengo700:20180714144705p:plain

この状態でゲームを実行すると下図のような感じになる。

f:id:kengo700:20180714145418p:plain

強制終了していたのはCampfire Storyのタブ?

f:id:kengo700:20180714145505p:plain

選択すると強制終了するドロレスとイワンは、ファイターのキャラクターのことか(DLCのブースターパックのシステムが良くわかってないので、違うかも)

f:id:kengo700:20180714145612p:plain

DLCを入れた状態で「REIS非公式日本語化パッチv.0.0.1」を当てて、ゲームをプレイしてみる

DLCの説明も日本語化できてる。

f:id:kengo700:20180714145922p:plain

そして「CampfireStories」タブを選ぶと確かに落ちる

f:id:kengo700:20180714150010p:plain

ゲームのフォルダを見ると、「dumps」フォルダ内にエラーログっぽいものができていて、ダブルクリックするとVisual Studioで見れた。

f:id:kengo700:20180714150431p:plain

新しいバッチファイルを作り、DLCのファイルをアンパックしてみる

.\ReisUnpackBin\ReisUnpack.exe unpack ".\OriginalDLCData_20180714\dlc_001.tim" ".\Unpack_DLC1"
.\ReisUnpackBin\ReisUnpack.exe unpack ".\OriginalDLCData_20180714\dlc_002.tim" ".\Unpack_DLC2"
pause

中身を見たところ、「dlc_001.tim」が「More To Explore」、「dlc_002.tim」が「The Emperor's Challenge」っぽい。

DLCで日本人キャラも追加されてたのか…

f:id:kengo700:20180714153137p:plain

ざっと見たところ、素材ファイル以外の、プログラムの挙動に関係しそうなのは、下記ファイル(\dlc\blastfromthepast\build.lua)ぐらいか。

f:id:kengo700:20180714153327p:plain

全然わからないので、開発元に翻訳ファイルを送りつける方が早いかもしれない... (その場合に問題になりそうなのは、ゲーム側で改行対応が必要になることと、もともと英語版しかないので多言語対応に工数が掛かりそうなこと)

とりあえず最新の翻訳データを反映した日本語化パッチを作り、自分でも遊びつつ考えよう。

日本語化パッチ(アルファ版)の更新

作業所から最新の翻訳データをcsv形式でダウンロードし、自作ソフトでluaファイルへ埋め込み。

リパックしてオリジナルファイルに上書きし、テストプレイ。

とりあえずクリアまでプレイできることを確認。

f:id:kengo700:20180715011451p:plain

udm差分ファイル作成ツールで差分パッチを作成し、zip圧縮してGoogleドライブにアップロード。

ネット回線が貧弱でアップロードに時間がかかってしまった。公開は明日にしよう。

7/15~

Twitterで告知

さらに作業所にパッチのダウンロードリンクを追記

また日本語化パッチの不具合について(特に短時間のフリーズと、DLC関連の強制終了)、解決が難しそうなので、開発元に相談することを検討する。

ただし翻訳データの扱いについて何も決めていなかったので、データを開発元へ提供していいかを作業所の掲示板で翻訳者さまに確認してみる。

7/17~

f:id:kengo700:20180717192946p:plain

  1. 神はいると思う?

  2. 有志翻訳作業所で見た

さっそく開発元に相談してみる。拙い英語で伝わるかは分からないけど...

https://steamcommunity.com/app/296970/discussions/0/3596571824775086273/

作業所の改修

8/12~

校正作業の進捗を可視化するために、毎日どれくらい文章を修正したかカウントし、記録しておくことにする

下訳完成後は校正量も多くなり、徐々に修正が必要な箇所が減っていって落ち着くはずなので、それがある一定以下になったときに校正作業完了と判断する

毎日自動的にバックアップを取っているので、そのバックアップを取った後に、最新のバックアップファイルと、一つ前のバックアップファイルを読み込み、訳文を比較して、変更があった数をカウントする

試行錯誤して下記のようなスクリプトを作成

function UpdateProofreadProgress(){
  var file_workspace = DriveApp.getFileById('1K_ystmDAq6fkRNPV7VMvSBWxqbqLpBSzZgti-ICaaV4');
  var today = new Date();
  var file_name_new = file_workspace.getName()+'-'+Utilities.formatDate(today, 'JST', 'yyyy-MM-dd') + '-06';
//  today.setDate(today.getDate() - 1);
  var yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
  var file_name_old = file_workspace.getName()+'-'+Utilities.formatDate(yesterday, 'JST', 'yyyy-MM-dd') + '-06';
  
//  Logger.log(file_name_old);
//  Logger.log(file_name_new);
  
  var folder = DriveApp.getFolderById('0B1yUXrJn16wCbWVCOTEzZEZnMUU');  // バックアップファイルが入っているフォルダ
 var file_old = folder.getFilesByName(file_name_old).next();
 var file_new = folder.getFilesByName(file_name_new).next();

  var spreadsheet_old = SpreadsheetApp.openById(file_old.getId());
  var spreadsheet_new = SpreadsheetApp.openById(file_new.getId());
  
  var sheet_old = spreadsheet_old.getSheetByName("翻訳テキスト");
  var sheet_new = spreadsheet_new.getSheetByName("翻訳テキスト");
  
  var text_old = sheet_old.getRange(2,3,sheet_old.getLastRow()).getValues();
  var text_new = sheet_new.getRange(2,3,sheet_new.getLastRow()).getValues();
  
  if(text_old.length > text_new.length){
    var rowMax = text_new.length;
  }else{
    var rowMax = text_old.length;
  }
  
  var count_proofread = 0;
  for (var row = 0; row < rowMax; row++) {
    if( text_new[row].toString() != text_old[row].toString() ){
      count_proofread++;
//      Logger.log(text_old[row]);
//      Logger.log(text_new[row]);
    }
  }

//  Logger.log(count_proofread);

  var spreadsheet = SpreadsheetApp.openByUrl(url);
  var sheet_output = spreadsheet.getSheetByName(sheetName_output);  
  var start_date = new Date(sheet_output.getRange(cell_startDate).getValue());

  // 経過日数を計算、参考:http://codaholic.org/?p=65 
  var diff = Math.floor((today - start_date)/1000/60/60/24);

  // 出力
  sheet_output.getRange(diff+1, output_column+1).setValue(count_proofread);  
  
}

これをバックアップスクリプトの後にスケジュール実行する

とりあえず試した限りではちゃんと記録できてそう

f:id:kengo700:20180812162403p:plain

日本語化パッチ(ベータ版)作成

校正

おわりに

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