SkyrimのModをプログラムで生成する - 6 - セル情報とワールド情報を扱う

  • 0
    いいね
  • 0
    コメント

    はじめに

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

    前回はModファイルのクラスについて扱いました。今回はその際に登場したセル情報とワールド情報を扱うクラスについてです。

    セル情報を扱うクラス

    セル情報は他と構造が異なります。基本的には次のようになっています。

    + グループ
        - グループヘッダー
        + ブロック(グループ)
            - グループヘッダー
            + サブブロック(グループ)
                - グループヘッダー
                + セル(レコード)
                    - レコードヘッダー
                    - セル情報
                    + セル情報詳細(グループ)
                        - グループヘッダー
                        + セル配置(グループ)
                            - グループヘッダー
                            + セルデータ(レコード)
                                - レコードヘッダー
                                + フィールド
                                    - フィールドヘッダー
                                    - フィールドデータ
    

    SSEEditで見ると次のようになっています。
    image

    通常は、グループの中にはレコードがあるだけですが、セルの場合はグループが入れ子になっています。
    まずは、セル全体のグループを扱うクラスです。

        public class TesCell : TesGroup
        {
            public TesList<TesCellBlock> Blocks = new TesList<TesCellBlock>();
    
            public TesCell(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    Blocks.Add(new TesCellBlock(fr.GetGroup()));
                }
                OutputItems.Add(Blocks);
            }
        }
    

    これまでは、グループの後はレコードが続くため、レコードの読み取りを行っていましたが、セルではレコードが続くわけではないため、レコードの読み取りを行わないように、TesGroupクラスのコンストラクタを変更しています。

    TesGroup
            public TesGroup(TesFileReader fr, bool readRecord = true)
            {
                GRUP = new TesString(fr);
                DataSize = new TesUInt32(fr);
                Other = new TesBytes(fr.GetBytes(16));
                OutputItems.Add(GRUP);
                OutputItems.Add(DataSize);
                OutputItems.Add(Other);
    
                if (readRecord)
                {
                    while (!fr.EOF)
                    {
                        Records.Add(new TesRecord(fr.GetRecord()));
                    }
                }
                OutputItems.Add(Records);
            }
    

    続いて、ブロックのグループ

        public class TesCellBlock : TesGroup
        {
            public TesList<TesCellSubBlock> SubBlocks = new TesList<TesCellSubBlock>();
    
            public TesCellBlock(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    SubBlocks.Add(new TesCellSubBlock(fr.GetGroup()));
                }
                OutputItems.Add(SubBlocks);
            }
        }
    

    続いてブロックの中のサブブロック

        public class TesCellSubBlock : TesGroup
        {
            public TesList<TesRecordCell> Cells = new TesList<TesRecordCell>();
    
            public TesCellSubBlock(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    Cells.Add(new TesRecordCell(fr.GetCell()));
                }
                OutputItems.Add(Cells);
            }
        }
    

    ブロックとサブブロックはSSEEditで見た感じではBlock 0Block 9までの範囲で各ブロック内はそれぞれSub-Block 0Sub-Block 9までの範囲となっていると思われます。Creation Kitでセルを新規作成するといずれかのブロックのいずれかのサブブロックに割り当てられるようで、どのように設定されるのかの詳細は不明です。
    また、この後に続くセルそのものについての情報を扱うレコードについても解析できていませんので、プログラムで新規生成するのは難しく、現状では一旦Creation Kitでセルを作成し、そこにデータを追加する形となります。

    なお、ここまでは基本的にグループ範囲を読み取っていましたが、セルについては別途読み取り範囲の方法が異なるので、読み取りようのメソッドを追加しました。

    TesFileReader
            public TesFileReader GetCell(bool next = true)
            {
                //CELLレコードのサイズ
                long count = GetUInt32(4, false) + 24;
    
                //読込みサイズがファイルサイズと一致する場合、GRUPなし
                if (pos + count < len && GetTypeID(count).Equals("GRUP"))
                {
                    //CELLレコード後のGROUPのサイズを加算
                    count += GetUInt32(count + 4, false);
                }
    
                TesFileReader result = new TesFileReader(br, pos, pos + count);
                if (next)
                    pos += count;
    
                return result;
            }
    

    セル内に「セル情報詳細(グループ)」が無く、セルそのもののレコードしかないケースもあるため、読み取り範囲のチェックを行っています。

    続いて、1つのセルを扱うクラスですが、グループのようなものではあるのですが、Modファイル内での形式としてはレコードなので、レコードクラスを継承し、レコードデータと共にグループを内包するクラスとなります。

       public class TesRecordCell : TesRecord
        {
            public TesBytes CellInfo { get; }
            public TesCellMain CellMain { get; }
    
            public TesRecordCell(TesFileReader fr) : base(fr, false)
            {
                CellInfo = new TesBytes(fr.GetBytes(Header.DataSize));
                OutputItems.Add(CellInfo);
    
                if (!fr.EOF)
                {
                    CellMain = new TesCellMain(fr.GetGroup());
                    OutputItems.Add(CellMain);
                }
            }
            public override uint Recalc()
            {
                uint result = OutputItems.Recalc();
                Header.DataSize.Value = (uint)CellInfo.Count;
                return result;
            }
            public override uint ItemCount()
            {
                uint result = 1;
                if (CellMain != null)
                    result += CellMain.ItemCount();
                return result;
            }
        }
    

    TesGroupと同様に、TesRecordでも、コンストラクタを変更します。

    TesRecord
            public TesRecord(TesFileReader fr, bool readFiled = true)
            {
                Header = new TesHeader(fr);
                OutputItems.Add(Header);
    
                if (readFiled)
                {
                    if (Header.Signature == "NAVM" || Header.Signature == "LAND")
                    {
                        OutputItems.Add(new TesBytes(fr.GetBytes(Header.DataSize)));
                    }
                    else
                    {
                        while (!fr.EOF)
                        {
                            TesField field = ReadField(fr) ?? new TesField(fr.GetField());
                            AddField(field);
                        }
                    }
                }
            }
    

    また、この後の説明でする「セルデータ(レコード)」にあるナビメッシュなどは、通常のレコードとは異なる他、解析できていませんので、そのままバイトデータを読み取るようにしています。

    では続いて、セル情報詳細を扱うクラスです。

        public class TesCellMain : TesGroup
        {
            public TesList<TesCellMainSub> Subs = new TesList<TesCellMainSub>();
    
            public TesCellMain(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    Subs.Add(new TesCellMainSub(fr.GetGroup()));
                }
                OutputItems.Add(Subs);
            }
        }
    

    続いて、セル配置を扱うクラスです。

        public class TesCellMainSub : TesGroup
        {
            public Dictionary<string, TesList<TesRecord>> DicRecords { get; } = new Dictionary<string, TesList<TesRecord>>();
    
            public TesCellMainSub(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    AddRecord(new TesRecord(fr.GetRecord()));
                }
            }
            public void AddRecord(TesRecord record)
            {
                Records.Add(record);
                if (!DicRecords.ContainsKey(record.Header.Signature))
                    DicRecords.Add(record.Header.Signature, new TesList<TesRecord>());
    
                DicRecords[record.Header.Signature].Add(record);
            }
            public override uint ItemCount()
            {
                uint result = (uint)Records.Count + 1;
                return result;
            }
        }
    

    SSEEditでの、PersistentTemporaryにあたる部分です。
    ここにセル内に配置するあらゆるオブジェクトがレコードして登録されます。
    なお、どっちか片方か、両方の2つしかないため、リストにしなくても良いのかもしれませんが、とりあえずリストにしています。

    これで、セル情報を扱えるようになりました。

    ワールド情報を扱うクラス

    ワールド情報はほぼセル情報と同じような形式ですが、まずワールド情報があり、各ワールド内に直接セル情報が続く場合と、セルと同じくブロック、サブブロック、セルとなる場合があります。

    + グループ
        - グループヘッダー
        * (繰り返し)
            - ワールド情報(レコード)
            + ワールド情報詳細(グループ)
                - グループヘッダー
                    + セル情報詳細(グループ) ※ある場合
                        ※以降はセルの時と同じ
                    + ブロック(グループ) ※ある場合
                        ※以降はセルの時と同じ
    

    ではまず、ワールド情報全体を扱うクラスです。

        public class TesWorldspace : TesGroup
        {
            public new TesList<TesRecordWorldspace> Records = new TesList<TesRecordWorldspace>();
    
            public TesWorldspace(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    Records.Add(new TesRecordWorldspace(fr.GetCell()));
                }
                OutputItems.Add(Records);
            }
        }
    

    各ワールドについては、「ワールド情報(レコード)」と「ワールド情報詳細(グループ)」が対になっています。
    これは、「セル(レコード)」の時と同じ方法で読み取れます。

    続いて、ワールド情報を扱うクラスです。

        public class TesRecordWorldspace : TesBase
        {
            public Dictionary<string, TesList<TesField>> Fields { get; } = new Dictionary<string, TesList<TesField>>();
    
            public TesRecord WRLD { get; }
            public TesRecordWorldspaceMain Main { get; }
    
            public TesRecordWorldspace(TesFileReader fr)
            {
                WRLD = new TesRecord(fr.GetRecord());
                Main = new TesRecordWorldspaceMain(fr.GetGroup());
                OutputItems.Add(WRLD);
                OutputItems.Add(Main);
            }
        }
    

    「ワールド情報(レコード)」と「ワールド情報詳細(グループ)」を対で扱うためにTesBaseを基底として、それぞれのクラスを持つようにしました。

    最後に、ワールド情報詳細を扱うクラスです。

        public class TesRecordWorldspaceMain : TesGroup
        {
            public TesRecordCell Cell { get; }
            public TesList<TesCellBlock> Blocks { get; }
    
            public TesRecordWorldspaceMain(TesFileReader fr) : base(fr, false)
            {
                while (!fr.EOF)
                {
                    string id = fr.GetTypeID();
                    if (id.Equals("CELL"))
                    {
                        Cell = new TesRecordCell(fr.GetCell());
                        OutputItems.Add(Cell);
                    }
                    else if (id.Equals("GRUP"))
                    {
                        if (Blocks == null)
                        {
                            Blocks = new TesList<TesCellBlock>();
                            OutputItems.Add(Blocks);
                        }
                        Blocks.Add(new TesCellBlock(fr.GetGroup()));
                    }
                }
            }
        }
    

    ワールド情報詳細では1つのセルがある場合と、複数のブロックがある場合がありますので、直前のSignatureをチェックして読み取るようにしています。
    なお、セルの時とは異なり、ブロックとサブブロックはX軸とY軸での座標形式となっているようです。
    SSEEditで見ると次のようになっています。
    image

    以上、これでセル情報とワールド情報が扱えるようになりました。
    この2つが特に特別な構造となっていますので、これでほぼ大体のデータの読み取りと変更が行えるようになったと思います。

    以上

    前回 次回
    5 - Modファイルを扱う 7 - ストリングテーブルを扱う