SKSE上でのデータのセーブ/ロード

目的

SKSEプラグインを用いてデータの保存を行えるサンプルを提供します。

SKSEプラグイン上で処理するデータは、静的変数を用いればゲーム中は値を保持し続けますが、ゲーム終了時にはすべて破棄されます。

このため本来はPapyrusスクリプトでデータを保存する(Tips参照)必要がありますが、このページのサンプルによりSKSEプラグイン上でデータの保存が可能となり、Papyrus上の変数などに戻すような手間が不要になります。

前提

SKSE64プラグイン開発環境構築手順にて、skse 本体のコンパイルができる状態にあることが前提となります。

プラグインのサンプルプロジェクトである samplePlugin を元にデータのセーブ/ロードを実装します。

手順概要

データの保存のために、SKSESelrializationInterfaceクラスが提供されています。これを利用してデータのセーブ/ロードを実現します。

以下の手順によって実装します。

  1. SKSESerizalizationIntarfaceのインスタンス化
    • 自分のSKSEプラグインでSKSESerializationInterfaceを利用できるようにします。
  2. セーブ/ロード時のコールバック関数の作成と登録
    • セーブ時及びロード時に動作させる関数を作成し、実際に呼び出されるよう登録します。
  3. セーブ用関数の実装
    • SKSEプラグイン内の変数の値を保存する処理を実装します。
  4. ロード用関数の実装
    • SKSEプラグイン内の変数へ値を読み込ませる処理を実装します。
  5. セーブ/ロードの確認
    • skse64_loader.exeからSKYRIMを起動し、セーブ/ロードが行われていることを確認します。

手順詳細

以下の手順はすべてsamplePluginプロジェクトのmain.cppに対して行います。

SKSESerializationInterfaceのインスタンス化

グローバル変数としてSKSESerializationInterfaceのポインタを作成します。

IDebugLog	gLog;
UInt32 g_skseVersion;
PluginHandle	g_pluginHandle = kPluginHandle_Invalid;
SKSEMessagingInterface	* g_messaging = nullptr;
//*************SKSESerializationIntarfaceポインタ作成*************
SKSESerializationInterface	* g_serialization = NULL;

void SKSEMessageHandler(SKSEMessagingInterface::Message* msg)
// ...以下省略...


宣言したg_serialization変数をSKSEPlugin_Query関数の実行時にインスタンス化させます。

extern "C"
{
bool SKSEPlugin_Query(const SKSEInterface * skse, PluginInfo * info)
// ...前半省略...
	g_messaging = (SKSEMessagingInterface *)skse->QueryInterface(kInterface_Messaging);
	if (!g_messaging)
	{
		return false;
	}

	/*************追加部分*************/
	// get the serialization interface and query its version
	g_serialization = (SKSESerializationInterface *)skse->QueryInterface(kInterface_Serialization);
	if (!g_serialization)
	{
		_MESSAGE("couldn't get serialization interface");

		return false;
	}

	if (g_serialization->version < SKSESerializationInterface::kVersion)
	{
		_MESSAGE("serialization interface too old (%d expected %d)", g_serialization->version, SKSESerializationInterface::kVersion);

		return false;
	}
	/*************追加部分*************/

	return true;
}

セーブ/ロード時のコールバック関数の作成と登録

セーブ/ロード時に実行させるための関数を記述します。

void Serialization_Save(SKSESerializationInterface * intfc)
{
}
void Serialization_Load(SKSESerializationInterface * intfc)
{
}
extern C {
// ...以下省略...


上記二つの関数を、SKSESerializationInterface(g_seriarization変数)に登録し、実際にセーブやロードを実行した際に呼び出されるようにします。 この処理は SKSEPlugin_Load内に実装します。

  • ここで最初に実行しているSetUniqueIDメソッドですが、第二引数の文字列がプラグインの識別子として動作し、異なるSKSEプラグイン間でのデータの誤ったロードを防ぐようになっていると思われます(未確認
  • サンプルでは'TEST'としていますが、自作のMODで実装する際は異なる単語にした方が良いです。
bool SKSEPlugin_Load(const SKSEInterface * skse)
{
	if (g_messaging)
	{
		g_messaging->RegisterListener(g_pluginHandle, "SKSE", SKSEMessageHandler);
	}
	/*************追加部分*************/
	// ### this must be a UNIQUE ID, change this and email me the ID so I can let you know if someone else has already taken it
	g_serialization->SetUniqueID(g_pluginHandle, 'TEST');

	g_serialization->SetSaveCallback(g_pluginHandle, Serialization_Save);
	g_serialization->SetLoadCallback(g_pluginHandle, Serialization_Load);
	/*************追加部分*************/

	return true;
}

セーブ用関数の実装

セーブ用関数Serialization_Save上でセーブ処理を実装します。

セーブ処理には引数として受け取るintfcの、以下の2つのメソッドを利用します。

OpenRecord SKSEプラグイン内で保存するデータの住み分けやプラグイン更新時に
古いデータを読み込まないようバージョン管理を行う
WriteRecordData 変数に格納されたデータを実際に保存する


実装サンプルは以下のようになります。ここではtestInt変数内の「64」という数字を保存しています。

const UInt32 kSerializationDataVersion = 1; // グローバルで宣言

void Serialization_Save(SKSESerializationInterface * intfc)
{
	_MESSAGE("save");

	SInt32 testInt = 64;

	if (intfc->OpenRecord('DATA', kSerializationDataVersion))
	{
		intfc->WriteRecordData(&testInt, sizeof(testInt));
		_MESSAGE("save DATA... int:%ld", testInt);
	}
}
  • OpenRecordメソッドは、以降にWriteRecordDataにより保存するデータに対して識別子とバージョン情報を付与します。
    第一引数 識別子。文字列を登録する。
    第二引数 バージョン情報。ロード時にも用いるためグローバル変数を利用することを推奨。


  • WriteRecordDataメソッドにより、変数内の値を保存します。
    第一引数 保存する値を格納した変数のポインタ
    第二引数 第一引数の変数のサイズ

ロード用関数の実装

ロード用関数Serialization_Load上でロード処理を実装します。

ロード処理には引数として受け取るintfcの、以下の2つのメソッドを利用します。

GetNextRecordInfo 保存時のOpenRecordメソッドで設定したパラメータを引き出す
ReadRecordData 保存されているデータを読み込んで変数に格納する


実装サンプルは以下のようになります。ここではtestInt変数に保存した値を読み込んでいます。

void Serialization_Load(SKSESerializationInterface * intfc)
{
	_MESSAGE("load");

	UInt32	type;
	UInt32	version;
	UInt32	length;
	bool	error = false;

	while (!error && intfc->GetNextRecordInfo(&type, &version, &length))
	{
		switch (type)
		{
		case 'DATA':
		{
			if (version == kSerializationDataVersion)
			{
				SInt32 testInt;
				if (!intfc->ReadRecordData(&testInt, sizeof(testInt)))
				{
					error = true;
					return;
				}

				_MESSAGE("read DATA... int:%ld", testInt);
			}
			else
			{
				error = true;
			}
		}
		break;

		default:
			_MESSAGE("unhandled type %08X", type);
			error = true;
			break;
		}
	}
}
  • GetNextRecordInfoメソッドは、OpenRecordメソッドで付与した情報を引き出して引数に格納します。
    第一引数 識別子の値を引数に格納します。このサンプルでは「DATA」になります。
    第二引数 バージョン情報の値を引数に格納します。このサンプルでは「1」になります。
    第三引数 登録したデータのサイズを取得できるようです。
    ReadRecordDataメソッドの第二引数にここの変数を入れても使えますが、
    複数のデータの保存には対応できないので使用していません(セーブ/ロードするデータの順序参照)

  • type変数の中に格納された識別子をチェックし、対応を変えます。複数の識別子をセーブ時に登録していた場合、GetNextRecordInfoを繰り返すたびに次の識別子(とそこで保存したデータ)を読み込めるようになります。
  • ReadRecordDataメソッドにより、変数内の値を保存します。
    第一引数 保存された値を格納するための引数のポインタ
    第二引数 第一引数の変数のサイズ

セーブ/ロードの確認

  1. ビルドを行い、samplePlugin.dllを作成します。
  2. Skyrim Special Edition\Data\SKSE\pluginsに上記ファイルを保存し、skse64_loader.exeからSkyrimを起動します。
  3. プレイを開始した後、保存してセーブデータを作成して終了します。
    • (デフォルトの場合)My Games\Skyrim Special Edition\SKSEにsamplePlugin.logが作成されるので、以下の内容が記載されていることを確認します。
      samplePlugin.dll
      save
      save DATA... int:64
      
  4. 再度Skyrimを起動し、保存したセーブデータをロードします。
    • 先ほどのsamplePlugin.logが更新されるので、以下の内容が記載されていることを確認します。
    • 3行目のところで、データが正常にセーブ/ロードされていることが分かります。
      samplePlugin.dll
      load
      read DATA... int:64
      

Tips

セーブ/ロードするデータの順序

WriteRecordDataやReadRecordDataは、連続して使うことで別のデータをセーブ/ロードできます。

ただし、セーブした順序と同じ順序でロードするので、セーブの順序とロードの順序は一貫性を持たせなければなりません。

以下が複数のデータのセーブ/ロードを行うサンプルです。

void Serialization_Save(SKSESerializationInterface * intfc)
{
	_MESSAGE("save");

	SInt32 testInt = 64;
	UInt32 testInt2 = 32;

	if (intfc->OpenRecord('DATA', kSerializationDataVersion))
	{
		intfc->WriteRecordData(&testInt, sizeof(testInt));
		intfc->WriteRecordData(&testInt2, sizeof(testInt2));
		_MESSAGE("save DATA... int:%ld uint:%lu", testInt, testInt2);
	}
}
void Serialization_Load(SKSESerializationInterface * intfc)
{
	_MESSAGE("load");

	UInt32	type;
	UInt32	version;
	UInt32	length;
	bool	error = false;

	while (!error && intfc->GetNextRecordInfo(&type, &version, &length))
	{
		switch (type)
		{
		case 'DATA':
		{
			if (version == kSerializationDataVersion)
			{
				SInt32 testInt;
				UInt32 testInt2;
				if (!intfc->ReadRecordData(&testInt, sizeof(testInt)))
				{
					error = true;
					return;
				}
				if (!intfc->ReadRecordData(&testInt2, sizeof(testInt2)))
				{
					error = true;
					return;
				} 
				_MESSAGE("read DATA... int:%ld uint:%lu", testInt, testInt2);
			}
			else
			{
				error = true;
			}
		}
		break;

		default:
			_MESSAGE("unhandled type %08X", type);
			error = true;
			break;
		}
	}
}

セーブ/ロードするデータの型について

WriteRecordData/ReadRecordDataでセーブ/ロードを行うことのできるデータはプリミティブ型(bool、int、float、charなど)に限られます。

そのため文字列データや配列データをそのままセーブ/ロードすることはできず、プリミティブ型に落とし込む必要があります(SInt32やUInt16などのプリミティブ型をtypedefで別名にしたものはそのまま利用可能)。

  • この時、「ロード時に文字列の長さや配列の長さが分からない」ことが問題になります。
  • 文字列や配列をセーブ/ロードする場合は、直前にその文字の長さや配列の長さを一緒に保存することでこの問題を回避できます。

以下が文字列や配列をセーブ/ロードするための関数サンプルです。

void Serialization_Save(SKSESerializationInterface * intfc)
{
	_MESSAGE("save");

	std::vector<float> testArray{ 1.5f, 2.9f, 4.3f };
	std::string testStr = "skse64";

	if (intfc->OpenRecord('DATA', kSerializationDataVersion))
	{
		//配列のセーブ
		SInt32 arrayLen = testArray.size();
		intfc->WriteRecordData(&arrayLen, sizeof(arrayLen)); //先に配列のサイズを保存
		for (int i = 0; i < testArray.size(); i++)
			intfc->WriteRecordData(&testArray[i], sizeof(testArray[i]));

		//文字列のセーブ
		SInt32 strLen = strlen(testStr.c_str());
		intfc->WriteRecordData(&strLen, sizeof(strLen)); //先に文字の長さを保存
		intfc->WriteRecordData(testStr.c_str(), strLen);
	}
}
void Serialization_Load(SKSESerializationInterface * intfc)
{
	_MESSAGE("load");

	UInt32	type;
	UInt32	version;
	UInt32	length;
	bool	error = false;

	while (!error && intfc->GetNextRecordInfo(&type, &version, &length))
	{
		switch (type)
		{
		case 'DATA':
		{
			if (version == kSerializationDataVersion)
			{
				//配列のロード
				SInt32 arrayLen;
				intfc->ReadRecordData(&arrayLen, sizeof(arrayLen)); //配列のサイズを取得

				std::vector<float> testArray;
				for (int i = 0; i < arrayLen; i++)
				{
					float bufFloat;
					intfc->ReadRecordData(&bufFloat, sizeof(bufFloat));
					testArray.push_back(bufFloat);
					_MESSAGE("read TEST... floatArray[%d]:%f", i, testArray[i]);
				}

				//文字列のロード
				SInt32 strLen;
				intfc->ReadRecordData(&strLen, sizeof(strLen)); //文字の長さを取得

				char *bufChar = new char[strLen + 1];
				intfc->ReadRecordData(bufChar, strLen);
				bufChar[strLen] = '\0';

				std::string testStr(bufChar);
				delete[] bufChar;

				_MESSAGE("read TEST... string:%s", testStr);
			}
			else
			{
				error = true;
			}
		}
		break;

		default:
			_MESSAGE("unhandled type %08X", type);
			error = true;
			break;
		}
	}
}

LE版Skyrimでの実装について

LE版のSkyrimの場合、skse1.7.3のplugin_exampleプロジェクト内でSerialization_Save/Serialization_Load関数がすでに実装されています。

そのため、前提条件におけるsamplePluginは用いず、skse1.7.3のソースだけで利用できます。また、手順1及び2は不要になります。