SkyrimのModをプログラムで生成する - 5 - Modファイルを扱う

#はじめに
この記事は、SkyrimというゲームソフトのModファイルのバリナリ解析とそれを利用してModを生成するC#のコードについて記載していくシリーズ物です。
ファイルフォーマットについての詳しい情報は以下のサイトがあります。(以降UESP)
[Tes5Mod:Mod File Format - The Unofficial Elder Scrolls Pages (UESP)](http://en.uesp.net/wiki/Tes5Mod:Mod_File_Format)

前回はフィールドについて扱いました。これで、一通りの形式を読込むための基礎ができたので、Modファイルそのものを扱うようにし、各クラスを整理します。

#Modファイル用クラス

```C#
    public class TesFile : TesBase
    {
        public TesTES4 TES4 { get; }
        public Dictionary<string, TesGroup> Groups { get; set; } = new Dictionary<string, TesGroup>();

        public TesFile(string path, List<string> idList = null)
        {
            TesFileReader fr = new TesFileReader(path);

            TES4 = new TesTES4(fr.GetRecord());
            OutputItems.Add(TES4);

            string id = null;
            while (!fr.EOF)
            {
                id = fr.GetTypeID(8);

                if (idList != null && !idList.Contains(id))
                {
                    fr.Seek(fr.GetUInt32(4, false));
                    continue;
                }

                switch (id)
                {
                    case "NPC_":
                        TesNPC npc_ = new TesNPC(fr.GetGroup());
                        OutputItems.Add(npc_);
                        Groups.Add(id, npc_);
                        break;
                    case "CELL":
                        TesCell cell = new TesCell(fr.GetGroup());
                        OutputItems.Add(cell);
                        Groups.Add(id, cell);
                        break;
                    case "WRLD":
                        TesWorldspace wrld = new TesWorldspace(fr.GetGroup());
                        OutputItems.Add(wrld);
                        Groups.Add(id, wrld);
                        break;
                    case "DIAL":
                        OutputItems.Add(new TesBytes(fr.GetBytes(fr.GetUInt32(4, false))));
                        break;
                    case "HAZD":
                        string id2 = fr.GetTypeID(24);
                        if (id2.Equals("HAZD"))
                            OutputItems.Add(new TesBytes(fr.GetBytes(fr.GetUInt32(4, false))));
                        else if (id2.Equals("GRUP"))
                            OutputItems.Add(new TesBytes(fr.GetBytes(fr.GetUInt32(4, false))));
                        break;
                    default:
                        TesGroup grup = new TesGroup(fr.GetGroup());
                        OutputItems.Add(grup);
                        Groups.Add(id, grup);
                        break;
                }
            }
        }

        public void Recalc()
        {
            uint count = 0;
            foreach (var x in OutputItems)
            {
                x.Recalc();
                count += x.ItemCount();
            }

            TES4.HEDR.NumberOfRecords.Value = count;
        }

        public void Save(string path)
        {
            Recalc();

            using (FileStream fs = new FileStream(path, FileMode.Create))
            using (BinaryWriter bw = new BinaryWriter(fs))
            {
                OutputItems.Write(bw);
            }
        }

        public bool Contains(string signature)
        {
            bool result = Groups.ContainsKey(signature);
            return result;
        }
        public TesGroup this[string signature]
        {
            get
            {
                TesGroup result = Groups[signature];
                return result;
            }
        }
        public TesCell Cell
        {
            get
            {
                TesCell result = null;
                if (Contains("CELL"))
                    result = (TesCell)this["CELL"];
                return result;
            }
        }
    }
```
コンストラクタの引数にファイルのパスと、読込む要素を指定するリストを受け取るようにします。
読込む要素のリストは、`Skyrim.esm`など特定の情報のみ読み取りしたい場合に利用します。
最初に固定で`TES4`部分をレコードとして読み取り、後はファイルの終わりまでグループを読み取り、`Signature`をキーとしてディクショナリーに追加していきます。
特定の`Signature`(`id`)については、特殊な対応が必要です。

**`NPC_` について**
`NPC_`は`Actor`の情報を扱っているレコードですが、フィールドについて`Signature`がなく、レコードヘッダー以降のレコードデータ部分が圧縮形式のようなデータとなっています。（この辺は全然解析が出来ていなくて詳細がわかりません）
このため、`TesGroup`クラスでは正しく読み取り出来ないため、専用のクラスを用意しました。

**`CELL`と`WRLD` について**
セル情報とワールド情報を扱うレコードは通常の構造より複雑になっています。このためこれらも専用のクラスを用意して読み取るようにしています。

**`DIAL` について**
ダイアログトピック情報を扱うレコードですが、こちらも特殊なデータ形式になっていたのか、十分に解析が出来ておらず、グループ全体を丸ごとただ読み取っているだけです。

**`HAZD` について**
`HAZD`については、どういうわけかファイル中に2箇所存在し、グループヘッダーの後に`HAZD`でレコードが続くものと、さらに`GRUP`でデータが存在してるようで、このように読み取るようにしています。
UESPによると、2つ目はレコードが無いとあるので、そのように処理してもよいかもしれません。

**その他について**
その他については、通常の構造をしていますので、`TesGroup`として読み取りします。

**`Recalc`**
`Recalc`では、バイト数の再計算を行い、ヘッダーの`DataSize`を修正すると共に、`TES4`のModファイル全体のレコード数の設定を行います。
※現在、レコード数の計測は正しく行えていません。このため最終調整としてSSEEditで読込み、保存してレコード数を正しく設定させる必要があります。

**`Save`**
再計算後、出力用のファイルに全てのバイトデータを書き出します。
この際、当初は`ToBytes()`を使っていましたが、データ量が多い場合当然ながらメモリオーバーしてしまうため、`Write`メソッドを`ITesBase`に追加し、末端でバイナリリーダーに書き込む形に変更しました。

**`Contains`**
指定の要素が存在するかのチェックのために用意。

**`this[]`**
全てのグループを`Signature`でアクセスできる用にそのままディクショナリーの`Groups`をパブリックにしていますが、インデクサーがあるとその後扱いやすいので用意しています。

**`Cell`**
セル情報など個別のクラス用のプロパティを用意しています。
といってもとりあえず`Cell`だけですが、今後`TesGroup`を継承して、各種要素を細かく扱えるクラスを用意した場合、個別のプロパティを用意するようにします。


・・・まぁなんというかつたない内容ですが、こんな感じに作ってみました。

#各クラスの修正
今回の内容に合わせて各クラスを修正します。なお、セル情報などは次回以降で説明いたします。

```C#:ITesBase
    public interface ITesBase
    {
        TesBytes ToBytes();
        uint Recalc();
        uint ItemCount();
        void Write(BinaryWriter bw);
    }
```
当初は`ToBytes()`と、`Recalc()`だけ用意していましたが、レコード数をカウントするためのメソッド`ItemCount`と書き込み用のメソッド`Write`を追加しました。

```C#:ITesBase
    public class TesBase : ITesBase
    {
        public TesList<ITesBase> OutputItems { get; } = new TesList<ITesBase>();

        public TesBase()
        {
        }

        public virtual TesBytes ToBytes()
        {
            return OutputItems.ToBytes();
        }
        public virtual uint Recalc()
        {
            return OutputItems.Recalc();
        }
        public virtual uint ItemCount()
        {
            return OutputItems.ItemCount();
        }

        public virtual void Write(BinaryWriter bw)
        {
            OutputItems.Write(bw);
        }
    }
```
インターフェースに追加したメソッドの追加に対応しました。

```C#:TesGroup
        public override uint ItemCount()
        {
            uint result = OutputItems.ItemCount() + 1;
            return result;
        }
```
グループクラスでは、`ItemCount`で自身分を含めてカウントするため`+1`しています。

```C#:TesRecord
        public override uint ItemCount()
        {
            return 1;
        }
```
レコードクラスでは、レコードそのものがカウントの対象のため`1`を返します。

```C#
        public uint ItemCount()
        {
            uint result = 0;
            return result;
        }
        public void Write(BinaryWriter bw)
        {
            bw.Write(ToBytes().ToArray());
        }
```
各データ型を扱うクラス(`TesBytes`, `TesUInt16`, `TesUInt32`, `TesFloat`, `TesString`)については`ItemCount`は`0`を返すようにし、`Write`で書込みするようにします。

今回の内容で、一応Modファイルを読み取り、変更を加えて、出力することができるようになったハズです。
次回以降はいくつかの特殊な構造のクラスなどの説明や、実際の利用方法などを説明していきたいと思います。

以上

|前回|次回|
|:--|--:|
|[4 - フィールドを扱う](http://qiita.com/rrryutaro/items/d49b7b610128fb69261f)|[6 - セル情報とワールド情報を扱う](http://qiita.com/rrryutaro/items/f9e37d761ca62303b343)|