SkyrimのModをプログラムで生成する - 1 - 基本のデータ構造など

# はじめに
この記事は、SkyrimというゲームソフトのModファイルのバリナリ解析とそれにそれを利用してModを生成するC#のコードについて記載していくシリーズ物とします。
以前に、バリナリ解析について以下の記事を書いていますが、それの実践版のようなものです。
http://qiita.com/rrryutaro/items/032aa75351842ab7885f

これまで作ってきたプログラムが大分汚くなってきたことや、Visual Studio 2017も出ていることなので、作り直すことにしました。
なお、趣味で適当に作っているプログラムで、仕組みなどはもっとよい方法があるかと思います。また、ソースコードなどほとんどコメントもいれていませんし、ネーミングセンスも悪いです。

#SkyrimのModのファイルフォーマットについて
以下のサイトで解説されています。
http://en.uesp.net/wiki/Tes5Mod:Mod_File_Format

これを知ったのはつい最近で、これまでは自己流で解析していました。
もっとも、構造自体は難しくないため、元のファイルを読込んで、再度書き出すといったことや、一部のデータを書きかえる程度であれば、それに対応する仕組みを用意すれば、自動生成できるようになります。

SkyrimのModのファイルフォーマットの基本構造は"GRUP"という文字列の後に、uint型でデータサイズがあり、これが大きな一組のデータになります。
SSEEditで表示されるツリーの各ノード(Action, Activatorなど)がコレにあたります。
![image](https://qiita-image-store.s3.amazonaws.com/0/156225/205146f3-e81c-79bd-e160-6470859cfd48.png)

次に、その中にレコードとしてそのカテゴリの1つぶんのデータが書き込まれています。例えば武器データなら、武器1つ分です。
![image](https://qiita-image-store.s3.amazonaws.com/0/156225/6920c58f-caee-ac28-11bd-19341edf65ae.png)

レコード内にはさらに、その内容に応じて設定値を表現するフィールドがあります。武器の名称や、ダメージの値などです。
上図での右ペインのリスト内容がコレにあたります。

なお、一部のデータは構造が異なりますが、この辺を押さえておけば大体のことができます。

#基本のデータ型など
まず、基本となるインタフェースと基本となるバイトデータを扱うクラスを用意しました。
以下のソースコードはとりあえず最低限を記載しています。

```C#
    public interface ITesBase
    {
        TesBytes ToBytes();
        uint Recalc();
    }
```

```C#
    public class TesBytes : List<byte>, ITesBase
    {
        public TesBytes()
        {
        }
        public TesBytes(TesBytes value)
        {
            this.AddRange(value);
        }
        public TesBytes(byte[] value)
        {
            this.AddRange(value);
        }
    }
```

`ITesBase`インタフェースの`ToBytes`と`Recalc`は各データを扱うクラスに実装し、ファイル出力時に呼出して、変更箇所のサイズ計算を行うために使用します。
`TesBytes`はバイトリストを継承する形にしました。とりあえず、追加削除が楽なのでこの形にしています。

次に、各データの区切毎に配列で扱えるように、インターフェースを実装したリストを作成します。

```C#
    public class TesList<T> : List<T>, ITesBase where T : ITesBase
    {
        public TesBytes ToBytes()
        {
            TesBytes result = new TesBytes();
            foreach (var x in this)
            {
                result.AddRange(x.ToBytes());
            }
            return result;
        }
        public uint Recalc()
        {
            uint result = 0;
            foreach (var x in this)
            {
                result += x.Recalc();
            }
            return result;
        }
    }
```

次に、各データ要素を扱う基本クラスを用意します。

```C#
    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();
        }
    }
```

基本となる仕組みは`OutputItems`のリストに特定の区切ごとのデータを格納し、データの順番を保持するようにします。

#ファイルリーダーの作成
ファイル読込については次のようにしています。
Modのファイルを読込み、"GRUP"から次の"GRUP"までを個別のリーダーとして利用するため、内部ではバリナリリーダーを共有し、ポジションとレングスで範囲を読込めるようにしたつもりです。
巨大内グループ要素などはOutofmemoryを引き起こすので、フィールドなど少ない範囲を読み取る際に実際にバイト読み込みを行い、それまでは、ポジションをずらしていく形です。

```C#
    public class TesFileReader
    {
        private BinaryReader br;
        private long pos;
        private long len;
        private bool cp;

        public TesFileReader(string path)
        {
            br = new BinaryReader(new FileStream(path, FileMode.Open, FileAccess.Read));
            pos = 0;
            len = br.BaseStream.Length;
            cp = false;
        }
        private TesFileReader(BinaryReader br, long pos, long len)
        {
            this.br = br;
            this.pos = pos;
            this.len = len;
            this.cp = true;
        }
        ~TesFileReader()
        {
            if (!cp)
                br.Close();
        }
        public bool EOF
        {
            get
            {
                bool result = len <= pos;
                return result;
            }
        }
        public void Seek(long count)
        {
            pos += count;
        }
        private TesBytes Read(long count, bool next)
        {
            if (pos != br.BaseStream.Position)
                br.BaseStream.Seek(pos, SeekOrigin.Begin);

            if (Int32.MaxValue < count)
                throw new Exception();

            TesBytes result = new TesBytes(br.ReadBytes((int)count));

            if (!next)
                br.BaseStream.Seek(-count, SeekOrigin.Current);

            return result;
        }
        public TesBytes GetBytes(long count, long offset = 0, bool next = true)
        {
            if (len < pos + count + offset)
                throw new Exception();

            TesBytes result = Read(offset + count, next);
            if (0 < offset)
                result = result.GetOffset(offset, count);

            if (next)
                pos += count;

            return result;
        }
    }
```

いやぁ～ それにしても、記事を書くって大変ですね。
この情報が他の誰に有益なのだろうかとも思いますが、このシリーズはとりあえず書くだけ書いて、その上で役に立つ情報となるようにしていこうと考えています。

以上、今回はここまでです。

|前回|次回|
|:--|--:|
||[2 - グループを扱う](http://qiita.com/rrryutaro/items/220515d8971c26f786df)|