はじめに
この記事は、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など)がコレにあたります。
次に、その中にレコードとしてそのカテゴリの1つぶんのデータが書き込まれています。例えば武器データなら、武器1つ分です。
レコード内にはさらに、その内容に応じて設定値を表現するフィールドがあります。武器の名称や、ダメージの値などです。
上図での右ペインのリスト内容がコレにあたります。
なお、一部のデータは構造が異なりますが、この辺を押さえておけば大体のことができます。
基本のデータ型など
まず、基本となるインタフェースと基本となるバイトデータを扱うクラスを用意しました。
以下のソースコードはとりあえず最低限を記載しています。
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
インタフェースのToBytes
とRecalc
は各データを扱うクラスに実装し、ファイル出力時に呼出して、変更箇所のサイズ計算を行うために使用します。
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 - グループを扱う |