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

  • 0
    いいね
  • 0
    コメント

    はじめに

    この記事は、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

    次に、その中にレコードとしてそのカテゴリの1つぶんのデータが書き込まれています。例えば武器データなら、武器1つ分です。
    image

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

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

    基本のデータ型など

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

        public interface ITesBase
        {
            TesBytes ToBytes();
            uint Recalc();
        }
    
        public class TesBytes : List<byte>, ITesBase
        {
            public TesBytes()
            {
            }
            public TesBytes(TesBytes value)
            {
                this.AddRange(value);
            }
            public TesBytes(byte[] value)
            {
                this.AddRange(value);
            }
        }
    

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

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

        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;
            }
        }
    

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

        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を引き起こすので、フィールドなど少ない範囲を読み取る際に実際にバイト読み込みを行い、それまでは、ポジションをずらしていく形です。

        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 - グループを扱う