LoginSignup
4
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-04-01

はじめに

この記事は、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 - グループを扱う
4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6