2016/04/01
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止中

XP3以外のデータファイルを使うプラグインを作る

■はじめに

本書では、吉里吉里2/KAG3で、XP3以外のデータファイルを使う方法について述べる。 述べるというかそういうプラグイン実装(のたたき台)を提供するというか。

せっかちな人向けに、先に書いておく。ソースコードつきバイナリは こちらから入手可能。 あ、バイナリはそのまま吉里吉里Zでも動きました。

■XP3ファイルの問題点

ご存知のように、吉里吉里は、dataディレクトリが存在するならそれを使うし、 data.xp3 が存在するならそれを使う。基本的に、ゲームリリース時にはデータを XP3形式のファイルにまとめるのが世界の決まりだ。暗号化の有無はさておき、 吉里吉里で頒布されているゲームにおいて、XP3以外のデータファイルの存在を、 我輩は寡聞にして知らない。

ことほど左様に一般的なXP3であるが、(ユーザにとっては別にメリットも デメリットもないけれど、)ゲームメーカーにとっては大きな問題を抱えている。 言えば、ユーザによるデータの展開が容易であることだ。これはXP3の以下の 特徴に起因する。

つまり、違法コピーや展開データの放流に対し、あまりに心もとない。 違法コピー防止については、昨今ようやくネットワークアクティベーションなどが 出てきたが、サーバ保守のコスト、ユーザ情報の的確な保護などの手間もあり、 零細メーカー(や同人屋)には苦しいのが現状だ。

で、この現状を打開しようとすると、「吉里吉里が、 『ボクがかんがえたさいきょうフォーマットのデータファイル』を読めるように なってくれるといいんじゃないか?」という考えが浮かんでくる。

■StorageMediaプラグインというモノ

実は、そういうのを実現するために、吉里吉里には 「StorageMediaプラグイン(≒iTVPStorageMediaクラス)」という枠組みが 既に実装されている。よーしじゃぁソレ使って独自フォーマットのデータファイルを 読めるようなプラグインを書こう! …と話が進めばいいんだけど、実は世界には全然そういう情報が転がってなくて 実装するのがスッゲー難しいのでした。 きっとめんどくさいから誰も使ってないんだろうなー、と思いつつ、それでも 唯一引っかかるのは、 電波とどいた?の2010/10/05 「吉里吉里 StorageMedia プラグイン」の項。 うへぇ、もう5年半も前の話なんですなぁ。

ざっくり言うと、

  1. あるファイル形式を扱うためのIStreamクラスを継承したストリームを作り、
  2. それをiTVStorageMediaを継承したクラスから操作するようにして、
  3. TVPRegisterStorageMedia() で吉里吉里に登録する

と、これにより吉里吉里から独自のファイル形式を持ったデータファイルを 扱えるようになる、というもの。ざっくり言い過ぎだ!

で、よく上のリンクを読むと、既にサンプルプラグインを用意 頂いているとのこと。なるほどなるほど、じゃぁ早速読んでみよっと、と ソースコードを追いかけるが…難しいよ!我輩は「できない方の人」なんで、 詳細なクラスリファレンスもなしにこんなの読めないよ!とか早くも挫折しそうに。 あと、せっかく用意されているサンプルプラグインのうち、今回の用途に 一番流用できそうなminizipプラグインが、思ってたより随分複雑で、 さらっと流用するには随分ハードルが高い。これじゃXP3以外のデータファイルを 使うプラグインを、パンピーが作るのは相当難しそうだ…。

というわけで、ここはイッパツ我輩が「スッゲぇシンプルなStorageMedia プラグイン」を作って、例によって WTFPLで公開して、好きに使ってもらうというのがいいんじゃないか、 と思ったわけで。

■ssmファイル仕様

ということで、本書では、XP3 に変わる SSM(Simple Storage Media) というデータファイル形式を提案する。以降これを ssmファイル と呼称する。 仕様は以下の通り。

圧縮・暗号化・ファイル破損チェックなどの機能は、後から好きな人が好きなように 追加すればいいのだから、そういうのは考慮せず、なるべくシンプルに作成することを 心がける。 重ねて書くが、本書の目的は「スバらしいデータファイル形式を提案すること」 ではなく、 「独自のデータファイル形式を実装する際のたたき台になるシンプルなコードを提供すること」 なのであるよ。そこは間違えんごつ。

■ssmファイルの構造

ssmファイルの構造は以下の通り、 ファイルヘッダ部→データ部→管理部、という三つだけで構成される、 これ以上は削れないんじゃないかというくらい、実にシンプル。

セクション名 データ名 サイズ(byte) データ 備考
ファイルヘッダ部 SSMヘッダ文字列 8 "SSMHEAD\0" ssmファイルであることを示す文字列
バージョン 2 0 現在は0のみ
データ部インデックス 8 ssmファイル先頭からのデータ部の開始位置。通常はファイルヘッダの直後なので、 26(=8+2+8+8)
ファイル管理部インデックス 8 ssmファイル先頭からのファイル管理部の開始位置。通常はデータ部の直後
データ部 データ - 単純にデータの連続であり、ファイルの区切りなども存在しない
ファイル管理部 ファイル管理部ヘッダ文字列 8 "SSMENTRY" ssmファイル内に格納されているファイルの管理部を示す文字列
ファイル数 8 ssmファイル内に格納されているファイルの数
データインデックス1 8 ssmファイル中の、このファイルの開始位置
データサイズ1 8 ssmファイル中の、このファイルのサイズ
ファイル名長1 2 このファイルのファイル名の長さ(バイト数。wchar_t なので文字列長x2されていることに注意)
ファイル名1 - このファイルのファイル名(wchar_t なので2byte 1組、末尾に'\0'なし)。半角英文字は全て小文字に変換済み
データインデックス2 8 二つ目の管理データ
データサイズ2 8
ファイル名長2 2
ファイル名2 -
以降、管理データがファイル数分続く

■ssmファイルを作成するコマンド

(XP3にkrkrrel.exeがあるように、)データファイル形式を変えるなら、 そのデータファイルを作成するコマンドが必要だ。 とはいえ、上のファイル構造さえわかっていれば、 あとはssmファイルを生成するコマンドを作ることは簡単。 今回は、吉里吉里とは全然無関係にssmファイルを生成するスーパーシンプルな コマンドラインツール「mkssmfs.exe」を作成した。「ttstr ≒ wstring だろ!」とか 「tjs_char ≒ wchar_t だろ!」とかそういう割り切りが入ってるので、 気になる人は書き直すと吉。

使い方は至ってシンプルで、以下の通り。オプション?なにそれおいしいの?

# mkssmfs.exe 作成するssmファイル名 ssmに纏めるディレクトリ

これにより、「ssmに纏めるディレクトリ」以下の全てのファイルが、 (ディレクトリ構造なく)「作成するssmファイル名」に纏められる。 たとえば、data ディレクトリ以下の全ファイルを data.ssm に纏めるには、 次のように実行する。事前にmkssmfs.exeにパスを通しておくこと。

# mkssmfs.exe data.ssm data

ほら簡単。

安全のため、mkssmfs.exeは、最初は "作成するssmファイル名.incomplete" で ファイルを作成し、エラーが無ければ最後にこのファイル名から ".incomplete" を 取る。 また、「作成するssmファイル名」が既に存在する場合はエラーになる。 加えて、ディレクトリ中に同名のファイルが存在した場合は作成中にエラーになる。 ちょっとは考えたよ我輩!

ソースコードはVC++2010で作成した。吉里吉里に無関係に 作ったから、吉里吉里のソースコードからtp_stub.hとかをコピーしてくる必要はない。 なんか変更あった時にソレどうなんよというそしりは受けて立つ!(えー)

■ssmファイルを使用するための吉里吉里プラグイン

ということで、吉里吉里からssmファイルを使用するStorageMediaプラグイン、 ssmfs.tpm (ソースコードつきバイナリはこちら) を作成した。 実際にはdllであり、 一番最初に吉里吉里に読ませるために、拡張子を tpm に変更している

吉里吉里がこのプラグインを読み込むことで、TJS上から以下の二つの関数が 使用可能になる。

  1. Storages.mountSSM(ssmファイル名)

    ssmファイル(内ファイル)を吉里吉里からアクセスできるようにする。 例えばkrkr.exeと同じ場所にあるabcd.ssm(内ファイル)をアクセスできるように するなら、以下のように指定する。

    Storages.mountSSM(System.exePath + "abcd.ssm");
    

    成功・失敗をtrue/falseで返す。実際にはファイルが存在しなかったらイキナリ 吉里吉里全体をエラー終了させちゃうけどな!

    StorageMediaプラグインを利用した場合、それが提供するストレージ中の ファイルは、"メディア名://ドメイン名/ディレクトリ/ファイル" のように指定する 必要がある。ssmfs.tpm では、メディア名は "ssm" 固定、ドメイン名は 「ssmファイル名」のファイル名部分を抜き出したものとしている。 従って、この後、実際に abcd.ssm 内のファイルに アクセスするには、"ssm://abcd.ssm/ファイル名" のように指定する必要が あることに注意。以下に例を示す。

    kag.fore.base.loadImages(%[storage:"ssm://abcd.ssm/背景.jpg"]);
    

    "ファイル名" だけでアクセスできるようにしたい時は、以下の例のように、別途どこかで Storages.addAutoPath() を実行しておく必要がある。

    Storages.mountSSM(System.exePath + "abcd.ssm");
    Storages.addAutoPath("ssm://abcd.ssm/");
    (snip)
    kag.fore.base.loadImages(%[storage:"背景.jpg"]);
    

  2. Storages.umountSSM(ドメイン名)

    上のStorages.mountSSM()でアクセスできるようにしたssmファイルを、吉里吉里から 切り離す。以降このssmファイルにはアクセスできなくなる。 例えばドメイン名 abcd.ssm をアクセスできなくするなら、以下のように指定する。

    Storages.unmountSSM("abcd.ssm");
    

    成功・失敗をtrue/falseで返す。

    引数が「ssmファイル名」ではなく「ドメイン名」であることに注意。

加えて、ssmfs.tpm中では、自身が読み込まれた最初に、デフォルトでTJSで言う 二行を実装している。詳細は main.cpp の PostRegistCallback() を読むよろし。 これにより、ssmfs.tpmを読み込ませるだけで、krkr.exeと同じディレクトリにある data.ssm 内のファイルに、パス指定なしにアクセスできるようになっている。

 Storages.mountSSM(System.exePath + "data.ssm");
 Storages.addAutoPath("ssm://data.ssm/"); 

残念ながら、全てのデータをdata.ssmに格納し、ssmfs.tpmと共にkrkr.exeと同じ ディレクトリに配置しても、 それだけではエラーになってゲームは起動できない。 現在の吉里吉里は、「起動時にdataディレクトリまたはdata.xp3が必要」という 制限があるためだ。これを回避するには、以下のいずれかの対策が必要になる。

  1. krkr.exeと同じ場所に、data という空のディレクトリを作る  
  2. krkr.exeと同じ場所に、data.xp3 というダミーのXP3ファイルを作って配置する(xp3形式であれば中身はなんでもよい)

今回のssmfs.tpmで読み込むdata.ssmの方が(後から Storages.addAutoPath()するから) 優先度が高いので、data.ssm中に存在するファイルがdata/やdata.xp3中にあっても、 そっちは単純に無視され、data.ssm中のファイルが使用される。 こういうちょいカッコ悪いことになるのは残念。 これなんとかならんやろか。我輩がなんとかする方法を知らんだけやろか。 TVPBeforeSystemInit()見ると、引数なしにkrkr.exe起動した場合は 避けられないように見えるが…もしなんとかする方法をご存じの方がいたら 教えて下さい。オプションやconf追加なしの方向で。

ssmfs.tpmで提供するssmファイル内ファイルがディレクトリ構成を持たないことは、 TJSスクリプトやKAGスクリプトを組んだ人が(例えば [image storage="fgimage/abc"]のように)ディレクトリ指定しない限り、問題にならない。というか、後にパッチ 当てる時にめんどくさいことになるので、こういうディレクトリ指定は可能な限り しない方がいい。
ただし、実は一か所だけ、KAG3システムではstartup.tjsの末尾で以下のように ディレクトリを指定した部分がある。そのため、ゲームの全ファイルをssmファイルに 纏めるのであれば、事前にここを "system/Initialize.tjs" から "Initialize.tjs" に変更しておく必要がある。

(snip)
// このスクリプトは一番最初に実行されるスクリプトです
// Scripts.execStorage("system/Initialize.tjs"); // system/Initialize.tjs を実行
Scripts.execStorage("Initialize.tjs"); // ↑をこのように変更

ssmfs.tpmのソースコードもVC++2010で作成した。 今は「エラーがあったらとにかくTVPThrowExceptionMessage()で強制終了しちゃう」 という実に男らしいコードになってるので注意。いや注意しても問答無用で 強制終了しちゃうけど。

■ssmfs.tpmソースコードの簡単な説明

クラスリファレンスがなくて作るのにスッゴい苦労したので、簡単にssmfs.tpmの ソースコードについて説明しておく。ソースコード中のコメントも適宜参照頂ければ。

ssmfs.tpmは、大きく三つのクラス定義から構成されている。これらは storage.cpp中で定義され、それぞれ以下のような機能を持つ。

SSMBaseクラス
ssmファイルを操作するクラス。ssmファイルの正当性のチェックや、 ssmファイル内のファイル管理部の読み込み、 指定されたssmファイル内ファイルに対するI/O機能を提供する。

ssmファイル一つにつきインスタンスが一つ作成される。ファイルI/Oはこの クラス内の唯一のIStreamで提供しているため、複数のIStreamからのアクセスを 考慮して、念のためpublicなメンバ関数にはクリティカルセクションを設定している。 要らんと思うんだけどminizip.dllには実装されてたので念のため。

SSMStreamクラス
ssmファイル内ファイルにアクセスするためのIStream継承クラス。 ssmファイル内ファイルを操作する際、吉里吉里から使用される。 実際のI/Oは、ここからSSMBaseクラスのメンバ関数を呼ぶことで行う。 吉里吉里がssmファイル内のファイルにアクセスする度に (SSMStorages::Open()内で)インスタンスが作成される。 従って、(seek()などで使う)ssmファイル内ファイルの現在の読み込み位置情報は、 このクラス中に保持する必要があることに注意。

SSMStoragesクラス
複数のssmファイルを管理するクラス。 iTVPStorageMediaクラスの派生であり、実際には吉里吉里はこれを通して ssmファイル内ファイルにアクセスする。ミソは以下の二つのメンバ関数。 このクラスではI/Oしてないじゃん、と思うだろうが、実際には Open()で得たSSMStreamを使って吉里吉里がI/Oするので、このクラスは I/Oする必要はないのでした。

iTVPStorageMediaクラスでは、ssmファイルをそれに対応する「ドメイン名」で アクセスする。先述のように、今回のssmfs.tpmでは、単純化のため、 指定されたファイル名からパスを取り除いたものをドメイン名として使うように 実装した。 同名のssmファイルを同時に複数使うことはまぁないだろうから、 これで困ることはない(と信じたい)。

例えば、System.execPath + "data.ssm" で指定されたssmファイルは、 ドメイン名="data.ssm" になり、その中のファイル "abc,jpg" は、以降 "ssm://data.ssm/abc.jpg" でアクセス可能になる。 TJS上で "Storages.addAutoPath('ssm://data.ssm/')" と 指定しておけば、KAGスクリプト・TJSスクリプト上でパスを省略して "abc.jpg" と 書くだけでもアクセス可能になる。

この他のクラス…SSMFileEntryはssmファイル内ファイルの情報を保持するだけだし、 SSMStoragesクラスは初期化関数の提供とStorageクラスにメンバ関数を 追加するだけなので、まぁ…両方知らなくてもいいん違う?

おまけ。ssmfsutil.h 冒頭の "#define DEBUG 0" を 1 に変更してコンパイルすると、 このプラグインが動いた時に、log()で指定された文字列がコンソールにもりもり 出力されるようになる。 StorageMediaプラグインの動きを知りたい人は、やってみると吉かもよ?

■簡単に使ってみる

今現在、以下のようなファイル構成で、普通に動く吉里吉里のゲームがあったとし、 そこでこのゲームをssmファイル上で動作させることを考える。

C:\Users\my\Desktop\ssmfstest>dir
 ドライブ C のボリューム ラベルは System です
 ボリューム シリアル番号は 1234-5678 です

 C:\Users\my\Desktop\ssmfstest のディレクトリ

2016/03/16  16:12    <DIR>          .
2016/03/16  16:12    <DIR>          ..
2016/03/16  16:12    <DIR>          data
2013/04/12  00:01         3,656,192 krkr.eXe
2016/03/18  13:43    <DIR>          plugin
2016/03/01  15:34    <DIR>          savedata
               1 個のファイル           3,656,192 バイト
               5 個のディレクトリ  59,389,132,800 バイトの空き領域

C:\Users\my\Desktop\ssmfstest>

手順は以下の三つだけ。あ、krkr.exeにも StorageMedia 周りのバグがあったので、krkr.exeを結構新しいの(2.32r2推奨)にしとかないと EAccessViolationとか 言われて終わるので注意。

  1. まず、data/startup.tjsの末尾を編集し、"system/Initialize.tjs" から "Initialize.tjs" に変更する。
  2. 次に、このディレクトリ上で、以下のコマンド群を実行する。 先にmkssmfs.exeをパスの通ったところに置いておくこと。

    > mkssmfs.exe data.ssm data
      (沢山メッセージ出るが最後に Done. と表示されて data.ssm が作成されればO.K.)
    > rename data data_org
      (dataディレクトリは使わないので削除してもよいが、一応別名で取っておく)
    > mkdir data
      (空のdataディレクトリを作る)
    

  3. 最後に、作っておいた ssmfs.dll を、krkr.exe と 同じディレクトリ(またはpluginディレクトリの中)に ssmfs.tpm という名前でコピー

以上ッ!これで、krkr.eXeを実行すると、data.ssmが自動的に読み込まれ、 data.ssm中のファイル「のみ」を使って動作するようになる。お試しあれッ! あ、試す前には必ずどっかにバックアップ取っといてね!(瑕疵担保責任回避的)。

今後パッチを適用することを考えると、本当は data/system/Initialize.tjs で Storages.addAutoPath()やuseArchiveIfExists()している箇所もssmファイルに 合わせて変更すべきだが、本書ではそれは重要ではないので、 無責任にも放置することにする。ご要望あれば追記します。

上の状態から ssmファイルを使わないように戻すなら、空のdataディレクトリと data.ssmとssmfs.tpmファイル消して、data_orgをdataに改名するだけ。 これも簡単。

■おわりに

というわけで、随分苦労したけれど、XP3に代わる 独自フォーマットのデータファイル形式 ssm を実装することができたよ。 我輩が作った部分 (ssmfs中のncb*とtp_*を除く)は WTFPLにするので好きに使って頂いて結構ですともさ。

今後は、これに以下の機能を付けてみたい。そうすれば、改ざん防止(ひいては アンチクラック)に一定の効果が見込めるだろうから。

破損確認のためにはファイル全体をチェックする必要があるため、読み込み性能が 低下する。従って、上を実現するには、data.ssmファイル中に配置するファイルを スクリプトなど小さいものに限り、 大きなファイルは今まで通りdata.xp3中に配置する、など考慮する必要があるだろう。

他にも、アンチクラックの仕組みをもりもり組み込めると無駄に楽しげ。 例えば読み込まれてるプラグインをチェックして、怪しげなのがあったら 割れ防止のために以降のI/Oをスッゲー遅くするとか(※1980年代のPCゲームの プロテクトに実際にあったアツい方法)。こういうのって考えただけで楽しくなる。 ♪夢は膨らむ、ディスクシステム!(ちゃりーん)