FArchiveのエラーにつきまして

いつもお世話になっております。

TMemoryWriter, FMemoryReader を使用してセーブデータのセーブとロードを実装しております。

セーブしたデータをロードしたときにFArchiveのIsErrorがtrueになるケースが発生しました。

発生時のデータは入手できず、再現もできていないため調査に難航しております。

サンプルコードを記載すると次のような実装になっております。

`// セーブ処理
FMemoryWriter memoryWriter(data, true, true);
FObjectAndNameAsStringProxyArchive ar(memoryWriter, false);
SaveData->Serialize(ar);
if (ar.IsError())
{
}
else
{
エラーはないためストレージにファイルとして保存します。
}

// ロード処理
FMemoryReader memoryReader(data, true);
FObjectAndNameAsStringProxyArchive ar(memoryReader, true);
SaveData->Serialize(ar);
if (ar.IsError())
{
// ここでエラーが発生しました。
}`ストレージのファイルにつきましてはハッシュ値を使用して破損チェックを行っているため、ファイルの破損は発生していない想定です。

セーブ処理ではar.IsError()はfalseとなり、ロード処理でar.IsError()がtrueになるケースにつきまして思い当たる点などございますでしょうか?

データの内容次第ではエンジン内部の処理により発生する可能性があるのかご教示いただけますと幸いです。

ご確認のほどよろしくお願いいたします。

FObjectAndNameAsStringProxyArchiveを用いたデシリアライゼーションは、標準のセーブロード用のAPIであるUGameplayStatics::LoadGameFromMemoryでも使われているもので特にバグ報告は見つけられていません。

しかし、UGameplayStatics::LoadGameFromMemoryの関数内ではデシリアライズの後にAr.IsErrorのチェックが行われていないため潜在的にエラービットが立つケースがあり得そうです。

エラービットはUSaveGameクラスに含まれるいずれかのプロパティによって立てられています。データ構造の中で使われるクラスにはどのようなものがありますか?

> ストレージのファイルにつきましてはハッシュ値を使用して破損チェックを行っているため、ファイルの破損は発生していない想定です。

これはエラーが出ていたとしてもデシリアライズされたデータは正常だったとという認識でよろしいでしょうか?

ご確認いただきましてありがとうございます。

回答させていただきます。

​> エラービットはUSaveGameクラスに含まれるいずれかのプロパティによって立てられています。データ構造の中で使われるクラスにはどのようなものがありますか?

主に次のものを組み合わせて使用しております。

FString

FName

TMap

TArray

UStruct

UENUM

int

float

bool

> > ストレージのファイルにつきましてはハッシュ値を使用して破損チェックを行っているため、ファイルの破損は発生していない想定です。

> これはエラーが出ていたとしてもデシリアライズされたデータは正常だったとという認識でよろしいでしょうか?

デシリアライズによりエラー(ar.IsError())が発生した後はクラッシュさせているため、データが正常かどうかは確認できておりません。

まず提示いただいたソースのようにシリアライズしたデータ列をそのままMemroyReaderに渡してデシリアライズした場合には、ファイルのIOを経由せずにメモリ上での操作で完結されていることとなりファイルの読み込みエラーは否定されます。

対象クラスは複雑なクラスを含まずに基本的な型がほとんどであることを前提とすれば、その中には可変長のデータ構造を持ち得るEnumや文字列、配列がありますがそれらはデシリアライズ中に要素数に異常な値が検出された場合にエラーをセットすることがありえることを除けば入力データサイズの不足によるエラー程度しか可能性はないように見えます。

> デシリアライズによりエラー(ar.IsError())が発生した後はクラッシュさせているため、データが正常かどうかは確認できておりません。

承知しました。問題発生時にセーブファイルの保存に成功されているのであれば再起動後のロードで正しく読めたかどうか確認できたかと思いますがこちらはいかがでしたか?

> まず提示いただいたソースのようにシリアライズしたデータ列をそのままMemroyReaderに渡してデシリアライズした場合には、ファイルのIOを経由せずにメモリ上での操作で完結されていることとなりファイルの読み込みエラーは否定されます。

提示したソースは「セーブ処理」と「ロード処理」のイメージをお伝えするために記載したものとなります。

「セーブ処理」と「ロード処理」が連続しているわけではございません。わかり難くてすみません。

実際には「セーブ処理」によりストレージにファイルを保存します。

その後ゲームを終了し、ゲームを再起動した後にストレージのファイルをロードしたところでエラーになりました。

> 対象クラスは複雑なクラスを含まずに基本的な型がほとんどであることを前提とすれば、その中には可変長のデータ構造を持ち得るEnumや文字列、配列がありますがそれらはデシリアライズ中に要素数に異常な値が検出された場合にエラーをセットすることがありえることを除けば入力データサイズの不足によるエラー程度しか可能性はないように見えます。

「セーブ処理ではar.IsError()はfalseとなり、ロード処理でar.IsError()がtrueになるケース」​につきましては次の2つの可能性があるという認識でよろしいでしょうか?

1.デシリアライズ中に​可変長のデータ構造を持ち得るEnumや文字列、配列の要素数に異常な値が検出された場合

2.​入力データサイズの不足

また​「入力データサイズの不足」につきまして詳細を教えていただけますでしょうか。​

​例えば「入力データサイズの不足」を検出する方法や回避する方法などを知りたいと考えております。

> > デシリアライズによりエラー(ar.IsError())が発生した後はクラッシュさせているため、データが正常かどうかは確認できておりません。

> 承知しました。問題発生時にセーブファイルの保存に成功されているのであれば再起動後のロードで正しく読めたかどうか確認できたかと思いますがこちらはいかがでしたか?

再起動後のロード処理でエラー(ar.IsError())になっておりました。

>「セーブ処理ではar.IsError()はfalseとなり、ロード処理でar.IsError()がtrueになるケース」につきましては次の2つの可能性があるという認識でよろしいでしょうか?

はい。エラーフラグがセットされる場合はその可能性があります。他の型やクラスを用いた場合には別の形式でエラーが出る可能性があります。詳しくはそれぞれの型のシリアライズ関数を参照してください。

> 例えば「入力データサイズの不足」を検出する方法や回避する方法などを知りたいと考えております。

保存されているファイルを正しく読み込んだのに関わらず、ファイルが破損していて途中で途切れてしまっているケースを事前に検出することは難しいです。

今回のようにデシリアライズ時のエラーを判定したり、セーブデータから計算したハッシュ値を埋め込んでデシリアライズ後にチェックを行うような方法を取ることになると思います。

そのうえでセーブファイルの破損が検出された場合にはユーザーに破損を通知する他には手がありません。

ファイルの破損を回避する方法としてはセーブデータの多重化などが考えられます。

ご回答いただきましてありがとうございます。

上記の情報を元に対策を検討させていただきます。

大変参考になりました。

ありがとうございました。