[UE4][UE5]ソースアセットからローカライズされたアセットのパスを取得する方法について

# 要約
- ソースアセット( 以後 **Source** ）とローカライズされたアセット( 以後 **Localized** )の間には参照関係がないため、ChunkID設定などで困るときがある
- SourceからLocalizedのパスを取得できる ```FPackageLocalizationUtil::ConvertSourceToLocalized```関数 が便利
- 上記関数を組み込んだPrimaryAssetLabelを用意すると、Localized の ChunkID設定対応が少し楽になるかも？

※ 今回はローカライズやChunk分けについてある程度知っているプログラマ向けの内容なので、少し説明がラフになっている箇所があります。ごめんなさい！

# はじめに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/4a92b2ec-bbe3-cd20-00df-66e61aaabd2b.png)
多言語対応する上でアセットのローカライズはほぼ必須です。地域ごとに異なるテクスチャ素材を使ったり、言語設定に応じた音声データを再生したりなど様々な用途で用いられます。

https://docs.unrealengine.com/5.0/ja/asset-localization-in-unreal-engine/

https://www.docswell.com/s/EpicGamesJapan/57G36Z-UE4_LocalizationDD_Features

ReferenceViewerで見ると一目瞭然なのですが、**SourceとLocalizedの間には参照関係がありません。ソフト参照もありません。** これは余計な処理・メモリ消費を避けるための仕様（のはず）ですが、一部の作業で困る場合があります。

例えばDLCを作るためには各アセットをChunk機能を使ってグループ分けをする必要があるのですが、上記の仕様によりSourceとLocalizedは個別にグループ分け操作・設定をする必要があります。何らかの参照関係があればSourceに対して対応すれば良いのですが…これは少し面倒です。

https://docs.unrealengine.com/4.27/ja/SharingAndReleasing/Patching/GeneralPatching/CookingAndChunking/

https://qiita.com/unknown_ds/items/f19ccf03ca689a65049c

こういったケースで便利なのが、SourceからLocalizedのパスを取得できる ```FPackageLocalizationUtil::ConvertSourceToLocalized```関数です。

# SourceからLocalizedのパスを取得する方法について
早速ですが、コードと使い方をぺたり。
```cpp:
#include "Internationalization/PackageLocalizationUtil.h"
bool UMyEditorBlueprintFunctionLibrary::ConvertSourceToLocalized(const FString& InSource, const FString& InCulture,
	FString& OutLocalized)
{
	return FPackageLocalizationUtil::ConvertSourceToLocalized(InSource, InCulture, OutLocalized);
}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/6d2db294-9dee-4908-c982-0007ddc8432d.png)

やってることは簡単で、
1. ```Get Localized Cultures```ノードでローカライズ対象の言語リストを取得
1. 各言語ごとのLocalized のパスを ```ConvertSourceToLocalized``` で取得

という感じで、結果は以下のようになります。
```cpp:
Source：
/Game/AssetLocalization/Test.Test

ローカライズ対象：
日本語、英語、中国語（簡体）

結果
/Game/L10N/ja/AssetLocalization/Test.Test
/Game/L10N/en/AssetLocalization/Test.Test
/Game/L10N/zh-Hans/AssetLocalization/Test.Test
```

かんたんですね！しかし、注意点があります。

# ConvertSourceToLocalized関数の注意点
```ConvertSourceToLocalized``` 関数は出力したアセットパス（ ```OutLocalized```)にアセットが存在するか、つまり対象の言語に対してローカライズ済みかは考慮していません。そのため、実現したい処理によっては各アセットパスにアセットが存在するか否かを確認する必要がでてきます。

例えばこんな感じ。BP版とcpp版の両方を載せておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/7dbd87ed-9a0c-7ccb-09f5-cfb16345dda2.png)
```cpp:
#include "AssetRegistry/AssetRegistryModule.h"

if (FPackageLocalizationUtil::ConvertSourceToLocalized(Source, Culture, OutLocalized))
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	FAssetData LocalizedAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*OutLocalized);

	if (LocalizedAssetData.IsValid())
	{
		// ローカライズ済みのアセットが存在する場合の処理

	}
}
```

Soruce からちゃんと存在する Localized のアセットパスを取得できるようになりました！これで**SourceのChunk IDと同じ値を Lozalicedに設定**したり、逆に**ローカライズされていないアセットの洗い出し**をしたりなど、色んなことができるようになります！

# ローカライズ済みのアセットがあったらAssetBundleに追加するオレオレPrimaryAssetLabelの作り方
最後に、```Label Assets In My Directory```を有効にして収集した各アセットに対してLocalizedが存在するかをチェックし、もし存在していたら同じ```Assset Bundle```に追加する```Primary Asset Label```派生クラスのコードを貼って終わりにします。 

```cpp:
#include "MyPrimaryAssetLabel.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "Engine/AssetManager.h"
#include "Internationalization/PackageLocalizationUtil.h"

#if WITH_EDITORONLY_DATA
void UMyPrimaryAssetLabel::UpdateAssetBundleData()
{
	Super::UpdateAssetBundleData();
	if (!UAssetManager::IsValid())
	{
		return;
	}

	FAssetBundleEntry* DirectoryBundleEntry = AssetBundleData.FindEntry(DirectoryBundle);
	if(DirectoryBundleEntry)
	{
		const TArray<FString> LocalizedCultureNames = FTextLocalizationManager::Get().GetLocalizedCultureNames(ELocalizationLoadFlags::Game);
		TArray<FSoftObjectPath> LocalizedPaths;
		
		for( FSoftObjectPath BundleAssetPath : DirectoryBundleEntry->BundleAssets )
		{
			for( FString LocalizedCultureName : LocalizedCultureNames )
			{
				FString LocalizedObjectPath;
				if (FPackageLocalizationUtil::ConvertSourceToLocalized(BundleAssetPath.ToString(), LocalizedCultureName, LocalizedObjectPath))
				{
					// Does this localized asset already exist?
					FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
					FAssetData LocalizedAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*LocalizedObjectPath);

					if (LocalizedAssetData.IsValid())
					{
						LocalizedPaths.Add(LocalizedAssetData.ToSoftObjectPath());
					}
				}
			}
		}

		if(!LocalizedPaths.IsEmpty())
		{
			AssetBundleData.AddBundleAssets(DirectoryBundle, MoveTemp(LocalizedPaths));
		}
	}

	// Update rules
    UAssetManager& Manager = UAssetManager::Get();
	FPrimaryAssetId PrimaryAssetId = GetPrimaryAssetId();
	Manager.SetPrimaryAssetRules(PrimaryAssetId, Rules);
}
#endif
```

これでSourceとLocalizedを同じChunkに入れたいのに別々に管理する…という面倒な作業が必要なくなります。ただ見ての通り、Primary Asset LabelのExplicit Asset(Blueprints) や Asset Collectionに対しての処理は割愛していますので、必要な場合は上記コードや```UPrimaryAssetLabel::UpdateAssetBundleData()```を参考にご対応くださいまし。

あとは、```Primary Asset Label```派生クラスをAssetManagerに登録することをお忘れなく（僕は忘れて数十分無駄にしました）

# おまけ
逆に Localized から Sourceのアセットパスを取得したい場合は ```FPackageLocalizationUtil::ConvertLocalizedToSource```関数を使うことになります。

おしまい！