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

  • 0
    いいね
  • 0
    コメント

    はじめに

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

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

    Modファイル用クラス

        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クラスでは正しく読み取り出来ないため、専用のクラスを用意しました。

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

    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を継承して、各種要素を細かく扱えるクラスを用意した場合、個別のプロパティを用意するようにします。

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

    各クラスの修正

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

    ITesBase
        public interface ITesBase
        {
            TesBytes ToBytes();
            uint Recalc();
            uint ItemCount();
            void Write(BinaryWriter bw);
        }
    

    当初はToBytes()と、Recalc()だけ用意していましたが、レコード数をカウントするためのメソッドItemCountと書き込み用のメソッドWriteを追加しました。

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

    インターフェースに追加したメソッドの追加に対応しました。

    TesGroup
            public override uint ItemCount()
            {
                uint result = OutputItems.ItemCount() + 1;
                return result;
            }
    

    グループクラスでは、ItemCountで自身分を含めてカウントするため+1しています。

    TesRecord
            public override uint ItemCount()
            {
                return 1;
            }
    

    レコードクラスでは、レコードそのものがカウントの対象のため1を返します。

            public uint ItemCount()
            {
                uint result = 0;
                return result;
            }
            public void Write(BinaryWriter bw)
            {
                bw.Write(ToBytes().ToArray());
            }
    

    各データ型を扱うクラス(TesBytes, TesUInt16, TesUInt32, TesFloat, TesString)についてはItemCount0を返すようにし、Writeで書込みするようにします。

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

    以上

    前回 次回
    4 - フィールドを扱う 6 - セル情報とワールド情報を扱う