はじめに
この記事は、SkyrimというゲームソフトのModファイルのバリナリ解析とそれを利用してModを生成するC#のコードについて記載していくシリーズ物です。
趣味で作っているものなので、あまり役立つ情報ではないかもしれませんが、他にバリナリデータを扱う場合や、自分自身のために記事を書いて整理することで、色々と見直しをかけることや備忘録として役立てようと思っています。
なお、SkyrimのModのファイルフォーマットについての解説などは、趣味のブログ(はてなブログ)で解説を書くようにしています。
これは、Skyrimの話の中心はブログで。
コードなど技術情報として共有できそうなことはQiitaで書くようにしているためです。
Skyrim Mod ファイルフォーマット解説 - 1 - RのSkyrim Mod開発日記
グループを扱うクラス
SkyrimのModのファイルフォーマットの基本構造は次のようになっています。
+ グループ
+ レコード
+ フィールド
多少異なりますが、簡単には、先頭4byteがデータ内容を示す文字列、次の4byte(フィールドは2byte)がデータサイズを示しています。
少し詳しく説明するとグループ、レコード、フィールドにはそれぞれ先頭にヘッダーがあります。グループとレコードは24byte、フィールドは6byteです。
この内、グループのデータサイズはヘッダーを含むサイズで、レコードとフィールドはヘッダーを含まないデータサイズとなっています。
このため、グループは"GRUP"の後、4byteのデータサイズ分を取り込めばひと固まりとなり、これを繰り返すことになります。
以下、それに対応するコードです。
public class TesGroup : TesBase
{
public TesBytes GRUP { get; }
public TesBytes DataSize { get; }
public TesBytes GrupData { get; }
public TesGroup(TesFileReader fr)
{
GRUP = fr.GetBytes(4);
DataSize = fr.GetBytes(4);
GrupData = fr.GetBytes(DataSize);
}
}
これだけでも、一応一通りのデータを取り込むことができるはずです。
しかし、使い勝手が悪いので、文字型や数値型などを扱うクラスを用意することにしました。
基本データ型を扱うクラス
文字型
[System.Diagnostics.DebuggerDisplay("{Value}")]
public class TesString : ITesBase
{
public string Value { get; set; }
public bool isNullTerminated;
public TesString(TesBytes value)
{
if (value[value.Count() - 1] == 0x00)
isNullTerminated = true;
this.Value = value.ToString();
}
public TesString(TesFileReader fr)
{
this.Value = fr.GetString(4);
this.isNullTerminated = false;
}
public TesBytes ToBytes()
{
TesBytes result = new TesBytes(Value, isNullTerminated);
return result;
}
public uint Recalc()
{
uint result = ToBytes().Recalc();
return result;
}
/// <summary>
/// stringへの代入時に暗黙の型変換
/// </summary>
/// <param name="obj"></param>
public static implicit operator string(TesString obj)
{
return obj.Value;
}
}
[System.Diagnostics.DebuggerDisplay("{Value}")]
はデバッグ時に文字列がわかるようにするために設定しています。
また、SkyrimのModで扱う文字列はNULL終端であるものとないものとがあるため、終端のチェックをするようにしています。
通常はTesBytes
を受け取って初期化するようにしますが、多くの場面で4バイトだけ読み取ることが多いため、Modのファイルリーダーを引数に受け取った場合、4バイトだけ読み取るようにしています。(あまり好ましい設計ではないかもしれませんが、楽なのでこうしています)
このようにクラス内に文字列の実体を持たせることで、文字列を書きかえても、ファイル出力時にToBytes()
で書き出すことで、データの順序が狂わないようにしています。
ただ、読み取る際にValueプロパティを参照するのはやや面倒であったため、string型へ代入する際などは暗黙の型変換を利用するようにしています。
続いて数値型です。
[System.Diagnostics.DebuggerDisplay("{Value} ({System.BitConverter.ToString(ToBytes().ToArray()).Replace(\"-\", \" \")})")]
public class TesUInt32 : ITesBase
{
public uint Value { get; set; }
public TesUInt32(uint value)
{
this.Value = value;
}
public TesUInt32(TesFileReader fr)
{
this.Value = fr.GetUInt32();
}
public TesBytes ToBytes()
{
TesBytes result = new TesBytes(Value);
return result;
}
public uint Recalc()
{
uint result = ToBytes().Recalc();
return result;
}
/// <summary>
/// uintへの代入時に暗黙の型変換
/// </summary>
/// <param name="obj"></param>
public static implicit operator uint(TesUInt32 obj)
{
return obj.Value;
}
}
デバッグ時の表示用に値とは別にバイナリエディタなどで確認している際にわかりやすくするように、バイト表示も加えています。
他のデータ型も同様の形で追加していきます。
なお、バイト型と他のデータ型との変換はバイト型を扱うTesBytes
で行うようにし、ファイルリーダーからの読み取りではそれ経由で変換するようにしました。GetString()
やGetUint32()
などを追加します。
public override string ToString()
{
//終端がNULLの場合、除外する
int count = this.Count;
if (this[count - 1].Equals(0x00))
--count;
string result = Encoding.UTF8.GetString(this.GetOffset(0, count).ToArray());
return result;
}
public uint ToUInt32()
{
uint result = BitConverter.ToUInt32(this.ToArray(), 0);
return result;
}
public string GetString(long count, long offset = 0, bool next = true)
{
TesBytes b = GetBytes(count, offset, next);
string result = b.ToString();
return result;
}
public uint GetUInt32(long offset = 0, bool next = true)
{
long count = 4;
TesBytes b = GetBytes(count, offset, next);
uint result = b.ToUInt32();
return result;
}
終端の判断などコードがちょっとアレですが・・・こんな感じでやっています。
ちなみに、一旦引数に入れていたり、戻り値用の変数名を一律result
としているのは、デバッグで楽だからこのようにしています。
ネーミングなどもう少しちゃんとした方が良いのかもしれませんが、趣味のプログラムでそういったことに悩みたくないので、昔ながらのやり方から離れられないでいます。(最近頭が古いなと良く思います)
その他の方も同じような感じで作っていきます。
先ほどのグループクラスを次のように変更しました。
public class TesGroup : TesBase
{
public TesString GRUP { get; }
public TesUInt32 DataSize { get; }
public TesBytes GrupData { get; }
public TesGroup(TesFileReader fr)
{
GRUP = new TesString(fr);
DataSize = new TesUInt32(fr);
GrupData = fr.GetBytes(DataSize);
}
}
ちなみに、当初は各基本型にファイルリーダーを受け取ったら暗黙変換するようにして代入を楽するようにしていましたが、データが読まれているかがわかりづらいのでやめました。
先ほどまでの形の方が何バイト読み進めているかがわかりやすいですが、決まった形なので、これでよしとしています。
以上。とりあえず、今回はここまでです。
前回 | 次回 |
---|---|
1 - 基本のデータ構造など | 3 - レコードを扱う |