はじめに
この記事は、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
クラスでは正しく読み取り出来ないため、専用のクラスを用意しました。
CELL
とWRLD
について
セル情報とワールド情報を扱うレコードは通常の構造より複雑になっています。このためこれらも専用のクラスを用意して読み取るようにしています。
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
を継承して、各種要素を細かく扱えるクラスを用意した場合、個別のプロパティを用意するようにします。
・・・まぁなんというかつたない内容ですが、こんな感じに作ってみました。
各クラスの修正
今回の内容に合わせて各クラスを修正します。なお、セル情報などは次回以降で説明いたします。
public interface ITesBase
{
TesBytes ToBytes();
uint Recalc();
uint ItemCount();
void Write(BinaryWriter bw);
}
当初はToBytes()
と、Recalc()
だけ用意していましたが、レコード数をカウントするためのメソッドItemCount
と書き込み用のメソッドWrite
を追加しました。
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);
}
}
インターフェースに追加したメソッドの追加に対応しました。
public override uint ItemCount()
{
uint result = OutputItems.ItemCount() + 1;
return result;
}
グループクラスでは、ItemCount
で自身分を含めてカウントするため+1
しています。
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
)についてはItemCount
は0
を返すようにし、Write
で書込みするようにします。
今回の内容で、一応Modファイルを読み取り、変更を加えて、出力することができるようになったハズです。
次回以降はいくつかの特殊な構造のクラスなどの説明や、実際の利用方法などを説明していきたいと思います。
以上
前回 | 次回 |
---|---|
4 - フィールドを扱う | 6 - セル情報とワールド情報を扱う |