その1 同じコンバート処理を繰り返し行うには?
データビルドは、一度実行すると自動的にどばーっと作業を進め、最終的にはゲームで使用する専用データを完全に作ってくれます。でも細かく見れば、その作業は「あるファイルに対するコンバート作業」の連続です。つまり、データビルドの仕組みを作る上で、同じコンバート処理を繰り返し行わせる事が必須となるわけです。
この章では、そういう連続した作業を行わせる方法について見て行きます。
@ 実行ファイルをforループで連続実行する方法
プログラムで同じ作業を繰り返し行うというと、forループが真っ先に思いつくと思います。と言う事は、コンバータに沢山のファイル情報(ファイルパス)を渡せば、forループ内で連続的なコンバート作業を行わせる事ができそうです。もちろん、それは正しい方法の一つかなと思います。しかし、別の方法もあるんです。
コンバータは余り複雑な事をしない方が作るのが楽です。ファイルを一つもらったら、コンバートしたファイルを一つ作って返す。そんな軽量なコンバータの方が簡単に作れます。ただ、このコンバータを連続して動かすには必然的に「何百回もexeを実行する」という、「起動のforループ」が必要になります。
起動を何百回も行わせる方法は色々とあります。コマンドラインにforループが書けるので、それを使う方法ももちろんありです。ただそんな中で私が経験し、「これは自由度が高いなぁ」と感じたのが「スクリプト言語を通したバッチ処理」でした。特に「JScript」というWindowsが入っていれば誰でも使える超超便利なスクリプトがあります。今回はこれを中心に話を進めて行くことにします。
A JScriptでHello World !
JScriptは数あるスクリプト言語の一つで、Windowsのシステムを使って色々な事をさせるのが得意です。例えば特定のファイルだけを選択して別のフォルダにコピーする、Excelファイルに対するダイレクトな書き込み、ネットワークのやりとりなどなど、非常に柔軟に様々な事ができるんです。もちろん、外部のexeファイルを実行させることもできます。
JScriptはテキストファイルで簡単に書く事ができます。肩慣らしとして、コンソール画面に「Hello World !」と描画するJScriptを作ってみましょう。
まず適当なフォルダ(HelloWorldフォルダとしておきます)を作り、そこに「HelloWorld.js」という空のファイルを新規に作ります。そのファイルをテキストエディタで開き、次のように記述します:
HelloWorld.js WScript.Echo("Hello World !");
たった1行ですが、立派なスクリプトです。WScript.Echo関数は、コマンドラインに引数の文字列を描画します。printfと同じですね。
次に、同じフォルダ内に「HelloWorld.bat」というバッチファイルを一つ作ります。そこに次のように記述します:
HelloWorld.bat cscript HelloWorld.js
pause
cscriptというのはcscript.exeという実行ファイルを起動させる事を表しています。この実行ファイルは引数(HelloWorld.js)のJScriptを動かしてくれます。2行目のpauseはバッチ処理の一時停止で、結果を見るために置いておきました。
HelloWorldフォルダの中にHelloWorld.jsとHelloWorld.batという2つのファイルがある状態で、バッチファイルを実行してみましょう。すると、次のような画面が出現するはずです:
cscript HelloWorld.jsというコマンドが実行され、「Hello World !」と表示されました。JScriptがちゃんと動いたわけです。こんな感じで、基本的にはバッチファイルにスクリプトを実行してもらうのがわかりやすいかなと思います。
さて、JScriptは言語なのでループや条件分岐をもちろん備えています。そして、JScript内から外部のexeを実行させる事もできるんです。早速やってみましょう。
B JScriptで外部exeを実行する
JScriptで外部exeを実行するには「Windows Scripting Host(WSH)」という仕組みを借ります。これはActiveXコンポーネントの一つですが、こ難しいことは置いておいて、使い方をざっくり知ってしまいましょう。
例として、メモ帳をJScriptから開いてみます。まず作業用のフォルダ(OpenNotepadフォルダ)を作り、そこに 空っぽのOpenNotePad.jsとOpenNotePad.batファイルを作ります。batファイルはAの時と同じでcscriptの引数にOpenNotePad.jsを渡しておきます。jsファイルは次のようにします:
OpenNotePad.js // WScript.Shellを作成
var wsh = new ActiveXObject("WScript.Shell");
// ノートパッドを起動
var exec = wsh.exec("C:/Windows/System32/notepad.exe")
// ノートパッドが閉じるまで待つ
while (exec.Status == 0) {
WScript.Sleep(100);
}
varというのはJScriptで使用される変数宣言で、何でも表せる万能型です。new ActiveXObjectというのがActiveXオブジェクトを作っている部分です。JScriptはJavaScriptの一部(の拡張)になっているので、書き方はJavaやC++にそっくりなんです。外部exeを実行する機能を持っているのはWScript.ShellというActiveXオブジェクトです。このオブジェクトのexecメソッドが実行ファイルを扱えます。メモ帳はデフォルトではC:\Windows\System32\notepad.exeにありますので、そのメソッドにパスを与えます。すると、この行が処理されればメモ帳が開きます!execメソッドの戻り値をexec変数に保持しています。この戻り値は実行したexeの状態を監視してくれます。exec.Statusにその状態が入っていますので、ループで監視しています。exec.Stateが0意外になったら閉じた事になります。
実際にこのスクリプトを動かしてみると、コマンドプロンプトと一緒にメモ帳が開くのが確認できるはずです。そして、メモ帳を閉じたら、コマンドプロンプト側のバッチ処理が進みpauseが処理されるのが分かると思います。
ここまで来ると、もう言わずもがなかもしれませんが、起動部分と待機部分を囲むforループを作れば、処理が一つ一つ進むスクリプトになるわけです。ノートパッドの場合は、こちらから閉じるボタンを押さないと処理がすすみませんが、コンバータは普通処理を終えると勝手に終了するように作るため、黙っていてもどんどんループ処理が遂行されていきます。
これで連続実行はできそうです。後は、沢山のファイル(パス)をJScriptで取得できれば、ファイルを一つ一つ処理するという事ができるようになります。
C JScriptでフォルダ内のファイルパスを列挙する
JScriptはWindowsのシステム処理を色々と動かせるので、もちろんファイル処理もできます。連続処理をするには特定のフォルダにあるファイルパスをどばーっと取得する仕組みが欲しいところです。これも、わりと簡単なんです(^-^)。
適当なフォルダ(EnumFile)にEnumFile.jsとEnumFile.batファイルを作ります。また検索するフォルダとしてresourceフォルダを作り、その中に適当なファイル(text01.txt、text02.txtなど)を入れておきましょう。バッチファイルはもう大丈夫ですよね。jsファイルは次のようにします:
EnumFile.js // ファイルシステムオブジェクトを作る
var fs = new ActiveXObject("Scripting.FileSystemObject")
// ファイル名を格納する配列
var filePathAry = [];
// resourceフォルダ内のファイルを列挙
var folderObj = fs.GetFolder("resource");
var files = new Enumerator(folderObj.Files); // ファイル列挙オブジェクト
while( !files.atEnd() ) {
// ファイルパスを取得
var filePath = files.item();
filePathAry.push( filePath );
files.moveNext();
}
// ファイル名を出力
for (var i = 0; i < filePathAry.length; i++)
WScript.Echo(filePathAry[i]);
Scripting.FileSystemObjectというのはファイル操作を担当するActiveXオブジェクトです。JScriptの配列はかなり自由度が高くて色々な宣言方法があるのですが、今は上のfilePathAryのような簡便法を使うことにします。fs.GetFolderメソッドに検索したいフォルダパスを与えるとそのフォルダ内の様々な情報を持ったフォルダオブジェクトを返してくれます。そのフォルダオブジェクト(folderObj)はFilesというメンバを持っていて、これをEnumeratorという列挙してくれる人に渡すと列挙オブジェクトが出来上がります(files)。列挙オブジェクトは保持している物をitemメソッドを通して一つずつ取り出す人です。
while文の中ではfiles.item()でファイルパスを一つ取り出します。そしてすぐにそれを配列に格納(filePathAry.pushメソッド)しています。files.moveNextを呼ぶと、次のファイルパスに列挙が移動します。これを繰り返せばフォルダ下のファイルパスが全部手に入ります。files.atEndメソッドがtrueになったら、もう列挙できるファイルパスが無くなっているのでループから抜けます。
後は配列に格納したファイルパスを一つずつ取り出して出力しているだけです。これだけのコードでフォルダ下のファイルパスが手に入るのですから楽なもんです。
さて、これで「exeを連続で起動する」という処理と、「フォルダ下のファイルパスを列挙する」という処理ができるようになりました。では本章の総まとめとして、簡単な暗号化アプリケーションをC++で作り、特定フォルダ下のファイルを全部暗号化して別のフォルダに保存するデータビルドの仕組みを作ってみましょう。
D 暗号化データビルド
今回の暗号化はサンプルなので、ファイルをバイナリとして開き、1byteずつデータを取り出し、その値に1足してファイルとして出力する簡単なものにします。コンソールアプリケーションで作ってしまいます。全ソースコードはこんな感じです:
SampleEncryption.cpp int _tmain(int argc, _TCHAR* argv[])
{
if (argc < 3) {
printf("Lack of Argument\n");
printf("SampleEncryption inputPath outputPath\n");
return -1;
}
const char* inputPath = argv[1];
const char* outputPath = argv[2];
FILE *pInputF = 0;
fopen_s( &pInputF, inputPath, "rb");
if (!pInputF) {
printf("Failed to open input path.[%s]\n", inputPath);
return -1;
}
FILE *pOutputF = 0;
fopen_s( &pOutputF, outputPath, "wb");
if (!pOutputF) {
printf("Failed to open output path.[%s]\n", outputPath);
fclose(pInputF);
return -1;
}
printf("Start Encrypting... ");
// ファイルサイズ算出
fseek(pInputF, 0, SEEK_END);
unsigned sz = ftell(pInputF);
fseek(pInputF, 0, SEEK_SET);
// 1バイトずつ読み込んで+1してバッファへ書きこむ
unsigned char* outBuf = new unsigned char[sz];
for (unsigned i = 0; i < sz; i++) {
unsigned char c = 0;
fread(&c, 1, 1, pInputF);
outBuf[i] = (++c);
}
// ファイルに出力
fwrite(outBuf, sz, 1, pOutputF);
delete[] outBuf;
fclose(pInputF);
fclose(pOutputF);
printf("done.\n");
return 0;
}
コマンドライン引数に入力ファイルパスと出力ファイルパスを渡すと、入力ファイルを暗号化して出力ファイルへ書き込みます。このソースをコンソールアプリケーションとしてビルドしてSampleEncryption.exeという実行ファイルを作っておきます。テストをするとこんな感じです:
上はコマンドラインを手打ちしたわけですが、これをJScriptさんに自動的にやってもらおうというわけです。
JScript側の作業に移ります。まず適当なフォルダ(Encryptionフォルダ)を作り、もうすっかりおなじみになったと思いますが、空のEncryption.batとEncryption.jsファイルを作成します。また暗号化実行ファイルであるSampleEncryption.exeも同じフォルダ内に入れておきます。
今回はこのフォルダ下にあるresourceフォルダ内の全ファイルを暗号化してEncryptedフォルダに格納するデータビルドを作ってみることにします。Encryptionフォルダ内は次のような構成になります:
バッチファイルはEncryption.jsを実行するだけです。
SampleEncryption.cpp cscript Encryption.js
pause
Encryption.jsを見てみましょう:
Encryption.js // WSHとファイルシステムオブジェクトを作る
var fs = new ActiveXObject("Scripting.FileSystemObject")
var wsh = new ActiveXObject("WScript.Shell");
// ファイル名を格納する配列
var filePathAry = [];
// resourceフォルダ内のファイルを列挙
var folderObj = fs.GetFolder("resource"); // resourceフォルダオブジェクト取得
var files = new Enumerator(folderObj.Files); // ファイル列挙オブジェクト作成
while( !files.atEnd() ) {
// ファイルパスを取得
var filePath = files.item();
filePathAry.push( filePath );
files.moveNext();
}
// かき集めたファイルを暗号化
for (var i = 0; i < filePathAry.length; i++) {
// 出力ファイルパス作成
var outputPath = "encrypted\\" + fs.GetFileName(filePathAry[i]) + "enc";
// resourceフォルダ以下の1ファイルを暗号化
var exec = wsh.exec("SampleEncryption.exe " + filePathAry[i] + " " + outputPath);
// 暗号化が終わるまで待つ
while (exec.Status == 0) {
WScript.Echo(exec.StdOut.ReadAll()); // exeが吐き出す文字列をコンソールに出力
WScript.Sleep(10);
}
if (exec.Status.ExitCode == 0)
WScript.Echo(" -> " + outputPath + "[" + exec.Status + "]");
}
短いスクリプトです。最初にWSHとファイル操作をするFileSystemObject ActiveXオブジェクトを作成しています。次にresourceフォルダ内の操作を行うフォルダオブジェクトをfs.GetFolderメソッドで取得しています。このオブジェクト内にすでにファイル名リストがあります(fokderObj.Files)。これを列挙してくれるのがEnumeratorです。while文を通してそのファイルパスをfilePathAry配列に全部格納します。
次に集めたファイルパスを使って暗号化実行ファイルを連続実行させています。wsh.execメソッド部分が肝です。execメソッドの引数はコマンドライン文字列そのものなので、SampleEncryption.exe <入力ファイルパス> <出力ファイルパス>という並びにしています。出力ファイルパスは本当はちゃんと命名規則に従ったファイル名にするべきなのですが、今回はサンプルなので単に拡張子の後ろに"enc"を付記するに留めました。
暗号化には多少なりとも時間がかかりますので、処理が終了するまで待ちます。この時exec.StdOut.ReadAll()というメソッドを呼び出してそれをEchoしていますが、このメソッドは.exe内でprintf等の標準出力が出力した文字列をコンソールに描画します。個のメソッドを通さないと、コンソール画面に何も表示されません。処理が終了したら終了した旨を告げています。これを前ファイルに対して処理しています。
実際に先のフォルダ構成内のEncryption.batを実行すると、resourceファイル内にある複数のファイルに対していっぺんに暗号化が行われ、rencyptedフォルダ内に格納されます:
本章では、データビルドを構築する上でかかせない「処理の自動化」について、JScriptを用いる方法を見てきました。本章で挙げたサンプルはすべてこちらからダウンロードできますので、試しに動かしてみると感じがつかめると思います(^-^)。