[UE4] 誤った方法で uasset を拡張して開発を続け、エンジンアップデートで詰んだと思った話

# uasset の拡張方法
はじめに uasset の拡張方法を説明する。

アセットデータ（uasset) を拡張するにはオブジェクトバージョンを追加する必要がある。
オブジェクトバージョンの定義は `Engine/Source/Runtime/Core/Public/UObject/ObjectVersion.h` で行う。


プロジェクト（ライセンシー）側で追加するには `enum EUnrealEngineObjectLicenseeUE4Version` に追加する。
`enum EUnrealEngineObjectUE4Version` はエンジン側が定義しているオブジェクトバージョンであり、こちらに追加するとエンジンアップデート時に困ったことになる。

```diff:EUnrealEngineObjectLicenseeUE4Version
    VER_LIC_NONE = 0,
+   VER_LIC_CUSTOM_PARAMETER,
    // - this needs to be the last line (see note below)
    VER_LIC_AUTOMATIC_VERSION_PLUS_ONE,
    VER_LIC_AUTOMATIC_VERSION = VER_LIC_AUTOMATIC_VERSION_PLUS_ONE - 1
```

オブジェクトファイルのバージョンを取得するには `FArchive::LicenseeUE4Ver()` or `FPackageFileSummary::GetFileVersionLicenseeUE4()` or `UPackage::LinkerLicenseeVersion` を利用する。

データの読み書きは次のようにする。

```cpp
if (Ar.LicenseeUE4Ver() >= VER_LIC_CUSTOM_PARAMETER) {
	Ar << CustomParameter;
}
```
このコードでシリアライズ/デシリアライズが行われる。 `Ar.IsSaving()` でどちらの処理かわかる。
シリアライズ/デシリアライズはシークしながら行われるため、該当機能の `Serialize(...)` の最後に追加し、順番を変更してはならない。

[アセットとパッケージのバージョンを管理する | Unreal Engine ドキュメント](https://docs.unrealengine.com/ja/Programming/UnrealArchitecture/VersioningAssetsAndPackages/index.html)
> UE4Ver 関数呼び出しを LicenseeUE4Ver に変更すると、正式な Epic のバージョン番号ではなく、ライセンシーのバージョン番号を使用するようにコードが変更されます。 この方法は、独自のバージョンの Unreal Engine 4 を管理している非 Epic ユーザーにお勧めです。


# 本題

本記事では失敗談として `EUnrealEngineObjectUE4Version` にバージョンを追加してしまった経緯、問題、回避策を記す。

uassets の拡張方法を知りたいだけの方は、ここで回れ右。


# 誤改造から問題発覚まで

マテリアルの設定を追加するため、エンジンのコードをお手本に改造を行った。
エンジンの実装は `EUnrealEngineObjectUE4Version` を参照しているため、何の疑いもなく `EUnrealEngineObjectUE4Version` に拡張バージョンの定義を追加をした。それから半年以上、何度かエンジンのアップデートを行ったが、何の問題も起きなかった。
この時のエンジンのバージョンは確か4.21。

今回必要に迫られ、4.24 へアップデートし問題が発覚した。
4.24 では `EUnrealEngineObjectUE4Version` にバージョンが追加されていたのだ。
4.23 までの **UE4Version** は **517** だが、拡張バージョンを追加したことで **518** となっていた。
4.24 では、エンジン側でバージョンが追加され **UE4Version** は **518** となっている。

この競合状態が原因で、ロード処理が失敗し、エディタが起動する前にクラッシュするようになってしまった。

とりあえず、追加した拡張バージョンを **518**、エンジン側で追加されたバージョンを **519** にしてしまえばいいやと思っていた。
ロードに失敗していたプロジェクトコンテンツはロードできるようになり、別のクラッシュが発生していたが、時間も遅かったため、適当に [Qiita にまとめて](https://qiita.com/Takezoh/items/d9ea8f4bdabbab218a3f)帰宅した。

次の日、作業を再開してエンジンコンテンツがロードできないことに気づいた。
エンジンコンテンツに新しく追加・更新されたファイルも **UE4Version = 518** を持っていたのだ。

拡張したエンジンで作成した **518** なファイルは、プロジェクト側の拡張データを持つが、エンジン側で新規に実装されたデータは持っていない。
最新のエンジンコンテンツ **518** なファイルは、エンジン側で新規に実装されたデータを持つが、プロジェクト側の拡張データは持っていない。


この時点では、誤った改造を行ったことに気づいておらず、「詰んだ」とツイートしたり、UDN に質問を投げたりしていた。ごめんなさい。

調査を進めるため、コードを読んでいたら `VersionLicensee` という文字が目に入った。

**Licensee** ... **ライセンシーバージョン** ？

半年以上前の過ちに気づいた。


私と同じ過ちを犯す同士が必ずいるだろうと思い、ここに記録を残しておく。

# プロジェクト前提

エンジンコンテンツはアップデート時に衝突を起こすため、修正が必要であればプロジェクト側にコピーして行っており、エンジンコンテンツに拡張データを持つアセットは存在しない。

# 競合回避処理の実装

## バージョン定義を修正
`EUnrealEngineObjectUE4Version` には過去に追加したバージョンとエンジン側で追加されたバージョンが同じ値になるように修正。

```diff:EUnrealEngineObjectUE4Version
	// Changed the source orientation of point lights to match spot lights (z axis)
	VER_UE4_POINTLIGHT_SOURCE_ORIENTATION,
	// LocalizationId has been added to the package summary (editor-only)
	VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID,
	// Fixed case insensitive hashes of wide strings containing character values from 128-255
	VER_UE4_FIX_WIDE_STRING_CRC,
+	// Yomuneo Custom: Mobile Fully Rough
+	VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH,
+	// Added package owner to allow private references
+	VER_UE4_ADDED_PACKAGE_OWNER = VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH,

	// -----<new versions can be added before this line>-------------------------------------------------
	// - this needs to be the last line (see note below)

	VER_UE4_AUTOMATIC_VERSION_PLUS_ONE,
	VER_UE4_AUTOMATIC_VERSION = VER_UE4_AUTOMATIC_VERSION_PLUS_ONE - 1
```
`EUnrealEngineObjectUE4Version` にバージョンを追加。

```diff:EUnrealEngineObjectLicenseeUE4Version
	VER_LIC_NONE = 0,
+	VER_LIC_MOBILE_FULLY_ROUGH,
	// - this needs to be the last line (see note below)
	VER_LIC_AUTOMATIC_VERSION_PLUS_ONE,
	VER_LIC_AUTOMATIC_VERSION = VER_LIC_AUTOMATIC_VERSION_PLUS_ONE - 1
```
## 拡張データを持つプロジェクトコンテンツを判定する
`VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH` より古いバージョンは拡張データを持っておらず、バージョンアップ後のエンジンで更新したコンテンツファイルのライセンシーバージョンは `VER_LIC_MOBILE_FULLY_ROUGH` となる。
また、最新のエンジンコンテンツは `VER_UE4_ADDED_PACKAGE_OWNER == VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH` と `VER_LIC_NONE` をバージョン番号として持っている。
このため、誤ったバージョン番号で拡張データを持つファイルは `(UE4Ver == VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH) && (LicenseeUE4Ver == VER_LIC_NONE)` 且つファイルパスが `FPaths::ProjectContentDir()` で始まるファイルである。

```cpp

bool bIsLegacyProjectContent = [] (FArchive& Ar) {
	if (!(Ar.UE4Ver() == VER_UE4_YOMUNECO_MOBILE_FULLY_ROUGH && Ar.LicenseeUE4Ver() == VER_LIC_NONE)) {
		return false;
	}
	static auto ProjectContentDir = FPaths::ProjectContentDir();
	static bool bHasProjectContent = FPaths::EngineContentDir() != ProjectContentDir;
	return bHasProjectContent && Ar.GetArchiveName().StartsWith(ProjectContentDir);
} （ARCHIVE);
```

## シリアライズ処理を修正

エンジン側で追加された `VER_UE4_ADDED_PACKAGE_OWNER` のシリアライズ処理を `bIsLegacyProjectContent` なファイルで行わないように修正。

```cpp
if (BaseArchive.IsSaving() || (Sum.FileVersionUE4 >= VER_UE4_ADDED_PACKAGE_OWNER && !bIsLegacyProjectContent))
{
	... // VER_UE4_ADDED_PACKAGE_OWNER の処理
```

拡張データのシリアライズ処理では、新規に定義した `VER_LIC_MOBILE_FULLY_ROUGH` を持つファイルと `bIsLegacyProjectContent` なファイルで行うように修正。

```cpp
if (Ar.LicenseeUE4Ver() >= VER_LIC_MOBILE_FULLY_ROUGH || bIsLegacyProjectContent)
{
	... // 拡張データのロード処理
```

無事に起動することができた。。

# 副作用
コンテンツロード時に文字列比較を行うため、ロードが遅くなっていると思われる。
プロジェクトコンテンツを再保存すれば対処コードを削除することが出来るため、タイミングを見て全プロジェクトコンテンツを更新する。