UE5.7.4 Engine\Plugins\Runtime\GameFeatures

文件夹:Engine\Plugins\Runtime\GameFeatures

目录结构

GameFeatures/
├── Config
│   └── DedicatedServer
│       └── DedicatedServerInstallBundle.ini
├── Source
│   ├── GameFeatures
│   │   ├── Private
│   │   │   ├── Tests
│   │   │   │   ├── GameFeaturePluginTestHelper.cpp
│   │   │   │   ├── GameFeaturePluginTests.cpp
│   │   │   │   └── GameFeaturePluginTestsHelper.h
│   │   │   ├── GameFeatureAction_AddActorFactory.cpp
│   │   │   ├── GameFeatureAction_AddCheats.cpp
│   │   │   ├── GameFeatureAction_AddChunkOverride.cpp
│   │   │   ├── GameFeatureAction_AddComponents.cpp
│   │   │   ├── GameFeatureAction_AddWorldPartitionContent.cpp
│   │   │   ├── GameFeatureAction_AddWPContent.cpp
│   │   │   ├── GameFeatureAction_AudioActionBase.cpp
│   │   │   ├── GameFeatureAction_DataRegistry.cpp
│   │   │   ├── GameFeatureAction_DataRegistrySource.cpp
│   │   │   ├── GameFeatureAction.cpp
│   │   │   ├── GameFeatureData.cpp
│   │   │   ├── GameFeatureDataAssetDependencyGatherer.cpp
│   │   │   ├── GameFeatureDataAssetDependencyGatherer.h
│   │   │   ├── GameFeatureOptionalContentInstaller.cpp
│   │   │   ├── GameFeaturePluginOperationResult.cpp
│   │   │   ├── GameFeaturePluginStateMachine.cpp
│   │   │   ├── GameFeaturePluginStateMachine.h
│   │   │   ├── GameFeaturesModule.cpp
│   │   │   ├── GameFeaturesProjectPolicies.cpp
│   │   │   ├── GameFeaturesSubsystem.cpp
│   │   │   ├── GameFeaturesSubsystemSettings.cpp
│   │   │   ├── GameFeatureTypes.cpp
│   │   │   └── GameFeatureVersePathMapperCommandlet.cpp
│   │   ├── Public
│   │   │   ├── GameFeatureAction_AddActorFactory.h
│   │   │   ├── GameFeatureAction_AddCheats.h
│   │   │   ├── GameFeatureAction_AddChunkOverride.h
│   │   │   ├── GameFeatureAction_AddComponents.h
│   │   │   ├── GameFeatureAction_AddWorldPartitionContent.h
│   │   │   ├── GameFeatureAction_AddWPContent.h
│   │   │   ├── GameFeatureAction_AudioActionBase.h
│   │   │   ├── GameFeatureAction_DataRegistry.h
│   │   │   ├── GameFeatureAction_DataRegistrySource.h
│   │   │   ├── GameFeatureAction.h
│   │   │   ├── GameFeatureData.h
│   │   │   ├── GameFeatureOptionalContentInstaller.h
│   │   │   ├── GameFeaturePluginOperationResult.h
│   │   │   ├── GameFeaturesProjectPolicies.h
│   │   │   ├── GameFeaturesSubsystem.h
│   │   │   ├── GameFeaturesSubsystemSettings.h
│   │   │   ├── GameFeatureStateChangeObserver.h
│   │   │   ├── GameFeatureTypes.h
│   │   │   ├── GameFeatureTypesFwd.h
│   │   │   └── GameFeatureVersePathMapperCommandlet.h
│   │   └── GameFeatures.Build.cs
│   └── GameFeaturesEditor
│       ├── Private
│       │   ├── GameFeatureActionConvertContentBundleWorldPartitionBuilder.cpp
│       │   ├── GameFeatureActionConvertContentBundleWorldPartitionBuilder.h
│       │   ├── GameFeatureDataDetailsCustomization.cpp
│       │   ├── GameFeatureDataDetailsCustomization.h
│       │   ├── GameFeaturePluginMetadataCustomization.cpp
│       │   ├── GameFeaturePluginMetadataCustomization.h
│       │   ├── GameFeaturePluginTemplate.cpp
│       │   ├── GameFeaturesEditorModule.cpp
│       │   ├── GameFeaturesEditorSettings.h
│       │   ├── IllegalPluginDependenciesValidator.cpp
│       │   ├── IllegalPluginDependenciesValidator.h
│       │   ├── SGameFeatureStateWidget.cpp
│       │   └── SGameFeatureStateWidget.h
│       ├── Public
│       │   └── GameFeaturePluginTemplate.h
│       └── GameFeaturesEditor.Build.cs
├── Templates
│   └── GameFeaturePluginWithCode
│       └── Source
│           └── PLUGIN_NAMERuntime
│               ├── Private
│               │   └── PLUGIN_NAMERuntimeModule.cpp
│               ├── Public
│               │   └── PLUGIN_NAMERuntimeModule.h
│               └── PLUGIN_NAMERuntime.Build.cs
└── GameFeatures.uplugin

DedicatedServerInstallBundle.ini

[GameFeaturePlugins]
bGFPAreAlwaysResident=True

GameFeatures.uplugin

{
	"FileVersion": 3,
	"Version": 1,
	"VersionName": "1.0",
	"FriendlyName": "Game Features",
	"Description": "Support for modular Game Feature Plugins",
	"Category": "Gameplay",
	"CreatedBy": "Epic Games, Inc.",
	"CreatedByURL": "https://epicgames.com",
	"DocsURL": "",
	"MarketplaceURL": "",
	"SupportURL": "",
	"EnabledByDefault": false,
	"CanContainContent": false,
	"IsBetaVersion": true,
	"Installed": false,
	"Modules": [
		{
			"Name": "GameFeatures",
			"Type": "Runtime",
			"LoadingPhase": "PreDefault"
		},
		{
			"Name": "GameFeaturesEditor",
			"Type": "Editor",
			"LoadingPhase": "Default"
		}
	],
	"Plugins": [
		{
			"Name": "ModularGameplay",
			"Enabled": true
		},
		{
			"Name": "DataRegistry",
			"Enabled": true
		},
		{
			"Name": "AssetReferenceRestrictions",
			"Enabled": true
		},
		{
			"Name": "PluginUtils",
			"Enabled": true,
			"TargetAllowList": [ "Editor" ]
		},
		{
			"Name": "DataValidation",
			"Enabled": true,
			"TargetAllowList": [ "Editor" ]
		}
	]
}

GameFeatures.Build.cs

// Copyright Epic Games, Inc. All Rights Reserved.

namespace UnrealBuildTool.Rules
{
	public class GameFeatures : ModuleRules
	{
		public GameFeatures(ReadOnlyTargetRules Target) : base(Target)
		{
			bTreatAsEngineModule = true;

			PublicDependencyModuleNames.AddRange(
				new string[]
				{
					"Core",
					"CoreUObject",
					"DeveloperSettings",
					"Engine",
					"ModularGameplay",
					"DataRegistry"
				}
			);

			PrivateDependencyModuleNames.AddRange(
				new string[]
				{
					"AssetRegistry",
					"GameplayTags",
					"InstallBundleManager",
					"IoStoreOnDemandCore",
					"Json",
					"JsonUtilities",
					"PakFile",
					"Projects",
					"RenderCore", // required for FDeferredCleanupInterface
					"TraceLog",
				}
			);

			PublicIncludePathModuleNames.AddRange(
				new string[]
				{
					"IoStoreOnDemandCore",
				}
			);

			if (Target.bBuildEditor)
			{
				PrivateDependencyModuleNames.AddRange(
					new string[]
					{
						"UnrealEd",
						"PlacementMode",
						"PluginUtils",
					}
				);
			}
		}
	}
}

GameFeatureAction.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction.h"
#include "GameFeatureData.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction)

UGameFeatureData* UGameFeatureAction::GetGameFeatureData() const
{
	for (UObject* Obj = GetOuter(); Obj; Obj = Obj->GetOuter())
	{
		if (UGameFeatureData* GFD = Cast<UGameFeatureData>(Obj))
		{
			return GFD;
		}
	}

	return nullptr;
}

void UGameFeatureAction::OnGameFeatureActivating(FGameFeatureActivatingContext& Context)
{
	// Call older style if not overridden
	OnGameFeatureActivating();
}

bool UGameFeatureAction::IsGameFeaturePluginRegistered(bool bCheckForRegistering  /*= false*/) const
{
	UGameFeatureData* GameFeatureData = GetGameFeatureData();
	return !!GameFeatureData ? GameFeatureData->IsGameFeaturePluginRegistered(bCheckForRegistering) : false;
}

bool UGameFeatureAction::IsGameFeaturePluginActive(bool bCheckForActivating /*= false*/) const
{
	UGameFeatureData* GameFeatureData = GetGameFeatureData();
	return !!GameFeatureData ? GameFeatureData->IsGameFeaturePluginActive(bCheckForActivating) : false;
}

GameFeatureAction_AddActorFactory.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddActorFactory.h"
#include "GameFeatureData.h"
#include "Misc/MessageDialog.h"

#if WITH_EDITOR
#include "IPlacementModeModule.h"
#endif // WITH_EDITOR

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddActorFactory)

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddActorFactory

DEFINE_LOG_CATEGORY_STATIC(LogAddActorFactory, Log, All);

void UGameFeatureAction_AddActorFactory::OnGameFeatureRegistering()
{
	AddActorFactory();
}

void UGameFeatureAction_AddActorFactory::OnGameFeatureUnregistering()
{
	RemoveActorFactory();
}

#if WITH_EDITOR
void UGameFeatureAction_AddActorFactory::PostRename(UObject* OldOuter, const FName OldName)
{
	Super::PostRename(OldOuter, OldName);

	// If OldOuter is not GetTransientPackage(), but GetOuter() is GetTransientPackage(), then you were trashed.
	const UObject* MyOuter = GetOuter();
	const UPackage* TransientPackage = GetTransientPackage();
	if (OldOuter != TransientPackage && MyOuter == TransientPackage)
	{
		RemoveActorFactory();
	}
}

void UGameFeatureAction_AddActorFactory::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);

	const FName PropertyName = PropertyChangedEvent.GetPropertyName();
	if (PropertyName == GET_MEMBER_NAME_CHECKED(UGameFeatureAction_AddActorFactory, ActorFactory))
	{
		RemoveActorFactory();
		AddActorFactory();
	}
}
#endif // WITH_EDITOR

void UGameFeatureAction_AddActorFactory::AddActorFactory()
{
#if WITH_EDITOR
	if (ActorFactory.IsNull())
	{
		UE_LOG(LogAddActorFactory, Warning, TEXT("ActorFactory is null. Unable to add factory"));
		return;
	}
	if (UClass* FactoryClass = ActorFactory.LoadSynchronous())
	{
		if (!FactoryClass->IsChildOf(UActorFactory::StaticClass()))
		{
			UE_LOG(LogAddActorFactory, Error, TEXT("ActorFactory (%s) was not an ActorFactory class"), *FactoryClass->GetName());
			FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AddActorFactory_BadSubclass", "Selected class was not an ActorFactory class."));
			ActorFactory.Reset();
			return;
		}
		UE_LOG(LogAddActorFactory, Verbose, TEXT("Adding actor factory %s"), *FactoryClass->GetName());

		UActorFactory* NewFactory = NewObject<UActorFactory>(GetTransientPackage(), FactoryClass);
		if (NewFactory->bShouldAutoRegister)
		{
			FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AddActorFactory_AutoRegister", "The selected actor factory is set to auto register. Set the config variable bShouldAutoRegister to false before using this action."));
			ActorFactory.Reset();
			return;
		}
		GEditor->ActorFactories.Add(NewFactory);
		AddedFactory = NewFactory;

		if (IPlacementModeModule::IsAvailable())
		{
			IPlacementModeModule::Get().RegenerateItemsForCategory(FBuiltInPlacementCategories::AllClasses());
		}
	}
#endif // WITH_EDITOR
}

void UGameFeatureAction_AddActorFactory::RemoveActorFactory()
{
#if WITH_EDITOR
	if (UActorFactory* FactoryToRemove = Cast<UActorFactory>(AddedFactory.Get()))
	{
		UE_LOG(LogAddActorFactory, Verbose, TEXT("Removing actor factory %s"), *FactoryToRemove->GetName());

		GEditor->ActorFactories.Remove(FactoryToRemove);
		AddedFactory.Reset();

		if (IPlacementModeModule::IsAvailable())
		{
			IPlacementModeModule::Get().RegenerateItemsForCategory(FBuiltInPlacementCategories::AllClasses());
		}
	}
#endif // WITH_EDITOR
}

//////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE

GameFeatureAction_AddCheats.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddCheats.h"
#include "GameFramework/CheatManager.h"

#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddCheats)

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddCheats

void UGameFeatureAction_AddCheats::OnGameFeatureActivating()
{
	bIsActive = true;
	CheatManagerRegistrationHandle = UCheatManager::RegisterForOnCheatManagerCreated(FOnCheatManagerCreated::FDelegate::CreateUObject(this, &ThisClass::OnCheatManagerCreated));
}

void UGameFeatureAction_AddCheats::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	UCheatManager::UnregisterFromOnCheatManagerCreated(CheatManagerRegistrationHandle);

	for (TWeakObjectPtr<UCheatManagerExtension> ExtensionPtr : SpawnedCheatManagers)
	{
		if (UCheatManagerExtension* Extension = ExtensionPtr.Get())
		{
			UCheatManager* CheatManager = CastChecked<UCheatManager>(Extension->GetOuter());
			CheatManager->RemoveCheatManagerExtension(Extension);
		}
	}
	SpawnedCheatManagers.Empty();
	bIsActive = false;
}

#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_AddCheats::IsDataValid(FDataValidationContext& Context) const
{
	EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid);

	int32 EntryIndex = 0;
	for (const TSoftClassPtr<UCheatManagerExtension>& CheatManagerClassPtr : CheatManagers)
	{
		if (CheatManagerClassPtr.IsNull())
		{
			Result = EDataValidationResult::Invalid;
			Context.AddError(FText::Format(LOCTEXT("CheatEntryIsNull", "Null entry at index {0} in CheatManagers"), FText::AsNumber(EntryIndex)));
		}
		++EntryIndex;
	}

	return Result;
}
#endif

void UGameFeatureAction_AddCheats::OnCheatManagerCreated(UCheatManager* CheatManager)
{
	// First clean out any stale pointers
	for (int32 ManagerIdx = SpawnedCheatManagers.Num() - 1; ManagerIdx >= 0; --ManagerIdx)
	{
		if (!SpawnedCheatManagers[ManagerIdx].IsValid())
		{
			SpawnedCheatManagers.RemoveAtSwap(ManagerIdx);
		}
	}

	for (const TSoftClassPtr<UCheatManagerExtension>& CheatManagerClassPtr : CheatManagers)
	{
		if (!CheatManagerClassPtr.IsNull())
		{
			TSubclassOf<UCheatManagerExtension> CheatManagerClass = CheatManagerClassPtr.Get();
			if (CheatManagerClass != nullptr)
			{
				// The class is in memory. Spawn now.
				SpawnCheatManagerExtension(CheatManager, CheatManagerClass);
			}
			else if (bLoadCheatManagersAsync)
			{
				// The class is not in memory and we want to load async. Start async load now.
				TWeakObjectPtr<UGameFeatureAction_AddCheats> WeakThis(this);
				TWeakObjectPtr<UCheatManager> WeakCheatManager(CheatManager);
				LoadPackageAsync(CheatManagerClassPtr.GetLongPackageName(), FLoadPackageAsyncDelegate::CreateLambda(
					[WeakThis, WeakCheatManager, CheatManagerClassPtr](const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result)
				{
					if (Result == EAsyncLoadingResult::Succeeded)
					{
						UGameFeatureAction_AddCheats* StrongThis = WeakThis.Get();
						UCheatManager* StrongCheatManager = WeakCheatManager.Get();
						if (StrongThis && StrongThis->bIsActive && StrongCheatManager)
						{
							if (TSubclassOf<UCheatManagerExtension> LoadedCheatManagerClass = CheatManagerClassPtr.Get())
							{
								StrongThis->SpawnCheatManagerExtension(StrongCheatManager, LoadedCheatManagerClass);
							}
						}
					}
				}
				));
			}
			else
			{
				// The class is not in memory and we want to sync load. Load and spawn immediately.
				CheatManagerClass = CheatManagerClassPtr.LoadSynchronous();
				if (CheatManagerClass != nullptr)
				{
					SpawnCheatManagerExtension(CheatManager, CheatManagerClass);
				}
			}
		}
	}
};

void UGameFeatureAction_AddCheats::SpawnCheatManagerExtension(UCheatManager* CheatManager, const TSubclassOf<UCheatManagerExtension>& CheatManagerClass)
{
	if ((CheatManagerClass->ClassWithin == nullptr) || CheatManager->IsA(CheatManagerClass->ClassWithin))
	{
		UCheatManagerExtension* Extension = NewObject<UCheatManagerExtension>(CheatManager, CheatManagerClass);
		SpawnedCheatManagers.Add(Extension);
		CheatManager->AddCheatManagerExtension(Extension);
	}
}

//////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE


GameFeatureAction_AddChunkOverride.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddChunkOverride.h"
#include "Engine/AssetManager.h"
#include "Engine/AssetManagerSettings.h"
#include "GameFeatureData.h"
#include "Misc/MessageDialog.h"
#include "Misc/PathViews.h"

#if WITH_EDITOR
#include "Commandlets/ChunkDependencyInfo.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddChunkOverride)

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddChunkOverride

DEFINE_LOG_CATEGORY_STATIC(LogAddChunkOverride, Log, All);

namespace GameFeatureAction_AddChunkOverride
{
	static TMap<int32, TArray<FString>> ChunkIdToPluginMap;
	static TMap<FString, int32> PluginToChunkId;
}

UGameFeatureAction_AddChunkOverride::FShouldAddChunkOverride UGameFeatureAction_AddChunkOverride::ShouldAddChunkOverride;

void UGameFeatureAction_AddChunkOverride::OnGameFeatureRegistering()
{
	const bool bShouldAddChunkOverride = ShouldAddChunkOverride.IsBound() ? ShouldAddChunkOverride.Execute(GetTypedOuter<UGameFeatureData>()) : true;
	if (bShouldAddChunkOverride)
	{
		TWeakObjectPtr<UGameFeatureAction_AddChunkOverride> WeakThis(this);
		UAssetManager::CallOrRegister_OnCompletedInitialScan(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &UGameFeatureAction_AddChunkOverride::AddChunkIdOverride));
	}
}

void UGameFeatureAction_AddChunkOverride::OnGameFeatureUnregistering()
{
	RemoveChunkIdOverride();
}

#if WITH_EDITOR
TOptional<int32> UGameFeatureAction_AddChunkOverride::GetChunkForPackage(const FString& PackageName)
{
	if (GameFeatureAction_AddChunkOverride::PluginToChunkId.Num() == 0)
	{
		return TOptional<int32>();
	}

	static const FString EngineDir(TEXT("/Engine/"));
	static const FString GameDir(TEXT("/Game/"));
	if (PackageName.StartsWith(EngineDir, ESearchCase::CaseSensitive))
	{
		return TOptional<int32>();
	}
	else if (PackageName.StartsWith(GameDir, ESearchCase::CaseSensitive))
	{
		return TOptional<int32>();
	}
	else
	{
		FString MountPointName = FString(FPathViews::GetMountPointNameFromPath(PackageName));
		if (GameFeatureAction_AddChunkOverride::PluginToChunkId.Contains(MountPointName))
		{
			const int32 ExpectedChunkId = GameFeatureAction_AddChunkOverride::PluginToChunkId[MountPointName];
			return TOptional<int32>(ExpectedChunkId);
		}
	}
	return TOptional<int32>();
}

TArray<FString> UGameFeatureAction_AddChunkOverride::GetPluginNameFromChunkID(int32 ChunkID)
{
	return GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap.FindRef(ChunkID);
}

void UGameFeatureAction_AddChunkOverride::PostRename(UObject* OldOuter, const FName OldName)
{
	Super::PostRename(OldOuter, OldName);

	// If OldOuter is not GetTransientPackage(), but GetOuter() is GetTransientPackage(), then you were trashed.
	const UObject* MyOuter = GetOuter();
	const UPackage* TransientPackage = GetTransientPackage();
	if (OldOuter != TransientPackage && MyOuter == TransientPackage)
	{
		RemoveChunkIdOverride();
	}
}

void UGameFeatureAction_AddChunkOverride::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);

	const FName PropertyName = PropertyChangedEvent.GetPropertyName();
	if (PropertyName == GET_MEMBER_NAME_CHECKED(UGameFeatureAction_AddChunkOverride, bShouldOverrideChunk))
	{
		RemoveChunkIdOverride();
		// Generate a new value if we have an invalid chunkId
		if (bShouldOverrideChunk && ChunkId < 0)
		{
			UE_LOG(LogAddChunkOverride, Log, TEXT("Detected invalid ChunkId autogenerating new ID based on PluginName"));
			ChunkId = GenerateUniqueChunkId();
		}
		if (ChunkId >= 0)
		{
			AddChunkIdOverride();
		}
	}
	else if (PropertyName == GET_MEMBER_NAME_CHECKED(UGameFeatureAction_AddChunkOverride, ChunkId))
	{
		RemoveChunkIdOverride();
		AddChunkIdOverride();
	}
}

int32 UGameFeatureAction_AddChunkOverride::GetLowestAllowedChunkId()
{
	if (const UGameFeatureAction_AddChunkOverride* Action = UGameFeatureAction_AddChunkOverride::StaticClass()->GetDefaultObject<UGameFeatureAction_AddChunkOverride>())
	{
		return Action->LowestAllowedChunkIndexForAutoGeneration;
	}
	else
	{
		ensureMsgf(false, TEXT("Unable to get class default object for UGameFeatureAction_AddChunkOverride"));
		return INDEX_NONE;
	}
}

#endif // WITH_EDITOR

void UGameFeatureAction_AddChunkOverride::AddChunkIdOverride()
{
#if WITH_EDITOR
	if (!bShouldOverrideChunk)
	{
		return;
	}
	if (ChunkId < 0)
	{
		UE_LOG(LogAddChunkOverride, Error, TEXT("ChunkId is negative. Unable to override to a negative chunk"));
		return;
	}

	if (UGameFeatureData* GameFeatureData = GetTypedOuter<UGameFeatureData>())
	{
		UChunkDependencyInfo* DependencyInfo = GetMutableDefault<UChunkDependencyInfo>();
		

		if (FChunkDependency* ExistingDep = DependencyInfo->DependencyArray.FindByPredicate([CheckChunk = ChunkId](const FChunkDependency& ChunkDep)
			{
				return ChunkDep.ChunkID == CheckChunk;
			}))
		{
			// If we found this chunk it might have been auto generated. Update this instead of adding ours.
			if (ExistingDep->ParentChunkID == 0)
			{
				ExistingDep->ParentChunkID = ParentChunk;
			}
		}
		else
		{
			FChunkDependency NewChunkDependency;
			NewChunkDependency.ChunkID = ChunkId;
			NewChunkDependency.ParentChunkID = ParentChunk;
			DependencyInfo->DependencyArray.Add(NewChunkDependency);
		}
		DependencyInfo->GetOrBuildChunkDependencyGraph(ChunkId, true);

		FString PluginName;
		GameFeatureData->GetPluginName(PluginName);
		TArray<FString>& PluginsInChunk = GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap.FindOrAdd(ChunkId);
		PluginsInChunk.Add(PluginName);
		GameFeatureAction_AddChunkOverride::PluginToChunkId.Add(PluginName, ChunkId);
		UE_LOG(LogAddChunkOverride, Log, TEXT("Plugin(%s) will cook assets into chunk(%d)"), *PluginName, ChunkId);

		UAssetManager& Manager = UAssetManager::Get();

		FPrimaryAssetRules GFDRules;
		GFDRules.ChunkId = ChunkId;
		Manager.SetPrimaryAssetRules(GameFeatureData->GetPrimaryAssetId(), GFDRules);

		for (const FPrimaryAssetTypeInfo& AssetTypeInfo : GameFeatureData->GetPrimaryAssetTypesToScan())
		{
			FPrimaryAssetRulesCustomOverride Override;
			Override.PrimaryAssetType = FPrimaryAssetType(AssetTypeInfo.PrimaryAssetType);
			Override.FilterDirectory.Path = FString::Printf(TEXT("/%s"), *PluginName);
			Override.Rules.ChunkId = ChunkId;
			Manager.ApplyCustomPrimaryAssetRulesOverride(Override);
		}
	}
#endif // WITH_EDITOR
}

void UGameFeatureAction_AddChunkOverride::RemoveChunkIdOverride()
{
#if WITH_EDITOR
	// Remove primary asset rules by setting the override the default.
	if (UGameFeatureData* GameFeatureData = GetTypedOuter<UGameFeatureData>())
	{
		FString PluginName;
		GameFeatureData->GetPluginName(PluginName);
		if (!GameFeatureAction_AddChunkOverride::PluginToChunkId.Contains(PluginName))
		{
			UE_LOG(LogAddChunkOverride, Verbose, TEXT("No chunk override found for (%s) Skipping override removal"), *PluginName);
			return;
		}

		const int32 ChunkIdOverride = GameFeatureAction_AddChunkOverride::PluginToChunkId[PluginName];
		if (GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap.Contains(ChunkIdOverride))
		{
			GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap[ChunkIdOverride].Remove(PluginName);
			if (GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap[ChunkIdOverride].IsEmpty())
			{
				GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap.Remove(ChunkIdOverride);
			}
		}
		UE_LOG(LogAddChunkOverride, Log, TEXT("Removing ChunkId override (%d) for Plugin (%s)"), ChunkIdOverride, *PluginName);

		UAssetManager& Manager = UAssetManager::Get();

		Manager.SetPrimaryAssetRules(GameFeatureData->GetPrimaryAssetId(), FPrimaryAssetRules());
		for (const FPrimaryAssetTypeInfo& AssetTypeInfo : GameFeatureData->GetPrimaryAssetTypesToScan())
		{
			FPrimaryAssetRulesCustomOverride Override;
			Override.PrimaryAssetType = FPrimaryAssetType(AssetTypeInfo.PrimaryAssetType);
			Override.FilterDirectory.Path = FString::Printf(TEXT("/%s"), *PluginName);
			Manager.ApplyCustomPrimaryAssetRulesOverride(Override);
		}
	}
#endif // WITH_EDITOR
}

#if WITH_EDITOR
int32 UGameFeatureAction_AddChunkOverride::GenerateUniqueChunkId() const
{
	// Holdover auto-generation function until we can allow for Chunks to be specified by string name
	int32 NewChunkId = -1;
	UGameFeatureData* GameFeatureData = GetTypedOuter<UGameFeatureData>();
	if (ensure(GameFeatureData))
	{
		FString PluginName;
		GameFeatureData->GetPluginName(PluginName);

		uint32 NewId = GetTypeHash(PluginName);
		int16 SignedId = NewId;
		if (SignedId < 0)
		{
			SignedId = -SignedId;
		}
		NewChunkId = SignedId;
	}

	if (NewChunkId < LowestAllowedChunkIndexForAutoGeneration)
	{
		UE_LOG(LogAddChunkOverride, Warning, TEXT("Autogenerated ChunkId(%d) is lower than the config specified LowestAllowedChunkIndexForAutoGeneration(%d)"), NewChunkId, LowestAllowedChunkIndexForAutoGeneration);
		FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AddChunkOverride_InvalidId", "Autogenerated ChunkID is lower than config specified LowestAllowedChunkIndexForAutoGeneration. Please manually assign a valid Chunk Id"));
		NewChunkId = -1;
	}
	else if (GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap.Contains(NewChunkId))
	{
		UE_LOG(LogAddChunkOverride, Warning, TEXT("ChunkId(%d) is in use by %s. Unable to autogenerate unique id. Lowest allowed ChunkId(%d)"), NewChunkId, *FString::Join(GameFeatureAction_AddChunkOverride::ChunkIdToPluginMap[ChunkId], TEXT(",")), LowestAllowedChunkIndexForAutoGeneration);
		FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("AddChunkOverride_UsedChunkId", "Unable to auto generate unique valid Chunk Id. Please manually assign a valid Chunk Id"));
		NewChunkId = -1;
	}

	return NewChunkId;
}
#endif // WITH_EDITOR

//////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE


GameFeatureAction_AddComponents.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddComponents.h"
#include "AssetRegistry/AssetBundleData.h"
#include "Components/GameFrameworkComponentManager.h"
#include "Engine/GameInstance.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Engine/AssetManager.h"

#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddComponents)

static TAutoConsoleVariable<bool> CVarUseNewWorldTracking(
	TEXT("GameFeaturePlugin.AddComponentsAction.UseNewWorldTracking"),
	true,
	TEXT("If true, the AddComponents GFA will keep track of world changes and update added components when NetMode changes."),
	ECVF_Default);

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////
// FGameFeatureComponentEntry
FGameFeatureComponentEntry::FGameFeatureComponentEntry()
	: bClientComponent(true)
	, bServerComponent(true)
	, AdditionFlags(static_cast<uint8>(EGameFrameworkAddComponentFlags::None))
{
}

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddComponents

void UGameFeatureAction_AddComponents::OnGameFeatureActivating(FGameFeatureActivatingContext& Context)
{
	if (CVarUseNewWorldTracking.GetValueOnAnyThread())
	{
		// Bind once to static GameInstance delegates
		if (ActivationContextDataMap.Num() == 0)
		{
			GameInstanceStartHandle = FWorldDelegates::OnStartGameInstance.AddUObject(this, &UGameFeatureAction_AddComponents::HandleGameInstanceStart_NewWorldTracking);
			GameInstanceWorldChangedHandle = FWorldDelegates::OnGameInstanceWorldChanged.AddUObject(this, &UGameFeatureAction_AddComponents::HandleGameInstanceWorldChanged);
		}

		// Keep track of this activation for GameInstances that start later
		FActivationContextData& ActivationContextData = ActivationContextDataMap.Add(Context);

		// Process all existing WorldContexts that apply to this activation
		for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
		{
			if (Context.ShouldApplyToWorldContext(WorldContext))
			{
				if (WorldContext.OwningGameInstance)
				{
					AddGameInstanceForActivation(WorldContext.OwningGameInstance, ActivationContextData);
				}
			}
		}
	}
	else
	{
		FContextHandles& Handles = ContextHandles.FindOrAdd(Context);

		Handles.GameInstanceStartHandle = FWorldDelegates::OnStartGameInstance.AddUObject(this,
			&UGameFeatureAction_AddComponents::HandleGameInstanceStart, FGameFeatureStateChangeContext(Context));

		ensure(Handles.ComponentRequestHandles.Num() == 0);

		// Add to any worlds with associated game instances that have already been initialized
		for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
		{
			if (Context.ShouldApplyToWorldContext(WorldContext))
			{
				AddToWorld(WorldContext, Handles);
			}
		}
	}
}

void UGameFeatureAction_AddComponents::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	if (CVarUseNewWorldTracking.GetValueOnAnyThread())
	{
		ActivationContextDataMap.Remove(Context);
		
		if (ActivationContextDataMap.Num() == 0)
		{
			FWorldDelegates::OnStartGameInstance.Remove(GameInstanceStartHandle);
			GameInstanceStartHandle.Reset();

			FWorldDelegates::OnGameInstanceWorldChanged.Remove(GameInstanceWorldChangedHandle);
			GameInstanceWorldChangedHandle.Reset();
		}
	}
	else
	{
		FContextHandles& Handles = ContextHandles.FindOrAdd(Context);

		FWorldDelegates::OnStartGameInstance.Remove(Handles.GameInstanceStartHandle);

		// Releasing the handles will also remove the components from any registered actors too
		Handles.ComponentRequestHandles.Empty();
	}
}

#if WITH_EDITORONLY_DATA
void UGameFeatureAction_AddComponents::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
	if (UAssetManager::IsInitialized())
	{
		for (const FGameFeatureComponentEntry& Entry : ComponentList)
		{
			if (Entry.bClientComponent)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, Entry.ComponentClass.ToSoftObjectPath().GetAssetPath());
			}
			if (Entry.bServerComponent)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, Entry.ComponentClass.ToSoftObjectPath().GetAssetPath());
			}
		}
	}
}
#endif

#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_AddComponents::IsDataValid(FDataValidationContext& Context) const
{
	EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid);

	int32 EntryIndex = 0;
	for (const FGameFeatureComponentEntry& Entry : ComponentList)
	{
		if (Entry.ActorClass.IsNull())
		{
			Result = EDataValidationResult::Invalid;
			Context.AddError(FText::Format(LOCTEXT("ComponentEntryHasNullActor", "Null ActorClass at index {0} in ComponentList"), FText::AsNumber(EntryIndex)));
		}

		if (Entry.ComponentClass.IsNull())
		{
			Result = EDataValidationResult::Invalid;
			Context.AddError(FText::Format(LOCTEXT("ComponentEntryHasNullComponent", "Null ComponentClass at index {0} in ComponentList"), FText::AsNumber(EntryIndex)));
		}

		++EntryIndex;
	}

	return Result;
}
#endif

void UGameFeatureAction_AddComponents::AddToWorld(const FWorldContext& WorldContext, FContextHandles& Handles)
{
	UWorld* World = WorldContext.World();
	UGameInstance* GameInstance = WorldContext.OwningGameInstance;

	if ((GameInstance != nullptr) && (World != nullptr) && World->IsGameWorld())
	{
		if (UGameFrameworkComponentManager* GFCM = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance))
		{
			const ENetMode NetMode = World->GetNetMode();
			const bool bIsServer = NetMode != NM_Client;
			const bool bIsClient = NetMode != NM_DedicatedServer;

			UE_LOG(LogGameFeatures, Verbose, TEXT("Adding components for %s to world %s (client: %d, server: %d)"), *GetPathNameSafe(this), *World->GetDebugDisplayName(), bIsClient ? 1 : 0, bIsServer ? 1 : 0);
			
			for (const FGameFeatureComponentEntry& Entry : ComponentList)
			{
				const bool bShouldAddRequest = (bIsServer && Entry.bServerComponent) || (bIsClient && Entry.bClientComponent);
				if (bShouldAddRequest)
				{
					if (!Entry.ActorClass.IsNull())
					{
						UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Adding component to world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
						UE_SCOPED_ENGINE_ACTIVITY(TEXT("Adding component to world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
						TSubclassOf<UActorComponent> ComponentClass = Entry.ComponentClass.LoadSynchronous();
						if (ComponentClass)
						{
							Handles.ComponentRequestHandles.Add(GFCM->AddComponentRequest(Entry.ActorClass, ComponentClass, static_cast<EGameFrameworkAddComponentFlags>(Entry.AdditionFlags)));
						}
						else if (!Entry.ComponentClass.IsNull())
						{
							UE_LOG(LogGameFeatures, Error, TEXT("[GameFeatureData %s]: Failed to load component class %s. Not applying component."), *GetPathNameSafe(this), *Entry.ComponentClass.ToString());
						}
					}
				}
			}
		}
	}
}

void UGameFeatureAction_AddComponents::HandleGameInstanceStart(UGameInstance* GameInstance, FGameFeatureStateChangeContext ChangeContext)
{
	if (FWorldContext* WorldContext = GameInstance->GetWorldContext())
	{
		if (ChangeContext.ShouldApplyToWorldContext(*WorldContext))
		{
			FContextHandles* Handles = ContextHandles.Find(ChangeContext);
			if (ensure(Handles))
			{
				AddToWorld(*WorldContext, *Handles);
			}
		}
	}
}

void UGameFeatureAction_AddComponents::HandleGameInstanceStart_NewWorldTracking(UGameInstance* GameInstance)
{
	if (FWorldContext* WorldContext = GameInstance->GetWorldContext())
	{
		FObjectKey GameInstanceKey(GameInstance);

		// Add this GameInstance to all activation contexts that it applies to
		for (TPair<FGameFeatureStateChangeContext, FActivationContextData>& ActivationContextPair : ActivationContextDataMap)
		{
			const FGameFeatureStateChangeContext& ActivationContext = ActivationContextPair.Key;
			FActivationContextData& ActivationContextData = ActivationContextPair.Value;

			if (ActivationContext.ShouldApplyToWorldContext(*WorldContext))
			{
				AddGameInstanceForActivation(GameInstance, ActivationContextData);
			}
		}
	}
}

void UGameFeatureAction_AddComponents::HandleGameInstanceWorldChanged(UGameInstance* GameInstance, UWorld* OldWorld, UWorld* NewWorld)
{
	FObjectKey GameInstanceKey(GameInstance);

	for (TPair<FGameFeatureStateChangeContext, FActivationContextData>& ActivationContextPair : ActivationContextDataMap)
	{
		if (FGameInstanceData* GameInstanceData = ActivationContextPair.Value.GameInstanceDataMap.Find(FObjectKey(GameInstance)))
		{
			UGameFrameworkComponentManager* GFCM = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance);
			if (NewWorld && NewWorld->IsGameWorld() && GFCM)
			{
				// New world may have a different NetMode, update component requests
				UpdateComponentsOnManager(NewWorld, GFCM, *GameInstanceData);
			}
			else
			{
				// World set to null, reset component requests
				GameInstanceData->WorldNetMode = NM_MAX;
				GameInstanceData->ComponentRequestHandles.Reset();
			}
		}
		
	}
}

void UGameFeatureAction_AddComponents::AddGameInstanceForActivation(TNotNull<UGameInstance*> GameInstance, FActivationContextData& ActivationContextData)
{
	FGameInstanceData& GameInstanceData = ActivationContextData.GameInstanceDataMap.FindOrAdd(FObjectKey(GameInstance));

	UWorld* World = GameInstance->GetWorld();
	UGameFrameworkComponentManager* GFCM = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance);
	if (!World || !World->IsGameWorld() || !GFCM)
	{
		return;
	}

	UpdateComponentsOnManager(World, GFCM, GameInstanceData);
}

void UGameFeatureAction_AddComponents::UpdateComponentsOnManager(TNotNull<UWorld*> World, TNotNull<UGameFrameworkComponentManager*> Manager, FGameInstanceData& GameInstanceData)
{
	const bool bInitialAdd = (GameInstanceData.WorldNetMode == NM_MAX);
	const bool bWasServer = !bInitialAdd && (GameInstanceData.WorldNetMode != NM_Client);
	const bool bWasClient = !bInitialAdd && (GameInstanceData.WorldNetMode != NM_DedicatedServer);

	const ENetMode NetMode = World->GetNetMode();
	const bool bIsServer = NetMode != NM_Client;
	const bool bIsClient = NetMode != NM_DedicatedServer;
	GameInstanceData.WorldNetMode = NetMode;

	// No change in NetMode that affected client/server conditions, no components to update
	if ((bWasServer == bIsServer) && (bWasClient == bIsClient))
	{
		return;
	}

	if (bInitialAdd)
	{
		// Fill our handle array with null entries to start
		GameInstanceData.ComponentRequestHandles.AddDefaulted(ComponentList.Num());

		UE_LOG(LogGameFeatures, Verbose, TEXT("Adding components for %s to world %s (client: %d, server: %d)"), *GetPathNameSafe(this), *World->GetDebugDisplayName(), bIsClient ? 1 : 0, bIsServer ? 1 : 0);
	}
	else
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("Updating components for %s to world %s (client: %d->%d, server: %d->%d)"), *GetPathNameSafe(this), *World->GetDebugDisplayName(),
			bWasClient ? 1 : 0, bIsClient ? 1 : 0, bWasServer ? 1 : 0, bIsServer ? 1 : 0);
	}

	// Verify arrays are of equal length
	if (!ensure(ComponentList.Num() == GameInstanceData.ComponentRequestHandles.Num()))
	{
		return;
	}
	
	for (int32 i = 0; i < ComponentList.Num(); ++i)
	{
		const FGameFeatureComponentEntry& Entry = ComponentList[i];
		TSharedPtr<FComponentRequestHandle>& RequestHandle = GameInstanceData.ComponentRequestHandles[i];

		const bool bShouldAddRequest = (bIsServer && Entry.bServerComponent) || (bIsClient && Entry.bClientComponent);
		if (bShouldAddRequest && !RequestHandle.IsValid())
		{
			RequestHandle = AddComponentRequest(World, Manager, Entry);
		}
		else if (!bShouldAddRequest && RequestHandle.IsValid())
		{
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Removing component to world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
			UE_SCOPED_ENGINE_ACTIVITY(TEXT("Removing component from world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
			RequestHandle.Reset();
		}
	}
}

TSharedPtr<FComponentRequestHandle> UGameFeatureAction_AddComponents::AddComponentRequest(TNotNull<UWorld*> World, TNotNull<UGameFrameworkComponentManager*> Manager, const FGameFeatureComponentEntry& Entry)
{
	if (!Entry.ActorClass.IsNull())
	{
		UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Adding component to world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
		UE_SCOPED_ENGINE_ACTIVITY(TEXT("Adding component to world %s (%s)"), *World->GetDebugDisplayName(), *Entry.ComponentClass.ToString());
		TSubclassOf<UActorComponent> ComponentClass = Entry.ComponentClass.LoadSynchronous();
		if (ComponentClass)
		{
			return Manager->AddComponentRequest(Entry.ActorClass, ComponentClass, static_cast<EGameFrameworkAddComponentFlags>(Entry.AdditionFlags));
		}
		else if (!Entry.ComponentClass.IsNull())
		{
			UE_LOG(LogGameFeatures, Error, TEXT("[GameFeatureData %s]: Failed to load component class %s. Not applying component."), *GetPathNameSafe(this), *Entry.ComponentClass.ToString());
		}
	}

	return nullptr;
}

//////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE


GameFeatureAction_AddWPContent.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddWPContent.h"

#include "Misc/PackageName.h"
#include "UObject/Package.h"
#include "WorldPartition/ContentBundle/ContentBundleDescriptor.h"
#include "WorldPartition/ContentBundle/ContentBundleClient.h"
#include "GameFeatureData.h"
#include "GameFeaturesSubsystem.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddWPContent)

UGameFeatureAction_AddWPContent::UGameFeatureAction_AddWPContent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	ContentBundleDescriptor = ObjectInitializer.CreateDefaultSubobject<UContentBundleDescriptor>(this, TEXT("ContentBundleDescriptor"));

#if WITH_EDITOR
	if (UGameFeatureData* GameFeatureData = GetTypedOuter<UGameFeatureData>())
	{
		ContentBundleDescriptor->InitializeObject(GameFeatureData->GetName());
	}
#endif
}

void UGameFeatureAction_AddWPContent::OnGameFeatureRegistering()
{
	Super::OnGameFeatureRegistering();

	ContentBundleClient = FContentBundleClient::CreateClient(ContentBundleDescriptor, GetTypedOuter<UGameFeatureData>()->GetName());
	
#if WITH_EDITOR
	if (IsRunningCommandlet() && ContentBundleClient != nullptr)
	{
		ContentBundleClient->RequestContentInjection();
	}
#endif 
}

void UGameFeatureAction_AddWPContent::OnGameFeatureUnregistering()
{
	if (ContentBundleClient != nullptr)
	{
		ContentBundleClient->RequestUnregister();
		ContentBundleClient = nullptr;
	}

	Super::OnGameFeatureUnregistering();
}

void UGameFeatureAction_AddWPContent::OnGameFeatureActivating()
{
	Super::OnGameFeatureActivating();
	
	if (ContentBundleClient)
	{
		ContentBundleClient->RequestContentInjection();
	}
}

void UGameFeatureAction_AddWPContent::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	if (ContentBundleClient)
	{
		ContentBundleClient->RequestRemoveContent();
	}

	Super::OnGameFeatureDeactivating(Context);
}

GameFeatureAction_AddWorldPartitionContent.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AddWorldPartitionContent.h"
#include "WorldPartition/DataLayer/ExternalDataLayerEngineSubsystem.h"
#include "WorldPartition/DataLayer/ExternalDataLayerAsset.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeatureData.h"
#include "UObject/Package.h"
#include "Misc/PackageName.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AddWorldPartitionContent)

UGameFeatureAction_AddWorldPartitionContent::UGameFeatureAction_AddWorldPartitionContent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

void UGameFeatureAction_AddWorldPartitionContent::OnGameFeatureRegistering()
{
	Super::OnGameFeatureRegistering();
	if (ExternalDataLayerAsset)
	{
		check(IsValid(ExternalDataLayerAsset));
		UExternalDataLayerEngineSubsystem::Get().RegisterExternalDataLayerAsset(ExternalDataLayerAsset, this);
	}
}

void UGameFeatureAction_AddWorldPartitionContent::OnGameFeatureUnregistering()
{
	if (ExternalDataLayerAsset)
	{
		check(IsValid(ExternalDataLayerAsset));
		UExternalDataLayerEngineSubsystem::Get().UnregisterExternalDataLayerAsset(ExternalDataLayerAsset, this);
	}
	Super::OnGameFeatureUnregistering();
}

void UGameFeatureAction_AddWorldPartitionContent::OnGameFeatureActivating()
{
	Super::OnGameFeatureActivating();
	if (ExternalDataLayerAsset)
	{
		check(IsValid(ExternalDataLayerAsset));
		UExternalDataLayerEngineSubsystem::Get().ActivateExternalDataLayerAsset(ExternalDataLayerAsset, this);
	}
}

void UGameFeatureAction_AddWorldPartitionContent::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	if (ExternalDataLayerAsset)
	{
		check(IsValid(ExternalDataLayerAsset));
		UExternalDataLayerEngineSubsystem::Get().DeactivateExternalDataLayerAsset(ExternalDataLayerAsset, this);
	}
	Super::OnGameFeatureDeactivating(Context);
}

#if WITH_EDITOR

void UGameFeatureAction_AddWorldPartitionContent::PreEditChange(FProperty* PropertyThatWillChange)
{
	Super::PreEditChange(PropertyThatWillChange);

	PreEditChangeExternalDataLayerAsset.Reset();
	if (PropertyThatWillChange && PropertyThatWillChange->GetFName() == GET_MEMBER_NAME_CHECKED(UGameFeatureAction_AddWorldPartitionContent, ExternalDataLayerAsset))
	{
		PreEditChangeExternalDataLayerAsset = ExternalDataLayerAsset;
	}
}

void UGameFeatureAction_AddWorldPartitionContent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UGameFeatureAction_AddWorldPartitionContent, ExternalDataLayerAsset))
	{
		if (PreEditChangeExternalDataLayerAsset != ExternalDataLayerAsset)
		{
			OnExternalDataLayerAssetChanged(PreEditChangeExternalDataLayerAsset.Get(), ExternalDataLayerAsset);
		}
	}
	PreEditChangeExternalDataLayerAsset.Reset();

	Super::PostEditChangeProperty(PropertyChangedEvent);
}

void UGameFeatureAction_AddWorldPartitionContent::PreEditUndo()
{
	Super::PreEditUndo();

	PreEditUndoExternalDataLayerAsset = ExternalDataLayerAsset;
}

void UGameFeatureAction_AddWorldPartitionContent::PostEditUndo()
{
	if (PreEditUndoExternalDataLayerAsset != ExternalDataLayerAsset)
	{
		OnExternalDataLayerAssetChanged(PreEditUndoExternalDataLayerAsset.Get(), ExternalDataLayerAsset);
	}
	PreEditUndoExternalDataLayerAsset.Reset();

	Super::PostEditUndo();
}

void UGameFeatureAction_AddWorldPartitionContent::OnExternalDataLayerAssetChanged(const UExternalDataLayerAsset* OldAsset, const UExternalDataLayerAsset* NewAsset)
{
	// Detect if there's data associated to this EDL
	// Decide whether the data will be deleted or simply left there (unused)
	UExternalDataLayerEngineSubsystem& ExternalDataLayerEngineSubsystem = UExternalDataLayerEngineSubsystem::Get();
	if (OldAsset)
	{
		check(IsValid(OldAsset));
		if (ExternalDataLayerEngineSubsystem.IsExternalDataLayerAssetRegistered(OldAsset, this))
		{
			ExternalDataLayerEngineSubsystem.UnregisterExternalDataLayerAsset(OldAsset, this);
		}
	}

	if (NewAsset)
	{
		check(IsValid(NewAsset));
		if (IsGameFeaturePluginRegistered() && !ExternalDataLayerEngineSubsystem.IsExternalDataLayerAssetRegistered(NewAsset, this))
		{
			ExternalDataLayerEngineSubsystem.RegisterExternalDataLayerAsset(NewAsset, this);
		}
		if (IsGameFeaturePluginActive() && !ExternalDataLayerEngineSubsystem.IsExternalDataLayerAssetActive(NewAsset, this))
		{
			ExternalDataLayerEngineSubsystem.ActivateExternalDataLayerAsset(NewAsset, this);
		}
	}
}

#endif

GameFeatureAction_AudioActionBase.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_AudioActionBase.h"
#include "AudioDeviceManager.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_AudioActionBase)

#define LOCTEXT_NAMESPACE "GameFeatures"

void UGameFeatureAction_AudioActionBase::OnGameFeatureActivating(FGameFeatureActivatingContext& Context)
{
	DeviceCreatedHandle = FAudioDeviceManagerDelegates::OnAudioDeviceCreated.AddUObject(this, &UGameFeatureAction_AudioActionBase::OnDeviceCreated);
	DeviceDestroyedHandle = FAudioDeviceManagerDelegates::OnAudioDeviceDestroyed.AddUObject(this, &UGameFeatureAction_AudioActionBase::OnDeviceDestroyed);

	// Add to any existing devices
	if (FAudioDeviceManager* AudioDeviceManager = FAudioDeviceManager::Get())
	{
		AudioDeviceManager->IterateOverAllDevices([this](Audio::FDeviceId DeviceId, FAudioDevice* InDevice)
		{
			AddToDevice(FAudioDeviceManager::Get()->GetAudioDevice(DeviceId));
		});
	}
}

void UGameFeatureAction_AudioActionBase::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	// Remove from any existing devices
	if (FAudioDeviceManager* AudioDeviceManager = FAudioDeviceManager::Get())
	{
		AudioDeviceManager->IterateOverAllDevices([this](Audio::FDeviceId DeviceId, FAudioDevice* InDevice)
		{
			RemoveFromDevice(FAudioDeviceManager::Get()->GetAudioDevice(DeviceId));
		});
	}

	FAudioDeviceManagerDelegates::OnAudioDeviceCreated.Remove(DeviceCreatedHandle);
	FAudioDeviceManagerDelegates::OnAudioDeviceDestroyed.Remove(DeviceDestroyedHandle);
}

void UGameFeatureAction_AudioActionBase::OnDeviceCreated(Audio::FDeviceId InDeviceId)
{
	if (FAudioDeviceManager* AudioDeviceManager = FAudioDeviceManager::Get())
	{
		AddToDevice(AudioDeviceManager->GetAudioDevice(InDeviceId));
	}	
}

void UGameFeatureAction_AudioActionBase::OnDeviceDestroyed(Audio::FDeviceId InDeviceId)
{
	if (FAudioDeviceManager* AudioDeviceManager = FAudioDeviceManager::Get())
	{
		FAudioDeviceHandle AudioDeviceHandle = AudioDeviceManager->GetAudioDevice(InDeviceId);
		if (AudioDeviceHandle.IsValid())
		{
			RemoveFromDevice(AudioDeviceHandle);
		}
	}
}

#undef LOCTEXT_NAMESPACE

GameFeatureAction_DataRegistry.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_DataRegistry.h"
#include "AssetRegistry/AssetBundleData.h"
#include "GameFeaturesSubsystemSettings.h"
#include "GameFeaturesSubsystem.h"
#include "DataRegistrySubsystem.h"

#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_DataRegistry)

#define LOCTEXT_NAMESPACE "GameFeatures"

void UGameFeatureAction_DataRegistry::OnGameFeatureRegistering()
{
	Super::OnGameFeatureRegistering();

	if (ShouldPreloadAtRegistration())
	{
		// TODO: Right now this loads the source for both editor and runtime usage, in the future the preload could be changed to only allow resolves and not full data gets

		UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
		if (ensure(DataRegistrySubsystem))
		{
			for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
			{
				if (!RegistryToAdd.IsNull())
				{
					const FSoftObjectPath RegistryPath = RegistryToAdd.ToSoftObjectPath();

					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureRegistering %s: Preloading DataRegistry %s for editor preview"), *GetPathName(), *RegistryPath.ToString())
					DataRegistrySubsystem->LoadRegistryPath(RegistryPath);
				}
			}
		}
	}
}

void UGameFeatureAction_DataRegistry::OnGameFeatureActivating()
{
	Super::OnGameFeatureActivating();

	if (ShouldPreloadAtRegistration())
	{
		// This already happened at registration
		return;
	}

	UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
	if (ensure(DataRegistrySubsystem))
	{
		for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
		{
			if (!RegistryToAdd.IsNull())
			{
				const FSoftObjectPath RegistryPath = RegistryToAdd.ToSoftObjectPath();

#if !UE_BUILD_SHIPPING
				// If we're after data registry startup, then this asset should already exist in memory from either the bundle preload or game-specific logic
				if (DataRegistrySubsystem->AreRegistriesInitialized())
				{
					UDataRegistry* LoadedRegistry = RegistryToAdd.Get();
					if (!LoadedRegistry)
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureActivating %s: DataRegistry %s was not loaded before activation, this may cause a long hitch"), *GetPathName(), *RegistryPath.ToString())
					}
					else
					{
						// Need to verify this isn't already registered
						TArray<UDataRegistry*> RegistryList;
						DataRegistrySubsystem->GetAllRegistries(RegistryList);

						if (RegistryList.Contains(LoadedRegistry))
						{
							UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureActivating %s: DataRegistry %s is already enabled from another source! This can cause problems on deactivation"), *GetPathName(), *RegistryPath.ToString())
						}
					}
				}
#endif

				DataRegistrySubsystem->LoadRegistryPath(RegistryPath);
			}
		}
	}
}

void UGameFeatureAction_DataRegistry::OnGameFeatureUnregistering()
{
	Super::OnGameFeatureUnregistering();

	if (ShouldPreloadAtRegistration())
	{
		UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
		if (ensure(DataRegistrySubsystem))
		{
			for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
			{
				if (!RegistryToAdd.IsNull())
				{
					const FSoftObjectPath RegistryPath = RegistryToAdd.ToSoftObjectPath();

					// This should only happen when the user is manually changing phase via the feature editor UI
					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureUnregistering %s: Temporarily disabling preloaded DataRegistry %s"), *GetPathName(), *RegistryPath.ToString())

					DataRegistrySubsystem->IgnoreRegistryPath(RegistryPath);
				}
			}
		}
	}
}

void UGameFeatureAction_DataRegistry::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	Super::OnGameFeatureDeactivating(Context);

	if (ShouldPreloadAtRegistration())
	{
		// This will only happen at unregistration
		return;
	}

	UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
	if (ensure(DataRegistrySubsystem))
	{
		for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
		{
			if (!RegistryToAdd.IsNull())
			{
				// This does not do any reference counting, warning above will hit if this is registered from two separate sources and then the first to deactivate will remove it
				const FSoftObjectPath RegistryPath = RegistryToAdd.ToSoftObjectPath();
				DataRegistrySubsystem->IgnoreRegistryPath(RegistryPath);
			}
		}
	}
}

bool UGameFeatureAction_DataRegistry::ShouldPreloadAtRegistration()
{
	if (IsRunningCommandlet())
	{
		return bPreloadInCommandlets;
	}
	else
	{
		return GIsEditor && bPreloadInEditor;
	}
}

#if WITH_EDITORONLY_DATA
void UGameFeatureAction_DataRegistry::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
	Super::AddAdditionalAssetBundleData(AssetBundleData);
	for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
	{
		if(!RegistryToAdd.IsNull())
		{
			const FTopLevelAssetPath RegistryPath = RegistryToAdd.ToSoftObjectPath().GetAssetPath();

			// Add for both clients and servers, this will not work properly for games that do not set those bundle states
			// @TODO: If another way to preload specific assets is added, switch to that so it works regardless of bundles
			AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, RegistryPath);
			AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, RegistryPath);
		}
	}
}
#endif // WITH_EDITORONLY_DATA

#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_DataRegistry::IsDataValid(FDataValidationContext& Context) const
{
	EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid);

	int32 EntryIndex = 0;
	for (const TSoftObjectPtr<UDataRegistry>& RegistryToAdd : RegistriesToAdd)
	{
		if (RegistryToAdd.IsNull())
		{
			Context.AddError(FText::Format(LOCTEXT("DataRegistryMissingSource", "No valid data registry specified at index {0} in RegistriesToAdd"), FText::AsNumber(EntryIndex)));
			Result = EDataValidationResult::Invalid;
		}

		++EntryIndex;
	}

	return Result;
}
#endif // WITH_EDITOR

#undef LOCTEXT_NAMESPACE


GameFeatureAction_DataRegistrySource.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureAction_DataRegistrySource.h"
#include "AssetRegistry/AssetBundleData.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Engine/CurveTable.h"
#include "GameFeaturesProjectPolicies.h"
#include "DataRegistrySubsystem.h"
#include "Engine/DataTable.h"

#if WITH_EDITOR
#include "Misc/DataValidation.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureAction_DataRegistrySource)

#define LOCTEXT_NAMESPACE "GameFeatures"

void UGameFeatureAction_DataRegistrySource::OnGameFeatureRegistering()
{
	Super::OnGameFeatureRegistering();

	if (ShouldPreloadAtRegistration())
	{
		// TODO: Right now this loads the source for both editor and runtime usage, in the future the preload could be changed to only allow resolves and not full data gets

		UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
		if (ensure(DataRegistrySubsystem))
		{
			for (const FDataRegistrySourceToAdd& RegistrySource : SourcesToAdd)
			{
				// Don't check the client/server flags as they won't work properly
				TMap<FDataRegistryType, TArray<FSoftObjectPath>> AssetMap;
				TArray<FSoftObjectPath>& AssetList = AssetMap.Add(RegistrySource.RegistryToAddTo);

				if (!RegistrySource.DataTableToAdd.IsNull())
				{
					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureRegistering %s: Preloading DataRegistrySource %s for editor preview"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
						AssetList.Add(RegistrySource.DataTableToAdd.ToSoftObjectPath());
				}

				if (!RegistrySource.CurveTableToAdd.IsNull())
				{
					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureRegistering %s: Preloading DataRegistrySource %s for editor preview"), *GetPathName(), *RegistrySource.CurveTableToAdd.ToString())
						AssetList.Add(RegistrySource.CurveTableToAdd.ToSoftObjectPath());
				}

				DataRegistrySubsystem->PreregisterSpecificAssets(AssetMap, RegistrySource.AssetPriority);
			}
		}
	}
}

void UGameFeatureAction_DataRegistrySource::OnGameFeatureActivating()
{
	Super::OnGameFeatureActivating();

	if (ShouldPreloadAtRegistration())
	{
		// This already happened at registration
		return;
	}

	UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
	if (ensure(DataRegistrySubsystem))
	{
		UGameFeaturesProjectPolicies& Policy = UGameFeaturesSubsystem::Get().GetPolicy<UGameFeaturesProjectPolicies>();
		bool bIsClient, bIsServer;

		Policy.GetGameFeatureLoadingMode(bIsClient, bIsServer);

		for (const FDataRegistrySourceToAdd& RegistrySource : SourcesToAdd)
		{
			const bool bShouldAdd = (bIsServer && RegistrySource.bServerSource) || (bIsClient && RegistrySource.bClientSource);
			if (bShouldAdd)
			{
				TMap<FDataRegistryType, TArray<FSoftObjectPath>> AssetMap;
				TArray<FSoftObjectPath>& AssetList = AssetMap.Add(RegistrySource.RegistryToAddTo);

				if (!RegistrySource.DataTableToAdd.IsNull())
				{
					AssetList.Add(RegistrySource.DataTableToAdd.ToSoftObjectPath());
				}

				if (!RegistrySource.CurveTableToAdd.IsNull())
				{
					AssetList.Add(RegistrySource.CurveTableToAdd.ToSoftObjectPath());
				}

#if !UE_BUILD_SHIPPING
				// If we're after data registry startup, then this asset should already exist in memory from either the bundle preload or game-specific logic
				if (DataRegistrySubsystem->AreRegistriesInitialized())
				{
					if (!RegistrySource.DataTableToAdd.IsNull() && !RegistrySource.DataTableToAdd.IsValid())
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureActivating %s: DataRegistry source asset %s was not loaded before activation, this may cause a long hitch"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
					}

					if (!RegistrySource.CurveTableToAdd.IsNull() && !RegistrySource.CurveTableToAdd.IsValid())
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureActivating %s: DataRegistry source asset %s was not loaded before activation, this may cause a long hitch"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
					}
				}
#endif

				// This will either load the sources immediately, or schedule them for load when registries are initialized
				DataRegistrySubsystem->PreregisterSpecificAssets(AssetMap, RegistrySource.AssetPriority);
			}
		}
	}
}

void UGameFeatureAction_DataRegistrySource::OnGameFeatureUnregistering()
{
	Super::OnGameFeatureUnregistering();

	if (ShouldPreloadAtRegistration())
	{
		// This should only happen when the user is manually changing phase via the feature editor UI
		UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
		if (ensure(DataRegistrySubsystem))
		{
			for (const FDataRegistrySourceToAdd& RegistrySource : SourcesToAdd)
			{
				if (!RegistrySource.DataTableToAdd.IsNull())
				{
					if (!DataRegistrySubsystem->UnregisterSpecificAsset(RegistrySource.RegistryToAddTo, RegistrySource.DataTableToAdd.ToSoftObjectPath()))
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureUnregistering %s: DataRegistry data table %s failed to unregister"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
					}
					else
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureUnregistering %s: Temporarily disabling preloaded data table %s"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
					}
				}

				if (!RegistrySource.CurveTableToAdd.IsNull())
				{
					if (!DataRegistrySubsystem->UnregisterSpecificAsset(RegistrySource.RegistryToAddTo, RegistrySource.CurveTableToAdd.ToSoftObjectPath()))
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureUnregistering %s: DataRegistry curve table %s failed to unregister"), *GetPathName(), *RegistrySource.CurveTableToAdd.ToString())
					}
					else
					{
						UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureUnregistering %s: Temporarily disabling preloaded curve table %s"), *GetPathName(), *RegistrySource.CurveTableToAdd.ToString())
					}
				}
			}
		}
	}
}

void UGameFeatureAction_DataRegistrySource::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	Super::OnGameFeatureDeactivating(Context);

	if (ShouldPreloadAtRegistration())
	{
		// This will only happen at unregistration
		return;
	}

	UDataRegistrySubsystem* DataRegistrySubsystem = UDataRegistrySubsystem::Get();
	if (ensure(DataRegistrySubsystem))
	{
		for (const FDataRegistrySourceToAdd& RegistrySource : SourcesToAdd)
		{
			if (!RegistrySource.DataTableToAdd.IsNull())
			{
				if (!DataRegistrySubsystem->UnregisterSpecificAsset(RegistrySource.RegistryToAddTo, RegistrySource.DataTableToAdd.ToSoftObjectPath()))
				{
					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureDeactivating %s: DataRegistry data table %s failed to unregister"), *GetPathName(), *RegistrySource.DataTableToAdd.ToString())
				}
			}

			if (!RegistrySource.CurveTableToAdd.IsNull())
			{
				if (!DataRegistrySubsystem->UnregisterSpecificAsset(RegistrySource.RegistryToAddTo, RegistrySource.CurveTableToAdd.ToSoftObjectPath()))
				{
					UE_LOG(LogGameFeatures, Log, TEXT("OnGameFeatureDeactivating %s: DataRegistry curve table %s failed to unregister"), *GetPathName(), *RegistrySource.CurveTableToAdd.ToString())
				}
			}
		}
	}
}

bool UGameFeatureAction_DataRegistrySource::ShouldPreloadAtRegistration()
{
	// We want to preload in interactive editor sessions only
	return (GIsEditor && !IsRunningCommandlet() && bPreloadInEditor);
}

#if WITH_EDITORONLY_DATA
void UGameFeatureAction_DataRegistrySource::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
	Super::AddAdditionalAssetBundleData(AssetBundleData);
	for (const FDataRegistrySourceToAdd& RegistrySource : SourcesToAdd)
	{
		// Register table assets for preloading, this will only work if the game uses client/server bundle states
		// @TODO: If another way of preloading data is added, client+server sources should use that instead

		if (!RegistrySource.DataTableToAdd.IsNull())
		{
			const FTopLevelAssetPath DataTableSourcePath = RegistrySource.DataTableToAdd.ToSoftObjectPath().GetAssetPath();
			if (RegistrySource.bClientSource)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, DataTableSourcePath);
			}
			if (RegistrySource.bServerSource)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, DataTableSourcePath);
			}
		}

		if (!RegistrySource.CurveTableToAdd.IsNull())
		{
			const FTopLevelAssetPath CurveTableSourcePath = RegistrySource.CurveTableToAdd.ToSoftObjectPath().GetAssetPath();
			if (RegistrySource.bClientSource)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, CurveTableSourcePath);
			}
			if (RegistrySource.bServerSource)
			{
				AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, CurveTableSourcePath);
			}
		}
	}
}
#endif // WITH_EDITORONLY_DATA

#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_DataRegistrySource::IsDataValid(FDataValidationContext& Context) const
{
	EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid);

	int32 EntryIndex = 0;
	for (const FDataRegistrySourceToAdd& Entry : SourcesToAdd)
	{
		if (Entry.CurveTableToAdd.IsNull() && Entry.DataTableToAdd.IsNull())
		{
			Context.AddError(FText::Format(LOCTEXT("DataRegistrySourceMissingSource", "No valid data table or curve table specified at index {0} in SourcesToAdd"), FText::AsNumber(EntryIndex)));
			Result = EDataValidationResult::Invalid;
		}

		if (!Entry.CurveTableToAdd.IsNull())
		{
			FAssetData TableAssetData = IAssetRegistry::Get()->GetAssetByObjectPath(Entry.CurveTableToAdd.ToSoftObjectPath());

			// This will catch normal curve tables, composites, and any reasonably named subclass without doing a slow load
			if (!TableAssetData.IsValid() || !TableAssetData.AssetClassPath.GetAssetName().ToString().Contains(TEXT("CurveTable")))
			{
				Context.AddError(FText::Format(LOCTEXT("DataRegistrySourceMissingCurveTable", "Path {0} does not point to valid curvetable at index {1} in SourcesToAdd"), FText::FromString(Entry.CurveTableToAdd.ToString()), FText::AsNumber(EntryIndex)));
				Result = EDataValidationResult::Invalid;
			}
		}

		if (!Entry.DataTableToAdd.IsNull())
		{
			FAssetData TableAssetData = IAssetRegistry::Get()->GetAssetByObjectPath(Entry.DataTableToAdd.ToSoftObjectPath());

			// This will catch normal data tables, composites, and any reasonably named subclass without doing a slow load
			if (!TableAssetData.IsValid() || !TableAssetData.AssetClassPath.GetAssetName().ToString().Contains(TEXT("DataTable")))
			{
				Context.AddError(FText::Format(LOCTEXT("DataRegistrySourceMissingDataTable", "Path {0} does not point to valid datatable at index {1} in SourcesToAdd"), FText::FromString(Entry.DataTableToAdd.ToString()), FText::AsNumber(EntryIndex)));
				Result = EDataValidationResult::Invalid;
			}
		}

		if (Entry.bServerSource == false && Entry.bClientSource == false)
		{
			Context.AddError(FText::Format(LOCTEXT("DataRegistrySourceNeverUsed", "Source not specified to load on either client or server, it will be unused at index {0} in SourcesToAdd"), FText::AsNumber(EntryIndex)));
			Result = EDataValidationResult::Invalid;
		}

		if (Entry.RegistryToAddTo.IsNone())
		{
			Context.AddError(FText::Format(LOCTEXT("DataRegistrySourceInvalidRegistry", "Source specified an invalid name (NONE) as the target registry at index {0} in SourcesToAdd"), FText::AsNumber(EntryIndex)));
			Result = EDataValidationResult::Invalid;
		}

		++EntryIndex;
	}

	return Result;
}
#endif // WITH_EDITOR

#if WITH_EDITOR
void UGameFeatureAction_DataRegistrySource::AddSource(const FDataRegistrySourceToAdd& NewSource)
{
	SourcesToAdd.Add(NewSource);
}
#endif // WITH_EDITOR

#undef LOCTEXT_NAMESPACE


GameFeatureData.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureData.h"
#include "Algo/Accumulate.h"
#include "AssetRegistry/AssetData.h"
#include "Engine/AssetManager.h"
#include "GameFeaturesSubsystem.h"
#include "InstallBundleUtils.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/ConfigContext.h"
#include "Misc/ConfigUtilities.h"
#include "UObject/AssetRegistryTagsContext.h"
#include "UObject/CoreRedirects.h"
#include "DeviceProfiles/DeviceProfile.h"
#include "DeviceProfiles/DeviceProfileFragment.h"
#include "DeviceProfiles/DeviceProfileManager.h"
#include "Interfaces/IPluginManager.h"

#if WITH_EDITOR
#include "Settings/EditorExperimentalSettings.h"
#include "WorldPartition/ContentBundle/ContentBundleDescriptor.h"
#include "WorldPartition/ContentBundle/ContentBundlePaths.h"
#include "WorldPartition/DataLayer/ExternalDataLayerAsset.h"
#include "WorldPartition/DataLayer/ExternalDataLayerHelper.h"
#include "GameFeatureAction_AddWorldPartitionContent.h"
#include "GameFeatureAction_AddWPContent.h"
#include "Misc/DataValidation.h"
#include "Engine/Level.h"
#include "AssetRegistry/AssetRegistryState.h"
#include "AssetRegistry/PathTree.h"
#include "Misc/PathViews.h"
#endif

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureData)

#define LOCTEXT_NAMESPACE "GameFeatures"

//@TODO: GameFeaturePluginEnginePush: Editing actions/etc... for auto-activated plugins is a poor user experience;
// the changes won't take effect until the editor is restarted or deactivated/reactivated - should probably bounce
// them for you in pre/post edit change (assuming all actions properly handle unloading...)

#if WITH_EDITORONLY_DATA
void UGameFeatureData::UpdateAssetBundleData()
{
	Super::UpdateAssetBundleData();
	
	for (UGameFeatureAction* Action : Actions)
	{
		if (Action)
		{
			Action->AddAdditionalAssetBundleData(AssetBundleData);
		}
	}
}
#endif // WITH_EDITORONLY_DATA

#if WITH_EDITOR
EDataValidationResult UGameFeatureData::IsDataValid(FDataValidationContext& Context) const
{
	EDataValidationResult Result = CombineDataValidationResults(Super::IsDataValid(Context), EDataValidationResult::Valid);

	int32 EntryIndex = 0;
	for (const UGameFeatureAction* Action : Actions)
	{
		if (Action)
		{
			EDataValidationResult ChildResult = Action->IsDataValid(Context);
			Result = CombineDataValidationResults(Result, ChildResult);
		}
		else
		{
			Result = EDataValidationResult::Invalid;
			Context.AddError(FText::Format(LOCTEXT("ActionEntryIsNull", "Null entry at index {0} in Actions"), FText::AsNumber(EntryIndex)));
		}

		++EntryIndex;
	}

	return Result;
}
#endif

static TAutoConsoleVariable<bool> CVarAllowRuntimeDeviceProfiles(
	TEXT("GameFeaturePlugin.AllowRuntimeDeviceProfiles"),
	true,
	TEXT("Allow game feature plugins to generate device profiles from config based on existing parents"),
	ECVF_Default);

void UGameFeatureData::InitializeBasePluginIniFile(const FString& PluginInstalledFilename)
{
	const FString PluginName = FPaths::GetBaseFilename(PluginInstalledFilename);

	static bool bUseNewDynamicLayers = IConsoleManager::Get().FindConsoleVariable(TEXT("ini.UseNewDynamicLayers"))->GetInt() != 0;
	bool bIncludePluginNameInBranchName = true;

	// DEPRECATED NAMING PATH - must keep because these files are read in as a single file, not in a hierarchical way, so 
	// they don't have the + syntax for arrays
	{
		const FString PluginConfigDir = FPaths::GetPath(PluginInstalledFilename) / TEXT("Config/");
		const FString EngineConfigDir = FPaths::EngineConfigDir();

		const bool bIsBaseIniName = false;
		const bool bForceReloadFromDisk = false;
		const bool bWriteDestIni = false;

		// This will be the generated path including platform
		FString PluginConfigFilename = GConfig->GetConfigFilename(*PluginName);

		// Try the deprecated path first that doesn't include the Default prefix
		FConfigFile& PluginConfig = GConfig->Add(PluginConfigFilename, FConfigFile());
		if (FConfigCacheIni::LoadExternalIniFile(PluginConfig, *PluginName, *EngineConfigDir, *PluginConfigDir, bIsBaseIniName, nullptr, bForceReloadFromDisk, bWriteDestIni))
		{
			// This is the deprecated loading path that doesn't handle cases like + in arrays
			UE_LOG(LogGameFeatures, Log, TEXT("Loaded deprecated config %s, rename to start with Default for normal parsing"), *PluginConfigFilename);

			// register this plugin, so the ConfigContext.Load, and future loads, know about it
			if (bUseNewDynamicLayers)
			{
				TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(PluginName);
				FConfigCacheIni::RegisterPlugin(*Plugin->GetName(), Plugin->GetBaseDir(), Plugin->GetExtensionBaseDirs(), DynamicLayerPriority::GameFeature, bIncludePluginNameInBranchName);
			}

			FCoreRedirects::ReadRedirectsFromIni(PluginConfigFilename);
			ReloadConfigs(PluginConfig);

			return;
		}
	}

	if (bUseNewDynamicLayers)
	{
		TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(PluginName);

		// attempt to load config branch named for the plugin

		UE_LOG(LogGameFeatures, Verbose, TEXT("Loading GameFeature base plugin hierarchy for %s"), *PluginName);

		// register this plugin, so the ConfigContext.Load, and future loads, know about it
		FConfigCacheIni::RegisterPlugin(*Plugin->GetName(), Plugin->GetBaseDir(), Plugin->GetExtensionBaseDirs(), DynamicLayerPriority::GameFeature, bIncludePluginNameInBranchName);

		// load the plugin inis and track the modified sections
		FConfigContext Context = FConfigContext::ReadIntoGConfig();
		FString PluginConfigFilename;
		Context.ConfigFileTag = *Plugin->GetName();
		Context.Load(*PluginName, PluginConfigFilename);

		FConfigFile* PluginConfig = GConfig->Find(PluginConfigFilename);
		if (PluginConfig)
		{
			ReloadConfigs(*PluginConfig);
		}

		// @todo move this into ReloadObjectsFromModifiedConfigSections?
		FCoreRedirects::ReadRedirectsFromIni(PluginConfigFilename);

		return;
	}
	
	{
		// Now use old method to load the plugin hierarchy without the registering plugin stuff above
		FString PluginConfigFilename = GConfig->GetConfigFilename(*PluginName);
		FConfigFile& PluginConfig = GConfig->Add(PluginConfigFilename, FConfigFile());
		FConfigContext Context = FConfigContext::ReadIntoPluginFile(PluginConfig, *FPaths::GetPath(PluginInstalledFilename),
				IPluginManager::Get().FindPluginFromPath(PluginName)->GetExtensionBaseDirs());

		if (!Context.Load(*PluginName))
		{
			// Nothing to add, remove from map
			GConfig->Remove(PluginConfigFilename);
		}
		else
		{
			FCoreRedirects::ReadRedirectsFromIni(PluginConfigFilename);
			ReloadConfigs(PluginConfig);
		}
	}
}

void UGameFeatureData::InitializeHierarchicalPluginIniFiles(const FString& PluginInstalledFilename)
{
	static bool bUseNewDynamicLayers = IConsoleManager::Get().FindConsoleVariable(TEXT("ini.UseNewDynamicLayers"))->GetInt() != 0;
	if (bUseNewDynamicLayers)
	{
		const FName PluginName = *FPaths::GetBaseFilename(PluginInstalledFilename);
		//TSharedPtr<IPlugin> PluginSystemPlugin = IPluginManager::Get().FindPlugin(PluginName);
		//	checkf(FPaths::GetPath(PluginInstalledStandardFilename) == PluginSystemPlugin->GetBaseDir(), TEXT("Expected plugin system to have matching BaseDir to GFD plugin"));

		UE_LOG(LogGameFeatures, Verbose, TEXT("Loading GameFeature config modification for %s"), *PluginName.ToString());
		
		UE::DynamicConfig::PerformDynamicConfig(PluginName, [PluginName](FConfigModificationTracker* ChangeTracker)
			{
				// set which sections to track for cvars, with their priority
				ChangeTracker->CVars.Add(TEXT("ConsoleVariables")).CVarPriority = (int)ECVF_SetByPluginLowPriority;
				ChangeTracker->CVars.Add(TEXT("ConsoleVariables_HighPriority")).CVarPriority = (int)ECVF_SetByPluginHighPriority;

				// apply plugin modifications from this plugin to the everything
				FConfigCacheIni::AddPluginToAllBranches(PluginName, ChangeTracker);
			});

		UE::DynamicConfig::PerformDynamicConfig(PluginName, [PluginName](FConfigModificationTracker* ChangeTracker)
			{
				{
					// set which sections to track for cvars, with their priority
					ChangeTracker->CVars.Add(TEXT("ConsoleVariables")).CVarPriority = (int)ECVF_SetByHotfix;
					ChangeTracker->CVars.Add(TEXT("ConsoleVariables_HighPriority")).CVarPriority = (int)ECVF_SetByHotfix;

					// give hotfix a chance to run at that point
#define HOTFIX_BRANCH(Ini) UE::DynamicConfig::HotfixPluginForBranch.Broadcast(PluginName, #Ini, ChangeTracker);
					ENUMERATE_KNOWN_INI_FILES(HOTFIX_BRANCH);
#undef HOTFIX_BRANCH
				}
			});

		return;
	}

	UDeviceProfileManager& DeviceProfileManager = UDeviceProfileManager::Get();
	FString PlatformName = FPlatformProperties::IniPlatformName();

#if ALLOW_OTHER_PLATFORM_CONFIG
	const UDeviceProfile* PreviewDeviceProfile = DeviceProfileManager.GetPreviewDeviceProfile();
	if (PreviewDeviceProfile)
	{
		PlatformName = PreviewDeviceProfile->ConfigPlatform.IsEmpty() ? PreviewDeviceProfile->DeviceType : PreviewDeviceProfile->ConfigPlatform;
	}
#endif

	FString PluginInstalledStandardFilename = PluginInstalledFilename;
	if (!FPaths::IsRelative(PluginInstalledStandardFilename))
	{
		FPaths::MakeStandardFilename(PluginInstalledStandardFilename);
	}

	const FString PluginName = FPaths::GetBaseFilename(PluginInstalledStandardFilename);
	const FString PlatformExtensionDir = FPaths::ProjectPlatformExtensionDir(*PlatformName);
	const FString EngineConfigDir = FPaths::EngineConfigDir();
	const FString PluginConfigDir = FPaths::GetPath(PluginInstalledStandardFilename) / TEXT("Config/");
	const FString PluginPlatformConfigDir = FPaths::Combine(PluginConfigDir, PlatformName);
	const FString PluginPlatformExtensionDir = FPaths::GetPath(PluginInstalledStandardFilename).Replace(*FPaths::ProjectDir(), *PlatformExtensionDir) / TEXT("Config");

	// We're going to test a lot of paths, so only do it if some config actually exists
	if (!FPaths::DirectoryExists(PluginConfigDir) && !FPaths::DirectoryExists(PluginPlatformConfigDir) && !FPaths::DirectoryExists(PluginPlatformExtensionDir))
	{
		return;
	}

	const bool bIsBaseIniName = false;
	const bool bForceReloadFromDisk = false;
	const bool bWriteDestIni = false;
	const bool bCreateDeviceProfiles = CVarAllowRuntimeDeviceProfiles->GetBool();

	struct FIniLoadingParams
	{
		FIniLoadingParams(const FString& InName, bool bInUsePlatformDir = false, bool bInCreateDeviceProfiles = false)
			: Name(InName), bUsePlatformDir(bInUsePlatformDir), bCreateDeviceProfiles(bInCreateDeviceProfiles)
		{}

		FString Name;
		bool bUsePlatformDir;
		bool bCreateDeviceProfiles;
	};

	// Engine.ini and DeviceProfiles.ini will also support platform extensions
	TArray<FIniLoadingParams> IniFilesToLoad = { FIniLoadingParams(TEXT("Input")),
		FIniLoadingParams(TEXT("Game")), FIniLoadingParams(TEXT("Game"), true),
		FIniLoadingParams(TEXT("Engine")), FIniLoadingParams(TEXT("Engine"), true),
#if UE_EDITOR
		FIniLoadingParams(TEXT("Editor")),
#endif
		FIniLoadingParams(TEXT("DeviceProfiles"), false, bCreateDeviceProfiles),
		FIniLoadingParams(TEXT("DeviceProfiles"), true, bCreateDeviceProfiles)
	};

	// Create overridden device profiles for each matching rule in config
	auto InsertRuntimeDeviceProfilesIntoConfig = [&](FConfigFile& PluginConfig, const FConfigFile& ExistingConfig)
	{
		TMap<FString, FConfigSection> ConfigSectionsToAdd;

		TArray<FString> ResultingProfiles;

		for (auto& Section : AsConst(PluginConfig))
		{
			FString RuleName, ParentClass;
			if (Section.Key.Split(TEXT(" "), &RuleName, &ParentClass))
			{
				// Early reject anything that's not handled in here
				const bool bIsRuntimeDeviceProfileRule = (ParentClass == "RuntimeDeviceProfileRule");
				const bool bIsDeviceProfileFragment = (ParentClass == UDeviceProfileFragment::StaticClass()->GetName());
				if (!(bIsRuntimeDeviceProfileRule || bIsDeviceProfileFragment))
				{
					continue;
				}

				// Check the existing config because at this point in time it has already been hotfixed from the base empty config
				// Those CVars are also always without a +-. prefix because they're the result of the hotfix applied to the empty config.
				// @todo: we cannot use hotfixes to remove CVars because HF happens way before GFPs have a chance to load.
				// The hotfix process is destructive and doesn't leave us an opportunity to read the hotfix delta when loading GFPs.
				TArray<FConfigValue> HotfixCVars;
				if (const FConfigSection* HotfixSection = ExistingConfig.FindSection(Section.Key))
				{
					HotfixSection->MultiFind("CVars", HotfixCVars);
				}

				// Extract key-value pairs for CVars and FragmentIncludes, keeping the +-. prefix
				TMultiMap<FName, FConfigValue> PluginCVars;
				TMultiMap<FName, FConfigValue> FragmentIncludes;
				for (const auto& Entry : Section.Value)
				{
					const FString& EntryKey = Entry.Key.ToString();
					if (EntryKey.RightChop(1).StartsWith("CVars"))
					{
						PluginCVars.Add(Entry.Key, Entry.Value);
					}
					else if (EntryKey.RightChop(1).StartsWith("FragmentIncludes"))
					{
						FragmentIncludes.Add(Entry.Key, Entry.Value);
					}
				}

				// Check if a CVar should be either included, or removed for a new hotfix value
				auto ShouldKeepCVar = [&HotfixCVars](const FString& Key, FString& Value) -> bool
				{
					for (const FConfigValue& HotfixCVarData : HotfixCVars)
					{
						FString HotfixCVarKey, HotfixCVarValue;
						if (HotfixCVarData.GetValue().Split(TEXT("="), &HotfixCVarKey, &HotfixCVarValue) && HotfixCVarKey == Key && HotfixCVarValue != Value)
						{
							Value = HotfixCVarValue;
							return false;
						}
					}
					return true;
				};

				// Process new runtime device profile
				if (bIsRuntimeDeviceProfileRule)
				{
					UE_LOG(LogGameFeatures, Log, TEXT("Game feature '%s' found runtime device profile rule %s"), *PluginName, *RuleName);

					// Extract metadata
					const FConfigValue* ParentProfileName = Section.Value.Find("ParentProfileName");
					const FConfigValue* ProfileSuffix = Section.Value.Find("ProfileSuffix");

					if (ParentProfileName && ProfileSuffix)
					{
						// We need to load all candidate device profiles here or else we won't be able to create a child for them
						TArray<FString> LoadableProfileNames = DeviceProfileManager.GetLoadableProfileNames(*PlatformName);
						for (const FString& ProfileName : LoadableProfileNames)
						{
							DeviceProfileManager.FindProfile(ProfileName, true, *PlatformName);
						}

						for (const UDeviceProfile* Profile : DeviceProfileManager.Profiles)
						{
							// Check if one of the parents for this profile is the one this rule applies to
							bool bProfileHasCompatibleParent = false;
							const UDeviceProfile* CurrentProfile = Profile;
							do
							{
								bProfileHasCompatibleParent = CurrentProfile->GetName() == ParentProfileName->GetValue();
								CurrentProfile = CurrentProfile->GetParentProfile();
							} while (!bProfileHasCompatibleParent && CurrentProfile != nullptr);

							// Create the config for a runtime profile
							if (bProfileHasCompatibleParent && !Profile->GetName().EndsWith(ProfileSuffix->GetValue()))
							{
								// Ignore duplicates: only the first match in a given config file will be accepted
								const FString FinalProfileName = Profile->GetName() + ProfileSuffix->GetValue();
								if (ResultingProfiles.Contains(FinalProfileName))
								{
									UE_LOG(LogGameFeatures, Log, TEXT("Ignoring profile %s that has already been overriden as %s"), *Profile->GetName(), *FinalProfileName);
									continue;
								}

								FConfigSection RuntimeProfile;
								RuntimeProfile.Add("DeviceType", PlatformName);
								RuntimeProfile.Add("BaseProfileName", FConfigValue(Profile->GetName()));

								UE_LOG(LogGameFeatures, Log, TEXT("Creating override for base profile %s"), *Profile->GetName());

								// Inject the parent's matched fragments into the config, if any
								if (Profile->GetName().Contains("MatchedFragments"))
								{
									FString MatchingRulesSectionName = Profile->GetName() + TEXT(" ") + UDeviceProfile::StaticClass()->GetName();
									FString MatchingRulesArrayName = TEXT("MatchingRules");
									TArray<FString> MatchingRulesArray;

#if ALLOW_OTHER_PLATFORM_CONFIG
									FConfigCacheIni* PlatformConfigSystem = FConfigCacheIni::ForPlatform(*PlatformName);
#else
									FConfigCacheIni* PlatformConfigSystem = GConfig;
#endif

									PlatformConfigSystem->GetArray(*MatchingRulesSectionName, *MatchingRulesArrayName, MatchingRulesArray, GDeviceProfilesIni);
									UE_LOG(LogGameFeatures, Log, TEXT("Found %d fragment matching rules"), MatchingRulesArray.Num());

									for (const FString& Rule : MatchingRulesArray)
									{
										RuntimeProfile.Add("+MatchingRules", FConfigValue(Rule));
									}
								}

								// Add fragment includes
								for (const auto& FragmentInclude : FragmentIncludes)
								{
									RuntimeProfile.Add(FragmentInclude.Key, FConfigValue(FragmentInclude.Value.GetValue()));
								}

								// Add direct CVars
								for (const auto& CVar : PluginCVars)
								{
									FString CVarKey, CVarValue;
									if (CVar.Value.GetValue().Split(TEXT("="), &CVarKey, &CVarValue) && ShouldKeepCVar(CVarKey, CVarValue))
									{
										UE_LOG(LogGameFeatures, Log, TEXT(" Found CVar: %s=%s"), *CVarKey, *CVarValue);
										RuntimeProfile.Add(CVar.Key, FConfigValue(CVarKey + "=" + CVarValue));
									}
								}

								// Add hotfix CVars
								for (const auto& CVar : HotfixCVars)
								{
									FString CVarKey, CVarValue;
									if (CVar.GetValue().Split(TEXT("="), &CVarKey, &CVarValue) && !PluginCVars.Contains(FName(*CVarKey)))
									{
										UE_LOG(LogGameFeatures, Log, TEXT(" Added CVar: %s=%s"), *CVarKey, *CVarValue);
										RuntimeProfile.Add("+CVars", FConfigValue(CVarKey + "=" + CVarValue));
									}
								}

								ConfigSectionsToAdd.Add(FinalProfileName + TEXT(" ") + UDeviceProfile::StaticClass()->GetName(), RuntimeProfile);
								ResultingProfiles.Add(FinalProfileName);
							}
						}
					}
					else
					{
						UE_LOG(LogGameFeatures, Warning, TEXT("Game feature '%s' has invalid runtime device profile with parent %s, suffix %s, %d CVars, %d fragments"),
							*PluginName, ParentProfileName ? *ParentProfileName->GetValue() : TEXT("null"), ProfileSuffix ? *ProfileSuffix->GetValue() : TEXT("null"),
							FragmentIncludes.Num(), PluginCVars.Num());
					}
				}

				// Hotfix device profile fragments
				else if (bIsDeviceProfileFragment)
				{
					UE_LOG(LogGameFeatures, Log, TEXT("Game feature '%s' found device profile fragment %s"), *PluginName, *RuleName);

					if (HotfixCVars.Num() > 0)
					{
						// Update existing CVars
						for (const auto& CVar : PluginCVars)
						{
							FString PluginCVarKey, PluginCVarValue;
							if (CVar.Value.GetValue().Split(TEXT("="), &PluginCVarKey, &PluginCVarValue) && !ShouldKeepCVar(PluginCVarKey, PluginCVarValue))
							{
								UE_LOG(LogGameFeatures, Log, TEXT(" Removed CVar: %s"), *CVar.Value.GetValue());
								PluginConfig.RemoveFromSection(*Section.Key, CVar.Key, CVar.Value.GetValue());
							}
							else
							{
								UE_LOG(LogGameFeatures, Log, TEXT(" Kept CVar: %s=%s"), *PluginCVarKey, *PluginCVarValue);
							}
						}

						// Add new hotfix CVars
						for (const auto& CVar : HotfixCVars)
						{
							FString HotfixCVarKey, HotfixCVarValue;
							if (CVar.GetValue().Split(TEXT("="), &HotfixCVarKey, &HotfixCVarValue) && !PluginCVars.Contains(FName(*HotfixCVarKey)))
							{
								UE_LOG(LogGameFeatures, Log, TEXT(" Added CVar: %s=%s"), *HotfixCVarKey, *HotfixCVarValue);
								PluginConfig.AddToSection(*Section.Key, "+CVars", HotfixCVarKey + "=" + HotfixCVarValue);
							}
						}
					}
				}
			}
		}

		PluginConfig.Append(ConfigSectionsToAdd);
	};

	// Create device profiles for this plugin from config
	auto LoadDeviceProfilesFromConfig = [&](const FConfigFile& Config)
	{
		for (TPair<const FString&, const FConfigSection&> Section : Config)
		{
			FString ProfileName;
			FString ParentClass;
			if (Section.Key.Split(TEXT(" "), &ProfileName, &ParentClass) && ParentClass == UDeviceProfile::StaticClass()->GetName())
			{
				const FConfigValue* DeviceType = Section.Value.Find("DeviceType");
				if (DeviceType)
				{
					UE_LOG(LogGameFeatures, Log, TEXT("Game feature '%s' adding new device profile %s"), *PluginName, *ProfileName);
					DeviceProfileManager.CreateProfile(ProfileName, DeviceType->GetValue(), FString(), *PlatformName);
				}
			}
		}
	};

	// @todo: Likely we need to track the diffs this config caused and/or store versions/layers in order to unwind settings during unloading/deactivation
	for (const FIniLoadingParams& Ini : IniFilesToLoad)
	{
		const FString PluginIniName = Ini.bUsePlatformDir ? (PlatformName + PluginName + Ini.Name) : PluginName + Ini.Name;

		FString ConfigDirectory;
		if (Ini.bUsePlatformDir)
		{
			// We'll look first in the platform extension directory, then in the plugin's platform directory
			if (FPaths::FileExists(FPaths::Combine(PluginPlatformExtensionDir, PluginIniName + ".ini")))
			{
				ConfigDirectory = PluginPlatformExtensionDir;
			}
			else
			{
				ConfigDirectory = PluginPlatformConfigDir;
			}
		}
		else
		{
			ConfigDirectory = PluginConfigDir;
		}
		ConfigDirectory += TEXT("/");

		// @note: Loading the INI in this manner in order to have a record of relevant sections that were changed so that affected objects can be reloaded. By virtue of how
		// this is parsed (standalone instead of being treated as a combined diff), the actual data within the sections will likely be incorrect. As an example, users adding
		// to an array with the "+" syntax will have the "+" incorrectly embedded inside the data in the temp FConfigFile. It's properly handled in the Combine() below where the
		// actual INI changes are computed.
		FConfigFile Config;
		if (FConfigCacheIni::LoadExternalIniFile(Config, *PluginIniName, *EngineConfigDir, *ConfigDirectory, bIsBaseIniName, nullptr, bForceReloadFromDisk, bWriteDestIni) && (Config.Num() > 0))
		{
			UE_LOG(LogGameFeatures, Log, TEXT("Game feature '%s' loaded config file %s"), *PluginName, *PluginIniName);

			// Need to get the in-memory config filename, the on disk one is likely not up to date
			FString IniFile = GConfig->GetConfigFilename(*Ini.Name);

			// Ensure we push new device profile config to the appropriate config branch - GConfig could be Windows while we're previewing a console
			FConfigFile* ExistingConfig = nullptr;
#if ALLOW_OTHER_PLATFORM_CONFIG
			if (Ini.bCreateDeviceProfiles && Ini.bUsePlatformDir && !FPlatformProperties::RequiresCookedData())
			{
				FConfigCacheIni* PlatformConfigSystem = FConfigCacheIni::ForPlatform(*PlatformName);
				ExistingConfig = PlatformConfigSystem->FindConfigFile(GDeviceProfilesIni);
			}
#endif

			if (ExistingConfig == nullptr)
			{
				ExistingConfig = GConfig->FindConfigFile(IniFile);
			}

			if (ExistingConfig)
			{
				if (Ini.bCreateDeviceProfiles)
				{
					InsertRuntimeDeviceProfilesIntoConfig(Config, *ExistingConfig);
				}

				FString ConfigAsString;
				Config.WriteToString(ConfigAsString, PluginIniName);

				// @todo: Might want to consider modifying the engine level's API here to allow for a combination that yields affected
				// sections and/or optionally just does the reload itself. This route is less efficient than it needs to be, resulting in parsing twice, 
				// once above and once in the Combine() call. Using Combine() here specifically so that special INI syntax (+, ., etc.) is parsed correctly.
				const FString PluginIniPath = FString::Printf(TEXT("%s%s.ini"), *ConfigDirectory, *PluginIniName);
				ExistingConfig->CombineFromBuffer(ConfigAsString, PluginIniPath);

				FConfigFile::OverrideFromCommandline(ExistingConfig, Ini.Name);

				if (Ini.bCreateDeviceProfiles)
				{
					LoadDeviceProfilesFromConfig(Config);
				}
				else
				{
					ReloadConfigs(Config);
				}
			}
		}
	}
}

void UGameFeatureData::InitializeHierarchicalPluginIniFiles(const TArrayView<FString>& PluginInstalledFilenames)
{
	static bool bUseNewDynamicLayers = IConsoleManager::Get().FindConsoleVariable(TEXT("ini.UseNewDynamicLayers"))->GetInt() != 0;
	if (bUseNewDynamicLayers)
	{
		TArray<FName> PluginNames;
		PluginNames.Reserve(PluginInstalledFilenames.Num());
		for (const FString& PluginInstalledFilename : PluginInstalledFilenames)
		{
			PluginNames.Emplace(*FPaths::GetBaseFilename(PluginInstalledFilename));
		}

		if (UE_GET_LOG_VERBOSITY(LogGameFeatures) >= ELogVerbosity::Verbose)
		{
			FString PluginList = FString::Printf(TEXT("[%s]"), *Algo::Accumulate(PluginInstalledFilenames, FString(), [](FString InResult, const FString& InName)
			{
				InResult = InResult.IsEmpty() ? InName : InResult + "," + InName;
				return InResult;
			}));
			UE_LOG(LogGameFeatures, Verbose, TEXT("Loading GameFeature config modification for %s"), *PluginList);
		}

		UE::DynamicConfig::PerformDynamicConfig("InitializeHierarchicalPluginIniFiles", [&PluginNames](FConfigModificationTracker* ChangeTracker)
			{
				// set which sections to track for cvars, with their priority
				ChangeTracker->CVars.Add(TEXT("ConsoleVariables")).CVarPriority = (int)ECVF_SetByPluginLowPriority;
				ChangeTracker->CVars.Add(TEXT("ConsoleVariables_HighPriority")).CVarPriority = (int)ECVF_SetByPluginHighPriority;

				// apply plugin modifications from this plugin to the everything
				FConfigCacheIni::AddMultiplePluginsToAllBranches(PluginNames, ChangeTracker);
			});

		UE::DynamicConfig::PerformDynamicConfig("InitializeHierarchicalPluginIniFiles", [&PluginNames](FConfigModificationTracker* ChangeTracker)
			{
				{
					// set which sections to track for cvars, with their priority
					ChangeTracker->CVars.Add(TEXT("ConsoleVariables")).CVarPriority = (int)ECVF_SetByHotfix;
					ChangeTracker->CVars.Add(TEXT("ConsoleVariables_HighPriority")).CVarPriority = (int)ECVF_SetByHotfix;

					for (const FName PluginName : PluginNames)
					{
						// give hotfix a chance to run at that point
#define HOTFIX_BRANCH(Ini) UE::DynamicConfig::HotfixPluginForBranch.Broadcast(PluginName, #Ini, ChangeTracker);
						ENUMERATE_KNOWN_INI_FILES(HOTFIX_BRANCH);
#undef HOTFIX_BRANCH
					}
				}
			});
	}
	else
	{
		for (const FString& PluginInstalledFilename : PluginInstalledFilenames)
		{
			InitializeHierarchicalPluginIniFiles(PluginInstalledFilename);
		}
	}
}

void UGameFeatureData::ReloadConfigs(FConfigFile& PluginConfig)
{
	// Reload configs so objects get the changes
	for (const auto& ConfigEntry : AsConst(PluginConfig))
	{
		// Skip out if someone put a config section in the INI without any actual data
		if (ConfigEntry.Value.Num() == 0)
		{
			continue;
		}

		const FString& SectionName = ConfigEntry.Key;

		// @todo: This entire overarching process is very similar in its goals as that of UOnlineHotfixManager::HotfixIniFile.
		// Could consider a combined refactor of the hotfix manager, the base config cache system, etc. to expose an easier way to support this pattern

		// INI files might be handling per-object config items, so need to handle them specifically
		const int32 PerObjConfigDelimIdx = SectionName.Find(" ");
		if (PerObjConfigDelimIdx != INDEX_NONE)
		{
			const FString ObjectName = SectionName.Left(PerObjConfigDelimIdx);
			const FString ClassName = SectionName.Mid(PerObjConfigDelimIdx + 1);

			// Try to find the class specified by the per-object config
			UClass* ObjClass = UClass::TryFindTypeSlow<UClass>(*ClassName, EFindFirstObjectOptions::NativeFirst | EFindFirstObjectOptions::EnsureIfAmbiguous);
			if (ObjClass)
			{
				// Now try to actually find the object it's referencing specifically and update it
				// @note: Choosing not to warn on not finding it for now, as Fortnite has transient uses instantiated at run-time (might not be constructed yet)
				UObject* PerObjConfigObj = StaticFindFirstObject(ObjClass, *ObjectName, EFindFirstObjectOptions::ExactClass, ELogVerbosity::Warning, TEXT("UGameFeatureData::ReloadConfigs"));
				if (PerObjConfigObj)
				{
					// Intentionally using LoadConfig instead of ReloadConfig, since we do not want to call modify/preeditchange/posteditchange on the objects changed when GIsEditor
					PerObjConfigObj->LoadConfig(nullptr, nullptr, UE::LCPF_ReloadingConfigData | UE::LCPF_ReadParentSections, nullptr);
				}
			}
			else
			{
				UE_LOG(LogGameFeatures, Warning, TEXT("Couldn't find PerObjectConfig class %s for %s while processing %s, config changes won't be reloaded."), *ClassName, *ObjectName, *PluginConfig.Name.ToString());
			}
		}
		// Standard INI section case
		else
		{
			// Find the affected class and push updates to all instances of it, including children
			// @note:	Intentionally not using the propagation flags inherent in ReloadConfig to handle this, as it utilizes a naive complete object iterator
			//			and tanks performance pretty badly
			UClass* ObjClass = FindFirstObject<UClass>(*SectionName, EFindFirstObjectOptions::EnsureIfAmbiguous | EFindFirstObjectOptions::NativeFirst);
			if (ObjClass)
			{
				TArray<UObject*> FoundObjects;
				GetObjectsOfClass(ObjClass, FoundObjects, true, RF_NoFlags);
				for (UObject* CurFoundObj : FoundObjects)
				{
					if (IsValid(CurFoundObj))
					{
						// Intentionally using LoadConfig instead of ReloadConfig, since we do not want to call modify/preeditchange/posteditchange on the objects changed when GIsEditor
						CurFoundObj->LoadConfig(nullptr, nullptr, UE::LCPF_ReloadingConfigData | UE::LCPF_ReadParentSections, nullptr);
					}
				}
			}
		}
	}
}

#if WITH_EDITOR

TArray<UClass*> UGameFeatureData::GetDisallowedActions() const
{
	TArray<UClass*> DisallowedClasses;

	if (!GetDefault<UEditorExperimentalSettings>()->bEnableWorldPartitionExternalDataLayers)
	{
		DisallowedClasses.Add(UGameFeatureAction_AddWorldPartitionContent::StaticClass());
	}

	return DisallowedClasses;
}

void UGameFeatureData::GetDependencyDirectoriesFromAssetData(const FAssetData& AssetData, TArray<FString>& OutDependencyDirectories)
{
	const FString MountPoint = FPackageName::GetPackageMountPoint(AssetData.PackagePath.ToString()).ToString();

	TArray<FGuid> ContentBundleGuids = ContentBundlePaths::ParseContentBundleGuids(AssetData);
	for (const FGuid& ContentBundleGuid : ContentBundleGuids)
	{
		FString ContentBundleExternalActorPath;
		if (ContentBundlePaths::BuildContentBundleExternalActorPath(MountPoint, ContentBundleGuid, ContentBundleExternalActorPath))
		{
			const FString ExternalActorPath = ULevel::GetExternalActorsPath(ContentBundleExternalActorPath);
			OutDependencyDirectories.Add(ExternalActorPath);
		}
	}

	TArray<FExternalDataLayerUID> ExternalDataLayerUIDs;
	FExternalDataLayerHelper::GetExternalDataLayerUIDs(AssetData, ExternalDataLayerUIDs);
	for (const FExternalDataLayerUID& ExternalDataLayerUID : ExternalDataLayerUIDs)
	{
		FString ExternalDataLayerRootPath;
		if (FExternalDataLayerHelper::BuildExternalDataLayerRootPath(MountPoint, ExternalDataLayerUID, ExternalDataLayerRootPath))
		{
			const FString ExternalActorsPath = ULevel::GetExternalActorsPath(ExternalDataLayerRootPath);
			OutDependencyDirectories.Add(ExternalActorsPath);
		}
	}
}

void UGameFeatureData::GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const
{
	PRAGMA_DISABLE_DEPRECATION_WARNINGS;
	Super::GetAssetRegistryTags(OutTags);
	PRAGMA_ENABLE_DEPRECATION_WARNINGS;
}

void UGameFeatureData::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
{
	Super::GetAssetRegistryTags(Context);

	TArray<FGuid> ContentBundleGuids;
	TArray<FExternalDataLayerUID> ExternalDataLayerUIDs;

	ForEachObjectWithOuter(this, [&ExternalDataLayerUIDs, &ContentBundleGuids](UObject* Object)
	{
		if (UGameFeatureAction_AddWPContent* WPAction = Cast<UGameFeatureAction_AddWPContent>(Object))
		{
			if (const UContentBundleDescriptor* ContentBundleDescriptor = WPAction->GetContentBundleDescriptor())
			{
				ContentBundleGuids.Add(ContentBundleDescriptor->GetGuid());
			}
		}

		if (UGameFeatureAction_AddWorldPartitionContent* WPAction = Cast<UGameFeatureAction_AddWorldPartitionContent>(Object))
		{
			if (const UExternalDataLayerAsset* ExternalDataLayerAsset = WPAction->GetExternalDataLayerAsset())
			{
				ExternalDataLayerUIDs.Add(ExternalDataLayerAsset->GetUID());
			}
		}
	});

	ContentBundlePaths::AddRegistryTags(Context, ContentBundleGuids);
	FExternalDataLayerHelper::AddAssetRegistryTags(Context, ExternalDataLayerUIDs);
}
#endif

FString UGameFeatureData::GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	const FString BundleName = FString::Printf(TEXT("GFP_%.*s"), PluginName.Len(), PluginName.GetData());
	if (bEvenIfDoesntExist)
	{
		return BundleName;
	}

	if (InstallBundleUtil::HasInstallBundleInConfig(BundleName))
	{
		return BundleName;
	}
	else
	{
		return TEXT("");
	}
}

FString UGameFeatureData::GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	const FString OptionalBundleName = FString::Printf(TEXT("GFP_%.*sOptional"), PluginName.Len(), PluginName.GetData());
	if (bEvenIfDoesntExist)
	{
		return OptionalBundleName;
	}

	if (InstallBundleUtil::HasInstallBundleInConfig(OptionalBundleName))
	{
		return OptionalBundleName;
	}
	else
	{
		return TEXT("");
	}
}

void UGameFeatureData::GetPluginName(FString& PluginName) const
{
	UGameFeatureData::GetPluginName(this, PluginName);
}

void UGameFeatureData::GetPluginName(const UGameFeatureData* GFD, FString& PluginName)
{
	if (GFD)
	{
		const bool bIsTransient = (GFD->GetFlags() & RF_Transient) != 0;
		if (bIsTransient)
		{
			PluginName = GFD->GetName();
		}
		else
		{
			const FString GameFeaturePath = GFD->GetOutermost()->GetName();
			if (ensureMsgf(UAssetManager::GetContentRootPathFromPackageName(GameFeaturePath, PluginName), TEXT("Must be a valid package path with a root. GameFeaturePath: %s"), *GameFeaturePath))
			{
				// Trim the leading and trailing slashes
				PluginName = PluginName.LeftChop(1).RightChop(1);
			}
			else
			{
				// Not a great fallback but better than nothing. Make sure this asset is in the right folder so we can get the plugin name.
				PluginName = GFD->GetName();
			}
		}
	}
}

bool UGameFeatureData::IsGameFeaturePluginRegistered(bool bCheckForRegistering /*= false*/) const
{
	FString PluginURL;
	FString PluginName;
	GetPluginName(PluginName);
	if (UGameFeaturesSubsystem::Get().GetPluginURLByName(PluginName, PluginURL))
	{
		return UGameFeaturesSubsystem::Get().IsGameFeaturePluginRegistered(PluginURL, bCheckForRegistering);
	}
	return false;
}

bool UGameFeatureData::IsGameFeaturePluginActive(bool bCheckForActivating /*= false*/) const
{
	FString PluginURL;
	FString PluginName;
	GetPluginName(PluginName);
	if (UGameFeaturesSubsystem::Get().GetPluginURLByName(PluginName, PluginURL))
	{
		return UGameFeaturesSubsystem::Get().IsGameFeaturePluginActive(PluginURL, bCheckForActivating);
	}
	return false;
}

#if WITH_EDITOR
FGameFeatureDataExternalAssetsPathCache::FGameFeatureDataExternalAssetsPathCache()
{
	// Install delegate on PathsAdded event
	// This is the only thing we watch to invalidate the cache
	// 
	// Reasoning:
	// 
	// Any newly added Actor in a previously absent EDL or CB for a level causes a directory to be created so we get invalidated and the path map is updated
	// Removed actors leave empty paths behind temporarily , we'll report a few empty paths, but no wrong dependency will be created from those empty paths
	// Newly mounted/unmounted plugins also fire this event so that'll update the cache 
	FExternalObjectAndActorDependencyGatherer::SetExternalAssetPathsProvider(this);

	// Of note....
	// 
	// If mounting 2 plugins in sequence, and one of those plugins has dependencies on the other
	// plugin there's a possibility we'll be gathering and updating the cache before the 2nd plugin is 
	// mounted. 
	// 
	// For example if plugin A has a world with EDL content in plugin B, if the dependencies for world A
	// are gathered before the plugin B is mounted we'll not detect it's actors and the gather 
	// will not be redone on world A after plugin B is mounted. 
}

FGameFeatureDataExternalAssetsPathCache::~FGameFeatureDataExternalAssetsPathCache()
{
	if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
	{
		AssetRegistry->OnPathsAdded().Remove(OnPathAddedDelegateHandle);
	}

	FExternalObjectAndActorDependencyGatherer::SetExternalAssetPathsProvider(nullptr);
}

void FGameFeatureDataExternalAssetsPathCache::OnPathsAdded(TConstArrayView<FStringView>)
{
	// next update will rebuild it
	bCacheIsUpToDate = false;
}

void FGameFeatureDataExternalAssetsPathCache::UpdateCache(const FUpdateCacheContext& Context)
{	
	if (bCacheIsUpToDate)
	{
		return;
	}

	TRACE_CPUPROFILER_EVENT_SCOPE(FGameFeatureDataExternalAssetsPathCache::UpdateCache);

	if (!OnPathAddedDelegateHandle.IsValid())
	{
		OnPathAddedDelegateHandle = IAssetRegistry::Get()->OnPathsAdded().AddRaw(this, &FGameFeatureDataExternalAssetsPathCache::OnPathsAdded);
	}

	bCacheIsUpToDate= true;
	PerLevelAssetDirectories.Reset();

	auto EnumerateAllAssetsOfTypeRecursive = [&Context](UClass* Class, bool bNativeOnly, auto AssetLambda)
	{
		if(bNativeOnly)
		{
			TArray<UClass*> DerivedClasses;
			GetDerivedClasses(Class, DerivedClasses, true);
			DerivedClasses.Add(Class);

			for(UClass* DerivedClass : DerivedClasses)
			{
				Context.AssetRegistryState.EnumerateAssetsByClassPathName(DerivedClass->GetClassPathName(), [&AssetLambda](const FAssetData* AssetData)->bool
				{
					return AssetLambda(*AssetData);
				});
			}
		}
		else
		{
			FARFilter Filter;	
			Filter.bRecursiveClasses = true;
			Filter.ClassPaths = { Class->GetClassPathName() };
				
			Context.AssetRegistryState.EnumerateAssets(Context.CompileFilterFunc(Filter), {}, AssetLambda);
		}
	};

	// Build set of all levels for validation while discovering them in external actors
	EnumerateAllAssetsOfTypeRecursive(UWorld::StaticClass(), true, [this](const FAssetData& AssetData)->bool
	{
		AllLevels.Add(AssetData.PackageName);
		return true;
	});

	EnumerateAllAssetsOfTypeRecursive(UGameFeatureData::StaticClass(), true, [this, &Context](const FAssetData& AssetData)->bool
	{
		auto GetLevelSubPaths = [this, &Context](FStringView RootPath, FStringView ParentPath, FName ParentPathName, auto GetLevelSubPathsFn)->void
		{
			Context.CachedPathTree.EnumerateSubPaths(ParentPathName, [&RootPath, &ParentPath, &GetLevelSubPathsFn,this, &ParentPathName](FName SubPath)
			{
				// Identify folder pattern where we switch from __ExternalActors__/EDL/EDLUID/The/Map/Path/MapName to the sub folders for External Actors
				// And verify against the Level set we found a real level path (just in case somebody likes really short paths)
				TStringBuilder<256> SubPathStringBuilder;
				SubPathStringBuilder << SubPath;

				FStringView LeafName = FPathViews::GetCleanFilename(FStringView(SubPathStringBuilder));
				if (LeafName.Len() == 1)
				{
					FStringView LevelString = ParentPath.RightChop(RootPath.Len());
					FName LevelPathName (LevelString);

					// could be a level
					if (AllLevels.Contains(LevelPathName))
					{
						// keep level 
						PerLevelAssetDirectories.Add(LevelPathName, ParentPathName);

						// once we've found one, there no point continuing
						return false;
					}
				}
				
				GetLevelSubPathsFn(RootPath, SubPathStringBuilder, SubPath, GetLevelSubPathsFn);

				return true;
			}, false);
		};

		TArray<FExternalDataLayerUID> EDLUIDs;
		FExternalDataLayerHelper::GetExternalDataLayerUIDs(AssetData, EDLUIDs);
		TArray<FGuid> ContentBundleGuids = ContentBundlePaths::ParseContentBundleGuids(AssetData);

		if (EDLUIDs.Num() || ContentBundleGuids.Num())
		{
			FString& MountPoint = GameFeatureDataAssetsToMountPoint.FindOrAdd(AssetData.PackagePath);
			if (MountPoint.IsEmpty())
			{
				MountPoint = FPackageName::GetPackageMountPoint(AssetData.PackagePath.ToString()).ToString();
			}

			for (const FExternalDataLayerUID& EDLUID : EDLUIDs)
			{
				FString EDLRootPath;
				if(FExternalDataLayerHelper::BuildExternalDataLayerActorsRootPath(MountPoint, EDLUID, EDLRootPath))
				{
					FName EDLRootPathName(EDLRootPath);
					GetLevelSubPaths(EDLRootPath, EDLRootPath, EDLRootPathName, GetLevelSubPaths);
				}
			}

		
			for (const FGuid& ContentBundleGuid : ContentBundleGuids)
			{
				FString CBRootPath;
				if (ContentBundlePaths::BuildContentBundleActorsRootPath(MountPoint, ContentBundleGuid, CBRootPath))
				{
					FName CBRootPathName(CBRootPath);
					GetLevelSubPaths(CBRootPath, CBRootPath, CBRootPathName, GetLevelSubPaths);
				}
			}
		}
		return true;
	});
}

TArray<FName> FGameFeatureDataExternalAssetsPathCache::GetPathsForPackage(FName LevelPath)
{
	TArray<FName> Paths;
	PerLevelAssetDirectories.MultiFind(LevelPath, Paths);
	return Paths;
}

#endif //#if WITH_EDITOR

#undef LOCTEXT_NAMESPACE


GameFeatureDataAssetDependencyGatherer.cpp

// Copyright Epic Games, Inc. All Rights Reserved.
#include "GameFeatureDataAssetDependencyGatherer.h"
#include "Misc/PackageName.h"

#if WITH_EDITOR

#include "AssetRegistry/ARFilter.h"
#include "GameFeatureData.h"
#include "AssetRegistry/AssetRegistryState.h"

// Register FGameFeatureDataAssetDependencyGatherer for UGameFeatureData class
REGISTER_ASSETDEPENDENCY_GATHERER(FGameFeatureDataAssetDependencyGatherer, UGameFeatureData);

void FGameFeatureDataAssetDependencyGatherer::GatherDependencies(FGatherDependenciesContext& Context) const
{
	FARFilter Filter;
	Filter.bRecursivePaths = true;
	Filter.bIncludeOnlyOnDiskAssets = true;

	TArray<FString> DependencyDirectories;
	UGameFeatureData::GetDependencyDirectoriesFromAssetData(Context.GetAssetData(), DependencyDirectories);
	for (const FString& DependencyDirectory : DependencyDirectories)
	{
		Context.GetOutDependencyDirectories().Add(DependencyDirectory);
		Filter.PackagePaths.Add(*DependencyDirectory);
	}

	if (Filter.PackagePaths.Num() > 0)
	{
		TArray<FAssetData> FilteredAssets;
		Context.GetAssetRegistryState().GetAssets(Context.CompileFilter(Filter), {}, FilteredAssets, true);

		for (const FAssetData& FilteredAsset : FilteredAssets)
		{
			Context.GetOutDependencies().Emplace(IAssetDependencyGatherer::FGathereredDependency{ FilteredAsset.PackageName,
				UE::AssetRegistry::EDependencyProperty::Game | UE::AssetRegistry::EDependencyProperty::Build });
		}
	}
}

#endif

GameFeatureDataAssetDependencyGatherer.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#if WITH_EDITOR

#include "AssetRegistry/AssetDependencyGatherer.h"

class FGameFeatureDataAssetDependencyGatherer : public IAssetDependencyGatherer
{
public:
	
	FGameFeatureDataAssetDependencyGatherer() = default;
	virtual ~FGameFeatureDataAssetDependencyGatherer() = default;

	virtual void GatherDependencies(FGatherDependenciesContext& Context) const override;
};

#endif

GameFeatureOptionalContentInstaller.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureOptionalContentInstaller.h"

#include "Algo/AllOf.h"
#include "GameFeaturePluginOperationResult.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeatureTypes.h"
#include "Logging/StructuredLog.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureOptionalContentInstaller)

namespace GameFeatureOptionalContentInstaller
{
	static const ELogVerbosity::Type InstallBundleManagerVerbosityOverride = ELogVerbosity::Verbose;

	static const FStringView ErrorNamespace = TEXTVIEW("GameFeaturePlugin.OptionalDownload.");

	static TAutoConsoleVariable<bool> CVarEnableOptionalContentInstaller(TEXT("GameFeatureOptionalContentInstaller.Enable"), 
		true,
		TEXT("Enable optional content installer"));
}

const FName UGameFeatureOptionalContentInstaller::GFOContentRequestName = TEXT("GFOContentRequest");

TMulticastDelegate<void(const FString& PluginName, const UE::GameFeatures::FResult&)> UGameFeatureOptionalContentInstaller::OnOptionalContentInstalled;
TMulticastDelegate<void()> UGameFeatureOptionalContentInstaller::OnOptionalContentInstallStarted;
TMulticastDelegate<void(const bool bInstallSuccessful)> UGameFeatureOptionalContentInstaller::OnOptionalContentInstallFinished;

void UGameFeatureOptionalContentInstaller::BeginDestroy()
{
	IConsoleManager::Get().UnregisterConsoleVariableSink_Handle(CVarSinkHandle);
	Super::BeginDestroy();
}

void UGameFeatureOptionalContentInstaller::Init(
	TUniqueFunction<TArray<FName>(FString)> InGetOptionalBundlePredicate,
	TUniqueFunction<TArray<FName>(FString)> InGetOptionalKeepBundlePredicate)
{
	GetOptionalBundlePredicate = MoveTemp(InGetOptionalBundlePredicate);
	GetOptionalKeepBundlePredicate = MoveTemp(InGetOptionalKeepBundlePredicate);
	BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

	// Create the cvar sink
	CVarSinkHandle = IConsoleManager::Get().RegisterConsoleVariableSink_Handle(
		FConsoleCommandDelegate::CreateUObject(this, &UGameFeatureOptionalContentInstaller::OnCVarsChanged));
	bEnabledCVar = GameFeatureOptionalContentInstaller::CVarEnableOptionalContentInstaller.GetValueOnGameThread();
}

void UGameFeatureOptionalContentInstaller::Enable(bool bInEnable)
{
	bool bOldEnabled = IsEnabled();
	bEnabled = bInEnable;
	bEnabledCVar = GameFeatureOptionalContentInstaller::CVarEnableOptionalContentInstaller.GetValueOnGameThread();
	bool bNewEnabled = IsEnabled();

	if (bOldEnabled != bNewEnabled)
	{
		if (bNewEnabled)
		{
			OnEnabled();
		}
		else
		{
			OnDisabled();
		}
	}
}

void UGameFeatureOptionalContentInstaller::UninstallContent()
{
	for (const FString& GFP : RelevantGFPs)
	{
		UE_LOG(LogGameFeatures, Log, TEXT("Uninstalling Optional bundles for %s"), *GFP);
		ReleaseContent(GFP, EInstallBundleReleaseRequestFlags::RemoveFilesIfPossible);
	}
	RelevantGFPs.Empty();
}

void UGameFeatureOptionalContentInstaller::EnableCellularDownloading(bool bEnable)
{
	if (bAllowCellDownload == bEnable)
	{
		return;
	}

	bAllowCellDownload = bEnable;
	BundleManager->SetCellularPreference(bAllowCellDownload ? 1 : 0);

	// Update flags on active requests
	for ( TPair<FString, FGFPInstall>& Pair : ActiveGFPInstalls)
	{
		BundleManager->UpdateContentRequestFlags(Pair.Value.BundlesEnqueued,
			bEnable ? EInstallBundleRequestFlags::None : EInstallBundleRequestFlags::CheckForCellularDataUsage,
			bEnable ? EInstallBundleRequestFlags::CheckForCellularDataUsage : EInstallBundleRequestFlags::None);
	}
}

bool UGameFeatureOptionalContentInstaller::HasOngoingInstalls() const
{
	return ActiveGFPInstalls.Num() > 0;
}

float UGameFeatureOptionalContentInstaller::GetAllInstallsProgress()
{
	if (TotalProgressTracker.IsSet())
	{
		TotalProgressTracker->ForceTick();
		return TotalProgressTracker->GetCurrentCombinedProgress().ProgressPercent;
	}

	if (ActiveGFPInstalls.Num() > 0 && RelevantGFPs.Num() > 0)
	{
		// Start the tracker for next calls to this function
		StartTotalProgressTracker();
	}

	// Return 1 if some optional bundles are installed, 0 if none are installed or active installs are present 
	return ActiveGFPInstalls.Num() == 0 && RelevantGFPs.Num() > 0 ? 1.f : 0.f;
}

bool UGameFeatureOptionalContentInstaller::UpdateContent(const FString& PluginName, bool bIsPredownload)
{
	TArray<FName> Bundles = GetOptionalBundlePredicate(PluginName);

	bool bIsAvailable = false;
	if (!Bundles.IsEmpty())
	{
		TValueOrError<FInstallBundleCombinedInstallState, EInstallBundleResult> MaybeInstallState = BundleManager->GetInstallStateSynchronous(Bundles, true);
		if (MaybeInstallState.HasValue())
		{
			const FInstallBundleCombinedInstallState& InstallState = MaybeInstallState.GetValue();
			bIsAvailable = Algo::AllOf(Bundles, [&InstallState](FName BundleName) { return InstallState.IndividualBundleStates.Contains(BundleName); });
			bIsAvailable &= ensureMsgf(InstallState.IndividualBundleStates.Num() <= Bundles.Num(),
				TEXT("UGameFeatureOptionalContentInstaller does not support dependencies tracking. Check optional install bundle dependencies for plugin %s."),
				*PluginName);
		}
	}

	if (!bIsAvailable)
	{
		return false;
	}

	for (const FName& Bundle : Bundles)
	{
		UE_LOG(LogGameFeatures, Log, TEXT("Requesting update for %s"), *Bundle.ToString());
	}
	EInstallBundleRequestFlags InstallFlags = EInstallBundleRequestFlags::AsyncMount | EInstallBundleRequestFlags::ExplicitUpdateList;
	if (bIsPredownload)
	{
		InstallFlags |= EInstallBundleRequestFlags::SkipMount;
	}
	if (!bAllowCellDownload)
	{
		InstallFlags |= EInstallBundleRequestFlags::CheckForCellularDataUsage;
	}

	TValueOrError<FInstallBundleRequestInfo, EInstallBundleResult> MaybeRequest = BundleManager->RequestUpdateContent(
		Bundles, 
		InstallFlags, 
		GameFeatureOptionalContentInstaller::InstallBundleManagerVerbosityOverride);

	if (MaybeRequest.HasError())
	{
		UE_LOGFMT(LogGameFeatures, Error, "Failed to request optional content for GFP {GFP}, Error: {Error}", 
			("GFP", PluginName),
			("Error", LexToString(MaybeRequest.GetError())));

		UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
			GameFeatureOptionalContentInstaller::ErrorNamespace.Len(), GameFeatureOptionalContentInstaller::ErrorNamespace.GetData(),
			LexToString(MaybeRequest.GetError())));
		OnOptionalContentInstalled.Broadcast(PluginName, ErrorResult);

		return false;
	}

	FInstallBundleRequestInfo& Request =  MaybeRequest.GetValue();
	if (!Request.BundlesEnqueued.IsEmpty())
	{
		const bool bIsOptionalContentInstallStart = ActiveGFPInstalls.Num() == 0;
	
		FGFPInstall& Pending = ActiveGFPInstalls.FindOrAdd(PluginName);
		
		if (bIsOptionalContentInstallStart)
		{
			// We call the delegate after adding the entry to 'ActiveGFPInstalls'. 
			// If we called it before then the code triggered by this delegate could request information from the current script
			// and since 'ActiveGFPInstalls' would be empty it would behave as if no installs were happening.
			OnOptionalContentInstallStarted.Broadcast();
		}

		if (!Pending.CallbackHandle.IsValid())
		{
			Pending.CallbackHandle = IInstallBundleManager::InstallBundleCompleteDelegate.AddUObject(this,
				&UGameFeatureOptionalContentInstaller::OnContentInstalled, PluginName);
		}

		// This should overwrite any previous pending request info
		Pending.BundlesEnqueued = MoveTemp(Request.BundlesEnqueued);
		Pending.bIsPredownload = bIsPredownload;
	}

	return true;
}

void UGameFeatureOptionalContentInstaller::OnContentInstalled(FInstallBundleRequestResultInfo InResult, FString PluginName)
{
	FGFPInstall* MaybeInstall = ActiveGFPInstalls.Find(PluginName);
	if (!MaybeInstall)
	{
		return;
	}

	FGFPInstall& GFPInstall = *MaybeInstall;
	if (!GFPInstall.BundlesEnqueued.Contains(InResult.BundleName))
	{
		return;
	}

	GFPInstall.BundlesEnqueued.Remove(InResult.BundleName);

	UE_LOG(LogGameFeatures, Log, TEXT("Finished install for %s"), *InResult.BundleName.ToString());
	if (InResult.Result != EInstallBundleResult::OK)
	{
		if (InResult.OptionalErrorCode.IsEmpty())
		{
			UE_LOGFMT(LogGameFeatures, Error, "Failed to install optional bundle {Bundle} for GFP {GFP}, Error: {Error}",
				("Bundle", InResult.BundleName),
				("GFP", PluginName),
				("Error", LexToString(InResult.Result)));
		}
		else
		{
			UE_LOGFMT(LogGameFeatures, Error, "Failed to install optional bundle {Bundle} for GFP {GFP}, Error: {Error}",
				("Bundle", InResult.BundleName),
				("GFP", PluginName),
				("Error", InResult.OptionalErrorCode));
		}

		//Use OptionalErrorCode and/or OptionalErrorText if available
		const FString ErrorCodeEnding = (InResult.OptionalErrorCode.IsEmpty()) ? LexToString(InResult.Result) : InResult.OptionalErrorCode;
		FText ErrorText = InResult.OptionalErrorCode.IsEmpty() ? UE::GameFeatures::CommonErrorCodes::GetErrorTextForBundleResult(InResult.Result) : InResult.OptionalErrorText;
		UE::GameFeatures::FResult ErrorResult = UE::GameFeatures::FResult(
			MakeError(FString::Printf(TEXT("%.*s%s"), GameFeatureOptionalContentInstaller::ErrorNamespace.Len(), GameFeatureOptionalContentInstaller::ErrorNamespace.GetData(), *ErrorCodeEnding)),
			MoveTemp(ErrorText)
		);
		OnOptionalContentInstalled.Broadcast(PluginName, ErrorResult);

		// Cancel any remaining downloads
		BundleManager->CancelUpdateContent(GFPInstall.BundlesEnqueued);
	}

	if (GFPInstall.BundlesEnqueued.IsEmpty())
	{
		if (GFPInstall.bIsPredownload)
		{
			// Predownload shouldn't pin any cached bundles so release them now

			// Delay call to ReleaseBundlesIfPossible. We don't want to release them from within the complete callback.
			FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this, 
			[this, PluginName, bInstalled = InResult.bContentWasInstalled](float)
			{
				// A machine is active, don't release
				if (!RelevantGFPs.Contains(PluginName))
				{
					ReleaseContent(PluginName);
				}

				if (bInstalled)
				{
					OnOptionalContentInstalled.Broadcast(PluginName, MakeValue());
				}

				return false;
			}));
		}
		else if (InResult.bContentWasInstalled)
		{
			OnOptionalContentInstalled.Broadcast(PluginName, MakeValue());
		}

		// book keeping
		IInstallBundleManager::InstallBundleCompleteDelegate.Remove(GFPInstall.CallbackHandle);
		ActiveGFPInstalls.Remove(PluginName);

		if (ActiveGFPInstalls.Num() == 0)
		{
			const bool bInstallSuccessful = RelevantGFPs.Num() > 0;
			OnOptionalContentInstallFinished.Broadcast(bInstallSuccessful);
			TotalProgressTracker.Reset();
		}
	}
}

void UGameFeatureOptionalContentInstaller::ReleaseContent(const FString& PluginName, EInstallBundleReleaseRequestFlags Flags)
{
	TArray<FName> Bundles = GetOptionalBundlePredicate(PluginName);
	if (Bundles.IsEmpty())
	{
		return;
	}
	TArray<FName> KeepBundles = GetOptionalKeepBundlePredicate(PluginName);

	Flags |= EInstallBundleReleaseRequestFlags::ExplicitRemoveList;

	BundleManager->RequestReleaseContent(
		Bundles, 
		Flags,
		KeepBundles, 
		GameFeatureOptionalContentInstaller::InstallBundleManagerVerbosityOverride);
}

void UGameFeatureOptionalContentInstaller::OnEnabled()
{
	ensure(RelevantGFPs.IsEmpty());
	RelevantGFPs.Empty();

	UGameFeaturesSubsystem::Get().ForEachGameFeature([this](FGameFeatureInfo&& Info) -> void
	{
		if (Info.CurrentState >= EGameFeaturePluginState::Downloading)
		{
			if (UpdateContent(Info.Name, false))
			{
				RelevantGFPs.Add(Info.Name);
			}
		}
	});
}

void UGameFeatureOptionalContentInstaller::OnDisabled()
{
	for (const FString& GFP : RelevantGFPs)
	{
		ReleaseContent(GFP);
	}

	RelevantGFPs.Empty();
	TotalProgressTracker.Reset();
}

bool UGameFeatureOptionalContentInstaller::IsEnabled() const
{
	return bEnabled && bEnabledCVar;
}

void UGameFeatureOptionalContentInstaller::OnCVarsChanged()
{
	Enable(bEnabled); // Check if CVar changed IsEnabled() and if so, call callbacks
}

void UGameFeatureOptionalContentInstaller::StartTotalProgressTracker()
{
	TotalProgressTracker.Reset();
	BundleManager->CancelAllGetContentStateRequestsForTag(GFOContentRequestName);
	
	TArray<FName> AllActiveBundleInstalls;
	for (const TTuple<FString, FGFPInstall>& ActiveInstall : ActiveGFPInstalls)
	{
		for (const FName& BundleEnqueued : ActiveInstall.Value.BundlesEnqueued)
		{
			AllActiveBundleInstalls.AddUnique(BundleEnqueued);
		}
	}

	if (AllActiveBundleInstalls.Num() > 0 && RelevantGFPs.Num() > 0)
	{
		// Start a new progress tracker for the currently active bundle installs. Auto tick is disabled.
		TotalProgressTracker.Emplace(false);
		
		TWeakObjectPtr This_WeakPtr = this;	
		BundleManager->GetContentState(AllActiveBundleInstalls, EInstallBundleGetContentStateFlags::None, false,
			FInstallBundleGetContentStateDelegate::CreateLambda([This_WeakPtr](FInstallBundleCombinedContentState BundleContentState)
			{
				if (This_WeakPtr.IsValid())
				{
					const TStrongObjectPtr<UGameFeatureOptionalContentInstaller> This_StrongPtr = This_WeakPtr.Pin();
					if (This_StrongPtr->TotalProgressTracker.IsSet())
					{
						TArray<FName> RequiredBundlesForTracking;
						for (const TPair<FName, FInstallBundleContentState>& BundleState : BundleContentState.IndividualBundleStates)
						{
							RequiredBundlesForTracking.Add(BundleState.Key);
						}
						This_StrongPtr->TotalProgressTracker->SetBundlesToTrackFromContentState(BundleContentState, MoveTemp(RequiredBundlesForTracking));
					}
				}
			}), GFOContentRequestName);
	}
}

void UGameFeatureOptionalContentInstaller::OnGameFeaturePredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	if (!IsEnabled())
	{
		return;
	}

	UpdateContent(PluginName, true);
	// Predownloads are not 'relevant', they don't have an active state machine
}

void UGameFeatureOptionalContentInstaller::OnGameFeatureDownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	if (!IsEnabled())
	{
		return;
	}

	if (UpdateContent(PluginName, false))
	{
		RelevantGFPs.Add(PluginName);
	}
}

void UGameFeatureOptionalContentInstaller::OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL)
{
    // Used for already downloaded cached plugins that do not download at startup but register.
    if (!IsEnabled() || RelevantGFPs.Contains(PluginName))
    {
        return;
    }

    if (UpdateContent(PluginName, false))
    {
        RelevantGFPs.Add(PluginName);
    }
}

void UGameFeatureOptionalContentInstaller::OnGameFeatureReleasing(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	if (!IsEnabled())
	{
		return;
	}

	ReleaseContent(PluginName);

	RelevantGFPs.Remove(PluginName);
}

GameFeaturePluginOperationResult.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturePluginOperationResult.h"
#include "GameFeaturesSubsystem.h" // needed for log category
#include "InstallBundleTypes.h"
#include "Internationalization/Internationalization.h"
#include "Logging/LogMacros.h"

namespace UE::GameFeatures
{
	FResult::FResult(FErrorCodeType ErrorCodeIn)
		: ErrorCode(MoveTemp(ErrorCodeIn))
		, OptionalErrorText()
	{
	}

	FResult::FResult(FErrorCodeType ErrorCodeIn, FText ErrorTextIn)
		: ErrorCode(MoveTemp(ErrorCodeIn))
		, OptionalErrorText(MoveTemp(ErrorTextIn))
	{
	}

	FString ToString(const FResult& Result)
	{
		TStringBuilder<512> Out;
		if (Result.HasValue())
		{
			Out << TEXT("Success");
		}
		else
		{
			Out << TEXT("ErrorCode=") << Result.GetError();
			if (!Result.OptionalErrorText.IsEmpty())
			{
				Out << TEXT(", ErrorText=") << Result.OptionalErrorText.ToString();
			}
		}
		return Out.ToString();
	}

	namespace CommonErrorCodes
	{
		const FText Generic_FatalError = NSLOCTEXT("GameFeatures", "ErrorCodes.GenericFatalError", "A fatal error has occurred installing the game feature. An update to the application may be needed. Please check for updates and restart the application.");
		const FText Generic_ConnectionError = NSLOCTEXT("GameFeatures", "ErrorCodes.ConnectionGenericError", "An internet connection error has occurred. Please try again later.");
		const FText Generic_MountError = NSLOCTEXT("GameFeatures", "ErrorCodes.MountGenericError", "An error has occurred loading data for this game feature. Please try again later.");

		const FText BundleResult_NeedsUpdate = NSLOCTEXT("GameFeatures", "ErrorCodes.BundleResult.NeedsUpdate", "An application update is required to install this game feature. Please restart the application after downloading any required updates.");
		const FText BundleResult_NeedsCacheSpace = NSLOCTEXT("GameFeatures", "ErrorCodes.BundleResult.NeedsCacheSpace", "Unable to allocate enough space in the cache to install this game feature.");
		const FText BundleResult_NeedsDiskSpace = NSLOCTEXT("GameFeatures", "ErrorCodes.BundleResult.NeedsDiskSpace", "You do not have enough disk space to install this game feature. Please try again after clearing up disk space.");
		const FText BundleResult_DownloadCancelled = NSLOCTEXT("GameFeatures", "ErrorCodes.BundleResult.DownloadCancelled", "This game feature download was canceled.");

		const FText ReleaseResult_Generic = NSLOCTEXT("GameFeatures", "ErrorCodes.ReleaseResult.Generic", "There was an error uninstalling the content for this game feature. Please restart the application and try again.");
		const FText ReleaseResult_Cancelled = NSLOCTEXT("GameFeatures", "ErrorCodes.ReleaseResult.Cancelled", "This game feature uninstall was canceled.");

		const FText& GetErrorTextForBundleResult(EInstallBundleResult ErrorResult)
		{
			switch (ErrorResult)
			{
				//These errors mean an app update is available that we either don't have or failed to get.
				case EInstallBundleResult::FailedPrereqRequiresLatestClient:
				case EInstallBundleResult::FailedPrereqRequiresLatestContent:
				{
					return BundleResult_NeedsUpdate;
				}


				//These are generally unrecoverable and mean something is seriously wrong with the data for this build
				case EInstallBundleResult::InitializationError:
				case EInstallBundleResult::MetadataError:
				{
					return Generic_FatalError;
				}

				//Not enough space in cache to install the files
				case EInstallBundleResult::FailedCacheReserve:
				{
					return BundleResult_NeedsCacheSpace;
				}

				//All of these are indicative of not having enough disk space to install the required files
				case EInstallBundleResult::InstallerOutOfDiskSpaceError:
				case EInstallBundleResult::ManifestArchiveError:
				{
					return BundleResult_NeedsDiskSpace;
				}

				case EInstallBundleResult::UserCancelledError:
				{
					return BundleResult_DownloadCancelled;
				}

				//Intentionally just show generic error for all these cases
				case EInstallBundleResult::InstallError:
				case EInstallBundleResult::ConnectivityError:
				case EInstallBundleResult::InitializationPending:
				{
					return Generic_ConnectionError;
				}

				//Show generic error for anything missing but log an error
				default:
				{
					UE_LOG(LogGameFeatures, Error, TEXT("Missing error text for EInstallBundleResult %s"), LexToString(ErrorResult));
					return Generic_ConnectionError;
				}
			}
		}

		const FText& GetErrorTextForReleaseResult(EInstallBundleReleaseResult ErrorResult)
		{
			switch (ErrorResult)
			{
				case EInstallBundleReleaseResult::UserCancelledError:
				{
					return ReleaseResult_Cancelled;
				}
			
				case EInstallBundleReleaseResult::ManifestArchiveError:
				case EInstallBundleReleaseResult::MetadataError:
				{
					return ReleaseResult_Generic;
				}

				default:
				{
					//Show generic error for anything missing but log an error
					UE_LOG(LogGameFeatures, Error, TEXT("Missing error text for EInstallBundleReleaseResult %s"), LexToString(ErrorResult));
					return ReleaseResult_Generic;
				}
			}
		}
		const FText& GetGenericFatalError()
		{
			return Generic_FatalError;
		}
		const FText& GetGenericConnectionError()
		{
			return Generic_ConnectionError;
		}
		const FText& GetGenericMountError()
		{
			return Generic_MountError;
		}
		const FText& GetGenericReleaseResult()
		{
			return ReleaseResult_Generic;
		}
	}

}	// namespace UE::GameFeatures

GameFeaturePluginStateMachine.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturePluginStateMachine.h"

#include "AssetRegistry/AssetRegistryState.h"
#include "Engine/StreamableManager.h"
#include "GameFeatureData.h"
#include "GameplayTagsManager.h"
#include "Engine/AssetManager.h"
#include "GenericPlatform/GenericPlatformMisc.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/LowLevelMemTracker.h"
#include "IPlatformFilePak.h"
#include "InstallBundleUtils.h"
#include "BundlePrereqCombinedStatusHelper.h"
#include "Interfaces/IPluginManager.h"
#include "Internationalization/PackageLocalizationManager.h"
#include "Internationalization/StringTable.h"
#include "IO/IoStoreOnDemand.h"
#include "Logging/StructuredLog.h"
#include "Materials/MaterialInterface.h"
#include "Misc/AsciiSet.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/ConfigUtilities.h"
#include "Misc/CoreDelegates.h"
#include "Misc/EnumRange.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/ScopedSlowTask.h"
#include "Misc/WildcardString.h"
#include "Algo/Accumulate.h"
#include "Algo/AllOf.h"
#include "Algo/Find.h"
#include "Misc/TVariantMeta.h"
#include "RenderDeferredCleanup.h"
#include "Serialization/MemoryReader.h"
#include "String/ParseTokens.h"
#include "String/LexFromString.h"
#include "Tasks/Pipe.h"
#include "GameFeaturesProjectPolicies.h"
#include "UObject/NameTypes.h"
#include "UObject/ObjectRename.h"
#include "UObject/ReferenceChainSearch.h"
#include "UObject/UObjectAllocator.h"
#include "UObject/UObjectIterator.h"
#include "UObject/NoExportTypes.h"
#include "Misc/PathViews.h"
#include "Containers/Queue.h"
#include "ShaderCodeLibrary.h"
#include "DeviceProfiles/DeviceProfileManager.h"
#include "Trace/Trace.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeaturePluginStateMachine)

#define LOCTEXT_NAMESPACE "GameFeatureDataStateMachine"

#if WITH_EDITOR
#include "PluginUtils.h"
#include "Misc/App.h"
#endif //if WITH_EDITOR

LLM_DEFINE_TAG(GFP, TEXT("GameFeatures"), TEXT("UObject"));

namespace UE::GameFeatures
{
	static const FString StateMachineErrorNamespace(TEXT("GameFeaturePlugin.StateMachine."));

	static UE::GameFeatures::FResult CanceledResult = MakeError(StateMachineErrorNamespace + TEXT("Canceled"));

	static int32 ShouldLogMountedFiles = 0;
	static FAutoConsoleVariableRef CVarShouldLogMountedFiles(TEXT("GameFeaturePlugin.ShouldLogMountedFiles"),
		ShouldLogMountedFiles,
		TEXT("Should the newly mounted files be logged."));

	static FString GVerifyPluginSkipList;
	static FAutoConsoleVariableRef CVarVerifyPluginSkipList(TEXT("PluginManager.VerifyUnload.SkipList"),
		GVerifyPluginSkipList,
		TEXT("Comma-separated list of names of plugins for which to skip verification."),
		ECVF_Default);

	static bool bDeferLocalizationDataLoad = true;
	static FAutoConsoleVariableRef CVarDeferLocalizationLoading(TEXT("GameFeaturePlugin.DeferLocalizationDataLoad"),
		bDeferLocalizationDataLoad,
		TEXT("True if we should defer loading the localization data until 'loading' (new behavior), or false to load it on 'mounting' (old behavior)."));

	static TAutoConsoleVariable<bool> CVarAsyncLoad(TEXT("GameFeaturePlugin.AsyncLoad"),
		true,
		TEXT("Enable to use async loading as well async downloading and registering"));

	static TAutoConsoleVariable<bool> CVarAllowForceMonolithicShaderLibrary(TEXT("GameFeaturePlugin.AllowForceMonolithicShaderLibrary"),
		true,
		TEXT("Enable to force only searching for monolithic shader libs when possible"));

	static TAutoConsoleVariable<bool> CVarForceSyncRegisterStartupPlugins(TEXT("GameFeaturePlugin.ForceSyncRegisterStartupPlugins"),
		true,
		TEXT("If true, all plugins loaded during startup will be synchronously registered to ensure things are initialized in time, this only applies if AsyncLoad is enabled"));

	static TAutoConsoleVariable<bool> CVarForceSyncLoadShaderLibrary(TEXT("GameFeaturePlugin.ForceSyncLoadShaderLibrary"),
		true,
		TEXT("Enable to force shaderlibs to be opened on the game thread"));

	static TAutoConsoleVariable<bool> CVarForceSyncAssetRegistryAppend(TEXT("GameFeaturePlugin.ForceSyncAssetRegistryAppend"),
		false,
		TEXT("Enable to force calls to IAssetRegistry::AppendState to happen on the game thread"));

	static TAutoConsoleVariable<bool> CVarWaitForDependencyDeactivation(TEXT("GameFeaturePlugin.WaitForDependencyDeactivation"),
		false,
		TEXT("Enable to make block deactivation until all dependencies are deactivated. Warning - this can lead to failure to unload"));

	static TAutoConsoleVariable<bool> CVarEnableAssetStreaming(TEXT("GameFeaturePlugin.EnableAssetStreaming"),
		true,
		TEXT("Enable experimental asset streaming"));

	static TAutoConsoleVariable<bool> CVarEnableBatchProcessing(TEXT("GameFeaturePlugin.EnableBatchProcessing"),
		false,
		TEXT("Enable batch processing when processing multiple plugins and specified in protocol options."));

	/*extern*/ TAutoConsoleVariable<bool> CVarAllowMissingOnDemandDependencies(TEXT("GameFeaturePlugin.AllowMissingOnDemandDependencies"),
		false,
		TEXT("Don't fail on missing on demand (IAD) asset dependencies"));

	bool ShouldDeferLocalizationDataLoad()
	{
		// Note: We don't defer localization data loading in the editor, as the editor only needs to mount plugins to use them
		return !GIsEditor && bDeferLocalizationDataLoad;
	}

	void MountLocalizationData(UGameFeaturePluginStateMachine* CurrentMachine, FGameFeaturePluginStateMachineProperties& StateProperties)
	{
		check(IsInGameThread());
		check(CurrentMachine && &CurrentMachine->GetProperties() == &StateProperties);

		StateProperties.bIsLoadingLocalizationData = true;
		IPluginManager::Get().MountExplicitlyLoadedPluginLocalizationData(StateProperties.PluginName, [WeakMachine = TWeakObjectPtr<UGameFeaturePluginStateMachine>(CurrentMachine), &StateProperties, bAllowAsyncLoading = StateProperties.AllowAsyncLoading()](bool bLoadedLocalization, const FString& PluginName)
		{
			if (UGameFeaturePluginStateMachine* StateMachine = WeakMachine.Get();
				StateMachine && ensureAlways(&StateMachine->GetProperties() == &StateProperties))
			{
				if (IsInGameThread())
				{
					StateProperties.bIsLoadingLocalizationData = false;
				}
				else if (bAllowAsyncLoading)
				{
					ExecuteOnGameThread(UE_SOURCE_LOCATION, [WeakMachine, &StateProperties]()
					{
						if (UGameFeaturePluginStateMachine* StateMachine = WeakMachine.Get();
							StateMachine && ensureAlways(&StateMachine->GetProperties() == &StateProperties))
						{
							StateProperties.bIsLoadingLocalizationData = false;
							StateProperties.OnRequestUpdateStateMachine.ExecuteIfBound();
						}
					});
				}
			}
		});
	}

	bool ShouldSkipVerify(const FString& PluginName)
	{
		static const FAsciiSet Wildcards("*?");
		bool bSkip = false;
		UE::String::ParseTokens(MakeStringView(GVerifyPluginSkipList), TEXTVIEW(","), [&PluginName, &bSkip](FStringView Item) {
				if (bSkip) { return; }
				if (Item.Equals(PluginName, ESearchCase::IgnoreCase))
				{
					bSkip = true;
				}
				else if (FAsciiSet::HasAny(Item, Wildcards))
				{
					FString Pattern = FString(Item); // Need to copy to null terminate
					if (FWildcardString::IsMatchSubstring(*Pattern, *PluginName, *PluginName + PluginName.Len(), ESearchCase::IgnoreCase))
					{
						bSkip = true;
					}
				}
			});
		return bSkip;
	}

	// Return a higher number for packages which it's more important to include in leak reporting, when the number of leaks we want to report is limited. 
	int32 GetPackageLeakReportingPriority(UPackage* Package)
	{	
		int32 Priority = 0;
		ForEachObjectWithPackage(Package, [&Priority](UObject* Object)
		{
			if (UWorld* World = Cast<UWorld>(Object))
			{
				Priority = 100;
				return true;
			}
			else if (Cast<UMaterialInterface>(Object))
			{
				Priority = FMath::Max(Priority, 50);
				// keep iterating in case we find a world 
			}
			return true;
		}, false);
		return Priority;
	}

	static bool bRealtimeMode = false;

	class FRealtimeMode : public TSharedFromThis<FRealtimeMode>
	{
	public:
		~FRealtimeMode()
		{
			if (TickHandle.IsValid())
			{
				FTSTicker::RemoveTicker(MoveTemp(TickHandle));
			}

			FGameFeaturePluginRequestUpdateStateMachine UpdateRequest;
			while (UpdateRequests.Dequeue(UpdateRequest))
			{
				UpdateRequest.ExecuteIfBound();
			}
		}

		void AddUpdateRequest(FGameFeaturePluginRequestUpdateStateMachine UpdateRequest)
		{
			UpdateRequests.Enqueue(MoveTemp(UpdateRequest));
			EnableTick();
		}

	private:
		void EnableTick()
		{
			if (!TickHandle.IsValid())
			{
				TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateSP(this, &FRealtimeMode::Tick));
			}
		}

		bool Tick(float DeltaTime)
		{
			// Self-reference so we don't get destroyed during Tick
			TSharedRef<FRealtimeMode> SelfRef = AsShared();

			{
				constexpr double MaxFrameTime = 0.033; // 30fps
				constexpr double AllottedTime = MaxFrameTime / 2;
				const double StartTime = FPlatformTime::Seconds();

				FGameFeaturePluginRequestUpdateStateMachine UpdateRequest;
				while (UpdateRequests.Dequeue(UpdateRequest))
				{
					UpdateRequest.ExecuteIfBound();

					const double ElapsedTime = FPlatformTime::Seconds() - StartTime;
					if ((ElapsedTime > AllottedTime) || ((DeltaTime + ElapsedTime) > MaxFrameTime))
					{
						break;
					}
				}
			}

			if (UpdateRequests.IsEmpty())
			{
				TickHandle.Reset();
				return false;
			}
			else
			{
				return true;
			}
		}

	private:
		TQueue<FGameFeaturePluginRequestUpdateStateMachine> UpdateRequests;
		FTSTicker::FDelegateHandle TickHandle;
	};

	static TSharedPtr<FRealtimeMode> RealtimeMode;

	static FAutoConsoleVariableRef CVarRealtimeMode(TEXT("GameFeaturePlugin.RealtimeMode"),
		bRealtimeMode,
		TEXT("Sets whether GFS realtime mode is enabled; which distributes plugin state updates over several frames"),
		FConsoleVariableDelegate::CreateLambda(
			[](IConsoleVariable* Var)
			{
				if (Var->GetBool())
				{
					if (!RealtimeMode)
					{
						RealtimeMode = MakeShared<FRealtimeMode>();
					}
				}
				else
				{
					TSharedPtr<FRealtimeMode> Rm = MoveTemp(RealtimeMode);
					Rm.Reset();
				}
			}),
			ECVF_ReadOnly);

#if WITH_EDITOR
	TMap<FString, FGameFeaturePluginRequestUpdateStateMachine> PluginsToUnloadAssets;
	FTSTicker::FDelegateHandle UnloadPluginAssetsHandle;

	bool TickUnloadPluginAssets(float /*DeltaTime*/)
	{
		UnloadPluginAssetsHandle.Reset();
	
		TArray<FString> PluginNames;
		TArray<FGameFeaturePluginRequestUpdateStateMachine> UpdateStateMachineDelegates;
		{
			PluginNames.Reserve(PluginsToUnloadAssets.Num());
			UpdateStateMachineDelegates.Reserve(PluginsToUnloadAssets.Num());
			for (TPair<FString, FGameFeaturePluginRequestUpdateStateMachine>& PluginsToUnloadAsset : PluginsToUnloadAssets)
			{
				PluginNames.Add(PluginsToUnloadAsset.Key);
				UpdateStateMachineDelegates.Add(MoveTemp(PluginsToUnloadAsset.Value));
			}
			PluginsToUnloadAssets.Empty();
		}

		verify(FPluginUtils::UnloadPluginsAssets(PluginNames));

		for (const FGameFeaturePluginRequestUpdateStateMachine& UpdateStateMachineDelegate : UpdateStateMachineDelegates)
		{
			UpdateStateMachineDelegate.ExecuteIfBound();
		}

		return false;
	}

	void ScheduleUnloadPluginAssets(const FString& PluginName, const FGameFeaturePluginRequestUpdateStateMachine& UpdateStateMachineDelegate)
	{
		check(IsInGameThread());
		ensure(!PluginsToUnloadAssets.Contains(PluginName));
		PluginsToUnloadAssets.Add(PluginName, UpdateStateMachineDelegate);
		if (!UnloadPluginAssetsHandle.IsValid())
		{
			UnloadPluginAssetsHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateStatic(&TickUnloadPluginAssets));
		}
	}
#endif //if WITH_EDITOR

	enum class EGFPInstallLevel : uint8
	{
		Download = 0,
		Mount,
		AssetStream
	};

	struct FPluginInstallBundleReferencers
	{
		// GFPs using an install bundle and the relevant state of that GFP
		TMap<FStringView, EGFPInstallLevel> GFPs;
	};

	struct FPluginIoStoreOnDemandHandles
	{
		// IoStoreOnDemand assets required for initial download
		UE::IoStore::FOnDemandContentHandle DownloadHandle;
		// IoStoreOnDemand assets from AssetDependencyStreaming
		UE::IoStore::FOnDemandContentHandle StreamInHandle;
	};

	// class to manage GFPs sharing installation data
	class FGFPSharedInstallTracker
	{
	public:
		// The caller should pass all resolved bundle depenencies 
		void AddBundleRefs(const FStringView PluginName, const EGFPInstallLevel Level, const TConstArrayView<FName> Bundles)
		{
			for (const FName BundleName : Bundles)
			{
				FPluginInstallBundleReferencers& PluginRefs = InstallBundleToGFPRefs.FindOrAdd(BundleName);
				PluginRefs.GFPs.Emplace(PluginName, Level);
			}
		}

		// The caller should pass all resolved bundle depenencies 
		UE::IoStore::FOnDemandContentHandle AddOnDemandContentHandle(const FName Bundle, const EGFPInstallLevel Level)
		{
			switch(Level)
			{
			case EGFPInstallLevel::Download:
			{
				FPluginIoStoreOnDemandHandles& Handles = InstallBundleToOnDemandHandles.FindOrAdd(Bundle);
				if (!Handles.DownloadHandle.IsValid())
				{
					Handles.DownloadHandle = UE::IoStore::FOnDemandContentHandle::Create(Bundle.ToString());
				}
				return Handles.DownloadHandle;
			}
			case EGFPInstallLevel::AssetStream:
			{
				FPluginIoStoreOnDemandHandles& Handles = InstallBundleToOnDemandHandles.FindOrAdd(Bundle);
				if (!Handles.StreamInHandle.IsValid())
				{
					Handles.StreamInHandle = UE::IoStore::FOnDemandContentHandle::Create(Bundle.ToString() + TEXTVIEW("/deps"));
				}
				return Handles.StreamInHandle;
			}
			};

			return {};
		}

		// The caller should pass all resolved bundle depenencies 
		TArray<FName> Release(
			const FStringView PluginName, const EGFPInstallLevel InLevel, const TConstArrayView<FName> Bundles)
		{
			uint32 PluginNameHash = GetTypeHash(PluginName);
			TArray<FName> BundlesToRelease;
			for (const FName Bundle : Bundles)
			{
				bool bRelease = true;

				FPluginInstallBundleReferencers* PluginRefs = InstallBundleToGFPRefs.Find(Bundle);
				if (PluginRefs)
				{
					EGFPInstallLevel* Level = PluginRefs->GFPs.FindByHash(PluginNameHash, PluginName);
					if (Level && *Level >= InLevel)
					{
						switch(*Level)
						{
						case EGFPInstallLevel::Download:
							PluginRefs->GFPs.RemoveByHash(PluginNameHash, PluginName);
							break;
						case EGFPInstallLevel::Mount:
							(*Level) = EGFPInstallLevel::Download;
							break;
						case EGFPInstallLevel::AssetStream:
							(*Level) = EGFPInstallLevel::Mount;
							break;
						}
					}

					for (TPair<FStringView, EGFPInstallLevel>& GFPRef : PluginRefs->GFPs)
					{
						if (GFPRef.Value >= InLevel)
						{
							bRelease = false;
							break;
						}
					}

					if (PluginRefs->GFPs.IsEmpty())
					{
						InstallBundleToGFPRefs.Remove(Bundle);
					}
				}

				if (bRelease)
				{
					BundlesToRelease.Add(Bundle);
				}
			}

			switch(InLevel)
			{
			case EGFPInstallLevel::Download:
				for (const FName Bundle : BundlesToRelease)
				{
					InstallBundleToOnDemandHandles.Remove(Bundle);
				}
				break;
			case EGFPInstallLevel::AssetStream:
				for (const FName& Bundle : BundlesToRelease)
				{
					FPluginIoStoreOnDemandHandles* Handles = InstallBundleToOnDemandHandles.Find(Bundle);
					if (Handles)
					{
						Handles->StreamInHandle.Reset();
					}
				}
				break;
			}

			return BundlesToRelease;
		}

	private:
		TMap<FName, FPluginInstallBundleReferencers> InstallBundleToGFPRefs;
		TMap<FName, FPluginIoStoreOnDemandHandles> InstallBundleToOnDemandHandles;
	};

	static FGFPSharedInstallTracker GFPSharedInstallTracker;

	// Callback delegates are moved to the stack before broadcasting.
	// This class tracks callback delegates on the stack to handle removing callbacks from them
	// for state machines that are also on the stack.
	template<typename TCallbackMultiDelegate> 
		requires 
			std::is_same_v<TCallbackMultiDelegate, FDestinationGameFeaturePluginState::FOnDestinationStateReached> ||
			std::is_same_v<TCallbackMultiDelegate, FGameFeaturePluginStateMachineProperties::FOnTransitionCanceled>
	class TBroadcastingCallback
	{
	public:
		TCallbackMultiDelegate CallbackDelegate;

	private:
		TBroadcastingCallback<TCallbackMultiDelegate>* Next = nullptr;
		inline static TBroadcastingCallback<TCallbackMultiDelegate>* Head = nullptr;

	public:
		TBroadcastingCallback(TCallbackMultiDelegate&& InCallbackDelegate)
			: CallbackDelegate(MoveTemp(InCallbackDelegate))
			, Next(Head)
		{
			Head = this;
		}

		~TBroadcastingCallback()
		{
			Head = Next;
		}

		static void RemovePendingCallback(FDelegateHandle InHandle)
		{
			TBroadcastingCallback<TCallbackMultiDelegate>* Current = Head;
			while (Current)
			{
				Current->CallbackDelegate.Remove(InHandle);
				Current = Current->Next;
			}
		}

		static void RemovePendingCallback(FDelegateUserObject InObject)
		{
			TBroadcastingCallback<TCallbackMultiDelegate>* Current = Head;
			while (Current)
			{
				Current->CallbackDelegate.RemoveAll(InObject);
				Current = Current->Next;
			}
		}
	};

	using FBroadcastingOnDestinationStateReached = TBroadcastingCallback<FDestinationGameFeaturePluginState::FOnDestinationStateReached>;
	using FBroadcastingOnTransitionCanceled = TBroadcastingCallback<FGameFeaturePluginStateMachineProperties::FOnTransitionCanceled>;

	static FName GetStateName(EGameFeaturePluginState State)
	{
		switch (State)
		{
			#define X(inEnum, inText) \
				case EGameFeaturePluginState::inEnum: \
				{ \
					static const FName Name_##inEnum = FName(TEXT("GFP_") #inEnum); \
					return Name_##inEnum; \
				}

			GAME_FEATURE_PLUGIN_STATE_LIST(X)

			#undef X
		}

		return FName(TEXT("Unknown"));
	};
}

void FGameFeaturePluginStateStatus::SetTransition(EGameFeaturePluginState InTransitionToState)
{
	TransitionToState = InTransitionToState;
	TransitionResult.ErrorCode = MakeValue();
	TransitionResult.OptionalErrorText = FText();
}

void FGameFeaturePluginStateStatus::SetTransitionError(EGameFeaturePluginState TransitionToErrorState, UE::GameFeatures::FResult TransitionResultIn, bool bInSuppressErrorLog /*= false*/)
{
	if (ensureAlwaysMsgf(TransitionResultIn.HasError(), TEXT("Invalid call to SetTransitionError with an FResult that isn't an error! TransitionToErrorState: %s"), *UE::GameFeatures::ToString(TransitionToErrorState)))
	{
		TransitionResult = MoveTemp(TransitionResultIn);
	}
	else
	{
		//Logic error using a non-error FResult, so just generate a general error to keep the SetTransitionError intent
		TransitionResult = MakeError(TEXT("Invalid_Transition_Error"));
	}

	TransitionToState = TransitionToErrorState;
	bSuppressErrorLog = bInSuppressErrorLog;
}

UE::GameFeatures::FResult FGameFeaturePluginState::GetErrorResult(const FString& ErrorCode, const FText OptionalErrorText/*= FText()*/) const
{
	return GetErrorResult(TEXT(""), ErrorCode, OptionalErrorText);
}

UE::GameFeatures::FResult FGameFeaturePluginState::GetErrorResult(const FString& ErrorNamespaceAddition, const FString& ErrorCode, const FText OptionalErrorText/*= FText()*/) const
{
	const FString StateName = UE::GameFeatures::ToString(UGameFeaturesSubsystem::Get().GetPluginState(StateProperties.PluginIdentifier));
	const FString ErrorCodeEnding = ErrorNamespaceAddition.IsEmpty() ? ErrorCode : ErrorNamespaceAddition + ErrorCode;
	const FString CompleteErrorCode = FString::Printf(TEXT("%s%s.%s"), *UE::GameFeatures::StateMachineErrorNamespace, *StateName, *ErrorCodeEnding);
	return UE::GameFeatures::FResult(MakeError(CompleteErrorCode), OptionalErrorText);
}

UE::GameFeatures::FResult FGameFeaturePluginState::GetErrorResult(const FString& ErrorNamespaceAddition, const EInstallBundleResult ErrorResult) const
{
	UE::GameFeatures::FResult BaseResult = GetErrorResult(ErrorNamespaceAddition, LexToString(ErrorResult));
	BaseResult.OptionalErrorText = UE::GameFeatures::CommonErrorCodes::GetErrorTextForBundleResult(ErrorResult);
	return MoveTemp(BaseResult);
}

UE::GameFeatures::FResult FGameFeaturePluginState::GetErrorResult(const FString& ErrorNamespaceAddition, const EInstallBundleReleaseResult ErrorResult) const
{
	UE::GameFeatures::FResult BaseResult = GetErrorResult(ErrorNamespaceAddition, LexToString(ErrorResult));
	BaseResult.OptionalErrorText = UE::GameFeatures::CommonErrorCodes::GetErrorTextForReleaseResult(ErrorResult);
	return MoveTemp(BaseResult);
}


FGameFeaturePluginState::~FGameFeaturePluginState()
{
	CleanupDeferredUpdateCallbacks();
}

UE::GameFeatures::FResult FGameFeaturePluginState::TryUpdateProtocolOptions(const FGameFeatureProtocolOptions& NewOptions)
{
	UE::GameFeatures::FResult Result = StateProperties.ValidateProtocolOptionsUpdate(NewOptions);

	if (!Result.HasError())
	{
		StateProperties.ProtocolOptions = NewOptions;
	}
	return Result;
}

FDestinationGameFeaturePluginState* FGameFeaturePluginState::AsDestinationState()
{
	FDestinationGameFeaturePluginState* Ret = nullptr;

	EGameFeaturePluginStateType Type = GetStateType();
	if (Type == EGameFeaturePluginStateType::Destination || Type == EGameFeaturePluginStateType::Error)
	{
		Ret = static_cast<FDestinationGameFeaturePluginState*>(this);
	}

	return Ret;
}

FErrorGameFeaturePluginState* FGameFeaturePluginState::AsErrorState()
{
	FErrorGameFeaturePluginState* Ret = nullptr;

	if (GetStateType() == EGameFeaturePluginStateType::Error)
	{
		Ret = static_cast<FErrorGameFeaturePluginState*>(this);
	}

	return Ret;
}

void FGameFeaturePluginState::UpdateStateMachineDeferred(float Delay /*= 0.0f*/) const
{
	CleanupDeferredUpdateCallbacks();

	TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float dts) mutable
	{
		// @note Release FGameFeaturePluginState::TickHandle first in case the termination callback triggers a GC and destroys the state machine
		TickHandle.Reset();
		StateProperties.OnRequestUpdateStateMachine.ExecuteIfBound();
		return false;
	}), Delay);
}

void FGameFeaturePluginState::UpdateStateMachineImmediate() const
{
	StateProperties.OnRequestUpdateStateMachine.ExecuteIfBound();
}

void FGameFeaturePluginState::UpdateProgress(float Progress) const
{
	StateProperties.OnFeatureStateProgressUpdate.ExecuteIfBound(Progress);
}

bool FGameFeaturePluginState::CanBatchProcess() const
{
	// Batch processing is tick driven so is technically "async". 
	// Hence if we are sync loading, avoid using batch processing since it could impact order of operations.
	return UseAsyncLoading();
}

bool FGameFeaturePluginState::IsWaitingForBatchProcessing() const
{
	return StateProperties.IsWaitingForBatchProcessing();
}

bool FGameFeaturePluginState::WasBatchProcessed() const
{
	return StateProperties.WasBatchProcessed();
}

void FGameFeaturePluginState::CleanupDeferredUpdateCallbacks() const
{
	if (TickHandle.IsValid())
	{
		FTSTicker::RemoveTicker(TickHandle);
		TickHandle.Reset();
	}
}

bool FGameFeaturePluginState::ShouldVisitUninstallStateBeforeTerminal() const
{
	switch (StateProperties.GetPluginProtocol())
	{
		case (EGameFeaturePluginProtocol::InstallBundle):
		{
			//InstallBundleProtocol's have a MetaData that controlls if they uninstall currently
			return StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>().bUninstallBeforeTerminate;
		}

		//Default behavior is to just Terminate
		default:
		{
			return false;
		}
	}
}

bool FGameFeaturePluginState::AllowIniLoading() const
{
	switch (StateProperties.GetPluginProtocol())
	{
	case (EGameFeaturePluginProtocol::InstallBundle):
	{
		//InstallBundleProtocol's have a MetaData that controls if INI loading is allowed
		//The protocol default is not to allow INI loading since the source is likely untrusted
		return StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>().bAllowIniLoading;
	}

	//Default behavior is to allow INI loading
	default:
	{
		return true;
	}
	}
}

bool FGameFeaturePluginState::AllowAsyncLoading() const
{
	return StateProperties.AllowAsyncLoading();
}

bool FGameFeaturePluginState::UseAsyncLoading() const
{
	return AllowAsyncLoading() && UE::GameFeatures::CVarAsyncLoad.GetValueOnGameThread();
}

/*
=========================================================
  States
=========================================================
*/

template<typename TransitionPolicy>
struct FTransitionDependenciesGameFeaturePluginState : public FGameFeaturePluginState
{
	FTransitionDependenciesGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FGameFeaturePluginState(InStateProperties)
	{}

	virtual ~FTransitionDependenciesGameFeaturePluginState()
	{
		ClearDependencies();
	}

	virtual void BeginState() override
	{
		ClearDependencies();
		bCheckedRealtimeMode = false;
	}

	virtual void EndState() override
	{
		ClearDependencies();
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!bCheckedRealtimeMode)
		{
			bCheckedRealtimeMode = true;
			if (UE::GameFeatures::RealtimeMode)
			{
				UE::GameFeatures::RealtimeMode->AddUpdateRequest(StateProperties.OnRequestUpdateStateMachine);
				return;
			}
		}

		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_TransitionDependencies);
		checkf(!StateProperties.PluginInstalledFilename.IsEmpty(), TEXT("PluginInstalledFilename must be set by the loading dependencies phase. PluginURL: %s"), *StateProperties.PluginIdentifier.GetFullPluginURL());
		checkf(FPaths::GetExtension(StateProperties.PluginInstalledFilename) == TEXT("uplugin"), TEXT("PluginInstalledFilename must have a uplugin extension. PluginURL: %s"), *StateProperties.PluginIdentifier.GetFullPluginURL());

		UGameFeaturesSubsystem& GameFeaturesSubsystem = UGameFeaturesSubsystem::Get();
		if (!bRequestedDependencies)
		{
			TArray<UGameFeaturePluginStateMachine*> Dependencies;
			if (!TransitionPolicy::GetPluginDependencyStateMachines(StateProperties, Dependencies))
			{
				// Failed to query dependencies
				StateStatus.SetTransitionError(TransitionPolicy::GetErrorState(), GetErrorResult(TEXT("Failed_Dependency_Query")));
				return;
			}

			bRequestedDependencies = true;

			UE_CLOG(Dependencies.Num() > 0, LogGameFeatures, Verbose, TEXT("Found %i dependencies for %s"), Dependencies.Num(), *StateProperties.PluginName);

			const bool bAllowAsyncLoading = AllowAsyncLoading();
			RemainingDependencies.Reserve(Dependencies.Num());
			for (UGameFeaturePluginStateMachine* Dependency : Dependencies)
			{
				ensureMsgf(bAllowAsyncLoading || !Dependency->AllowAsyncLoading(), 
					TEXT("FGameFeaturePluginState::AllowAsyncLoading is false for %s but true for dependency being waited on %s"), 
					*StateProperties.PluginName, *Dependency->GetPluginURL());

				if (TransitionPolicy::ShouldWaitForDependencies() && 
					Dependency->IsInErrorState() &&
					Dependency->IsErrorStateUnrecoverable())
				{
					const EGameFeaturePluginState DependencyState = Dependency->GetCurrentState();
					const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
					const FStringView DepShortUrl = Dependency->GetPluginIdentifier().GetIdentifyingString();
					UE_LOG(LogGameFeatures, Error, TEXT("Plugin (%.*s) dependency (%.*s) failed and is in state %s"),
						ShortUrl.Len(), ShortUrl.GetData(),
						DepShortUrl.Len(), DepShortUrl.GetData(),
						*UE::GameFeatures::ToString(DependencyState));
					StateStatus.SetTransitionError(TransitionPolicy::GetErrorState(), GetErrorResult(TEXT("Failed_Dependency_Transition")));
					if (UGameFeaturePluginStateMachine* CurrentMachine = GameFeaturesSubsystem.FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier))
					{
						UE_LOG(LogGameFeatures, Error, TEXT("Setting plugin (%.*s) to be in unrecoverable error because dependency (%.*s) is in unrecoverable error"), 
							ShortUrl.Len(), ShortUrl.GetData(),
							DepShortUrl.Len(), DepShortUrl.GetData());
						CurrentMachine->SetUnrecoverableError();
					}
					return;
				}
				else
				{
					RemainingDependencies.Emplace(Dependency, MakeValue());
					TransitionDependency(Dependency);
				}
			}
		}

		for (FDepResultPair& Pair : RemainingDependencies)
		{
			UGameFeaturePluginStateMachine* RemainingDependency = Pair.Key.Get();
			if (!RemainingDependency)
			{
				// One of the dependency state machines was destroyed before finishing
				StateStatus.SetTransitionError(TransitionPolicy::GetErrorState(), GetErrorResult(TEXT("Dependency_Destroyed_Before_Finish")));
				return;
			}

			if (Pair.Value.HasError())
			{
				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				const FStringView DepShortUrl = RemainingDependency->GetPluginIdentifier().GetIdentifyingString();
				UE_LOG(LogGameFeatures, Error, TEXT("Plugin (%.*s) dependency (%.*s) failed to transition with error %s"),
					ShortUrl.Len(), ShortUrl.GetData(),
					DepShortUrl.Len(), DepShortUrl.GetData(),
					*Pair.Value.GetError());
				StateStatus.SetTransitionError(TransitionPolicy::GetErrorState(), GetErrorResult(TEXT("Failed_Dependency_Transition")));
				if (RemainingDependency->IsErrorStateUnrecoverable())
				{
					if (UGameFeaturePluginStateMachine* CurrentMachine = GameFeaturesSubsystem.FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier))
					{
						UE_LOG(LogGameFeatures, Error, TEXT("Setting plugin (%.*s) to be in unrecoverable error because dependency (%.*s) is in unrecoverable error"),
							ShortUrl.Len(), ShortUrl.GetData(),
							DepShortUrl.Len(), DepShortUrl.GetData());
						CurrentMachine->SetUnrecoverableError();
					}
				}
				return;
			}
		}

		if (RemainingDependencies.Num() == 0)
		{
			StateStatus.SetTransition(TransitionPolicy::GetTransitionState());
		}
	}

	void TransitionDependency(UGameFeaturePluginStateMachine* Dependency)
	{
		bool bSetDestination = false;

		if (TransitionPolicy::ExcludeDepedenciesFromBatchProcessing())
		{
			Dependency->ExcludeFromBatchProcessing();
		}
		
		if (TransitionPolicy::ShouldWaitForDependencies())
		{
			bSetDestination = Dependency->SetDestination(TransitionPolicy::GetDependencyStateRange(),
				FGameFeatureStateTransitionComplete::CreateRaw(this, &FTransitionDependenciesGameFeaturePluginState::OnDependencyTransitionComplete));
		}
		else
		{
			bSetDestination = Dependency->SetDestination(TransitionPolicy::GetDependencyStateRange(), 
				FGameFeatureStateTransitionComplete::CreateStatic(&FTransitionDependenciesGameFeaturePluginState::OnDependencyTransitionCompleteNoWait));
			if (bSetDestination)
			{
				OnDependencyTransitionComplete(Dependency, MakeValue());
			}
		}

		if (!bSetDestination)
		{
			const bool bCancelPending = Dependency->TryCancel(
				FGameFeatureStateTransitionCanceled::CreateRaw(this, &FTransitionDependenciesGameFeaturePluginState::OnDependencyTransitionCanceled));
			if (!ensure(bCancelPending))
			{
				OnDependencyTransitionComplete(Dependency, GetErrorResult(TEXT("Failed_Dependency_Transition")));
			}
		}
	}

	void OnDependencyTransitionCanceled(UGameFeaturePluginStateMachine* Dependency)
	{
		// Special case for terminal state since it cannot be exited, we need to make a new machine
		if (Dependency->GetCurrentState() == EGameFeaturePluginState::Terminal)
		{
			// Inherit dep protocol options if possible
			FGameFeatureProtocolOptions DepProtocolOptions;
			EGameFeaturePluginProtocol DepProtocol = Dependency->GetPluginIdentifier().GetPluginProtocol();
			if (DepProtocol == EGameFeaturePluginProtocol::InstallBundle && StateProperties.ProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>())
			{
				DepProtocolOptions = StateProperties.RecycleProtocolOptions();
			}

			UGameFeaturePluginStateMachine* NewMachine = UGameFeaturesSubsystem::Get().FindOrCreateGameFeaturePluginStateMachine(Dependency->GetPluginURL(), DepProtocolOptions);
			checkf(NewMachine != Dependency, TEXT("Game Feature Plugin %s should have already been removed from subsystem!"), *Dependency->GetPluginURL());

			const int32 Index = RemainingDependencies.IndexOfByPredicate([Dependency](const FDepResultPair& Pair)
				{
					return Pair.Key == Dependency;
				});

			check(Index != INDEX_NONE);
			FDepResultPair& FoundDep = RemainingDependencies[Index];
			FoundDep.Key = NewMachine;

			Dependency->RemovePendingTransitionCallback(this);
			Dependency->RemovePendingCancelCallback(this);

			Dependency = NewMachine;
		}

		// Now that the transition has been canceled, retry reaching the desired destination
		bool bSetDestination = false;
		if (TransitionPolicy::ShouldWaitForDependencies())
		{
			bSetDestination = Dependency->SetDestination(TransitionPolicy::GetDependencyStateRange(),
				FGameFeatureStateTransitionComplete::CreateRaw(this, &FTransitionDependenciesGameFeaturePluginState::OnDependencyTransitionComplete));
		}
		else
		{
			bSetDestination = Dependency->SetDestination(TransitionPolicy::GetDependencyStateRange(), 
				FGameFeatureStateTransitionComplete::CreateStatic(&FTransitionDependenciesGameFeaturePluginState::OnDependencyTransitionCompleteNoWait));
			if (bSetDestination)
			{
				OnDependencyTransitionComplete(Dependency, MakeValue());
			}
		}

		if (!ensure(bSetDestination))
		{
			OnDependencyTransitionComplete(Dependency, GetErrorResult(TEXT("Failed_Dependency_Transition")));
		}
	}

	void OnDependencyTransitionComplete(UGameFeaturePluginStateMachine* Dependency, const UE::GameFeatures::FResult& Result)
	{
		const int32 Index = RemainingDependencies.IndexOfByPredicate([Dependency](const FDepResultPair& Pair)
			{
				return Pair.Key == Dependency;
			});

		if (ensure(Index != INDEX_NONE))
		{
			FDepResultPair& FoundDep = RemainingDependencies[Index];

			if (Result.HasError())
			{
				FoundDep.Value = Result;
			}
			else
			{
				RemainingDependencies.RemoveAtSwap(Index, EAllowShrinking::No);
			}

			UpdateStateMachineImmediate();
		}
	}

	static void OnDependencyTransitionCompleteNoWait(UGameFeaturePluginStateMachine* Dependency, const UE::GameFeatures::FResult& Result)
	{
		if (Result.HasError())
		{
			if (Result.GetError() == UE::GameFeatures::CanceledResult.GetError())
			{
				UE_LOGFMT(LogGameFeatures, Warning, "Dependency {Dep} failed to transition because it was cancelled by another request {Error}",
					("Dep", Dependency->GetPluginIdentifier().GetIdentifyingString()),
					("Error", Result.GetError()));
			}
			else
			{

				UE_LOGFMT(LogGameFeatures, Error, "Dependency {Dep} failed to transition with error {Error}",
					("Dep", Dependency->GetPluginIdentifier().GetIdentifyingString()),
					("Error", Result.GetError()));
			}
		}
	}

	void ClearDependencies()
	{
		if (RemainingDependencies.Num() > 0)
		{
			for (FDepResultPair& Pair : RemainingDependencies)
			{
				UGameFeaturePluginStateMachine* RemainingDependency = Pair.Key.Get();
				if (RemainingDependency)
				{
					RemainingDependency->RemovePendingTransitionCallback(this);
					RemainingDependency->RemovePendingCancelCallback(this);
				}
			}

			// Also need to cleanup callbacks from any delegates currently on the stack
			UE::GameFeatures::FBroadcastingOnDestinationStateReached::RemovePendingCallback(this);
			UE::GameFeatures::FBroadcastingOnTransitionCanceled::RemovePendingCallback(this);

			RemainingDependencies.Empty();
		}

		bRequestedDependencies = false;
	}

	using FDepResultPair = TPair<TWeakObjectPtr<UGameFeaturePluginStateMachine>, UE::GameFeatures::FResult>;
	TArray<FDepResultPair> RemainingDependencies;
	bool bRequestedDependencies = false;
	bool bCheckedRealtimeMode = false;
};

struct FGameFeaturePluginState_Uninitialized : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Uninitialized(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		checkf(false, TEXT("UpdateState can not be called while uninitialized"));
	}
};

struct FGameFeaturePluginState_Terminal : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Terminal(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	bool bEnteredTerminalState = false;

	virtual UE::GameFeatures::FResult TryUpdateProtocolOptions(const FGameFeatureProtocolOptions& NewOptions) override
	{
		//Should never update our options during Terminal
		return GetErrorResult(TEXT("ProtocolOptions."), TEXT("Terminal"));
	}

	virtual void BeginState() override
	{
		checkf(!bEnteredTerminalState, TEXT("Plugin entered terminal state more than once! %s"), *StateProperties.PluginIdentifier.GetFullPluginURL());
		bEnteredTerminalState = true;

		UGameFeaturesSubsystem::Get().OnGameFeatureTerminating(StateProperties.PluginName, StateProperties.PluginIdentifier);
	}
};

struct FGameFeaturePluginState_UnknownStatus : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_UnknownStatus(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::UnknownStatus)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::UnknownStatus)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::CheckingStatus);

			UGameFeaturesSubsystem::Get().OnGameFeatureCheckingStatus(StateProperties.PluginIdentifier);
		}
	}
};

struct FGameFeaturePluginState_CheckingStatus : public FGameFeaturePluginState
{
	FGameFeaturePluginState_CheckingStatus(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	bool bParsedURL = false;
	bool bIsAvailable = false;

	virtual void BeginState() override
	{
		bParsedURL = false;
		bIsAvailable = false;
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!bParsedURL)
		{
			TValueOrError<void, FString> ParseUrlResult = StateProperties.ParseURL();
			bParsedURL = !ParseUrlResult.HasError();
			if (!bParsedURL)
			{
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorCheckingStatus, GetErrorResult(ParseUrlResult.GetError()));
				return;
			}
		}

		if (StateProperties.GetPluginProtocol() == EGameFeaturePluginProtocol::File)
		{
			bIsAvailable = FPaths::FileExists(StateProperties.PluginInstalledFilename);
		}
		else if (StateProperties.GetPluginProtocol() == EGameFeaturePluginProtocol::InstallBundle)
		{
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
			if (BundleManager == nullptr)
			{
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorCheckingStatus, GetErrorResult(TEXT("BundleManager_Null")));
				return;
			}

			if (BundleManager->GetInitState() == EInstallBundleManagerInitState::Failed)
			{
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorCheckingStatus, GetErrorResult(TEXT("BundleManager_Failed_Init")));
				return;
			}

			if (BundleManager->GetInitState() == EInstallBundleManagerInitState::NotInitialized)
			{
				// Just wait for any pending init
				UpdateStateMachineDeferred(1.0f);
				return;
			}

			FInstallBundlePluginProtocolMetaData& ProtocolMetadata = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();

			const bool bAddDependencies = true;
			TValueOrError<FInstallBundleCombinedInstallState, EInstallBundleResult> MaybeInstallState = BundleManager->GetInstallStateSynchronous(
				ProtocolMetadata.InstallBundles, bAddDependencies);
			if (MaybeInstallState.HasError())
			{
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorCheckingStatus, GetErrorResult(TEXT("BundleManager_Failed_GetInstallState")));
				return;
			}

			const FInstallBundleCombinedInstallState& InstallState = MaybeInstallState.GetValue();
			bIsAvailable = Algo::AllOf(ProtocolMetadata.InstallBundles, 
				[&InstallState](FName BundleName) { return InstallState.IndividualBundleStates.Contains(BundleName); });

			if (bIsAvailable)
			{
				// Update metadata with fully expanded dependency list. This can only be done after all bundles are known to be available,
				// otherwise unavailable bundles in the URL could be stripped from the list.
				ProtocolMetadata.InstallBundles.Empty(InstallState.IndividualBundleStates.Num());
				InstallState.IndividualBundleStates.GetKeys(ProtocolMetadata.InstallBundles);
				ProtocolMetadata.InstallBundlesWithAssetDependencies = InstallState.BundlesWithIoStoreOnDemand.Array();
			}
		}
		else
		{
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorCheckingStatus, GetErrorResult(TEXT("Unknown_Protocol")));
			return;
		}

		if (!bIsAvailable)
		{
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorUnavailable, GetErrorResult(TEXT("Plugin_Unavailable")));
			return;
		}

		UGameFeaturesSubsystem::Get().OnGameFeatureStatusKnown(StateProperties.PluginName, StateProperties.PluginIdentifier);
		StateStatus.SetTransition(EGameFeaturePluginState::StatusKnown);
	}
};

struct FGameFeaturePluginState_ErrorCheckingStatus : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorCheckingStatus(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorCheckingStatus)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorCheckingStatus)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::CheckingStatus);
		}
	}
};

struct FGameFeaturePluginState_ErrorUnavailable : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorUnavailable(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorUnavailable)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorUnavailable)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::CheckingStatus);
		}
	}
};

struct FGameFeaturePluginState_StatusKnown : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_StatusKnown(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::StatusKnown)
		{
			if (ShouldVisitUninstallStateBeforeTerminal())
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Uninstalling);
			}
			else
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
			}
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::StatusKnown)
		{
			if (StateProperties.GetPluginProtocol() != EGameFeaturePluginProtocol::File)
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Downloading);
			}
			else
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Installed);
			}
		}
	}
};

struct FGameFeaturePluginState_ErrorManagingData : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorManagingData(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorManagingData)
		{			
			StateStatus.SetTransition(EGameFeaturePluginState::Releasing);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorManagingData)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Downloading);
		}
	}
};

struct FGameFeaturePluginState_ErrorUninstalling : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorUninstalling(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorUninstalling)
		{
			if (ShouldVisitUninstallStateBeforeTerminal())
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Uninstalling);
			}
			else
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
			}
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorUninstalling)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::StatusKnown);
		}
	}
};

//Base class for our states that want to request to release data from InstallBundleManager
struct FBaseDataReleaseGameFeaturePluginState : public FGameFeaturePluginState
{
	FBaseDataReleaseGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) 
		: FGameFeaturePluginState(InStateProperties) 
		, Result(MakeValue())
	{}

	virtual ~FBaseDataReleaseGameFeaturePluginState()
	{}

	UE::GameFeatures::FResult Result;
	bool bWasDeleted = false;
	TArray<FName> PendingBundles;

	void CleanUp()
	{
		PendingBundles.Empty();
		IInstallBundleManager::ReleasedDelegate.RemoveAll(this);
	}

	void OnContentRemoved(FInstallBundleReleaseRequestResultInfo BundleResult)
	{
		if (!PendingBundles.Contains(BundleResult.BundleName))
		{
			return;
		}

		PendingBundles.Remove(BundleResult.BundleName);

		if (!Result.HasError() && BundleResult.Result != EInstallBundleReleaseResult::OK)
		{
			Result = GetErrorResult(TEXT("BundleManager.OnRemove_Failed."), BundleResult.Result);
		}

		if (PendingBundles.Num() > 0)
		{
			return;
		}

		if (Result.HasValue())
		{
			bWasDeleted = true;
		}

		UpdateStateMachineImmediate();
	}

	void BeginRemoveRequest()
	{
		CleanUp();

		Result = MakeValue();
		bWasDeleted = false;

		if (!ShouldReleaseContent())
		{
			bWasDeleted = true;
			return;
		}

		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
		check(BundleManager.IsValid());

		TArray<FName> InstallBundlesToRelease = UE::GameFeatures::GFPSharedInstallTracker.Release(
			StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::Download, GetInstallBundles());

		// Always set ExplicitRemoveList, GFPSharedInstallTracker has filtered out shared dependencies
		EInstallBundleReleaseRequestFlags ReleaseFlags = GetReleaseRequestFlags() | EInstallBundleReleaseRequestFlags::ExplicitRemoveList;
		TValueOrError<FInstallBundleReleaseRequestInfo, EInstallBundleResult> MaybeRequestInfo = BundleManager->RequestReleaseContent(
			InstallBundlesToRelease, ReleaseFlags);

		if (MaybeRequestInfo.HasError())
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue uninstall for the PluginURL(%.*s) because %s"), ShortUrl.Len(), ShortUrl.GetData(), LexToString(MaybeRequestInfo.GetError()));
			Result = GetErrorResult(TEXT("BundleManager.Begin."), MaybeRequestInfo.GetError());

			return;
		}

		FInstallBundleReleaseRequestInfo RequestInfo = MaybeRequestInfo.StealValue();

		if (EnumHasAnyFlags(RequestInfo.InfoFlags, EInstallBundleRequestInfoFlags::SkippedUnknownBundles))
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue uninstall for the PluginURL(%.*s) because failed to resolve install bundles!"), ShortUrl.Len(), ShortUrl.GetData());
			Result = GetErrorResult(TEXT("BundleManager.Begin."), TEXT("Resolve_Failed"), UE::GameFeatures::CommonErrorCodes::GetGenericReleaseResult());

			return;
		}

		if (RequestInfo.BundlesEnqueued.Num() == 0)
		{
			bWasDeleted = true;
		}
		else
		{
			PendingBundles = MoveTemp(RequestInfo.BundlesEnqueued);
			IInstallBundleManager::ReleasedDelegate.AddRaw(this, &FBaseDataReleaseGameFeaturePluginState::OnContentRemoved);
		}
	}

	virtual void BeginState() override
	{
		BeginRemoveRequest();
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!Result.HasValue())
		{
			StateStatus.SetTransitionError(GetFailureTransitionState(), Result);
			return;
		}

		if (!bWasDeleted)
		{
			return;
		}

		StateStatus.SetTransition(GetSuccessTransitionState());
	}

	virtual void EndState() override
	{
		CleanUp();
	}

	/** Controls what check is done to determine if this state should run or not */
	bool ShouldReleaseContent() const
	{
		switch (StateProperties.GetPluginProtocol())
		{
			case (EGameFeaturePluginProtocol::InstallBundle):
			{
				return true;
			}

			default:
			{
				return false;
			}
		}
	}

	virtual TConstArrayView<FName> GetInstallBundles()
	{
		TConstArrayView<FName> Ret;
		if (ShouldReleaseContent())
		{
			Ret = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>().InstallBundles;
		}
		return Ret;
	}

	/** Determine what kind of release request flags we submit */
	virtual EInstallBundleReleaseRequestFlags GetReleaseRequestFlags() const
	{
		// Always set ExplicitRemoveList, GFPSharedInstallTracker has filtered out shared dependencies
		EInstallBundleReleaseRequestFlags ReleaseFlags = EInstallBundleReleaseRequestFlags::ExplicitRemoveList;
		return ReleaseFlags;
	}

	/** Determines what state you transition to in the event of a success or failure to release content */
	virtual EGameFeaturePluginState GetSuccessTransitionState() const = 0;
	virtual EGameFeaturePluginState GetFailureTransitionState() const = 0;
};

struct FGameFeaturePluginState_Uninstalled : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Uninstalled(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::Uninstalled)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Terminal);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::Uninstalled)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::CheckingStatus);
		}
	}
};

struct FGameFeaturePluginState_Uninstalling : public FBaseDataReleaseGameFeaturePluginState
{
	FGameFeaturePluginState_Uninstalling(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FBaseDataReleaseGameFeaturePluginState(InStateProperties)
	{}

	virtual EGameFeaturePluginState GetSuccessTransitionState() const override
	{
		return EGameFeaturePluginState::Uninstalled;
	}
	
	virtual EGameFeaturePluginState GetFailureTransitionState() const override
	{
		return EGameFeaturePluginState::ErrorUninstalling;
	}

	//Must be overridden to determine what kind of release request we submit
	virtual EInstallBundleReleaseRequestFlags GetReleaseRequestFlags() const override
	{
		const EInstallBundleReleaseRequestFlags BaseFlags = FBaseDataReleaseGameFeaturePluginState::GetReleaseRequestFlags();
		return (BaseFlags | EInstallBundleReleaseRequestFlags::RemoveFilesIfPossible);
	}

	virtual UE::GameFeatures::FResult TryUpdateProtocolOptions(const FGameFeatureProtocolOptions& NewOptions) override
	{
		//Use base functionality to update our metadata
		UE::GameFeatures::FResult LocalResult = FGameFeaturePluginState::TryUpdateProtocolOptions(NewOptions);

		if (LocalResult.HasError())
		{
			return LocalResult;
		}

		//If we are no longer uninstalling before terminate, just exit now as a success immediately
		if (!ShouldVisitUninstallStateBeforeTerminal())
		{
			FBaseDataReleaseGameFeaturePluginState::CleanUp();

			Result = MakeValue();
			bWasDeleted = true;
			UpdateStateMachineImmediate();

			return LocalResult;
		}		
		
		//Restart our remove request to handle other changes
		BeginRemoveRequest();

		return LocalResult;
	}
};

struct FGameFeaturePluginState_Releasing : public FBaseDataReleaseGameFeaturePluginState
{
	FGameFeaturePluginState_Releasing(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FBaseDataReleaseGameFeaturePluginState(InStateProperties)
	{}

	virtual void BeginState() override
	{
		if (ShouldReleaseContent())
		{
			UGameFeaturesSubsystem::Get().OnGameFeatureReleasing(StateProperties.PluginName, StateProperties.PluginIdentifier);
		}

		FBaseDataReleaseGameFeaturePluginState::BeginState();
	}

	virtual EGameFeaturePluginState GetSuccessTransitionState() const override
	{
		return EGameFeaturePluginState::StatusKnown;
	}

	virtual EGameFeaturePluginState GetFailureTransitionState() const override
	{
		return EGameFeaturePluginState::ErrorManagingData;
	}
};

struct FGameFeaturePluginState_Downloading : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Downloading(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FGameFeaturePluginState(InStateProperties)
		, Result(MakeValue())
	{}

	virtual ~FGameFeaturePluginState_Downloading()
	{
		Cleanup();
	}

	UE::GameFeatures::FResult Result;
	bool bSuppressResultErrorLog = false;
	bool bPluginDownloaded = false;

	TArray<FName> PendingBundleDownloads;
	TUniquePtr<FInstallBundleCombinedProgressTracker> ProgressTracker;
	FTSTicker::FDelegateHandle ProgressUpdateHandle;
	FDelegateHandle GotContentStateHandle;

	// Required for callback lifetime safety
	struct FIoStoreOnDemandContext
	{
		TArray<UE::IoStore::FOnDemandInstallRequest> InstallRequests;
		int32 PendingInstalls = 0;
		bool bStateValid = true;

		void Cancel()
		{
			for (UE::IoStore::FOnDemandInstallRequest& Request : InstallRequests)
			{
				Request.Cancel();
			}
		}
	};
	TSharedPtr<FIoStoreOnDemandContext> IoStoreOnDemandContext;

	void EnsureAllowAsyncLoading() const
	{
		ensureMsgf(AllowAsyncLoading(), TEXT("FGameFeaturePluginState::AllowAsyncLoading is false while attempting to download GFP data for %s"), *StateProperties.PluginName);
	}

	void Cleanup()
	{
		if (ProgressUpdateHandle.IsValid())
		{
			FTSTicker::RemoveTicker(ProgressUpdateHandle);
			ProgressUpdateHandle.Reset();
		}

		if (GotContentStateHandle.IsValid())
		{
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
			if (BundleManager)
			{
				BundleManager->CancelAllGetContentStateRequests(GotContentStateHandle);
			}
			GotContentStateHandle.Reset();
		}

		IInstallBundleManager::InstallBundleCompleteDelegate.RemoveAll(this);
		IInstallBundleManager::PausedBundleDelegate.RemoveAll(this);

		Result = MakeValue();
		bSuppressResultErrorLog = false;
		bPluginDownloaded = false;
		PendingBundleDownloads.Empty();
		ProgressTracker = nullptr;

		if (IoStoreOnDemandContext)
		{
			IoStoreOnDemandContext->Cancel();
			IoStoreOnDemandContext->bStateValid = false;
			IoStoreOnDemandContext = nullptr;
		}
	}

	void OnGotContentState(FInstallBundleCombinedContentState BundleContentState)
	{
		GotContentStateHandle.Reset();

		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

		if (StateProperties.bTryCancel)
		{
			Result = UE::GameFeatures::CanceledResult;
			UpdateStateMachineImmediate();
			return;
		}

		const FInstallBundlePluginProtocolMetaData& MetaData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();

		UE::GameFeatures::GFPSharedInstallTracker.AddBundleRefs(StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::Download, MetaData.InstallBundles);

		const TConstArrayView<FName> InstallBundles = GetInstallBundles();
		const EInstallBundleRequestFlags InstallFlags = GetRequestFlags();
		TValueOrError<FInstallBundleRequestInfo, EInstallBundleResult> MaybeRequestInfo = BundleManager->RequestUpdateContent(InstallBundles, InstallFlags);

		if (MaybeRequestInfo.HasError())
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue download for the PluginURL(%.*s) because %s"), ShortUrl.Len(), ShortUrl.GetData(), LexToString(MaybeRequestInfo.GetError()));
			Result = GetErrorResult(TEXT("BundleManager.GotState."), LexToString(MaybeRequestInfo.GetError()));
			UpdateStateMachineImmediate();
			return;
		}

		FInstallBundleRequestInfo RequestInfo = MaybeRequestInfo.StealValue();

		if (EnumHasAnyFlags(RequestInfo.InfoFlags, EInstallBundleRequestInfoFlags::SkippedUnknownBundles))
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue download for the PluginURL(%.*s) because failed to resolve install bundles!"), ShortUrl.Len(), ShortUrl.GetData());
			Result = GetErrorResult(TEXT("BundleManager.GotState."), TEXT("Resolve_Failed"), UE::GameFeatures::CommonErrorCodes::GetGenericConnectionError());

			UpdateStateMachineImmediate();
			return;
		}

		if (RequestInfo.BundlesEnqueued.Num() == 0)
		{
			UpdateProgress(1.0f);
			InstallIoStoreOnDemandContent();
		}
		else
		{
			EnsureAllowAsyncLoading();
			PendingBundleDownloads = MoveTemp(RequestInfo.BundlesEnqueued);
			IInstallBundleManager::InstallBundleCompleteDelegate.AddRaw(this, &FGameFeaturePluginState_Downloading::OnInstallBundleCompleted);
			IInstallBundleManager::PausedBundleDelegate.AddRaw(this, &FGameFeaturePluginState_Downloading::OnInstallBundlePaused);

			ProgressTracker = MakeUnique<FInstallBundleCombinedProgressTracker>(false);
			ProgressTracker->SetBundlesToTrackFromContentState(BundleContentState, PendingBundleDownloads);

			ProgressUpdateHandle = FTSTicker::GetCoreTicker().AddTicker(
				FTickerDelegate::CreateRaw(this, &FGameFeaturePluginState_Downloading::OnUpdateProgress)/*, 0.1f*/);

			//If this setting is flipped then we should immediately request to pause downloads.
			//We still generate the downloads so that we have an accurate PendingBundleDownloads list
			const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();
			if (Options.bUserPauseDownload)
			{
				ChangePauseState(true);
			}
		}
	}

	void OnInstallBundleCompleted(FInstallBundleRequestResultInfo BundleResult)
	{
		if (!PendingBundleDownloads.Contains(BundleResult.BundleName))
		{
			return;
		}

		PendingBundleDownloads.Remove(BundleResult.BundleName);

		if (!Result.HasError() && BundleResult.Result != EInstallBundleResult::OK)
		{
			//Use OptionalErrorCode and/or OptionalErrorText if available
			const FString ErrorCodeEnding = (BundleResult.OptionalErrorCode.IsEmpty()) ? LexToString(BundleResult.Result) : BundleResult.OptionalErrorCode;
			const FText ErrorText = BundleResult.OptionalErrorCode.IsEmpty() ? UE::GameFeatures::CommonErrorCodes::GetErrorTextForBundleResult(BundleResult.Result) : BundleResult.OptionalErrorText;

			Result = GetErrorResult(TEXT("BundleManager.OnComplete."), ErrorCodeEnding, ErrorText);

			if (BundleResult.Result != EInstallBundleResult::UserCancelledError)
			{
				TryCancelState();
			}
		}

		if (PendingBundleDownloads.Num() > 0)
		{
			return;
		}

		OnUpdateProgress(0.0f);

		if (Result.HasError())
		{
			UpdateStateMachineImmediate();
			return;
		}

		InstallIoStoreOnDemandContent();
	}

	void InstallIoStoreOnDemandContent()
	{
		const FInstallBundlePluginProtocolMetaData& MetaData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();
		if (MetaData.InstallBundlesWithAssetDependencies.IsEmpty())
		{
			bPluginDownloaded = Result.HasValue();
			UpdateStateMachineImmediate();
			return;
		}

		UE::IoStore::IOnDemandIoStore* IoStore = UE::IoStore::TryGetOnDemandIoStore();
		if (!IoStore && !Result.HasError())
		{
			Result = GetErrorResult(TEXT("IoStoreOnDemand.ModuleNotFound"));
			UpdateStateMachineImmediate();
			return;
		}

		const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();

		IoStoreOnDemandContext = MakeShared<FIoStoreOnDemandContext>();
		IoStoreOnDemandContext->PendingInstalls = MetaData.InstallBundlesWithAssetDependencies.Num();

		for (const FName InstallBundle : MetaData.InstallBundlesWithAssetDependencies)
		{
			UE::IoStore::FOnDemandInstallArgs InstallArgs;
			InstallArgs.MountId = InstallBundle.ToString();
			InstallArgs.TagSets.Emplace(TEXTVIEW("mount")); // May not be a real tagset, but this will install all the mandatory untagged chunks
			InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::InstallSoftReferences;
			InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::CallbackOnGameThread;
			if (Options.bDoNotDownload)
			{
				InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::DoNotDownload;
			}
			
			InstallArgs.ContentHandle = UE::GameFeatures::GFPSharedInstallTracker.AddOnDemandContentHandle(
				InstallBundle, UE::GameFeatures::EGFPInstallLevel::Download);
			check(InstallArgs.ContentHandle.IsValid());

			// This should be pretty small, so not going to worry about progress here.
			IoStoreOnDemandContext->InstallRequests.Add(
				IoStore->Install(MoveTemp(InstallArgs),
				[this, LambdaOnDemandContext = IoStoreOnDemandContext](UE::IoStore::FOnDemandInstallResult&& OnDemandInstallResult)
				{
					if (!LambdaOnDemandContext->bStateValid)
					{
						// Owning state got cleaned up, bail
						return;
					}

					if (!OnDemandInstallResult.IsOk() && !Result.HasError())
					{
						const FText ErrorMessage	= OnDemandInstallResult.Error.GetValue().GetErrorMessage();
						FString ErrorCode			= FString(OnDemandInstallResult.Error.GetValue().GetModuleIdAndErrorCodeString());

						ErrorCode.ReplaceCharInline(TEXT(' '), TEXT('_'), ESearchCase::CaseSensitive);
						Result = GetErrorResult(TEXT("IoStoreOnDemand.OnComplete."), ErrorCode, FText::AsCultureInvariant(ErrorMessage));
						TryCancelState();
					}

					--IoStoreOnDemandContext->PendingInstalls;
					if (IoStoreOnDemandContext->PendingInstalls == 0)
					{
						bPluginDownloaded = Result.HasValue();
						UpdateStateMachineImmediate();
					}
				}));
		}
	}

	bool OnUpdateProgress(float dts)
	{
		if (ProgressTracker)
		{
			ProgressTracker->ForceTick();

			float Progress = ProgressTracker->GetCurrentCombinedProgress().ProgressPercent;
			UpdateProgress(Progress);

			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Download Progress: %f for PluginURL(%.*s)"), Progress, ShortUrl.Len(), ShortUrl.GetData());
		}

		return true;
	}

	void ChangePauseState(bool bPause)
	{
		if (PendingBundleDownloads.Num() > 0)
		{
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

			if (bPause)
			{
				BundleManager->PauseUpdateContent(PendingBundleDownloads);
			}
			else
			{
				BundleManager->ResumeUpdateContent(PendingBundleDownloads);
			}
			BundleManager->RequestPausedBundleCallback();

			//Use same text we use for InstallBundleManager's UserPaused as this was also a user pause
			const TCHAR* PauseReason = InstallBundleUtil::GetInstallBundlePauseReason(EInstallBundlePauseFlags::UserPaused);

			NotifyPauseChange(bPause, PauseReason);
		}
	}

	void OnInstallBundlePaused(FInstallBundlePauseInfo InPauseBundleInfo)
	{
		if (PendingBundleDownloads.Contains(InPauseBundleInfo.BundleName))
		{
			const bool bIsPaused = (InPauseBundleInfo.PauseFlags != EInstallBundlePauseFlags::None);
			const TCHAR* PauseReason = InstallBundleUtil::GetInstallBundlePauseReason(InPauseBundleInfo.PauseFlags);

			NotifyPauseChange(bIsPaused, PauseReason);
		}
	}

	void NotifyPauseChange(bool bIsPaused, FString PauseReason)
	{
		FGameFeaturePauseStateChangeContext Context(UE::GameFeatures::ToString(EGameFeaturePluginState::Downloading), PauseReason, bIsPaused);
		UGameFeaturesSubsystem::Get().OnGameFeaturePauseChange(StateProperties.PluginIdentifier, StateProperties.PluginName, Context);
	}

	virtual void BeginState() override
	{
		Cleanup();

		if (!ensure(ShouldDownloadContent()))
		{
			bPluginDownloaded = true;
			UpdateProgress(1.0f);
			return;
		}

		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
		const TConstArrayView<FName> InstallBundles = GetInstallBundles();

		const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();

		const bool bAddDependencies = false; // We already got all dependencies in the CheckingStatus state

		TValueOrError<FInstallBundleCombinedInstallState, EInstallBundleResult> MaybeInstallState =
			BundleManager->GetInstallStateSynchronous(InstallBundles, bAddDependencies);
		check(MaybeInstallState.HasValue());
		FInstallBundleCombinedInstallState& InstallState = MaybeInstallState.GetValue();

		const bool bAllUpToDate = InstallState.GetAllBundlesHaveState(EInstallBundleInstallState::UpToDate);

		// Handle bDoNotDownload flag before doing any async ops
		// if not up to date, check to see if we allow downloading
		if (Options.bDoNotDownload && !bAllUpToDate)
		{
			Result = GetErrorResult(TEXT("GFPStateMachine.DownloadNotAllowed"));
			bSuppressResultErrorLog = true; // Don't log an error if the user disallowed the download
			UpdateStateMachineImmediate();
			return;
		}

		UGameFeaturesSubsystem::Get().OnGameFeatureDownloading(StateProperties.PluginName, StateProperties.PluginIdentifier);

		if (!bAllUpToDate && InstallBundles.Num() > 1)
		{
			EnsureAllowAsyncLoading();
			GotContentStateHandle = BundleManager->GetContentState(InstallBundles, EInstallBundleGetContentStateFlags::None, bAddDependencies,
				FInstallBundleGetContentStateDelegate::CreateRaw(this, &FGameFeaturePluginState_Downloading::OnGotContentState));
		}
		else
		{
			// Only if all bundles up to date or only one bundle:
			// We only care about relative weighting here, so we don't need any of the other content state metadata.
			// We can just assume the weight is 1.0 and not have to wait for the full async call to get the rest of the metadata.
			FInstallBundleCombinedContentState HackContentState;
			HackContentState.IndividualBundleStates.Reserve(InstallState.IndividualBundleStates.Num());
			for (const TPair<FName, EInstallBundleInstallState>& Pair : InstallState.IndividualBundleStates)
			{
				FInstallBundleContentState& BundleContentState = HackContentState.IndividualBundleStates.Emplace(Pair.Key);
				BundleContentState.State = Pair.Value;
				BundleContentState.Weight = 1.0f;
			}
			HackContentState.BundlesWithIoStoreOnDemand = MoveTemp(InstallState.BundlesWithIoStoreOnDemand);
			OnGotContentState(MoveTemp(HackContentState));
		}
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!Result.HasValue())
		{
			const EGameFeaturePluginState FailState = GetFailureTransitionState();
			StateStatus.SetTransitionError(FailState, Result, bSuppressResultErrorLog);
			return;
		}

		if (!bPluginDownloaded)
		{
			return;
		}

		const EGameFeaturePluginState SuccessState = GetSuccessTransitionState();
		StateStatus.SetTransition(SuccessState);
	}

	virtual void TryCancelState() override
	{
		if (PendingBundleDownloads.Num() > 0)
		{
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
			BundleManager->CancelUpdateContent(PendingBundleDownloads);
			if (IoStoreOnDemandContext)
			{
				IoStoreOnDemandContext->Cancel();
			}
		}
	}

	virtual UE::GameFeatures::FResult TryUpdateProtocolOptions(const FGameFeatureProtocolOptions& NewOptions) override
	{
		//Need to update our BundleFlags for any bundles we are downloading
		EInstallBundleRequestFlags OldRequestFlags;
		bool OldUserPausedFlag;
		{
			const FInstallBundlePluginProtocolOptions& OldOptions = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();
			OldRequestFlags = OldOptions.InstallBundleFlags;
			OldUserPausedFlag = OldOptions.bUserPauseDownload;
		}

		UE::GameFeatures::FResult OptionsResult = FGameFeaturePluginState::TryUpdateProtocolOptions(NewOptions);
		if (OptionsResult.HasError())
		{
			return OptionsResult;
		}

		//if we don't have any in-progress downloads the default behavior is all we need
		if (PendingBundleDownloads.Num() == 0)
		{
			return OptionsResult;
		}

		const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();

		//Update our InstallBundleRequestFlags
		{
			EInstallBundleRequestFlags UpdatedRequestFlags = Options.InstallBundleFlags;

			EInstallBundleRequestFlags AddFlags = (UpdatedRequestFlags & (~OldRequestFlags));
			EInstallBundleRequestFlags RemoveFlags = ((~UpdatedRequestFlags) & OldRequestFlags);

			if ((AddFlags != EInstallBundleRequestFlags::None) || (RemoveFlags != EInstallBundleRequestFlags::None))
			{
				TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
				BundleManager->UpdateContentRequestFlags(PendingBundleDownloads, AddFlags, RemoveFlags);
			}
		}

		//Handle pausing or resuming the download if the bUserPauseDownload flag has changed
		{
			if (Options.bUserPauseDownload != OldUserPausedFlag)
			{
				ChangePauseState(Options.bUserPauseDownload);
			}
		}

		return OptionsResult;
	}

	virtual void EndState() override
	{
		Cleanup();
	}

	/** Controls what check is done to determine if this state should run or not */
	bool ShouldDownloadContent() const
	{
		switch (StateProperties.GetPluginProtocol())
		{
		case (EGameFeaturePluginProtocol::InstallBundle):
			return true;
		default:
			return false;
		}
	}

	virtual TConstArrayView<FName> GetInstallBundles()
	{
		TConstArrayView<FName> Ret;
		if (ShouldDownloadContent())
		{
			Ret = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>().InstallBundles;
		}
		return Ret;
	}

	/** Determine what kind of request flags we submit */
	EInstallBundleRequestFlags GetRequestFlags() const
	{
		//Pull our InstallFlags from the Options, but also make sure SkipMount is set as there is a separate mounting step that will re-request this
		//without SkipMount and then mount the data, this allows us to pre-download data without mounting it
		EInstallBundleRequestFlags InstallFlags = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>().InstallBundleFlags;
		InstallFlags |= EInstallBundleRequestFlags::SkipMount;
		return InstallFlags;
	}

	/** Determines what state you transition to in the event of a success or failure to release content */
	EGameFeaturePluginState GetSuccessTransitionState() const
	{
		return EGameFeaturePluginState::Installed;
	}

	EGameFeaturePluginState GetFailureTransitionState() const
	{
		return EGameFeaturePluginState::ErrorManagingData;
	}
};

struct FGameFeaturePluginState_Installed : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Installed(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination > EGameFeaturePluginState::Installed)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Mounting);
		}
		else if (StateProperties.Destination < EGameFeaturePluginState::Installed)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Releasing);
		}
	}
};

struct FGameFeaturePluginState_ErrorMounting : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorMounting(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorMounting)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Unmounting);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorMounting)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Mounting);
		}
	}
};

struct FGameFeaturePluginState_ErrorWaitingForDependencies : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorWaitingForDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorWaitingForDependencies)
		{
			// There is no cleaup state equivalent to EGameFeaturePluginState::WaitingForDependencies so just go back to unmounting
			StateStatus.SetTransition(EGameFeaturePluginState::Unmounting);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorWaitingForDependencies)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::WaitingForDependencies);
		}
	}
};

struct FGameFeaturePluginState_ErrorRegistering : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorRegistering(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorRegistering)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Unregistering);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorRegistering)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Registering);
		}
	}
};

struct FGameFeaturePluginState_Unmounting : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Unmounting(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	UE::GameFeatures::FResult Result = MakeValue();
	TArray<FName> PendingBundles;
	bool bUnmounting = false;
	bool bUnmounted = false;
	bool bCheckedRealtimeMode = false;

	void Unmount()
	{
		if (TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
			Plugin && Plugin->GetDescriptor().bExplicitlyLoaded)
		{
			if (!UE::GameFeatures::ShouldDeferLocalizationDataLoad())
			{
				IPluginManager::Get().UnmountExplicitlyLoadedPluginLocalizationData(StateProperties.PluginName);
			}

#if UE_MERGED_MODULES
			// We normally do not allow unloading code because it's difficult to make that safe.
			// For example, destructors can be called much later than we'd expect because of garbage collection or async work.
			// We allow this behavior for merged modular builds because the have a smaller scope (intended for console clients) and
			// unloading code to save memory is the main goal for that feature.
			static constexpr bool bAllowUnloadCode = true;
#else
			static constexpr bool bAllowUnloadCode = false;
#endif // UE_MERGED_MODULES

			// The asset registry listens to FPackageName::OnContentPathDismounted() and 
			// will automatically cleanup the asset registry state we added for this plugin.
			// This will also cause any assets we added to the asset manager to be removed.
			// Scan paths added to the asset manager should have already been cleaned up.
			FText FailureReason;
			if (!IPluginManager::Get().UnmountExplicitlyLoadedPlugin(StateProperties.PluginName, &FailureReason, bAllowUnloadCode))
			{
				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				ensureMsgf(false, TEXT("Failed to explicitly unmount the PluginURL(%.*s) because %s"), ShortUrl.Len(), ShortUrl.GetData(), *FailureReason.ToString());
				Result = GetErrorResult(TEXT("Plugin_Cannot_Explicitly_Unmount"));
				return;
			}
		}

		if (StateProperties.bAddedPluginToManager)
		{
			verify(IPluginManager::Get().RemoveFromPluginsList(StateProperties.PluginInstalledFilename));
			StateProperties.bAddedPluginToManager = false;
		}

		if (StateProperties.GetPluginProtocol() != EGameFeaturePluginProtocol::InstallBundle)
		{
			bUnmounted = true;
			return;
		}

		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
		check(BundleManager.IsValid());

		const TArray<FName>& InstallBundles = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>().InstallBundles;

		TArray<FName> InstallBundlesToRelease = UE::GameFeatures::GFPSharedInstallTracker.Release(
			StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::Mount, InstallBundles);

		EInstallBundleReleaseRequestFlags ReleaseFlags = 
			EInstallBundleReleaseRequestFlags::SkipReleaseUnmountOnly |
			EInstallBundleReleaseRequestFlags::ExplicitRemoveList; // Always set ExplicitRemoveList, GFPSharedInstallTracker has filtered out shared dependencies
		TValueOrError<FInstallBundleReleaseRequestInfo, EInstallBundleResult> MaybeRequestInfo = BundleManager->RequestReleaseContent(
			InstallBundlesToRelease, ReleaseFlags);

		if (MaybeRequestInfo.HasError())
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue unmount for the PluginURL(%.*s) because %s"), ShortUrl.Len(), ShortUrl.GetData(), LexToString(MaybeRequestInfo.GetError()));
			Result = GetErrorResult(TEXT("BundleManager.Begin."), MaybeRequestInfo.GetError());
			return;
		}

		FInstallBundleReleaseRequestInfo RequestInfo = MaybeRequestInfo.StealValue();

		if (EnumHasAnyFlags(RequestInfo.InfoFlags, EInstallBundleRequestInfoFlags::SkippedUnknownBundles))
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue unmount for the PluginURL(%.*s) because failed to resolve install bundles!"), ShortUrl.Len(), ShortUrl.GetData());
			Result = GetErrorResult(TEXT("BundleManager.Begin."), TEXT("Cannot_Resolve"), UE::GameFeatures::CommonErrorCodes::GetGenericConnectionError());
			return;
		}

		if (RequestInfo.BundlesEnqueued.Num() == 0)
		{
			bUnmounted = true;
		}
		else
		{
			PendingBundles = MoveTemp(RequestInfo.BundlesEnqueued);
			IInstallBundleManager::ReleasedDelegate.AddRaw(this, &FGameFeaturePluginState_Unmounting::OnContentReleased);
		}
	}

	void OnContentReleased(FInstallBundleReleaseRequestResultInfo BundleResult)
	{
		if (!PendingBundles.Contains(BundleResult.BundleName))
		{
			return;
		}

		PendingBundles.Remove(BundleResult.BundleName);

		if (!Result.HasError() && BundleResult.Result != EInstallBundleReleaseResult::OK)
		{
			Result = GetErrorResult(TEXT("BundleManager.OnReleased."), BundleResult.Result);
		}

		if (PendingBundles.Num() > 0)
		{
			return;
		}

		if (Result.HasValue())
		{
			bUnmounted = true;
		}

		UpdateStateMachineImmediate();
	}

	virtual void BeginState() override
	{
		Result = MakeValue();
		PendingBundles.Empty();
		bUnmounting = false;
		bUnmounted = false;
		bCheckedRealtimeMode = false;
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!bCheckedRealtimeMode)
		{
			bCheckedRealtimeMode = true;
			if (UE::GameFeatures::RealtimeMode)
			{
				UE::GameFeatures::RealtimeMode->AddUpdateRequest(StateProperties.OnRequestUpdateStateMachine);
				return;
			}
		}

		if (!bUnmounting)
		{
			bUnmounting = true;
			Unmount();
		}

		if (!Result.HasValue())
		{
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorMounting, Result);
			return;
		}

		if (!bUnmounted)
		{
			return;
		}

		StateStatus.SetTransition(EGameFeaturePluginState::Installed);
	}

	virtual void EndState() override
	{
		IInstallBundleManager::ReleasedDelegate.RemoveAll(this);
	}
};

struct FGameFeaturePluginState_Mounting : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Mounting(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FGameFeaturePluginState(InStateProperties)
		, Result(MakeValue())
	{}

	enum class ESubState : uint8
	{
		None = 0,
		MountPlugin,
		LoadAssetRegistry
	};
	FRIEND_ENUM_CLASS_FLAGS(ESubState);

	int32 NumObservedPostMountPausers = 0;
	int32 NumExpectedPostMountPausers = 0;
	TArray<FName> PendingBundles;
	FDelegateHandle PakFileMountedDelegateHandle;
	UE::GameFeatures::FResult Result;
	ESubState StartedSubStates = ESubState::None;
	ESubState CompletedSubStates = ESubState::None;
	bool bCheckedRealtimeMode = false;
	bool bForceMonolithicShaderLibrary = true;	// use monolithic unless a DLC plugin is chunked

	static UE::Tasks::FPipe ShaderlibPipe;

	void OnInstallBundleCompleted(FInstallBundleRequestResultInfo BundleResult)
	{
		if (!PendingBundles.Contains(BundleResult.BundleName))
		{
			return;
		}

		PendingBundles.Remove(BundleResult.BundleName);

		if (!Result.HasError() && BundleResult.Result != EInstallBundleResult::OK)
		{
			if (BundleResult.OptionalErrorCode.IsEmpty())
			{
				Result = GetErrorResult(TEXT("BundleManager.OnComplete."), BundleResult.Result);
			}
			else
			{
				Result = GetErrorResult(TEXT("BundleManager.OnComplete."), BundleResult.OptionalErrorCode, BundleResult.OptionalErrorText);
			}
		}

		if (bForceMonolithicShaderLibrary && BundleResult.bContainsChunks)
		{
			bForceMonolithicShaderLibrary = false;
		}

		if (PendingBundles.IsEmpty())
		{
			IInstallBundleManager::InstallBundleCompleteDelegate.RemoveAll(this);
			if (PakFileMountedDelegateHandle.IsValid())
			{
				FCoreDelegates::GetOnPakFileMounted2().Remove(PakFileMountedDelegateHandle);
				PakFileMountedDelegateHandle.Reset();
			}
			
			UpdateStateMachineImmediate();
		}
	}

	void OnPakFileMounted(const IPakFile& PakFile)
	{
		if (FPakFile* Pak = (FPakFile*)(&PakFile))
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			UE_LOG(LogGameFeatures, Display, TEXT("Mounted Pak File for (%.*s) with following files:"), ShortUrl.Len(), ShortUrl.GetData());
			TArray<FString> OutFileList;
			Pak->GetPrunedFilenames(OutFileList);
			for (const FString& FileName : OutFileList)
			{
				UE_LOG(LogGameFeatures, Display, TEXT("(%s)"), *FileName);
			}
		}
	}

	void OnPostMountPauserCompleted(FStringView InPauserTag)
	{
		check(IsInGameThread());
		ensure(NumExpectedPostMountPausers != INDEX_NONE);
		++NumObservedPostMountPausers;

		UE_LOG(LogGameFeatures, Display, TEXT("Post-mount of %s resumed by %.*s"), *StateProperties.PluginName, InPauserTag.Len(), InPauserTag.GetData());

		if (NumObservedPostMountPausers == NumExpectedPostMountPausers)
		{
			UpdateStateMachineImmediate();
		}
	}

	virtual bool UseAsyncLoading() const override
	{
		if (UE::GameFeatures::CVarForceSyncRegisterStartupPlugins.GetValueOnGameThread())
		{
			if (UGameFeaturesSubsystem::Get().GetPolicy().IsLoadingStartupPlugins())
			{
				return false;
			}
		}

		return FGameFeaturePluginState::UseAsyncLoading();
	}

	virtual void BeginState() override
	{
		NumObservedPostMountPausers = 0;
		NumExpectedPostMountPausers = 0;
		PendingBundles.Empty();
		PakFileMountedDelegateHandle.Reset();
		Result = MakeValue();
		StartedSubStates = ESubState::None;
		CompletedSubStates = ESubState::None;
		bCheckedRealtimeMode = false;
		bForceMonolithicShaderLibrary = false;

		if (StateProperties.GetPluginProtocol() != EGameFeaturePluginProtocol::InstallBundle)
		{
			return;
		}

		// Assume monolithic shader, will be set to false if chunks are detected
		bForceMonolithicShaderLibrary = UE::GameFeatures::CVarAllowForceMonolithicShaderLibrary.GetValueOnGameThread();
		
		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

		const FInstallBundlePluginProtocolMetaData& MetaData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();
		const TArray<FName>& InstallBundles = MetaData.InstallBundles;

		UE::GameFeatures::GFPSharedInstallTracker.AddBundleRefs(StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::Mount, InstallBundles);

		const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();
		const EInstallBundleRequestFlags InstallFlags = UseAsyncLoading() ?
			(Options.InstallBundleFlags | EInstallBundleRequestFlags::AsyncMount) : Options.InstallBundleFlags;

		// Make bundle manager use verbose log level for most logs.
		// We are already done with downloading, so we don't care about logging too much here unless mounting fails.
		const ELogVerbosity::Type InstallBundleManagerVerbosityOverride = ELogVerbosity::Verbose;
		TValueOrError<FInstallBundleRequestInfo, EInstallBundleResult> MaybeRequestInfo = BundleManager->RequestUpdateContent(InstallBundles, InstallFlags, InstallBundleManagerVerbosityOverride);

		if (MaybeRequestInfo.HasError())
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue mount for the PluginURL(%.*s) because %s"), ShortUrl.Len(), ShortUrl.GetData(), LexToString(MaybeRequestInfo.GetError()));
			Result = GetErrorResult(TEXT("BundleManager.Begin.CannotStart."), MaybeRequestInfo.GetError());
			return;
		}

		FInstallBundleRequestInfo RequestInfo = MaybeRequestInfo.StealValue();

		if (EnumHasAnyFlags(RequestInfo.InfoFlags, EInstallBundleRequestInfoFlags::SkippedUnknownBundles))
		{
			const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
			ensureMsgf(false, TEXT("Unable to enqueue mount for the PluginURL(%.*s) because failed to resolve install bundles!"), ShortUrl.Len(), ShortUrl.GetData());
			Result = GetErrorResult(TEXT("BundleManager.Begin."), TEXT("Resolve_Failed"));
			return;
		}

		if (RequestInfo.BundlesEnqueued.Num() > 0)
		{
			PendingBundles = MoveTemp(RequestInfo.BundlesEnqueued);
			IInstallBundleManager::InstallBundleCompleteDelegate.AddRaw(this, &FGameFeaturePluginState_Mounting::OnInstallBundleCompleted);
			if (UE::GameFeatures::ShouldLogMountedFiles)
			{
				// Track with a delegate handle to avoid unbinding if we don't use this. Unbinding this causes an occaisonal perf spike.
				PakFileMountedDelegateHandle = FCoreDelegates::GetOnPakFileMounted2().AddRaw(this, &FGameFeaturePluginState_Mounting::OnPakFileMounted);
			}
		}

		for (const FInstallBundleRequestResultInfo& BundleResult : RequestInfo.BundleResults)
		{
			if (bForceMonolithicShaderLibrary && BundleResult.bContainsChunks)
			{
				bForceMonolithicShaderLibrary = false;
			}
		}
	}

	void UpdateState_MountPlugin(bool bLoadPluginIniHierarchy)
	{
		if (EnumHasAnyFlags(StartedSubStates, ESubState::MountPlugin))
		{
			return;
		}

		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Mounting_Plugin);

		StartedSubStates |= ESubState::MountPlugin;

		if (Result.HasError())
		{
			CompletedSubStates |= ESubState::MountPlugin;
			return;
		}

		// Pre-mount
		// Normally the shader library itself listens to a "New Plugin mounted" (and "New Pakfile mounted") callback and the library is opened automatically. This switch governs whether the manual behavior is wanted.
		bool bManuallyOpenPluginShaderLibrary = true;
		{
			FGameFeaturePreMountingContext Context;
			UGameFeaturesSubsystem::Get().OnGameFeaturePreMounting(StateProperties.PluginName, StateProperties.PluginIdentifier, Context);
			bManuallyOpenPluginShaderLibrary = Context.bOpenPluginShaderLibrary;
		}

		checkf(!StateProperties.PluginInstalledFilename.IsEmpty(), TEXT("PluginInstalledFilename must be set by the Mounting. PluginURL: %s"), *StateProperties.PluginIdentifier.GetFullPluginURL());
		checkf(FPaths::GetExtension(StateProperties.PluginInstalledFilename) == TEXT("uplugin"), TEXT("PluginInstalledFilename must have a uplugin extension. PluginURL: %s"), *StateProperties.PluginIdentifier.GetFullPluginURL());

		// refresh the plugins list to let the plugin manager know about it
		const TSharedPtr<IPlugin> MaybePlugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
		const bool bNeedsPluginMount = (MaybePlugin == nullptr || MaybePlugin->GetDescriptor().bExplicitlyLoaded);

		if (MaybePlugin)
		{
			if (!FPaths::IsSamePath(MaybePlugin->GetDescriptorFileName(), StateProperties.PluginInstalledFilename))
			{
				Result = GetErrorResult(TEXT("Plugin_Name_Already_In_Use"));
			}
		}
		else
		{
			const bool bAddedPlugin = IPluginManager::Get().AddToPluginsList(StateProperties.PluginInstalledFilename);
			if (bAddedPlugin)
			{
				StateProperties.bAddedPluginToManager = true;
			}
			else
			{
				Result = GetErrorResult(TEXT("Failed_To_Register_Plugin"));
			}
		}

		// now load ini files if desired, now that we know the plugin has been loaded
		if (bLoadPluginIniHierarchy)
		{
			UGameFeatureData::InitializeBasePluginIniFile(StateProperties.PluginInstalledFilename);
		}


		if (Result.HasError() || !bNeedsPluginMount)
		{
			CompletedSubStates |= ESubState::MountPlugin;
			return;
		}

		if (bManuallyOpenPluginShaderLibrary)
		{
			// We want to control opening the shader lib
			FShaderCodeLibrary::DontOpenPluginShaderLibraryOnMount(StateProperties.PluginName);
		}

		if (!UseAsyncLoading() || UE::GameFeatures::CVarForceSyncLoadShaderLibrary.GetValueOnGameThread())
		{
			verify(IPluginManager::Get().MountExplicitlyLoadedPlugin(StateProperties.PluginName));
			if (!UE::GameFeatures::ShouldDeferLocalizationDataLoad())
			{
				UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier);
				UE::GameFeatures::MountLocalizationData(CurrentMachine, StateProperties);
			}
			if (bManuallyOpenPluginShaderLibrary)
			{
				TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
				FShaderCodeLibrary::OpenPluginShaderLibrary(*Plugin, bForceMonolithicShaderLibrary);
			}
			CompletedSubStates |= ESubState::MountPlugin;
			return;
		}

		verify(IPluginManager::Get().MountExplicitlyLoadedPlugin(StateProperties.PluginName));
		if (!UE::GameFeatures::ShouldDeferLocalizationDataLoad())
		{
			UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier);
			UE::GameFeatures::MountLocalizationData(CurrentMachine, StateProperties);
		}

		// Now load the shader lib in the background
		TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
		if (bManuallyOpenPluginShaderLibrary && Plugin->CanContainContent() && Plugin->IsEnabled())
		{
			// TEMP HACK - use a pipe because if this goes too wide we can end up blocking all available tasks.
			ShaderlibPipe.Launch(UE_SOURCE_LOCATION, [this, Plugin]
			{
				FShaderCodeLibrary::OpenPluginShaderLibrary(*Plugin, bForceMonolithicShaderLibrary);

				ExecuteOnGameThread(UE_SOURCE_LOCATION, [this]
				{
					CompletedSubStates |= ESubState::MountPlugin;
					UpdateStateMachineImmediate();
				});

			}, UE::Tasks::ETaskPriority::BackgroundHigh);

			return;
		}

		CompletedSubStates |= ESubState::MountPlugin;
	}

	void UpdateState_LoadAssetRegistry()
	{
		if (EnumHasAnyFlags(StartedSubStates, ESubState::LoadAssetRegistry))
		{
			return;
		}

		StartedSubStates |= ESubState::LoadAssetRegistry;

		if (Result.HasError())
		{
			CompletedSubStates |= ESubState::LoadAssetRegistry;
			return;
		}

		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Mounting_AR);

		// After the new plugin is mounted add the asset registry for that plugin.
		TSharedPtr<IPlugin> NewlyMountedPlugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
		if (!NewlyMountedPlugin || !NewlyMountedPlugin->CanContainContent())
		{
			CompletedSubStates |= ESubState::LoadAssetRegistry;
			return;
		}

		FString PluginAssetRegistry;
		{
			const FString PluginFolder = FPaths::GetPath(StateProperties.PluginInstalledFilename);
			TArray<FString> PluginAssetRegistrySearchPaths;
			FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
			// For GFPs cooked as DLC
			PluginAssetRegistrySearchPaths.Add(PluginFolder / TEXT("AssetRegistry.bin"));
			// For GFPs with a unique chunk
			PluginAssetRegistrySearchPaths.Add(FPaths::ProjectDir() / FString::Printf(TEXT("AssetRegistry_GFP_%s.bin"), *StateProperties.PluginName));
			for (FString& Path : PluginAssetRegistrySearchPaths)
			{
				// Optimization: if we're using pak files then only search paks (avoid unnecessary fallback to loose)
				bool bFileExists = PakPlatformFile != nullptr ? PakPlatformFile->FindFileInPakFiles(*Path) : IFileManager::Get().FileExists(*Path);
				if (bFileExists)
				{
					PluginAssetRegistry = MoveTemp(Path);
					break;
				}
			}

			if (PluginAssetRegistry.IsEmpty())
			{
				CompletedSubStates |= ESubState::LoadAssetRegistry;
				return;
			}
		}

		auto RefreshPackageLocalizationCacheForPlugin = [NewlyMountedPlugin]()
		{
			// We need to refresh the package localization cache for a GFP if it loaded cooked asset registry state, 
			// as we need the asset registry data to correctly build the package localization cache for the GFP
			if (NewlyMountedPlugin && NewlyMountedPlugin->CanContainContent())
			{
				FPackageLocalizationManager::Get().InvalidateRootSourcePath(NewlyMountedPlugin->GetMountedAssetPath());
			}
		};

		if (!UseAsyncLoading())
		{
			FAssetRegistryState PluginAssetRegistryState;
			if (FAssetRegistryState::LoadFromDisk(*PluginAssetRegistry, FAssetRegistryLoadOptions(), PluginAssetRegistryState))
			{
				IAssetRegistry& AssetRegistry = UAssetManager::Get().GetAssetRegistry();
				AssetRegistry.AppendState(PluginAssetRegistryState);
				RefreshPackageLocalizationCacheForPlugin();
			}
			else
			{
				Result = GetErrorResult(TEXT("Failed_To_Load_Plugin_AssetRegistry"));
			}

			CompletedSubStates |= ESubState::LoadAssetRegistry;
			return;
		}

		const bool bForceSyncAssetRegistryAppend = UE::GameFeatures::CVarForceSyncAssetRegistryAppend.GetValueOnGameThread();
		UE::Tasks::Launch(UE_SOURCE_LOCATION, [this, PluginAssetRegistry=MoveTemp(PluginAssetRegistry), bForceSyncAssetRegistryAppend, RefreshPackageLocalizationCacheForPlugin]
		{
			bool bSuccess = false;
			TSharedPtr<FAssetRegistryState> PluginAssetRegistryState = MakeShared<FAssetRegistryState>();
			if (FAssetRegistryState::LoadFromDisk(*PluginAssetRegistry, FAssetRegistryLoadOptions(), *PluginAssetRegistryState))
			{
				IAssetRegistry& AssetRegistry = UAssetManager::Get().GetAssetRegistry();
				if (!bForceSyncAssetRegistryAppend)
				{
					AssetRegistry.AppendState(*PluginAssetRegistryState);
					RefreshPackageLocalizationCacheForPlugin();
				}
				bSuccess = true;
			}

			ExecuteOnGameThread(UE_SOURCE_LOCATION, [this, PluginAssetRegistryState, bSuccess, bForceSyncAssetRegistryAppend, RefreshPackageLocalizationCacheForPlugin]
			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Mounting_ARComplete);

				if (!bSuccess)
				{
					Result = GetErrorResult(TEXT("Failed_To_Load_Plugin_AssetRegistry"));
				}
				else if (bForceSyncAssetRegistryAppend)
				{
					IAssetRegistry& AssetRegistry = UAssetManager::Get().GetAssetRegistry();
					AssetRegistry.AppendState(*PluginAssetRegistryState);
					RefreshPackageLocalizationCacheForPlugin();
				}

				CompletedSubStates |= ESubState::LoadAssetRegistry;
				UpdateStateMachineImmediate();
			});

		}, UE::Tasks::ETaskPriority::BackgroundHigh);
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		// Check if waiting for install bundles
		if (PendingBundles.Num() > 0)
		{
			return;
		}

		// Check if post-mount is paused
		if (NumExpectedPostMountPausers > 0)
		{
			// Check if post-mount unpaused
			if (NumExpectedPostMountPausers == NumObservedPostMountPausers)
			{
				NumExpectedPostMountPausers = INDEX_NONE;
				TransitionOut(StateStatus);
			}
			return;
		}

		if (!bCheckedRealtimeMode)
		{
			bCheckedRealtimeMode = true;
			if (UE::GameFeatures::RealtimeMode)
			{
				UE::GameFeatures::RealtimeMode->AddUpdateRequest(StateProperties.OnRequestUpdateStateMachine);
				return;
			}
		}

		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Mounting);
		
		UpdateState_MountPlugin(AllowIniLoading());
		UpdateState_LoadAssetRegistry();

		const bool bComplete = EnumHasAllFlags(CompletedSubStates, ESubState::MountPlugin | ESubState::LoadAssetRegistry);

		// Post-mount
		if (bComplete)
		{
			FGameFeaturePostMountingContext Context(StateProperties.PluginName, [this](FStringView InPauserTag) { OnPostMountPauserCompleted(InPauserTag); });
			NumExpectedPostMountPausers = INDEX_NONE;
			UGameFeaturesSubsystem::Get().OnGameFeaturePostMounting(StateProperties.PluginName, StateProperties.PluginIdentifier, Context);
			NumExpectedPostMountPausers = Context.NumPausers;

			// Check if we got post-mount paused
			if (NumExpectedPostMountPausers <= 0)
			{
				TransitionOut(StateStatus);
			}
		}
	}

	void TransitionOut(FGameFeaturePluginStateStatus& StateStatus)
	{
		if (Result.HasError())
		{
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorMounting, Result);
		}
		else
		{
			StateStatus.SetTransition(EGameFeaturePluginState::WaitingForDependencies);
		}
	}

	virtual void EndState() override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Mounting_EndState);
		IInstallBundleManager::InstallBundleCompleteDelegate.RemoveAll(this);
		if (PakFileMountedDelegateHandle.IsValid())
		{
			FCoreDelegates::GetOnPakFileMounted2().RemoveAll(this);
			PakFileMountedDelegateHandle.Reset();
		}
	}
};
ENUM_CLASS_FLAGS(FGameFeaturePluginState_Mounting::ESubState);
UE::Tasks::FPipe FGameFeaturePluginState_Mounting::ShaderlibPipe(TEXT("FGameFeaturePluginState_Mounting::ShaderlibPipe"));

struct FWaitingForDependenciesTransitionPolicy
{
	static bool GetPluginDependencyStateMachines(const FGameFeaturePluginStateMachineProperties& InStateProperties, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines)
	{
		UGameFeaturesSubsystem& GameFeaturesSubsystem = UGameFeaturesSubsystem::Get();

		return GameFeaturesSubsystem.FindOrCreatePluginDependencyStateMachines(
			InStateProperties.PluginIdentifier.GetFullPluginURL(), InStateProperties, OutDependencyMachines);
	}

	static FGameFeaturePluginStateRange GetDependencyStateRange()
	{
		return FGameFeaturePluginStateRange(EGameFeaturePluginState::Registered, EGameFeaturePluginState::Active);
	}

	static EGameFeaturePluginState GetTransitionState()
	{
		return UE::GameFeatures::CVarEnableAssetStreaming.GetValueOnGameThread() ? EGameFeaturePluginState::AssetDependencyStreaming : EGameFeaturePluginState::Registering;
	}

	static EGameFeaturePluginState GetErrorState()
	{
		return EGameFeaturePluginState::ErrorWaitingForDependencies;
	}

	static bool ExcludeDepedenciesFromBatchProcessing()
	{
		return false;
	}

	static bool ShouldWaitForDependencies()
	{
		return true;
	}
};

struct FGameFeaturePluginState_WaitingForDependencies : public FTransitionDependenciesGameFeaturePluginState<FWaitingForDependenciesTransitionPolicy>
{
	FGameFeaturePluginState_WaitingForDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FTransitionDependenciesGameFeaturePluginState(InStateProperties)
	{
	}
};

struct FGameFeaturePluginState_AssetDependencyStreamOut : public FGameFeaturePluginState
{
	FGameFeaturePluginState_AssetDependencyStreamOut(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FGameFeaturePluginState(InStateProperties)
	{}

	virtual void BeginState() override
	{
		if (StateProperties.GetPluginProtocol() != EGameFeaturePluginProtocol::InstallBundle)
		{
			return;
		}

		const FInstallBundlePluginProtocolMetaData& MetaData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();

		UE::GameFeatures::GFPSharedInstallTracker.Release(
			StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::AssetStream, MetaData.InstallBundlesWithAssetDependencies);
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		StateStatus.SetTransition(EGameFeaturePluginState::Unmounting);
	}
};

struct FGameFeaturePluginState_ErrorAssetDependencyStreaming : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorAssetDependencyStreaming(FGameFeaturePluginStateMachineProperties& InStateProperties) 
		: FErrorGameFeaturePluginState(InStateProperties) 
	{}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorAssetDependencyStreaming)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::AssetDependencyStreamOut);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorAssetDependencyStreaming)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::AssetDependencyStreaming);
		}
	}
};

struct FGameFeaturePluginState_AssetDependencyStreaming : public FGameFeaturePluginState
{
	FGameFeaturePluginState_AssetDependencyStreaming(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FGameFeaturePluginState(InStateProperties)
	{}

	virtual ~FGameFeaturePluginState_AssetDependencyStreaming()
	{
		Cleanup();
	}

	struct FIoStoreOnDemandProgress
	{
		FName InstallBundle;
		UE::IoStore::FOnDemandInstallProgress Progress;
	};

	// Required for callback lifetime safety
	struct FIoStoreOnDemandContext
	{
		TArray<UE::IoStore::FOnDemandInstallRequest> InstallRequests;
		TArray<FIoStoreOnDemandProgress> Progress;
		int32 PendingInstalls = 0;
		bool bStateValid = true;
	};

	TSharedPtr<FIoStoreOnDemandContext> IoStoreOnDemandContext;

	UE::GameFeatures::FResult Result{ MakeValue() };
	bool bComplete = false;

	void Cleanup()
	{
		Result = MakeValue();
		bComplete = false;

		if (IoStoreOnDemandContext)
		{
			for (UE::IoStore::FOnDemandInstallRequest& Request : IoStoreOnDemandContext->InstallRequests)
			{
				Request.Cancel();
			}
			IoStoreOnDemandContext->bStateValid = false;
			IoStoreOnDemandContext = nullptr;
		}
	}

	virtual void BeginState() override
	{
		Cleanup();

		if (StateProperties.GetPluginProtocol() != EGameFeaturePluginProtocol::InstallBundle)
		{
			bComplete = true;
			return;
		}

		const FInstallBundlePluginProtocolMetaData& MetaData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();

		if (MetaData.InstallBundlesWithAssetDependencies.IsEmpty())
		{
			bComplete = true;
			return;
		}

		UE::IoStore::IOnDemandIoStore* IoStore = UE::IoStore::TryGetOnDemandIoStore();
		if (!IoStore)
		{
			Result = GetErrorResult(TEXT("IoStoreOnDemand.ModuleNotFound"));
			return;
		}

		TValueOrError<TArray<EStreamingAssetInstallMode>, FString> MaybeInstallModes = 
			UGameFeaturesSubsystem::Get().GetPolicy().GetStreamingAssetInstallModes(
				StateProperties.PluginIdentifier.GetFullPluginURL(), MetaData.InstallBundlesWithAssetDependencies);

		if (MaybeInstallModes.HasError())
		{
			Result = GetErrorResult(TEXT("IoStoreOnDemand.InstallMode"), MaybeInstallModes.GetError());
			return;
		}

		const FInstallBundlePluginProtocolOptions& Options = StateProperties.ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();

		UE::GameFeatures::GFPSharedInstallTracker.AddBundleRefs(
			StateProperties.PluginName, UE::GameFeatures::EGFPInstallLevel::AssetStream, MetaData.InstallBundlesWithAssetDependencies);

		IoStoreOnDemandContext = MakeShared<FIoStoreOnDemandContext>();
		IoStoreOnDemandContext->PendingInstalls = MetaData.InstallBundlesWithAssetDependencies.Num();
		IoStoreOnDemandContext->Progress.Reserve(MetaData.InstallBundlesWithAssetDependencies.Num());

		const TArray<EStreamingAssetInstallMode>& InstallModes = MaybeInstallModes.GetValue();
		for (int i = 0; const FName InstallBundle : MetaData.InstallBundlesWithAssetDependencies)
		{
			IoStoreOnDemandContext->Progress.Emplace(FIoStoreOnDemandProgress{InstallBundle});

			const EStreamingAssetInstallMode InstallMode = InstallModes[i++];

			UE::IoStore::FOnDemandInstallArgs InstallArgs;
			InstallArgs.MountId = InstallBundle.ToString();
			if (InstallMode == EStreamingAssetInstallMode::GfpRequiredOnly)
			{
				InstallArgs.TagSets.Emplace(TEXTVIEW("required"));
			}
			InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::InstallSoftReferences;
			InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::CallbackOnGameThread;
			if (Options.bDoNotDownload)
			{
				InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::DoNotDownload;
			}
			if (UE::GameFeatures::CVarAllowMissingOnDemandDependencies.GetValueOnGameThread())
			{
				InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::AllowMissingDependencies;
			}

			InstallArgs.ContentHandle = UE::GameFeatures::GFPSharedInstallTracker.AddOnDemandContentHandle(
				InstallBundle, UE::GameFeatures::EGFPInstallLevel::AssetStream);

			IoStoreOnDemandContext->InstallRequests.Add(IoStore->Install(MoveTemp(InstallArgs),
				// On Complete
				[this, LambdaOnDemandContext = IoStoreOnDemandContext](const UE::IoStore::FOnDemandInstallResult& OnDemandInstallResult)
				{
					if (!LambdaOnDemandContext->bStateValid)
					{
						// Owning state got cleaned up, bail
						return;
					}

					if (!OnDemandInstallResult.IsOk() && !Result.HasError())
					{
						const FText ErrorMessage	= OnDemandInstallResult.Error.GetValue().GetErrorMessage();
						FString ErrorCode			= FString(OnDemandInstallResult.Error.GetValue().GetModuleIdAndErrorCodeString());

						ErrorCode.ReplaceCharInline(TEXT(' '), TEXT('_'), ESearchCase::CaseSensitive);
						Result = GetErrorResult(TEXT("IoStoreOnDemand.OnComplete."), ErrorCode, ErrorMessage);
						TryCancelState();
					}

					--IoStoreOnDemandContext->PendingInstalls;
					if (IoStoreOnDemandContext->PendingInstalls == 0)
					{
						bComplete = true;
						UpdateStateMachineImmediate();
					}

				},
				// On Progress
				[this, LambdaOnDemandContext = IoStoreOnDemandContext, InstallBundle](const UE::IoStore::FOnDemandInstallProgress& Progress)
				{
					if (!LambdaOnDemandContext->bStateValid)
					{
						// Owning state got cleaned up, bail
						return;
					}

					FIoStoreOnDemandProgress* MyProgress = Algo::FindBy(
						LambdaOnDemandContext->Progress, InstallBundle, &FIoStoreOnDemandProgress::InstallBundle);
					check(MyProgress);
					MyProgress->Progress = Progress;

					const UE::IoStore::FOnDemandInstallProgress SumProgress = Algo::TransformAccumulate(
						LambdaOnDemandContext->Progress,
						&FIoStoreOnDemandProgress::Progress,
						UE::IoStore::FOnDemandInstallProgress(),
						&UE::IoStore::FOnDemandInstallProgress::Combine);
					const float OverallProgress = SumProgress.GetRelativeProgress();

					StateProperties.OnFeatureStateProgressUpdate.ExecuteIfBound(OverallProgress);
				}));
		}
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (!Result.HasValue())
		{
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorAssetDependencyStreaming, Result);
			return;
		}

		if (!bComplete)
		{
			return;
		}

		StateStatus.SetTransition(EGameFeaturePluginState::Registering);
	}

	virtual void EndState() override
	{
		Cleanup();
	}

	virtual void TryCancelState() override
	{
		if (IoStoreOnDemandContext)
		{
			for (UE::IoStore::FOnDemandInstallRequest& InstallRequest : IoStoreOnDemandContext->InstallRequests)
			{
				InstallRequest.Cancel();
			}
		}
	}
};

struct FGameFeaturePluginState_Unregistering : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Unregistering(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	bool bHasUnloaded = false;
#if WITH_EDITOR
	bool bRequestedUnloadPluginAssets = false;
#endif //if WITH_EDITOR

	virtual void BeginState() override
	{
		bHasUnloaded = false;
#if WITH_EDITOR
		bRequestedUnloadPluginAssets = false;
#endif //if WITH_EDITOR
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (bHasUnloaded)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::AssetDependencyStreamOut);
			return;
		}

#if WITH_EDITOR
		if (bRequestedUnloadPluginAssets)
		{
			bHasUnloaded = true;
			UpdateStateMachineDeferred();
			return;
		}
#endif //if WITH_EDITOR

		if (StateProperties.GameFeatureData)
		{
			UStringTable::OnPluginUnloaded(StateProperties.PluginName);
			UGameFeaturesSubsystem::Get().OnGameFeatureUnregistering(StateProperties.GameFeatureData, StateProperties.PluginName, StateProperties.PluginIdentifier);

			UGameFeaturesSubsystem::RemoveGameFeatureFromAssetManager(StateProperties.GameFeatureData, StateProperties.PluginName, StateProperties.AddedPrimaryAssetTypes);
			StateProperties.AddedPrimaryAssetTypes.Empty();

			UGameFeaturesSubsystem::UnloadGameFeatureData(StateProperties.GameFeatureData);
		}
		StateProperties.GameFeatureData = nullptr;

		// Try to remove the gameplay tags, this might be ignored depending on project settings
		const FString PluginFolder = FPaths::GetPath(StateProperties.PluginInstalledFilename);
		UGameplayTagsManager::Get().RemoveTagIniSearchPath(PluginFolder / TEXT("Config") / TEXT("Tags"));

#if WITH_EDITOR
		// This will properly unload any plugin asset that could be opened in the editor
		// and ensure standalone packages get unloaded as well
		if (FApp::IsGame())
		{
			verify(FPluginUtils::UnloadPluginAssets(StateProperties.PluginName));

			bHasUnloaded = true;
			UpdateStateMachineDeferred();
		}
		else
		{
			bRequestedUnloadPluginAssets = true;
			UE::GameFeatures::ScheduleUnloadPluginAssets(StateProperties.PluginName, StateProperties.OnRequestUpdateStateMachine);
		}
#else
		bHasUnloaded = true;
		UpdateStateMachineDeferred();
#endif
	}
};

struct FGameFeaturePluginState_Registering : public FGameFeaturePluginState
{
	enum class ELoadGFDState : uint8
	{
		Pending = 0,
		Success,
		Cancelled,
		Failed
	};

	TSharedPtr<FStreamableHandle> GameFeatureDataHandle;
	ELoadGFDState LoadGFDState = ELoadGFDState::Pending;
	bool bCheckedRealtimeMode = false;
	TArray<FString, TInlineAllocator<2>> GameFeatureDataPaths;

	FGameFeaturePluginState_Registering(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}
	
	void TryAsyncLoadGameFeatureData(int32 Attempt = 0)
	{
		check(LoadGFDState == ELoadGFDState::Pending);

		if (!GameFeatureDataPaths.IsValidIndex(Attempt))
		{
			LoadGFDState = ELoadGFDState::Failed;
			UpdateStateMachineDeferred();
			return;
		}

		bool bIsLoading = false;

		GameFeatureDataHandle = UGameFeaturesSubsystem::LoadGameFeatureData(GameFeatureDataPaths[Attempt], true /*bStartStalled*/);
		if (GameFeatureDataHandle && GameFeatureDataHandle->IsLoadingInProgress())
		{
			// CurrentMachine owns `this`, so use it as a lifetime check for the `this` captured in the lambdas below, as they may be called after `this` is destroyed
			UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier);

			GameFeatureDataHandle->BindCancelDelegate(FStreamableDelegateWithHandle::CreateWeakLambda(CurrentMachine, [this](const TSharedPtr<FStreamableHandle>& Handle)
			{
				if (GameFeatureDataHandle != Handle)
				{
					// We're no longer in a state where we want to process this callback (maybe we already went through EndState)
					return;
				}

				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data loading was cancelled for URL %.*s"), ShortUrl.Len(), ShortUrl.GetData());

				LoadGFDState = ELoadGFDState::Cancelled;
				UpdateStateMachineDeferred();
			}));

			GameFeatureDataHandle->BindCompleteDelegate(FStreamableDelegateWithHandle::CreateWeakLambda(CurrentMachine, [this, Attempt](const TSharedPtr<FStreamableHandle>& Handle)
			{
				if (GameFeatureDataHandle != Handle)
				{
					// We're no longer in a state where we want to process this callback (maybe we already went through EndState)
					return;
				}

				StateProperties.GameFeatureData = Cast<UGameFeatureData>(GameFeatureDataHandle->GetLoadedAsset());
				if (!StateProperties.GameFeatureData && GameFeatureDataPaths.IsValidIndex(Attempt + 1))
				{
					TryAsyncLoadGameFeatureData(Attempt + 1);
					return;
				}

				if (StateProperties.GameFeatureData)
				{
					LoadGFDState = ELoadGFDState::Success;
				}
				else
				{
					LoadGFDState = ELoadGFDState::Failed;

					const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
					if (const TOptional<UE::UnifiedError::FError>& Error = GameFeatureDataHandle->GetError())
					{
						UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data loading failed with error [%s] for URL %.*s"), *LexToString(*Error), ShortUrl.Len(), ShortUrl.GetData());
					}
					else
					{
						UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data loading failed without an error for URL %.*s"), ShortUrl.Len(), ShortUrl.GetData());
					}
				}
				
				UpdateStateMachineDeferred();
			}));

			bIsLoading = true;
			GameFeatureDataHandle->StartStalledHandle();
		}

		if (!bIsLoading)
		{
			TryAsyncLoadGameFeatureData(Attempt + 1);
		}
	}

	virtual bool UseAsyncLoading() const override
	{
		if (UE::GameFeatures::CVarForceSyncRegisterStartupPlugins.GetValueOnGameThread())
		{
			if (UGameFeaturesSubsystem::Get().GetPolicy().IsLoadingStartupPlugins())
			{
				return false;
			}
		}
		return FGameFeaturePluginState::UseAsyncLoading();
	}

	virtual void BeginState() override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Registering_Begin);

		bCheckedRealtimeMode = false;

		const FString PluginFolder = FPaths::GetPath(StateProperties.PluginInstalledFilename);

		if (AllowIniLoading())
		{
			UGameplayTagsManager::Get().AddTagIniSearchPath(PluginFolder / TEXT("Config") / TEXT("Tags"), GConfig->GetStagedPluginConfigCache(FName(*StateProperties.PluginName)));
		}

		LoadGFDState = ELoadGFDState::Pending;

		TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(StateProperties.PluginName);
		ensure(Plugin.IsValid());

		// If the plugin contains content then load the GameFeatureData otherwise procedurally create one that is transient.
		if (!Plugin->GetDescriptor().bCanContainContent)
		{
			StateProperties.GameFeatureData = NewObject<UGameFeatureData>(GetTransientPackage(), FName(*StateProperties.PluginName), RF_Transient);
			LoadGFDState = ELoadGFDState::Success;
			return;
		}

		FString BackupGameFeatureDataPath = FString::Printf(TEXT("/%s/%s.%s"), *StateProperties.PluginName, *StateProperties.PluginName, *StateProperties.PluginName);

		FString PreferredGameFeatureDataPath = TEXT("/") + StateProperties.PluginName + TEXT("/GameFeatureData.GameFeatureData");

		if (AllowIniLoading())
		{
			// Allow game feature location to be overriden globally and from within the plugin
			FString OverrideIniPathName = StateProperties.PluginName + TEXT("_Override");
			FString OverridePath = GConfig->GetStr(TEXT("GameFeatureData"), *OverrideIniPathName, GGameIni);
			if (OverridePath.IsEmpty())
			{
				const FString SettingsOverride = PluginFolder / TEXT("Config") / TEXT("Settings.ini");
				if (FPaths::FileExists(SettingsOverride))
				{
					GConfig->LoadFile(SettingsOverride);
					OverridePath = GConfig->GetStr(TEXT("GameFeatureData"), TEXT("Override"), SettingsOverride);
					GConfig->UnloadFile(SettingsOverride);
				}
			}
			if (!OverridePath.IsEmpty())
			{
				PreferredGameFeatureDataPath = MoveTemp(OverridePath);
			}
		}

		// Temporary workaround for UE-309330
		// FPackageName::DoesPackageExist is unreliable when running with cook-on-the-fly, instead we must make multiple attempts
		// to load the cooked packages and handle failure/repeats
		if (IsRunningCookOnTheFly())
		{
			GameFeatureDataPaths.Emplace(MoveTemp(PreferredGameFeatureDataPath));
			GameFeatureDataPaths.Emplace(MoveTemp(BackupGameFeatureDataPath));
		}
		else
		{
			if (FPackageName::DoesPackageExist(PreferredGameFeatureDataPath))
			{
				GameFeatureDataPaths.Emplace(MoveTemp(PreferredGameFeatureDataPath));
			}
			else if (FPackageName::DoesPackageExist(BackupGameFeatureDataPath))
			{
				GameFeatureDataPaths.Emplace(MoveTemp(BackupGameFeatureDataPath));
			}
			else
			{
				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data package not found for URL %.*s"), ShortUrl.Len(), ShortUrl.GetData());

				LoadGFDState = ELoadGFDState::Failed;
				return;
			}
		}

		if (UseAsyncLoading())
		{
			TryAsyncLoadGameFeatureData();
		}
		else
		{
			FScopedSlowTask LoadingGameFeatureData(1.0f, 
				FText::Format(
					LOCTEXT("LoadingGameFeatureData", "Loading Game Feature Data for Plugin: {0}"), 
					FText::FromString(StateProperties.PluginName)));
			LoadingGameFeatureData.Visibility = ESlowTaskVisibility::Important;
			
			for (const FString& Path : GameFeatureDataPaths)
			{
				GameFeatureDataHandle = UGameFeaturesSubsystem::LoadGameFeatureData(Path);
				if (GameFeatureDataHandle)
				{
					GameFeatureDataHandle->WaitUntilComplete(0.0f, false);
					StateProperties.GameFeatureData = Cast<UGameFeatureData>(GameFeatureDataHandle->GetLoadedAsset());
				}
				if (StateProperties.GameFeatureData)
				{
					break;
				}
			}

			if (StateProperties.GameFeatureData)
			{
				LoadGFDState = ELoadGFDState::Success;
			}
			else
			{
				LoadGFDState = ELoadGFDState::Failed;

				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				if (const TOptional<UE::UnifiedError::FError>& Error = GameFeatureDataHandle->GetError())
				{
					UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data loading failed with error [%s] for URL %.*s"), *LexToString(*Error), ShortUrl.Len(), ShortUrl.GetData());
				}
				else
				{
					UE_LOG(LogGameFeatures, Error, TEXT("Game Feature Data loading failed without an error for URL %.*s"), ShortUrl.Len(), ShortUrl.GetData());
				}
			}
		}
	}

	virtual void EndState() override
	{
		GameFeatureDataHandle = nullptr;
		GameFeatureDataPaths.Empty();
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Registering_Update);

		if (!bCheckedRealtimeMode)
		{
			bCheckedRealtimeMode = true;
			if (UE::GameFeatures::RealtimeMode)
			{
				UE::GameFeatures::RealtimeMode->AddUpdateRequest(StateProperties.OnRequestUpdateStateMachine);
				return;
			}
		}

		if (!StateProperties.GameFeatureData)
		{
			check(LoadGFDState != ELoadGFDState::Success);

			if (LoadGFDState == ELoadGFDState::Pending)
			{
				return;
			}

			if (LoadGFDState == ELoadGFDState::Cancelled)
			{
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorRegistering, GetErrorResult(TEXT("Load_Cancelled_GameFeatureData")));
				return;
			}
		}

		if (StateProperties.GameFeatureData)
		{
			check(LoadGFDState == ELoadGFDState::Success);

			StateStatus.SetTransition(EGameFeaturePluginState::Registered);

			check(StateProperties.AddedPrimaryAssetTypes.Num() == 0);
			UGameFeaturesSubsystem::Get().AddGameFeatureToAssetManager(StateProperties.GameFeatureData, StateProperties.PluginName, StateProperties.AddedPrimaryAssetTypes);

			UGameFeaturesSubsystem::Get().OnGameFeatureRegistering(StateProperties.GameFeatureData, StateProperties.PluginName, StateProperties.PluginIdentifier);
		}
		else
		{
			check(LoadGFDState == ELoadGFDState::Failed);

			// The gamefeaturedata does not exist. The pak file may not be openable or this is a builtin plugin where the pak file does not exist.
			StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorRegistering, GetErrorResult(TEXT("Plugin_Missing_GameFeatureData")));
			if (UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier))
			{
				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				UE_LOG(LogGameFeatures, Error, TEXT("Setting %.*s to be in unrecoverable error as GameFeatureData is missing"), ShortUrl.Len(), ShortUrl.GetData());
				CurrentMachine->SetUnrecoverableError();
			}
		}
	}
};

struct FGameFeaturePluginState_Registered : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Registered(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination > EGameFeaturePluginState::Registered)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Loading);
		}
		else if (StateProperties.Destination < EGameFeaturePluginState::Registered)
		{
			StateStatus.SetTransition( EGameFeaturePluginState::Unregistering);
		}
	}
};

struct FGameFeaturePluginState_ErrorLoading : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorLoading(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorLoading)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Unloading);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorLoading)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Loading);
		}
	}
};

struct FGameFeaturePluginState_Unloading : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Unloading(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	virtual void BeginState() override
	{
		if (UE::GameFeatures::ShouldDeferLocalizationDataLoad())
		{
			IPluginManager::Get().UnmountExplicitlyLoadedPluginLocalizationData(StateProperties.PluginName);
		}
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		UnloadGameFeatureBundles(StateProperties.GameFeatureData);
		UGameFeaturesSubsystem::Get().OnGameFeatureUnloading(StateProperties.GameFeatureData, StateProperties.PluginIdentifier);

		StateStatus.SetTransition(EGameFeaturePluginState::Registered);
	}


	void UnloadGameFeatureBundles(const UGameFeatureData* GameFeatureToLoad)
	{
		if (GameFeatureToLoad == nullptr)
		{
			return;
		}

		const UGameFeaturesProjectPolicies& Policy = UGameFeaturesSubsystem::Get().GetPolicy();

		// Remove all bundles from feature data and completely unload everything else
		FPrimaryAssetId GameFeatureAssetId = GameFeatureToLoad->GetPrimaryAssetId();
		TSharedPtr<FStreamableHandle> Handle = UAssetManager::Get().ChangeBundleStateForPrimaryAssets({ GameFeatureAssetId }, {}, {}, /*bRemoveAllBundles=*/ true);
		ensureAlways(Handle == nullptr || Handle->HasLoadCompleted()); // Should be no handle since nothing is being loaded

		TArray<FPrimaryAssetId> AssetIds = Policy.GetPreloadAssetListForGameFeature(GameFeatureToLoad, /*bIncludeLoadedAssets=*/true);

		// Don't unload game feature data asset yet, that will happen in FGameFeaturePluginState_Unregistering
		ensureAlways(AssetIds.RemoveSwap(GameFeatureAssetId, EAllowShrinking::No) == 0);

		if (AssetIds.Num() > 0)
		{
			UAssetManager::Get().UnloadPrimaryAssets(AssetIds);
		}
	}
};

struct FGameFeaturePluginState_Loading : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Loading(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	TSharedPtr<FStreamableHandle> BundleHandle;

	virtual void BeginState() override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Loading_Begin);
		check(StateProperties.GameFeatureData);

		if (UE::GameFeatures::ShouldDeferLocalizationDataLoad())
		{
			UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier);
			UE::GameFeatures::MountLocalizationData(CurrentMachine, StateProperties);
		}

		LoadGameFeatureBundles(StateProperties.GameFeatureData);
	}

	virtual void EndState() override
	{
		BundleHandle = nullptr;
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Loading_Update);
		check(StateProperties.GameFeatureData);

		if (BundleHandle)
		{
			if (!UseAsyncLoading())
			{
				BundleHandle->WaitUntilComplete(0.0f, false);
			}

			if (BundleHandle->IsLoadingInProgress())
			{
				return;
			}

			if (BundleHandle->WasCanceled())
			{
				BundleHandle.Reset();
				StateStatus.SetTransitionError(EGameFeaturePluginState::ErrorLoading, GetErrorResult(TEXT("Load_Cancelled_Preload")));
				return;
			}
		}

		UGameFeaturesSubsystem::Get().OnGameFeatureLoading(StateProperties.GameFeatureData, StateProperties.PluginIdentifier);

		StateStatus.SetTransition(EGameFeaturePluginState::Loaded);
	}

	/** Loads primary assets and bundles for the specified game feature */
	void LoadGameFeatureBundles(const UGameFeatureData* GameFeatureToLoad)
	{
		check(GameFeatureToLoad);

		const UGameFeaturesProjectPolicies& Policy = UGameFeaturesSubsystem::Get().GetPolicy<UGameFeaturesProjectPolicies>();

		TArray<FPrimaryAssetId> AssetIdsToLoad = Policy.GetPreloadAssetListForGameFeature(GameFeatureToLoad);

		FPrimaryAssetId GameFeatureAssetId = GameFeatureToLoad->GetPrimaryAssetId();
		if (GameFeatureAssetId.IsValid())
		{
			AssetIdsToLoad.Add(GameFeatureAssetId);
		}

		if (AssetIdsToLoad.Num() > 0)
		{
			// CurrentMachine owns `this`, so use it as a lifetime check for the `this` captured in the lambdas below, as they may be called after `this` is destroyed
			UGameFeaturePluginStateMachine* CurrentMachine = UGameFeaturesSubsystem::Get().FindGameFeaturePluginStateMachine(StateProperties.PluginIdentifier);

			FAssetManagerLoadParams LoadParams;
			LoadParams.OnCancel = FStreamableDelegateWithHandle::CreateWeakLambda(CurrentMachine, [this](const TSharedPtr<FStreamableHandle>& Handle)
			{
				if (BundleHandle != Handle)
				{
					// We're no longer in a state where we want to process this callback (maybe we already went through EndState)
					return;
				}

				const FStringView ShortUrl = StateProperties.PluginIdentifier.GetIdentifyingString();
				UE_LOG(LogGameFeatures, Error, TEXT("Game Feature preloading was cancelled for URL %.*s"), ShortUrl.Len(), ShortUrl.GetData());
				UpdateStateMachineDeferred();
			});

			// This can't be bound to the handle after its created, AM may bind it internally
			LoadParams.OnComplete = FStreamableDelegateWithHandle::CreateWeakLambda(CurrentMachine, [this](const TSharedPtr<FStreamableHandle>& Handle)
			{
				if (BundleHandle != Handle)
				{
					// We're no longer in a state where we want to process this callback (maybe we already went through EndState)
					return;
				}

				UpdateStateMachineDeferred();
			});

			BundleHandle = UAssetManager::Get().LoadPrimaryAssets(AssetIdsToLoad, Policy.GetPreloadBundleStateForGameFeature(), MoveTemp(LoadParams));
		}
		else
		{
			BundleHandle.Reset();
		}
	}
};

struct FGameFeaturePluginState_Loaded : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Loaded(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination > EGameFeaturePluginState::Loaded)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::ActivatingDependencies);
		}
		else if (StateProperties.Destination < EGameFeaturePluginState::Loaded)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::Unloading);
		}
	}
};

struct FGameFeaturePluginState_ErrorDeactivatingDependencies : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorDeactivatingDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorDeactivatingDependencies)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::DeactivatingDependencies);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorDeactivatingDependencies)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::DeactivatingDependencies);
		}
	}
};

struct FDeactivatingDependenciesTransitionPolicy
{
	static bool GetPluginDependencyStateMachines(const FGameFeaturePluginStateMachineProperties& InStateProperties, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines)
	{
		UGameFeaturesSubsystem& GameFeaturesSubsystem = UGameFeaturesSubsystem::Get();

		return GameFeaturesSubsystem.FindPluginDependencyStateMachinesToDeactivate(
			InStateProperties.PluginIdentifier.GetFullPluginURL(), InStateProperties.PluginInstalledFilename, OutDependencyMachines);
	}

	static FGameFeaturePluginStateRange GetDependencyStateRange()
	{
		return FGameFeaturePluginStateRange(EGameFeaturePluginState::Terminal, EGameFeaturePluginState::Loaded);
	}

	static EGameFeaturePluginState GetTransitionState()
	{
		return EGameFeaturePluginState::Deactivating;
	}

	static EGameFeaturePluginState GetErrorState()
	{
		return EGameFeaturePluginState::ErrorDeactivatingDependencies;
	}

	static bool ExcludeDepedenciesFromBatchProcessing()
	{
		return false;
	}

	static bool ShouldWaitForDependencies()
	{
		return UE::GameFeatures::CVarWaitForDependencyDeactivation.GetValueOnGameThread();
	}
};

struct FGameFeaturePluginState_DeactivatingDependencies : public FTransitionDependenciesGameFeaturePluginState<FDeactivatingDependenciesTransitionPolicy>
{
	FGameFeaturePluginState_DeactivatingDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FTransitionDependenciesGameFeaturePluginState(InStateProperties)
	{
	}
};

struct FGameFeaturePluginState_Deactivating : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Deactivating(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	int32 NumObservedPausers = 0;
	int32 NumExpectedPausers = 0;
	bool bInProcessOfDeactivating = false;
	bool bHasUnloaded = false;

	virtual void BeginState() override
	{
		NumObservedPausers = 0;
		NumExpectedPausers = 0;
		bInProcessOfDeactivating = false;
		bHasUnloaded = false;

		static bool bUseNewDynamicLayers = IConsoleManager::Get().FindConsoleVariable(TEXT("ini.UseNewDynamicLayers"))->GetInt() != 0;
		if (bUseNewDynamicLayers)
		{
			FName Tag = *StateProperties.PluginName;
			UE::DynamicConfig::PerformDynamicConfig(Tag, [Tag](FConfigModificationTracker* ChangeTracker)
			{
				FConfigCacheIni::RemoveTagFromAllBranches(Tag, ChangeTracker);
				IConsoleManager::Get().UnsetAllConsoleVariablesWithTag(Tag);
			});
		}
	}

	void OnPauserCompleted(FStringView InPauserTag)
	{
		check(IsInGameThread());
		ensure(NumExpectedPausers != INDEX_NONE);
		++NumObservedPausers;

		UE_LOG(LogGameFeatures, Display, TEXT("Deactivation of %s resumed by %.*s"), *StateProperties.PluginName, InPauserTag.Len(), InPauserTag.GetData());

		if (NumObservedPausers == NumExpectedPausers)
		{
			UpdateStateMachineImmediate();
		}
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (bHasUnloaded)
		{
			check(NumExpectedPausers == NumObservedPausers);
			StateStatus.SetTransition(EGameFeaturePluginState::Loaded);
			return;
		}

		if (!bInProcessOfDeactivating)
		{
			// Make sure we won't complete the transition prematurely if someone registers as a pauser but fires immediately
			bInProcessOfDeactivating = true;
			NumExpectedPausers = INDEX_NONE;
			NumObservedPausers = 0;

			// Deactivate
			FGameFeatureDeactivatingContext Context(StateProperties.PluginName, [this](FStringView InPauserTag) { OnPauserCompleted(InPauserTag); });
			UGameFeaturesSubsystem::Get().OnGameFeatureDeactivating(StateProperties.GameFeatureData, StateProperties.PluginName, Context, StateProperties.PluginIdentifier);
			NumExpectedPausers = Context.NumPausers;

			// Since we are pausing work during this deactivation, also notify the OnGameFeaturePauseChange delegate
			if (NumExpectedPausers > 0)
			{
				FGameFeaturePauseStateChangeContext PauseContext(UE::GameFeatures::ToString(EGameFeaturePluginState::Deactivating), TEXT("PendingDeactivationCallbacks"), true);
				UGameFeaturesSubsystem::Get().OnGameFeaturePauseChange(StateProperties.PluginIdentifier, StateProperties.PluginName, PauseContext);
			}
		}

		if (NumExpectedPausers == NumObservedPausers)
		{
			//If we previously sent an OnGameFeaturePauseChange delegate we need to send that work is now unpaused
			if (NumExpectedPausers > 0)
			{
				FGameFeaturePauseStateChangeContext PauseContext(UE::GameFeatures::ToString(EGameFeaturePluginState::Deactivating), TEXT(""), false);
				UGameFeaturesSubsystem::Get().OnGameFeaturePauseChange(StateProperties.PluginIdentifier, StateProperties.PluginName, PauseContext);
			}

			if (!bHasUnloaded && StateProperties.Destination.MaxState == EGameFeaturePluginState::Loaded)
			{
				// If we aren't going farther than Loaded, GC now
				// otherwise we will defer until closer to our destination state
				bHasUnloaded = true;
				UpdateStateMachineDeferred();
			}
			else
			{
				StateStatus.SetTransition(EGameFeaturePluginState::Loaded);
			}
		}
		else
		{
			UE_LOG(LogGameFeatures, Log, TEXT("Game feature %s deactivation paused until %d observer tasks complete their deactivation"), *GetPathNameSafe(StateProperties.GameFeatureData), NumExpectedPausers - NumObservedPausers);
		}
	}
};

struct FGameFeaturePluginState_ErrorActivatingDependencies : public FErrorGameFeaturePluginState
{
	FGameFeaturePluginState_ErrorActivatingDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties) : FErrorGameFeaturePluginState(InStateProperties) {}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::ErrorActivatingDependencies)
		{
			// There is no cleaup state equivalent to EGameFeaturePluginState::ErrorActivatingDependencies so just go back to Unloading
			StateStatus.SetTransition(EGameFeaturePluginState::Unloading);
		}
		else if (StateProperties.Destination > EGameFeaturePluginState::ErrorActivatingDependencies)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::ActivatingDependencies);
		}
	}
};

struct FActivatingDependenciesTransitionPolicy
{
	static bool GetPluginDependencyStateMachines(const FGameFeaturePluginStateMachineProperties& InStateProperties, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines)
	{
		UGameFeaturesSubsystem& GameFeaturesSubsystem = UGameFeaturesSubsystem::Get();

		return GameFeaturesSubsystem.FindPluginDependencyStateMachinesToActivate(
			InStateProperties.PluginIdentifier.GetFullPluginURL(), InStateProperties.PluginInstalledFilename, OutDependencyMachines);
	}

	static FGameFeaturePluginStateRange GetDependencyStateRange()
	{
		return FGameFeaturePluginStateRange(EGameFeaturePluginState::Active, EGameFeaturePluginState::Active);
	}

	static EGameFeaturePluginState GetTransitionState()
	{
		return EGameFeaturePluginState::Activating;
	}

	static EGameFeaturePluginState GetErrorState()
	{
		return EGameFeaturePluginState::ErrorActivatingDependencies;
	}

	static bool ExcludeDepedenciesFromBatchProcessing()
	{
		return true;
	}

	static bool ShouldWaitForDependencies()
	{
		return true;
	}
};

struct FGameFeaturePluginState_ActivatingDependencies : public FTransitionDependenciesGameFeaturePluginState<FActivatingDependenciesTransitionPolicy>
{
	FGameFeaturePluginState_ActivatingDependencies(FGameFeaturePluginStateMachineProperties& InStateProperties)
		: FTransitionDependenciesGameFeaturePluginState(InStateProperties)
	{
	}
};

struct FGameFeaturePluginState_Activating : public FGameFeaturePluginState
{
	FGameFeaturePluginState_Activating(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	bool CanBatchProcess() const override
	{
		return FGameFeaturePluginState::CanBatchProcess() && AllowIniLoading();
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Activating);
		check(GEngine);
		check(StateProperties.GameFeatureData);

		// If this plugin caused localization data to load, we need that load to finish before marking it as active
		if (StateProperties.bIsLoadingLocalizationData)
		{
			if (AllowAsyncLoading())
			{
				return;
			}

			FTextLocalizationManager::Get().WaitForAsyncTasks();
			StateProperties.bIsLoadingLocalizationData = false;
		}

		if (IsWaitingForBatchProcessing())
		{
			return;
		}

		if(!WasBatchProcessed())
		{
			if (AllowIniLoading())
			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Activating_InitIni);
				StateProperties.GameFeatureData->InitializeHierarchicalPluginIniFiles(StateProperties.PluginInstalledFilename);
			}
		}

		{
			TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Activating_SendEvents);
			FGameFeatureActivatingContext Context;
			UGameFeaturesSubsystem::Get().OnGameFeatureActivating(StateProperties.GameFeatureData, StateProperties.PluginName, Context, StateProperties.PluginIdentifier);
		}

		StateStatus.SetTransition(EGameFeaturePluginState::Active);
	}

	static void BatchProcess(TConstArrayView<UGameFeaturePluginStateMachine*> GFPs)
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_BatchProcess_OnFenceCompleteActivating);
		UGameFeaturesSubsystem& GFPSubSys = UGameFeaturesSubsystem::Get();

		TArray<FString> PluginInstalledFilenames;
		PluginInstalledFilenames.Reserve(GFPs.Num());

		for (const UGameFeaturePluginStateMachine* GFPSM : GFPs)
		{
			PluginInstalledFilenames.Emplace(GFPSM->GetProperties().PluginInstalledFilename);
		}

		{
			TRACE_CPUPROFILER_EVENT_SCOPE(GFP_BatchActivating_InitIni);
			UGameFeatureData::InitializeHierarchicalPluginIniFiles(PluginInstalledFilenames);
		}
	}
};

struct FGameFeaturePluginState_Active : public FDestinationGameFeaturePluginState
{
	FGameFeaturePluginState_Active(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	virtual void BeginState() override
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Active);
		check(GEngine);

		{
			TRACE_CPUPROFILER_EVENT_SCOPE(GFP_Active_SendEvents);
			UGameFeaturesSubsystem::Get().OnGameFeatureActivated(StateProperties.GameFeatureData, StateProperties.PluginName, StateProperties.PluginIdentifier);
		}
	}

	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) override
	{
		if (StateProperties.Destination < EGameFeaturePluginState::Active)
		{
			StateStatus.SetTransition(EGameFeaturePluginState::DeactivatingDependencies);
		}
	}
};

/*
=========================================================
  State Machine
=========================================================
*/
namespace UE::GameFeatures
{
	/** Helper classes that look for existence of batch processing functions on states so we can identify these compile time */
	template<class T, class = void>
	struct TBatchProcessHelperWrapper
	{
		static bool ImplementsBatchProcess()
		{
			return false;
		}
		static void BatchProcess(TConstArrayView<UGameFeaturePluginStateMachine*>)
		{
			check(!"Not implemented");
		}
	};

	template<class T>
	struct TBatchProcessHelperWrapper<T, std::enable_if_t<std::is_invocable_r<void, decltype(T::BatchProcess), TConstArrayView<UGameFeaturePluginStateMachine*>>::value>>
	{
		static bool ImplementsBatchProcess()
		{
			return true;
		}

		static void BatchProcess(TConstArrayView<UGameFeaturePluginStateMachine*> GFPSMs)
		{
			T::BatchProcess(GFPSMs);
		}
	};

	struct FBatchProcessHelperFunctors
	{
		using FImplementsBatchProcessPtr = bool(*)();
		using FBatchProcessPtr = void (*)(TConstArrayView<UGameFeaturePluginStateMachine*>);

		FBatchProcessHelperFunctors() = default;

		FBatchProcessHelperFunctors(FImplementsBatchProcessPtr InImplementsBatchProcessPtr, FBatchProcessPtr InBatchProcessPtr)
			: ImplementsBatchProcess(InImplementsBatchProcessPtr)
			, BatchProcess(InBatchProcessPtr)
		{

		}
		
		FImplementsBatchProcessPtr ImplementsBatchProcess = nullptr;
		FBatchProcessPtr BatchProcess = nullptr;
	};

	#define GAME_FEATURE_PLUGIN_STATE_MAKE_BATCH_PROCESS_FN(inEnum, inText) FBatchProcessHelperFunctors(&TBatchProcessHelperWrapper<FGameFeaturePluginState_##inEnum>::ImplementsBatchProcess, &TBatchProcessHelperWrapper<FGameFeaturePluginState_##inEnum>::BatchProcess), 
	static TStaticArray<FBatchProcessHelperFunctors, EGameFeaturePluginState::MAX> BatchProcessingHelperFunctors = {
		GAME_FEATURE_PLUGIN_STATE_LIST(GAME_FEATURE_PLUGIN_STATE_MAKE_BATCH_PROCESS_FN)
	};
	#undef GAME_FEATURE_PLUGIN_STATE_MAKE_BATCH_PROCESS_FN
}

UGameFeaturePluginStateMachine::UGameFeaturePluginStateMachine(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
	, CurrentStateInfo(EGameFeaturePluginState::Uninitialized)
	, bInUpdateStateMachine(false)
	, bRegisteredAsTransitioningGFPSM(false)
{

}

void UGameFeaturePluginStateMachine::InitStateMachine(FGameFeaturePluginIdentifier InPluginIdentifier, const FGameFeatureProtocolOptions& InProtocolOptions)
{
	LLM_SCOPE_BYTAG(GFP);

	check(GetCurrentState() == EGameFeaturePluginState::Uninitialized);
	CurrentStateInfo.State = EGameFeaturePluginState::UnknownStatus;
	StateProperties = FGameFeaturePluginStateMachineProperties(
		MoveTemp(InPluginIdentifier),
		FGameFeaturePluginStateRange(CurrentStateInfo.State),
		FGameFeaturePluginRequestUpdateStateMachine::CreateUObject(this, &ThisClass::UpdateStateMachine),
		FGameFeatureStateProgressUpdate::CreateUObject(this, &ThisClass::UpdateCurrentStateProgress));

	StateProperties.ProtocolOptions = InProtocolOptions;

#define GAME_FEATURE_PLUGIN_STATE_MAKE_STATE(inEnum, inText) AllStates[EGameFeaturePluginState::inEnum] = MakeUnique<FGameFeaturePluginState_##inEnum>(StateProperties);
	GAME_FEATURE_PLUGIN_STATE_LIST(GAME_FEATURE_PLUGIN_STATE_MAKE_STATE)
#undef GAME_FEATURE_PLUGIN_STATE_MAKE_STATE

	CheckAddBatchingRequestForCurrentState();
	AllStates[CurrentStateInfo.State]->BeginState();
}

bool UGameFeaturePluginStateMachine::SetDestination(FGameFeaturePluginStateRange InDestination, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete, FDelegateHandle* OutCallbackHandle /*= nullptr*/)
{
	LLM_SCOPE_BYTAG(GFP);

	check(IsValidDestinationState(InDestination.MinState));
	check(IsValidDestinationState(InDestination.MaxState));

	bool bDestinationSet = false;
	bool bDestinationChanged = false;

	if (!InDestination.IsValid())
	{
		// Invalid range
	}
	else if (CurrentStateInfo.State == EGameFeaturePluginState::Terminal && !InDestination.Contains(EGameFeaturePluginState::Terminal))
	{
		// Can't tranistion away from terminal state
	}
	else if (!IsRunning())
	{
		// Not running so any new range is acceptable

		if (OutCallbackHandle)
		{
			OutCallbackHandle->Reset();
		}

		FDestinationGameFeaturePluginState* CurrState = AllStates[CurrentStateInfo.State]->AsDestinationState();

		if (InDestination.Contains(CurrentStateInfo.State))
		{
			OnFeatureStateTransitionComplete.ExecuteIfBound(this, MakeValue());
		}
		else
		{
			if (CurrentStateInfo.State < InDestination)
			{
				FDestinationGameFeaturePluginState* MinDestState = AllStates[InDestination.MinState]->AsDestinationState();
				FDelegateHandle CallbackHandle = MinDestState->OnDestinationStateReached.Add(MoveTemp(OnFeatureStateTransitionComplete));
				if (OutCallbackHandle)
				{
					*OutCallbackHandle = CallbackHandle;
				}
			}
			else if (CurrentStateInfo.State > InDestination)
			{
				FDestinationGameFeaturePluginState* MaxDestState = AllStates[InDestination.MaxState]->AsDestinationState();
				FDelegateHandle CallbackHandle = MaxDestState->OnDestinationStateReached.Add(MoveTemp(OnFeatureStateTransitionComplete));
				if (OutCallbackHandle)
				{
					*OutCallbackHandle = CallbackHandle;
				}
			}

			StateProperties.Destination = InDestination;
			UpdateStateMachine();
			bDestinationChanged = true;
		}

		bDestinationSet = true;
	}
	else if (TOptional<FGameFeaturePluginStateRange> NewDestination = StateProperties.Destination.Intersect(InDestination))
	{
		// The machine is already running so we can only transition to this range if it overlaps with our current range.
		// We can satisfy both ranges in this case.

		if (OutCallbackHandle)
		{
			OutCallbackHandle->Reset();
		}

		if (CurrentStateInfo.State < StateProperties.Destination)
		{
			StateProperties.Destination = *NewDestination;

			if (InDestination.Contains(CurrentStateInfo.State))
			{
				OnFeatureStateTransitionComplete.ExecuteIfBound(this, MakeValue());
			}
			else
			{
				FDestinationGameFeaturePluginState* MinDestState = AllStates[InDestination.MinState]->AsDestinationState();
				FDelegateHandle CallbackHandle = MinDestState->OnDestinationStateReached.Add(MoveTemp(OnFeatureStateTransitionComplete));
				if (OutCallbackHandle)
				{
					*OutCallbackHandle = CallbackHandle;
				}
				bDestinationChanged = true;
			}
		}
		else if(CurrentStateInfo.State > StateProperties.Destination)
		{
			StateProperties.Destination = *NewDestination;

			if (InDestination.Contains(CurrentStateInfo.State))
			{
				OnFeatureStateTransitionComplete.ExecuteIfBound(this, MakeValue());
			}
			else
			{
				FDestinationGameFeaturePluginState* MaxDestState = AllStates[InDestination.MaxState]->AsDestinationState();
				FDelegateHandle CallbackHandle = MaxDestState->OnDestinationStateReached.Add(MoveTemp(OnFeatureStateTransitionComplete));
				if (OutCallbackHandle)
				{
					*OutCallbackHandle = CallbackHandle;
				}
				bDestinationChanged = true;
			}
		}
		else
		{
			checkf(false, TEXT("IsRunning() returned true but state machine has reached destination!"));
		}

		bDestinationSet = true;
	}
	else
	{
		// The requested state range is completely outside the the current state range so reject the request
	}

#if !UE_BUILD_SHIPPING
	if (bDestinationChanged && UGameFeaturesSubsystem::Get().GetPluginDebugStateEnabled(GetPluginURL()))
	{
		PLATFORM_BREAK();
	}
#endif

	return bDestinationSet;
}

bool UGameFeaturePluginStateMachine::TryCancel(FGameFeatureStateTransitionCanceled OnFeatureStateTransitionCanceled, FDelegateHandle* OutCallbackHandle /*= nullptr*/)
{
	if (!IsRunning())
	{
		return false;
	}

	LLM_SCOPE_BYTAG(GFP);

	StateProperties.bTryCancel = true;
	FDelegateHandle CallbackHandle = StateProperties.OnTransitionCanceled.Add(MoveTemp(OnFeatureStateTransitionCanceled));
	if(OutCallbackHandle)
	{
		*OutCallbackHandle = CallbackHandle;
	}

	const EGameFeaturePluginState CurrentState = GetCurrentState();
	AllStates[CurrentState]->TryCancelState();

	return true;
}

UE::GameFeatures::FResult UGameFeaturePluginStateMachine::TryUpdatePluginProtocolOptions(const FGameFeatureProtocolOptions& InOptions, bool& bOutDidUpdate)
{
	bOutDidUpdate = false;

	if (StateProperties.ProtocolOptions == InOptions)
	{
		return MakeValue();
	}

	LLM_SCOPE_BYTAG(GFP);

	const EGameFeaturePluginState CurrentState = GetCurrentState();
	UE::GameFeatures::FResult Result = AllStates[CurrentState]->TryUpdateProtocolOptions(InOptions);
	bOutDidUpdate = Result.HasValue();

	return Result;
}

void UGameFeaturePluginStateMachine::RemovePendingTransitionCallback(FDelegateHandle InHandle)
{
	for (std::underlying_type<EGameFeaturePluginState>::type iState = 0;
		iState < EGameFeaturePluginState::MAX;
		++iState)
	{
		if (FDestinationGameFeaturePluginState* DestState = AllStates[iState]->AsDestinationState())
		{
			if (DestState->OnDestinationStateReached.Remove(InHandle))
			{
				break;
			}
		}
	}
}

void UGameFeaturePluginStateMachine::RemovePendingTransitionCallback(FDelegateUserObject DelegateObject)
{
	for (std::underlying_type<EGameFeaturePluginState>::type iState = 0;
		iState < EGameFeaturePluginState::MAX;
		++iState)
	{
		if (FDestinationGameFeaturePluginState* DestState = AllStates[iState]->AsDestinationState())
		{
			if (DestState->OnDestinationStateReached.RemoveAll(DelegateObject))
			{
				break;
			}
		}
	}
}

void UGameFeaturePluginStateMachine::RemovePendingCancelCallback(FDelegateHandle InHandle)
{
	StateProperties.OnTransitionCanceled.Remove(InHandle);
}

void UGameFeaturePluginStateMachine::RemovePendingCancelCallback(FDelegateUserObject DelegateObject)
{
	StateProperties.OnTransitionCanceled.RemoveAll(DelegateObject);
}

const FString& UGameFeaturePluginStateMachine::GetGameFeatureName() const
{
	FString PluginFilename;
	if (!StateProperties.PluginName.IsEmpty())
	{
		return StateProperties.PluginName;
	}
	else
	{
		return StateProperties.PluginIdentifier.GetFullPluginURL();
	}
}

const FGameFeaturePluginIdentifier& UGameFeaturePluginStateMachine::GetPluginIdentifier() const
{
	return StateProperties.PluginIdentifier;
}

const FString& UGameFeaturePluginStateMachine::GetPluginURL() const
{
	return StateProperties.PluginIdentifier.GetFullPluginURL();
}

const FGameFeatureProtocolMetadata& UGameFeaturePluginStateMachine::GetProtocolMetadata() const
{
	return StateProperties.ProtocolMetadata;
}

const FGameFeatureProtocolOptions& UGameFeaturePluginStateMachine::GetProtocolOptions() const
{
	return StateProperties.ProtocolOptions;
}

FGameFeatureProtocolOptions UGameFeaturePluginStateMachine::RecycleProtocolOptions() const
{
	return StateProperties.RecycleProtocolOptions();
}

const FString& UGameFeaturePluginStateMachine::GetPluginName() const
{
	return StateProperties.PluginName;
}

bool UGameFeaturePluginStateMachine::GetPluginFilename(FString& OutPluginFilename) const
{
	OutPluginFilename = StateProperties.PluginInstalledFilename;
	return !OutPluginFilename.IsEmpty();
}

EGameFeaturePluginState UGameFeaturePluginStateMachine::GetCurrentState() const
{
	return GetCurrentStateInfo().State;
}

FGameFeaturePluginStateRange UGameFeaturePluginStateMachine::GetDestination() const
{
	return StateProperties.Destination;
}

const FGameFeaturePluginStateInfo& UGameFeaturePluginStateMachine::GetCurrentStateInfo() const
{
	return CurrentStateInfo;
}

bool UGameFeaturePluginStateMachine::IsRunning() const
{
	return !StateProperties.Destination.Contains(CurrentStateInfo.State);
}

bool UGameFeaturePluginStateMachine::IsStatusKnown() const
{
	return GetCurrentState() == EGameFeaturePluginState::ErrorUnavailable || 
		   GetCurrentState() == EGameFeaturePluginState::Uninstalling || 
		   GetCurrentState() == EGameFeaturePluginState::ErrorUninstalling ||
		   GetCurrentState() >= EGameFeaturePluginState::StatusKnown;
}

bool UGameFeaturePluginStateMachine::IsAvailable() const
{
	ensure(IsStatusKnown());
	return GetCurrentState() >= EGameFeaturePluginState::StatusKnown;
}

bool UGameFeaturePluginStateMachine::IsInErrorState() const
{
	return IsValidErrorState(GetCurrentState());
}

bool UGameFeaturePluginStateMachine::AllowAsyncLoading() const
{
	return StateProperties.AllowAsyncLoading();
}

bool UGameFeaturePluginStateMachine::HasAssetStreamingDependencies() const
{
	ensure(IsStatusKnown());
	if (StateProperties.ProtocolMetadata.HasSubtype<FInstallBundlePluginProtocolMetaData>())
	{
		const FInstallBundlePluginProtocolMetaData& ProtocolData = StateProperties.ProtocolMetadata.GetSubtype<FInstallBundlePluginProtocolMetaData>();
		return !ProtocolData.InstallBundlesWithAssetDependencies.IsEmpty();
	}
	return false;
}

void UGameFeaturePluginStateMachine::SetWasLoadedAsBuiltIn()
{
	StateProperties.bWasLoadedAsBuiltInGameFeaturePlugin = true;
}

bool UGameFeaturePluginStateMachine::WasLoadedAsBuiltIn() const
{
	return StateProperties.bWasLoadedAsBuiltInGameFeaturePlugin;
}

UGameFeatureData* UGameFeaturePluginStateMachine::GetGameFeatureDataForActivePlugin()
{
	if (GetCurrentState() == EGameFeaturePluginState::Active)
	{
		return StateProperties.GameFeatureData;
	}

	return nullptr;
}

UGameFeatureData* UGameFeaturePluginStateMachine::GetGameFeatureDataForRegisteredPlugin(bool bCheckForRegistering /*= false*/)
{
	const EGameFeaturePluginState CurrentState = GetCurrentState();

	if (CurrentState >= EGameFeaturePluginState::Registered || (bCheckForRegistering && (CurrentState == EGameFeaturePluginState::Registering)))
	{
		return StateProperties.GameFeatureData;
	}

	return nullptr;
}

const FGameFeaturePluginStateMachineProperties& UGameFeaturePluginStateMachine::GetProperties() const
{
	return StateProperties;
}

bool UGameFeaturePluginStateMachine::IsErrorStateUnrecoverable() const
{
	return bIsInUnrecoverableError;
}

void UGameFeaturePluginStateMachine::SetUnrecoverableError()
{
	bIsInUnrecoverableError = true;
}

bool UGameFeaturePluginStateMachine::IsValidTransitionState(EGameFeaturePluginState InState) const
{
	check(InState != EGameFeaturePluginState::MAX);
	return AllStates[InState]->GetStateType() == EGameFeaturePluginStateType::Transition;
}

bool UGameFeaturePluginStateMachine::IsValidDestinationState(EGameFeaturePluginState InDestinationState) const
{
	check(InDestinationState != EGameFeaturePluginState::MAX);
	return AllStates[InDestinationState]->GetStateType() == EGameFeaturePluginStateType::Destination;
}

bool UGameFeaturePluginStateMachine::IsValidErrorState(EGameFeaturePluginState InDestinationState) const
{
	check(InDestinationState != EGameFeaturePluginState::MAX);
	return AllStates[InDestinationState]->GetStateType() == EGameFeaturePluginStateType::Error;
}

UE_TRACE_EVENT_BEGIN(Cpu, GFP_UpdateStateMachine, NoSync)
	UE_TRACE_EVENT_FIELD(UE::Trace::WideString, PluginName)
UE_TRACE_EVENT_END()

void UGameFeaturePluginStateMachine::UpdateStateMachine()
{
	LLM_SCOPE_BYTAG(GFP);

	const EGameFeaturePluginState InitialState = GetCurrentState();
	EGameFeaturePluginState CurrentState = InitialState;
	if (bInUpdateStateMachine)
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("Game feature state machine skipping update for %s in ::UpdateStateMachine. Current State: %s"), *GetGameFeatureName(), *UE::GameFeatures::ToString(CurrentState));
		return;
	}

	UE_TRACE_LOG_SCOPED_T(Cpu, GFP_UpdateStateMachine, CpuChannel)
		<< GFP_UpdateStateMachine.PluginName(*GetGameFeatureName());

	TOptional<TGuardValue<bool>> ScopeGuard(InPlace, bInUpdateStateMachine, true);
	
	using StateIt = std::underlying_type<EGameFeaturePluginState>::type;

	auto DoCallbacks = [this](const UE::GameFeatures::FResult& Result, StateIt Begin, StateIt End)
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_DoCallbacks);

		for (StateIt iState = Begin; iState < End; ++iState)
		{
			if (FDestinationGameFeaturePluginState* DestState = AllStates[iState]->AsDestinationState())
			{
				// Use a local callback on the stack. If SetDestination() is called from the callback then we don't want to stomp the callback
				// for the new state transition request.
				// Callback from terminal state could also trigger a GC that would destroy the state machine
				UE::GameFeatures::FBroadcastingOnDestinationStateReached LocalOnDestinationStateReached(MoveTemp(DestState->OnDestinationStateReached));
				DestState->OnDestinationStateReached.Clear();

				LocalOnDestinationStateReached.CallbackDelegate.Broadcast(this, Result);
			}
		}
	};

	auto DoCallback = [&DoCallbacks](const UE::GameFeatures::FResult& Result, StateIt InState)
	{
		DoCallbacks(Result, InState, InState + 1);
	};

	RegisterAsTransitioningStateMachine();

	bool bKeepProcessing = false;
	int32 NumTransitions = 0;
	const int32 MaxTransitions = 10000;
	do
	{
		bKeepProcessing = false;

		FGameFeaturePluginStateStatus StateStatus;
		{
			const FGameFeaturePluginStateMachineProperties& Props = AllStates[CurrentState]->StateProperties;
			FName LoaderName = Props.GameFeatureData ? Props.GameFeatureData->GetPackage()->GetFName() : FName(Props.PluginName);
			UE_TRACK_REFERENCING_PACKAGE_SCOPED(LoaderName, UE::GameFeatures::GetStateName(CurrentState));

			TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_UpdateState);
			AllStates[CurrentState]->UpdateState(StateStatus);
		}

		if (StateStatus.TransitionToState == CurrentState)
		{
			UE_LOG(LogGameFeatures, Fatal, TEXT("Game feature state %s transitioning to itself. GameFeature: %s"), *UE::GameFeatures::ToString(CurrentState), *GetGameFeatureName());
		}

		if (StateStatus.TransitionToState != EGameFeaturePluginState::Uninitialized)
		{
			UE_LOG(LogGameFeatures, Verbose, TEXT("Game feature '%s' transitioning state (%s -> %s)"), *GetGameFeatureName(), *UE::GameFeatures::ToString(CurrentState), *UE::GameFeatures::ToString(StateStatus.TransitionToState));
			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_EndState);
				AllStates[CurrentState]->EndState();
				CheckAndCancelBatchingRequestForCurrentState();
			}
			CurrentStateInfo = FGameFeaturePluginStateInfo(StateStatus.TransitionToState);
			CurrentState = StateStatus.TransitionToState;
			check(CurrentState != EGameFeaturePluginState::MAX);

			const FGameFeaturePluginStateMachineProperties& Props = AllStates[CurrentState]->StateProperties;
			FName LoaderName = Props.GameFeatureData ? Props.GameFeatureData->GetPackage()->GetFName() : FName(Props.PluginName);
			UE_TRACK_REFERENCING_PACKAGE_SCOPED(LoaderName, UE::GameFeatures::GetStateName(CurrentState));

			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_BeginState);
				CheckAddBatchingRequestForCurrentState();
				AllStates[CurrentState]->BeginState();
			}

			if (CurrentState == EGameFeaturePluginState::Terminal)
			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_BeginTerm);

				// Remove from gamefeature subsystem before calling back in case this GFP is reloaded on callback,
				// but make sure we don't get destroyed from a GC during a callback
				UGameFeaturesSubsystem::Get().BeginTermination(this);
			}

			if (StateProperties.bTryCancel && AllStates[CurrentState]->GetStateType() != EGameFeaturePluginStateType::Transition)
			{
				StateProperties.Destination = FGameFeaturePluginStateRange(CurrentState);

				StateProperties.bTryCancel = false;
				bKeepProcessing = false;

				// Make sure bInUpdateStateMachine is not set while processing callbacks if we are at our destination
				ScopeGuard.Reset();

				// For all callbacks, return the CanceledResult
				DoCallbacks(UE::GameFeatures::CanceledResult, 0, EGameFeaturePluginState::MAX);

				// Must be called after transtition callbacks, UGameFeaturesSubsystem::ChangeGameFeatureTargetStateComplete may remove the this machine from the subsystem
				UE::GameFeatures::FBroadcastingOnTransitionCanceled LocalOnTransitionCanceled(MoveTemp(StateProperties.OnTransitionCanceled));
				StateProperties.OnTransitionCanceled.Clear();
				LocalOnTransitionCanceled.CallbackDelegate.Broadcast(this);
			}
			else if (const bool bError = !StateStatus.TransitionResult.HasValue(); bError)
			{
				check(IsValidErrorState(CurrentState));
				StateProperties.Destination = FGameFeaturePluginStateRange(CurrentState);

				bKeepProcessing = false;
				
				// Make sure bInUpdateStateMachine is not set while processing callbacks if we are at our destination
				ScopeGuard.Reset();

				// In case of an error, callback all possible callbacks
				DoCallbacks(StateStatus.TransitionResult, 0, EGameFeaturePluginState::MAX);
			}
			else
			{
				bKeepProcessing = AllStates[CurrentState]->GetStateType() == EGameFeaturePluginStateType::Transition || !StateProperties.Destination.Contains(CurrentState);
				if (!bKeepProcessing)
				{
					// Make sure bInUpdateStateMachine is not set while processing callbacks if we are at our destination
					ScopeGuard.Reset();
				}

				DoCallback(StateStatus.TransitionResult, CurrentState);
			}

			if (!bKeepProcessing)
			{
				UnregisterAsTransitioningStateMachine();
			}

			if (CurrentState == EGameFeaturePluginState::Terminal)
			{
				TRACE_CPUPROFILER_EVENT_SCOPE(GFP_UpdateStateMachine_FinishTerm);

				UnregisterAsTransitioningStateMachine();

				check(bKeepProcessing == false);
				// Now that callbacks are done this machine can be cleaned up
				UGameFeaturesSubsystem::Get().FinishTermination(this);
				MarkAsGarbage();
			}
		}
		else if(!IsRunning())
		{
			UnregisterAsTransitioningStateMachine();
		}

		// Log our final state if we've finished transitioning
		if (!bKeepProcessing && InitialState != CurrentState)
		{
			if (!StateStatus.TransitionResult.HasValue())
			{
				constexpr static const TCHAR ErrLogFmt[] = TEXT("Game feature '%s' transition failed. Ending state: %s [%s, %s]. Result: %s");
				if (StateStatus.bSuppressErrorLog)
				{
					UE_LOG(LogGameFeatures, Display, ErrLogFmt,
						*GetGameFeatureName(),
						*UE::GameFeatures::ToString(CurrentState),
						*UE::GameFeatures::ToString(StateProperties.Destination.MinState),
						*UE::GameFeatures::ToString(StateProperties.Destination.MaxState),
						*UE::GameFeatures::ToString(StateStatus.TransitionResult));
				}
				else
				{
					UE_LOG(LogGameFeatures, Error, ErrLogFmt,
						*GetGameFeatureName(),
						*UE::GameFeatures::ToString(CurrentState),
						*UE::GameFeatures::ToString(StateProperties.Destination.MinState),
						*UE::GameFeatures::ToString(StateProperties.Destination.MaxState),
						*UE::GameFeatures::ToString(StateStatus.TransitionResult));
				}
			}
			else if (StateProperties.Destination.Contains(CurrentState))
			{
				UE_LOG(LogGameFeatures, Display, TEXT("Game feature '%s' transitioned successfully. Ending state: %s [%s, %s]"),
					*GetGameFeatureName(),
					*UE::GameFeatures::ToString(CurrentState),
					*UE::GameFeatures::ToString(StateProperties.Destination.MinState),
					*UE::GameFeatures::ToString(StateProperties.Destination.MaxState));
			}
		}

		if (NumTransitions++ > MaxTransitions)
		{
			UE_LOG(LogGameFeatures, Fatal, TEXT("Infinite loop in game feature state machine transitions. Current state %s. GameFeature: %s"), *UE::GameFeatures::ToString(CurrentState), *GetGameFeatureName());
		}
	} while (bKeepProcessing);
}

void UGameFeaturePluginStateMachine::UpdateCurrentStateProgress(float Progress)
{
	CurrentStateInfo.Progress = Progress;
}

void UGameFeaturePluginStateMachine::RegisterAsTransitioningStateMachine()
{
	if (bRegisteredAsTransitioningGFPSM)
	{
		return;
	}

	UGameFeaturesSubsystem::Get().RegisterRunningStateMachine(this);
	bRegisteredAsTransitioningGFPSM = true;
}

void UGameFeaturePluginStateMachine::UnregisterAsTransitioningStateMachine()
{
	if (!bRegisteredAsTransitioningGFPSM)
	{
		return;
	}

	UGameFeaturesSubsystem::Get().UnregisterRunningStateMachine(this);
	bRegisteredAsTransitioningGFPSM = false;
}

void UGameFeaturePluginStateMachine::CheckAddBatchingRequestForCurrentState()
{
	check(!StateProperties.BatchProcessingHandle.IsValid());

	const bool bCanBatchProcess =
		UE::GameFeatures::BatchProcessingHelperFunctors[CurrentStateInfo.State].ImplementsBatchProcess() &&
		StateProperties.CanBatchProcess() &&
		AllStates[CurrentStateInfo.State]->CanBatchProcess();
	
	if (bCanBatchProcess)
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("Game feature '%s' awaiting batch processing of state (%s)"), *GetGameFeatureName(), *UE::GameFeatures::ToString(CurrentStateInfo.State));
		StateProperties.BatchProcessingHandle = UGameFeaturesSubsystem::Get().AddBatchingRequest(
			CurrentStateInfo.State,
			StateProperties.OnRequestUpdateStateMachine);
	}
}

void UGameFeaturePluginStateMachine::CheckAndCancelBatchingRequestForCurrentState()
{
	// Reset batch processing state.
	StateProperties.bWasBatchProcessed = false;

	// If we are currently awaiting batch processing, cancel and update the state machine.
	if (StateProperties.IsWaitingForBatchProcessing())
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("Game feature '%s' cancelled batch processing of state (%s)"), *GetGameFeatureName(), *UE::GameFeatures::ToString(CurrentStateInfo.State));
		UGameFeaturesSubsystem::Get().CancelBatchingRequest(
			CurrentStateInfo.State,
			StateProperties.BatchProcessingHandle);

		StateProperties.BatchProcessingHandle.Reset();
		UpdateStateMachine();
	}
}

/* static */ void UGameFeaturePluginStateMachine::BatchProcess(EGameFeaturePluginState State, TConstArrayView<UGameFeaturePluginStateMachine*> GFPSMs)
{
	UE::GameFeatures::BatchProcessingHelperFunctors[State].BatchProcess(GFPSMs);
	for (UGameFeaturePluginStateMachine* GFPSM : GFPSMs)
	{
		GFPSM->StateProperties.BatchProcessingHandle.Reset();
		GFPSM->StateProperties.bWasBatchProcessed = true;
	}
}

void UGameFeaturePluginStateMachine::ExcludeFromBatchProcessing()
{
	// Ensure protocol options are updated to reflect the exclusion of this state machine from batch processing.
	if (StateProperties.ProtocolOptions.bBatchProcess)
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("Game feature '%s' excluded from batch processing"), *GetGameFeatureName());

		FGameFeatureProtocolOptions NewOptions = StateProperties.ProtocolOptions;
		NewOptions.bBatchProcess = false;

		bool bOutDidUpdate = false;
		TryUpdatePluginProtocolOptions(NewOptions, bOutDidUpdate);
		check(bOutDidUpdate);
		CheckAndCancelBatchingRequestForCurrentState();
	}	
}

FGameFeaturePluginStateMachineProperties::FGameFeaturePluginStateMachineProperties(
	FGameFeaturePluginIdentifier InPluginIdentifier,
	const FGameFeaturePluginStateRange& DesiredDestination,
	const FGameFeaturePluginRequestUpdateStateMachine& RequestUpdateStateMachineDelegate,
	const FGameFeatureStateProgressUpdate& FeatureStateProgressUpdateDelegate)
	: PluginIdentifier(MoveTemp(InPluginIdentifier))
	, Destination(DesiredDestination)
	, OnRequestUpdateStateMachine(RequestUpdateStateMachineDelegate)
	, OnFeatureStateProgressUpdate(FeatureStateProgressUpdateDelegate)
{
}

EGameFeaturePluginProtocol FGameFeaturePluginStateMachineProperties::GetPluginProtocol() const
{
	return PluginIdentifier.GetPluginProtocol();
}

FString FInstallBundlePluginProtocolMetaData::ToString() const
{
	FString ReturnedString;

	//Always encode InstallBundles
	ReturnedString = FString(UE::GameFeatures::PluginURLStructureInfo::OptionSeperator) +
		LexToString(EGameFeatureURLOptions::Bundles) + UE::GameFeatures::PluginURLStructureInfo::OptionAssignOperator;

	FNameBuilder NameBuilder;
	const FString BundlesList = FString::JoinBy(InstallBundles, UE::GameFeatures::PluginURLStructureInfo::OptionListSeperator,
	[&NameBuilder](const FName& BundleName)
	{
		BundleName.ToString(NameBuilder);
		return FStringView(NameBuilder);
	});
	ReturnedString.Append(BundlesList);

	// Only the generic version of CountBits is constexpr...
	static_assert(FGenericPlatformMath::CountBits(static_cast<uint64>(EGameFeatureURLOptions::All)) == 1, "Update this function to handle the newly added EGameFeatureInstallBundleProtocolOptions value!");

	return ReturnedString;
}

TValueOrError<FInstallBundlePluginProtocolMetaData, FString> FInstallBundlePluginProtocolMetaData::FromString(FStringView URLOptionsString)
{
	TArray<FName> InstallBundles;

	bool bParseSuccess = UGameFeaturesSubsystem::ParsePluginURLOptions(URLOptionsString, EGameFeatureURLOptions::Bundles,
	[&InstallBundles](EGameFeatureURLOptions Option, FStringView OptionName, FStringView OptionValue)
	{
		check(Option == EGameFeatureURLOptions::Bundles);
		InstallBundles.Emplace(OptionValue);
	});

	//We require to have InstallBundle names for this URL parse to be correct
	if (!bParseSuccess || InstallBundles.Num() == 0)
	{
		bParseSuccess = false;
		UE_LOG(LogGameFeatures, Error, TEXT("Error parsing InstallBundle protocol options URL %.*s"), URLOptionsString.Len(), URLOptionsString.GetData());
		return MakeError(TEXTVIEW("Bad_PluginURL"));
	}

	FInstallBundlePluginProtocolMetaData Ret;
	Ret.InstallBundles = MoveTemp(InstallBundles);

	return MakeValue<FInstallBundlePluginProtocolMetaData>(MoveTemp(Ret));
}

TValueOrError<void, FString> FGameFeaturePluginStateMachineProperties::ParseURL()
{
	const FStringView BadUrlError = TEXTVIEW("Bad_PluginURL");

	if (!ensureMsgf(!PluginIdentifier.IdentifyingURLSubset.IsEmpty(), TEXT("Unexpected empty IdentifyingURLSubset while parsing URL!")))
	{
		return MakeError(BadUrlError);
	}

	FStringView PluginPathFromURL;
	FStringView URLOptions;
	if (!UGameFeaturesSubsystem::ParsePluginURL(PluginIdentifier.GetFullPluginURL(), nullptr, &PluginPathFromURL, &URLOptions))
	{
		return MakeError(BadUrlError);
	}

	PluginInstalledFilename = PluginPathFromURL;
	PluginName = FPaths::GetBaseFilename(PluginInstalledFilename);

	if (PluginInstalledFilename.IsEmpty() || !PluginInstalledFilename.EndsWith(TEXT(".uplugin")))
	{
		ensureMsgf(false, TEXT("PluginInstalledFilename must have a uplugin extension. PluginInstalledFilename: %s"), *PluginInstalledFilename);
		return MakeError(BadUrlError);
	}

	//Do additional parsing of our Metadata from the options on our remaining URL
	if (GetPluginProtocol() == EGameFeaturePluginProtocol::InstallBundle)
	{
		TValueOrError<FInstallBundlePluginProtocolMetaData, FString> MaybeMetaData = FInstallBundlePluginProtocolMetaData::FromString(URLOptions);
		if (MaybeMetaData.HasError())
		{
			ensureMsgf(false, TEXT("Failure to parse URL %s into a valid FInstallBundlePluginProtocolMetaData"), *PluginIdentifier.GetFullPluginURL());
			return MakeError(MaybeMetaData.StealError());
		}

		FInstallBundlePluginProtocolMetaData& MetaData = *ProtocolMetadata.SetSubtype<FInstallBundlePluginProtocolMetaData>();
		MetaData = MaybeMetaData.StealValue();

		// Add default protocol options if they are not set yet
		if (!ProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>())
		{
			if (ProtocolOptions.HasSubtype<FNull>())
			{
				ProtocolOptions.SetSubtype<FInstallBundlePluginProtocolOptions>();
			}
			else
			{
				ensureMsgf(false, TEXT("Protocol options type is incorrect for URL %s"), *PluginIdentifier.GetFullPluginURL());
				return MakeError(BadUrlError);
			}
		}
	}
	else
	{
		// No protocol options for other (file) protocols right now
		if (!ProtocolOptions.HasSubtype<FNull>())
		{
			ensureMsgf(false, TEXT("Protocol options type is incorrect for URL %s"), *PluginIdentifier.GetFullPluginURL());
			return MakeError(BadUrlError);
		}
	}

	static_assert(static_cast<uint8>(EGameFeaturePluginProtocol::Count) == 3, "Update FGameFeaturePluginStateMachineProperties::ParseURL to handle any new Metadata parsing required for new EGameFeaturePluginProtocol. If no metadata is required just increment this counter.");

	return MakeValue();
}

UE::GameFeatures::FResult FGameFeaturePluginStateMachineProperties::ValidateProtocolOptionsUpdate(const FGameFeatureProtocolOptions& NewProtocolOptions) const
{
	if (GetPluginProtocol() == EGameFeaturePluginProtocol::InstallBundle)
	{
		const FStringView ShortUrl = PluginIdentifier.GetIdentifyingString();

		//Should never change our PluginProtocol
		if (!ensureAlwaysMsgf(NewProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>()
								,TEXT("Error with InstallBundle protocol FGameFeaturePluginStateMachineProperties having an invalid ProtocolOptions. URL: %.*s")
								,ShortUrl.Len(), ShortUrl.GetData()))
		{
			return MakeError(UE::GameFeatures::StateMachineErrorNamespace + TEXT("ProtocolOptions.Invalid_Protocol"));
		}

		if (ProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>())
		{
			const FInstallBundlePluginProtocolOptions& OldOptions = ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();
			const FInstallBundlePluginProtocolOptions& NewOptions = NewProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();

			if (!ensureMsgf(OldOptions.bAllowIniLoading == NewOptions.bAllowIniLoading, TEXT("Unexpected change to AllowIniLoading when updating ProtocolOptions. URL: %.*s "), ShortUrl.Len(), ShortUrl.GetData()))
			{
				return MakeError(UE::GameFeatures::StateMachineErrorNamespace + TEXT("ProtocolOptions.Invalid_Update"));
			}
		}

		return MakeValue();
	}

	if (NewProtocolOptions.HasSubtype<FNull>())
	{
		return MakeValue();
	}

	return MakeError(UE::GameFeatures::StateMachineErrorNamespace + TEXT("ProtocolOptions.Unknown_Protocol"));
}

FGameFeatureProtocolOptions FGameFeaturePluginStateMachineProperties::RecycleProtocolOptions() const
{
	FGameFeatureProtocolOptions Result = ProtocolOptions;
	if (Result.HasSubtype<FInstallBundlePluginProtocolOptions>())
	{
		// Don't allow unexpected uninstalls, otherwise respect any flags previously set by the game
		Result.GetSubtype<FInstallBundlePluginProtocolOptions>().bUninstallBeforeTerminate = false;
	}
	return Result;
}

bool FGameFeaturePluginStateMachineProperties::AllowAsyncLoading() const
{
	// Ticking is required for async loading
	// The local bForceSyncLoading should take precedence over UE::GameFeatures::CVarForceAsyncLoad
	return
		!ProtocolOptions.bForceSyncLoading &&
		UGameFeaturesSubsystem::Get().GetPolicy().AllowAsyncLoad(PluginIdentifier.GetFullPluginURL());
}

bool FGameFeaturePluginStateMachineProperties::CanBatchProcess() const
{
	return ProtocolOptions.bBatchProcess && UE::GameFeatures::CVarEnableBatchProcessing.GetValueOnGameThread();
}

bool FGameFeaturePluginStateMachineProperties::IsWaitingForBatchProcessing() const
{
	return BatchProcessingHandle.IsValid();
}

bool FGameFeaturePluginStateMachineProperties::WasBatchProcessed() const
{
	return bWasBatchProcessed;
}

#undef LOCTEXT_NAMESPACE 

GameFeaturePluginStateMachine.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Containers/Union.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeaturePluginOperationResult.h"
#include "GameFeatureTypes.h"
#include "GameFeaturePluginStateMachine.generated.h"

class UGameFeatureData;
class UGameFrameworkComponentManager;
class UGameFeaturePluginStateMachine;
struct FComponentRequestHandle;
enum class EInstallBundleResult : uint32;
enum class EInstallBundleReleaseResult : uint32;

namespace UE::GameFeatures
{
	extern TAutoConsoleVariable<bool> CVarAllowMissingOnDemandDependencies;
}

/*
*************** GameFeaturePlugin state machine graph ***************
Descriptions for each state are below in EGameFeaturePluginState.
Destination states have a *. These are the only states that external sources can ask to transition to via SetDestinationState().
Error states have !. These states become destinations if an error occurs during a transition.
Transition states are expected to transition the machine to another state after doing some work.

                         +--------------+
                         |              |
                         |Uninitialized |
                         |              |
                         +------+-------+
     +------------+             |
     |     *      |             |
     |  Terminal  <-------------~-----------------------------------------------
     |            |             |                                              |
     +--^------^--+             ----------------------------                   |
        |      |                                           |                   |
        |      |                                    +------v--------+          |
        |      |                                    |      *        |          |
        |      -------------------------------------+ UnknownStatus |          |
        |           ^                      ^        |               |          |
        |           |                      |        +-------+-------+          |
        |           |                      |                |                  |
        |    +------+-------+              |                |                  |
        |    |      *       |              |                |                  |
        |    | Uninstalled  +--------------~--------------->|                  |
        |    |              |              |                |                  |
        |    +------^-------+              |                |                  |
        |           |                      |                |                  |
        |    +------+-------+    *---------+---------+      |                  |
        |    |              |    |         !         |      |                  |
        |    | Uninstalling <----> ErrorUninstalling |      |                  |
        |    |              |    |                   |      |                  |
        |    +---^----------+    +---------+---------+      |                  |
        |        |                         |                |                  |
        |        |    ----------------------                |                  |
        |        |    |                                     |                  |
        |        |    |                     -----------------                  |
        |        |    |                     |                                  |
        |        |    |         +-----------v---+     +--------------------+   |
        |        |    |         |               |     |         !          |   |
        |        |    |         |CheckingStatus <-----> ErrorCheckingStatus+-->|
        |        |    |         |               |     |                    |   |
        |        |    |         +------+------^-+     +--------------------+   |
        |        |    |                |      |                                |
        |        |    |                |      |       +--------------------+   |
        ---------~    |                |      |       |         !          |   |
                 |    |<----------------      --------> ErrorUnavailable   +----
                 |    |                               |                    |
                 |    |                               +--------------------+
                 |    |
            +----+----v----+
            |      *       |
         ---> StatusKnown  +----------------------------------------------
         |  |              |                                 |           |
         |  +----------^---+                                 |           |
         |                                                   |           |
         |                                                   |           |
         |                                                   |           |
         |                                                   |           |
         |                                                   |           |
      +--+---------+      +-------------------+       +------v-------+   |
      |            |      |         !         |       |              |   |
      | Releasing  <------> ErrorManagingData <-------> Downloading  |   |
      |            |      |                   |       |              |   |
      +--^---------+      +-------------------+       +-------+------+   |
         |                                                   |           |
         |                                                   |           |
         |     +-------------+                               |           |
         |     |      *      |                               v           |
         ------+ Installed   <--------------------------------------------
               |             |
               +-^---------+-+
                 |         |
           ------~---------~--------------------------------
           |     |         |                               |
        +--v-----+--+    +-v---------+               +-----v--------------+
        |           |    |           |               |         !          |
        |Unmounting |    | Mounting  <---------------> ErrorMounting      |
        |           |    |           |               |                    |
        +--^-----^--+    +--+--------+               +--------------------+
           |     |          |
           ------~----------~-------------------------------
                 |          |                              |
                 |       +--v--------------------+   +-----+-----------------------+
                 |       |                       |   |         !                   |
                 |       |WaitingForDependencies <---> ErrorWaitingForDependencies |
                 |       |                       |   |                             |
                 |       +-----+-----------------+   +-----------------------------+
                 |             |
                 |    ---------~-----------------------------------
                 |    |        |                                  |
+----------------+----v---+ +--v----------------------+     +-----v-------------------------+
|                         | |                         |     |             !                 |
|AssetDependencyStreamOut | |AssetDependencyStreaming <-----> ErrorAssetDependencyStreaming |
|                         | |                         |     |                               |
+----------------^--------+ +--+----------------------+     +-------------------------------+
                 |             |
           ------~-------------~----------------------------
           |     |             |                           |
        +--v-----+----+  +-----v----- +              +-----v--------------+
        |             |  |            |              |         !          |
        |Unregistering|  |Registering <--------------> ErrorRegistering   |
        |             |  |            |              |                    |
        +--------^----+  ++-----------+              +--------------------+
                 |        |
               +-+--------v-+
               |      *     |
               | Registered |
               |            |
               +-^--------+-+
                 |        |
           ------~--------~---------------------------------------
           |     |        |                               ^      |
        +--v-----+--+  +--v--------+                      |    +-+------------+
        |           |  |           |                      |    |      !       |
        | Unloading |  |  Loading  <----------------------~----> ErrorLoading |
        |           |  |           |                      |    |              |
        +--------^--+  +--+--------+                      |    +--------------+
                 |        |                               |
               +-+--------v-+                             |
               |      *     |                             |
               |   Loaded   |                             |
               |            |                             |
               +-^--------+-+                             |
		         |        |                               |
        +--------+---+  +-v------------------------+   +--+--------------------------+
        |            |  |                          |   |             !               |
        |Deactivating|  |  ActivatingDependencies  <---> ErrorActivatingDependencies |
        |            |  |                          |   |                             |
        +-^----------+  +---------------------+----+   +-----------------------------+
          |                                   |
		  |  +-----------------------------+  |
		  |  |              !              |  |
		  |  |ErrorDeactivatingDependencies|  |
		  |  |                             |  |
		  |  +--^--------------------------+  |
		  |     |                             |
		+-+-----v----------------+          +-v----------+
		|		                 |          |            |
		|DeactivatingDependencies|          | Activating |
		|	   	                 |          |            |
		+----------------------^-+          +---+--------+
		                       |                |
		     	             +-+----------------v-+
                             |          *         |
                             |       Active       |
                             |                    |
                             +--------------------+
*/

struct FGameFeaturePluginStateRange
{
	EGameFeaturePluginState MinState = EGameFeaturePluginState::Uninitialized;
	EGameFeaturePluginState MaxState = EGameFeaturePluginState::Uninitialized;

	FGameFeaturePluginStateRange() = default;

	FGameFeaturePluginStateRange(EGameFeaturePluginState InMinState, EGameFeaturePluginState InMaxState)
		: MinState(InMinState), MaxState(InMaxState)
	{}

	explicit FGameFeaturePluginStateRange(EGameFeaturePluginState InState)
		: MinState(InState), MaxState(InState)
	{}

	bool IsValid() const { return MinState <= MaxState; }

	bool Contains(EGameFeaturePluginState InState) const
	{
		return InState >= MinState && InState <= MaxState;
	}

	bool Overlaps(const FGameFeaturePluginStateRange& Other) const
	{
		return Other.MinState <= MaxState && Other.MaxState >= MinState;
	}

	TOptional<FGameFeaturePluginStateRange> Intersect(const FGameFeaturePluginStateRange& Other) const
	{
		TOptional<FGameFeaturePluginStateRange> Intersection;

		if (Overlaps(Other))
		{
			Intersection.Emplace(FMath::Max(Other.MinState, MinState), FMath::Min(Other.MaxState, MaxState));
		}

		return Intersection;
	}

	bool operator==(const FGameFeaturePluginStateRange& Other) const { return MinState == Other.MinState && MaxState == Other.MaxState; }
	bool operator<(const FGameFeaturePluginStateRange& Other) const { return MaxState < Other.MinState; }
	bool operator>(const FGameFeaturePluginStateRange& Other) const { return MinState > Other.MaxState; }
};

inline bool operator<(EGameFeaturePluginState State, const FGameFeaturePluginStateRange& StateRange)
{
	return State < StateRange.MinState;
}

inline bool operator<(const FGameFeaturePluginStateRange& StateRange, EGameFeaturePluginState State)
{
	return StateRange.MaxState < State;
}

inline bool operator>(EGameFeaturePluginState State, const FGameFeaturePluginStateRange& StateRange)
{
	return State > StateRange.MaxState;
}

inline bool operator>(const FGameFeaturePluginStateRange& StateRange, EGameFeaturePluginState State)
{
	return StateRange.MinState > State;
}

struct FInstallBundlePluginProtocolMetaData
{
	FInstallBundlePluginProtocolMetaData() = default;
	FInstallBundlePluginProtocolMetaData(TArray<FName> InInstallBundles) : InstallBundles(MoveTemp(InInstallBundles)) {}

	TArray<FName> InstallBundles;
	TArray<FName> InstallBundlesWithAssetDependencies;

	/** Functions to convert to/from the URL FString representation of this metadata **/
	FString ToString() const;
	static TValueOrError<FInstallBundlePluginProtocolMetaData, FString> FromString(FStringView URLOptionsString);
};

struct FGameFeatureProtocolMetadata : public TUnion<FInstallBundlePluginProtocolMetaData, FNull>
{
	FGameFeatureProtocolMetadata() { SetSubtype<FNull>(); }
	FGameFeatureProtocolMetadata(const FInstallBundlePluginProtocolMetaData& InData) : TUnion(InData) {}
	FGameFeatureProtocolMetadata(FNull InOptions) { SetSubtype<FNull>(InOptions); }
};

/** Notification that a state transition is complete */
DECLARE_DELEGATE_TwoParams(FGameFeatureStateTransitionComplete, UGameFeaturePluginStateMachine* /*Machine*/, const UE::GameFeatures::FResult& /*Result*/);

/** Notification that a state transition is canceled */
DECLARE_DELEGATE_OneParam(FGameFeatureStateTransitionCanceled, UGameFeaturePluginStateMachine* /*Machine*/);

/** A request for other state machine dependencies */
DECLARE_DELEGATE_RetVal_TwoParams(bool, FGameFeaturePluginRequestStateMachineDependencies, const FString& /*DependencyPluginURL*/, TArray<UGameFeaturePluginStateMachine*>& /*OutDependencyMachines*/);

/** A request to update progress for the current state */
DECLARE_DELEGATE_OneParam(FGameFeatureStateProgressUpdate, float Progress);

/** The common properties that can be accessed by the states of the state machine */
USTRUCT()
struct FGameFeaturePluginStateMachineProperties
{
	GENERATED_BODY()

	/** 
	* The Identifier used to find this Plugin. Parsed from the supplied PluginURL at creation.
	* Every protocol will have its own style of identifier URL that will get parsed to generate this.
	* For example, if the file is simply on disk, you can use file:../../../YourGameModule/Plugins/MyPlugin/MyPlugin.uplugin
	**/
	FGameFeaturePluginIdentifier PluginIdentifier;

	/** Filename on disk of the .uplugin file. */
	FString PluginInstalledFilename;
	
	/** Name of the plugin. */
	FString PluginName;

	/** Metadata parsed from the URL for a specific protocol. */
	FGameFeatureProtocolMetadata ProtocolMetadata;

	/** Additional options for a specific protocol. */
	FGameFeatureProtocolOptions ProtocolOptions;

	/** The desired state during a transition. */
	FGameFeaturePluginStateRange Destination;

	/** Whether this plugin has an async localization load in-flight */
	bool bIsLoadingLocalizationData : 1 = false;

	/** Tracks whether or not this state machine added the plugin to the plugin manager. */
	bool bAddedPluginToManager : 1 = false;

	/** Was this plugin loaded using LoadBuiltInGameFeaturePlugin */
	bool bWasLoadedAsBuiltInGameFeaturePlugin : 1 = false;

	/** Whether this state machine should attempt to cancel the current transition */
	bool bTryCancel : 1 = false;

	/** Tracks if the current state was batch processed */
	bool bWasBatchProcessed : 1 = false;

	/** Batch Processing handle if requested for batch processed */
	FDelegateHandle BatchProcessingHandle;

	TArray<FName> AddedPrimaryAssetTypes;

	/** The data asset describing this game feature */
	UPROPERTY(Transient)
	TObjectPtr<UGameFeatureData> GameFeatureData = nullptr;

	/** Callbacks for when the current state transition is cancelled */
	DECLARE_MULTICAST_DELEGATE_OneParam(FOnTransitionCanceled, UGameFeaturePluginStateMachine* /*Machine*/);
	FOnTransitionCanceled OnTransitionCanceled;

	/** Delegate to request the state machine be updated. */
	FGameFeaturePluginRequestUpdateStateMachine OnRequestUpdateStateMachine;

	/** Delegate for when a feature state needs to update progress. */
	FGameFeatureStateProgressUpdate OnFeatureStateProgressUpdate;

	FGameFeaturePluginStateMachineProperties() = default;
	FGameFeaturePluginStateMachineProperties(
		FGameFeaturePluginIdentifier InPluginIdentifier,
		const FGameFeaturePluginStateRange& DesiredDestination,
		const FGameFeaturePluginRequestUpdateStateMachine& RequestUpdateStateMachineDelegate,
		const FGameFeatureStateProgressUpdate& FeatureStateProgressUpdateDelegate);

	EGameFeaturePluginProtocol GetPluginProtocol() const;

	TValueOrError<void, FString> ParseURL();

	/** Checks to see if any invalid data was changed during a URL update. True if data updated was all values expected to be changed. */
	UE::GameFeatures::FResult ValidateProtocolOptionsUpdate(const FGameFeatureProtocolOptions& NewProtocolOptions) const;

	/** Returns protocol options suitable for reuse by another state machine */
	FGameFeatureProtocolOptions RecycleProtocolOptions() const;

	/** Whether this machine is allowed to be asynchronous */
	bool AllowAsyncLoading() const;

	bool CanBatchProcess() const;
	bool IsWaitingForBatchProcessing() const;
	bool WasBatchProcessed() const;
};

/** Input and output information for a state's UpdateState */
struct FGameFeaturePluginStateStatus
{
private:
	/** Holds the current error for any state transition. */
	UE::GameFeatures::FResult TransitionResult = MakeValue();

	/** The state to transition to after UpdateState is complete. */
	EGameFeaturePluginState TransitionToState = EGameFeaturePluginState::Uninitialized;

	/** Whether to suppress error logging if TransitionResult is an error. */
	bool bSuppressErrorLog = false;

	friend class UGameFeaturePluginStateMachine;

public:
	void SetTransition(EGameFeaturePluginState InTransitionToState);
	void SetTransitionError(EGameFeaturePluginState TransitionToErrorState, UE::GameFeatures::FResult TransitionResult, bool bInSuppressErrorLog = false);
};

enum class EGameFeaturePluginStateType : uint8
{
	Transition,
	Destination,
	Error
};

struct FDestinationGameFeaturePluginState;
struct FErrorGameFeaturePluginState;

/** Base class for all game feature plugin states */
struct FGameFeaturePluginState
{
	FGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : StateProperties(InStateProperties) {}
	virtual ~FGameFeaturePluginState();

	/** Called when this state becomes the active state */
	virtual void BeginState() {}

	/** Process the state's logic to decide if there should be a state transition. */
	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) {}

	/** Attempt to cancel any pending state transition. */
	virtual void TryCancelState() {}

	/** Called if we have updated the protocol options for this FGameFeaturePluginState.
		Returns false if no update occured or the update failed. True on successful update. */
	virtual UE::GameFeatures::FResult TryUpdateProtocolOptions(const FGameFeatureProtocolOptions& NewOptions);
	
	/** Called when this state is no longer the active state */
	virtual void EndState() {}

	/** Returns the type of state this is */
	virtual EGameFeaturePluginStateType GetStateType() const { return EGameFeaturePluginStateType::Transition; }

	FDestinationGameFeaturePluginState* AsDestinationState();
	FErrorGameFeaturePluginState* AsErrorState();

	/** The common properties that can be accessed by the states of the state machine */
	FGameFeaturePluginStateMachineProperties& StateProperties;

	void UpdateStateMachineDeferred(float Delay = 0.0f) const;
	void UpdateStateMachineImmediate() const;

	void UpdateProgress(float Progress) const;

	virtual bool CanBatchProcess() const;
	bool IsWaitingForBatchProcessing() const;
	bool WasBatchProcessed() const;

protected:
	/** Builds an end FResult with some minimal error information with overrides for common types we 
		need to generate errors from */
	UE::GameFeatures::FResult GetErrorResult(const FString& ErrorCode, const FText OptionalErrorText = FText()) const;
	UE::GameFeatures::FResult GetErrorResult(const FString& ErrorNamespaceAddition, const FString& ErrorCode, const FText OptionalErrorText = FText()) const;
	UE::GameFeatures::FResult GetErrorResult(const FString& ErrorNamespaceAddition, const EInstallBundleResult ErrorResult) const;
	UE::GameFeatures::FResult GetErrorResult(const FString& ErrorNamespaceAddition, const EInstallBundleReleaseResult ErrorResult) const;

	/** Returns true if this state should transition to the Uninstalled state. 
	    returns False if it should just go directly to the Terminal state instead. */
	bool ShouldVisitUninstallStateBeforeTerminal() const;

	bool AllowIniLoading() const;

	bool AllowAsyncLoading() const;
	virtual bool UseAsyncLoading() const;

private:
	void CleanupDeferredUpdateCallbacks() const;

	mutable FTSTicker::FDelegateHandle TickHandle;
};

/** Base class for destination game feature plugin states */
struct FDestinationGameFeaturePluginState : public FGameFeaturePluginState
{
	FDestinationGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : FGameFeaturePluginState(InStateProperties) {}

	/** Returns the type of state this is */
	virtual EGameFeaturePluginStateType GetStateType() const override { return EGameFeaturePluginStateType::Destination; }

	DECLARE_MULTICAST_DELEGATE_TwoParams(FOnDestinationStateReached, UGameFeaturePluginStateMachine* /*Machine*/, const UE::GameFeatures::FResult& /*Result*/);
	FOnDestinationStateReached OnDestinationStateReached;
};

/** Base class for error game feature plugin states */
struct FErrorGameFeaturePluginState : public FDestinationGameFeaturePluginState
{
	FErrorGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : FDestinationGameFeaturePluginState(InStateProperties) {}

	/** Returns the type of state this is */
	virtual EGameFeaturePluginStateType GetStateType() const override { return EGameFeaturePluginStateType::Error; }
};

/** Information about a given plugin state, used to expose information to external code */
struct FGameFeaturePluginStateInfo
{
	/** The state this info represents */
	EGameFeaturePluginState State = EGameFeaturePluginState::Uninitialized;

	/** The progress of this state. Relevant only for transition states. */
	float Progress = 0.0f;

	FGameFeaturePluginStateInfo() = default;
	explicit FGameFeaturePluginStateInfo(EGameFeaturePluginState InState) : State(InState) {}
};

/** A state machine to manage transitioning a game feature plugin from just a URL into a fully loaded and active plugin, including registering its contents with other game systems */
UCLASS()
class UGameFeaturePluginStateMachine : public UObject
{
	GENERATED_BODY()

public:
	UGameFeaturePluginStateMachine(const FObjectInitializer& ObjectInitializer);

	/** Initializes the state machine and assigns the URL for the plugin it manages. This sets the machine to the 'UnknownStatus' state. */
	void InitStateMachine(FGameFeaturePluginIdentifier InPluginIdentifier, const FGameFeatureProtocolOptions& InProtocolOptions);

	/** Asynchronously transitions the state machine to the destination state range and reports when it is done. 
	  * DestinationState must be of type EGameFeaturePluginStateType::Destination.
	  * If returns true and OnFeatureStateTransitionComplete is not called immediately, OutCallbackHandle will be set
	  * Returns false and does not callback if a transition is already in progress and the destination range is not compatible with the current range. */
	bool SetDestination(FGameFeaturePluginStateRange InDestination, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete, FDelegateHandle* OutCallbackHandle = nullptr);

	/** Cancel the current transition if possible */
	bool TryCancel(FGameFeatureStateTransitionCanceled OnFeatureStateTransitionCanceled, FDelegateHandle* OutCallbackHandle = nullptr);

	/** Update the current PluginURL data for this plugin if possible. Returns false if this update fails or
		if the supplied InPluginURL matches the existing URL data for this plugin.**/
	UE::GameFeatures::FResult TryUpdatePluginProtocolOptions(const FGameFeatureProtocolOptions& InOptions, bool& bOutDidUpdate);

	/** Remove any pending callback from SetDestination */
	void RemovePendingTransitionCallback(FDelegateHandle InHandle);

	/** Remove any pending callback from SetDestination */
	void RemovePendingTransitionCallback(FDelegateUserObject DelegateObject);

	/** Remove any pending callback from TryCancel */
	void RemovePendingCancelCallback(FDelegateHandle InHandle);

	/** Remove any pending callback from TryCancel */
	void RemovePendingCancelCallback(FDelegateUserObject DelegateObject);

	/** Returns the PluginIdentifier used to identify this plugin uniquely to the GameFeaturePlugin Subsystem */
	const FGameFeaturePluginIdentifier& GetPluginIdentifier() const;

	/** Returns the name of the game feature. Before StatusKnown, this returns the URL. */
	const FString& GetGameFeatureName() const;

	/** Returns the URL */
	const FString& GetPluginURL() const;

	/** Returns protocol medata */
	const FGameFeatureProtocolMetadata& GetProtocolMetadata() const;

	/** Returns any protocol options */
	const FGameFeatureProtocolOptions& GetProtocolOptions() const;

	/** Returns protocol options suitable for reuse by another state machine */
	FGameFeatureProtocolOptions RecycleProtocolOptions() const;

	/** Returns the plugin name if known (plugin must have been registered to know the name). */
	const FString& GetPluginName() const;

	/** Returns the uplugin filename of the game feature. Before StatusKnown, this returns false. */
	bool GetPluginFilename(FString& OutPluginFilename) const;

	/** Returns the enum state for this machine */
	EGameFeaturePluginState GetCurrentState() const;

	/** Returns the state range this machine is trying to move to */
	FGameFeaturePluginStateRange GetDestination() const;

	/** Returns information about the current state */
	const FGameFeaturePluginStateInfo& GetCurrentStateInfo() const;

	/** Returns true if attempting to reach a new destination state */
	bool IsRunning() const;

	/** Returns true if the state is at least StatusKnown so we can query info about the game feature plugin */
	bool IsStatusKnown() const;

	/** Returns true if the plugin is available to download/load. Only call if IsStatusKnown is true */
	bool IsAvailable() const;

	/** Returns true if the plugin is in an error state */
	bool IsInErrorState() const;

	/** Whether this machine is allowed to be asynchronous */
	bool AllowAsyncLoading() const;

	/** Returns true if the plugin will stream dependencies after installing. Only call if IsStatusKnown is true */
	bool HasAssetStreamingDependencies() const;

	void SetWasLoadedAsBuiltIn();

	bool WasLoadedAsBuiltIn() const;

	/** If the plugin is activated already, we will retrieve its game feature data */
	UGameFeatureData* GetGameFeatureDataForActivePlugin();

	/** If the plugin is registered already, we will retrieve its game feature data */
	UGameFeatureData* GetGameFeatureDataForRegisteredPlugin(bool bCheckForRegistering = false);

	const FGameFeaturePluginStateMachineProperties& GetProperties() const;

	/** Returns true if the state machine is in an unrecoverable error state. */
	bool IsErrorStateUnrecoverable() const;

	/** Sets that the state machine is in an unrecoverable error. Used when the GFD is missing as that can't be recovered from. */
	void SetUnrecoverableError();

	/** Request to exclude this plugin from batch processing, for e.g. if the plugin is a dependency and has batch processing enabled */
	void ExcludeFromBatchProcessing();

	/** Helper static function to trigger state specific batch processing functions */
	static void BatchProcess(EGameFeaturePluginState State, TConstArrayView<UGameFeaturePluginStateMachine*> GFPSMs);	

private:
	/** Returns true if the specified state is not a transition state */
	bool IsValidTransitionState(EGameFeaturePluginState InState) const;

	/** Returns true if the specified state is a destination state */
	bool IsValidDestinationState(EGameFeaturePluginState InDestinationState) const;

	/** Returns true if the specified state is a error state */
	bool IsValidErrorState(EGameFeaturePluginState InDestinationState) const;

	/** Processes the current state and looks for state transitions */
	void UpdateStateMachine();

	/** Update Progress for current state */
	void UpdateCurrentStateProgress(float Progress);

	/** Helper to register the state machine with the manager as a state machine in transiton */
	void RegisterAsTransitioningStateMachine();

	/** Helper to unregister the state machine with the manager as a state machine in transiton */
	void UnregisterAsTransitioningStateMachine();
	
	/** Adds a batching request for current state if state can be batched */
	void CheckAddBatchingRequestForCurrentState();

	/** Cancels batching re1quest for current state if one exists */
	void CheckAndCancelBatchingRequestForCurrentState();

	/** Information about the current state */
	FGameFeaturePluginStateInfo CurrentStateInfo;

	/** The common properties that can be accessed by the states of the state machine */
	UPROPERTY(transient)
	FGameFeaturePluginStateMachineProperties StateProperties;

	/** All state machine state objects */
	TUniquePtr<FGameFeaturePluginState> AllStates[EGameFeaturePluginState::MAX];

	/** True when we are currently executing UpdateStateMachine, to avoid reentry */
	bool bInUpdateStateMachine;

	/** True when the state machine can not transition out of the current error state or if requested to would likely fail */
	bool bIsInUnrecoverableError = false;

	/** True when we are registered as a state machine with in flight transitions */
	bool bRegisteredAsTransitioningGFPSM;
};

GameFeatureTypes.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureTypes.h"

#include "Containers/UnrealString.h"
#include "Containers/StringView.h"

namespace UE::GameFeatures
{
#define GAME_FEATURE_PLUGIN_STATE_TO_STRING(inEnum, inText) case EGameFeaturePluginState::inEnum: return TEXT(#inEnum);
	FString ToString(EGameFeaturePluginState InType)
	{
		switch (InType)
		{
			GAME_FEATURE_PLUGIN_STATE_LIST(GAME_FEATURE_PLUGIN_STATE_TO_STRING)
		default:
			check(0);
			return FString();
		}
	}
#undef GAME_FEATURE_PLUGIN_STATE_TO_STRING
}

const TCHAR* LexToString(EGameFeatureURLOptions InOption)
{
#define GAME_FEATURE_PLUGIN_URL_OPTIONS_STRING(inEnum, inVal) case EGameFeatureURLOptions::inEnum: return TEXT(#inEnum);
	switch (InOption)
	{
		GAME_FEATURE_PLUGIN_URL_OPTIONS_LIST(GAME_FEATURE_PLUGIN_URL_OPTIONS_STRING)
	}
#undef GAME_FEATURE_PLUGIN_URL_OPTIONS_STRING

	check(0);
	return TEXT("");
}

void LexFromString(EGameFeatureURLOptions& ValueOut, const FStringView& StringIn)
{
	ValueOut = EGameFeatureURLOptions::None;
	
	for (EGameFeatureURLOptions OptionToCheck : MakeFlagsRange(EGameFeatureURLOptions::All))
	{
		if (FStringView(LexToString(OptionToCheck)) == StringIn)
		{
			ValueOut = OptionToCheck;
			return;
		}
	}
}


GameFeatureVersePathMapperCommandlet.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureVersePathMapperCommandlet.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryState.h"
#include "CoreGlobals.h"
#include "Engine/AssetManager.h"
#include "GameFeatureData.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Misc/App.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/Paths.h"
#include "Misc/PathViews.h"
#include "HAL/FileManager.h"
#include "Interfaces/ITargetPlatform.h"
#include "Interfaces/ITargetPlatformManagerModule.h"
#include "Interfaces/IPluginManager.h"
#include "Logging/StructuredLog.h"
#include "InstallBundleUtils.h"
#include "JsonObjectConverter.h"
#include "Algo/Transform.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureVersePathMapperCommandlet)

DEFINE_LOG_CATEGORY_STATIC(LogGameFeatureVersePathMapper, Log, All);

namespace GameFeatureVersePathMapper
{
	struct FArgs
	{
		FString DevARPath;

		FString OutputPath;

		const ITargetPlatform* TargetPlatform = nullptr;

		bool bWithCloudCookedPlugins = true;

		static TOptional<FArgs> Parse(const TCHAR* CmdLineParams)
		{
			UE_LOGFMT(LogGameFeatureVersePathMapper, Display, "Parsing command line");

			FArgs Args;

			// Optional path to dev asset registry
			FString DevARFilename;
			if (FParse::Value(CmdLineParams, TEXT("-DevAR="), DevARFilename))
			{
				if (IFileManager::Get().FileExists(*DevARFilename) && FPathViews::GetExtension(DevARFilename) == TEXTVIEW("bin"))
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Display, "Using dev asset registry path '{Path}'", DevARFilename);
					Args.DevARPath = DevARFilename;
				}
				else
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "-DevAR did not specify a valid path.");
					return {};
				}
			}

			// Required output path
			if (!FParse::Value(CmdLineParams, TEXT("-Output="), Args.OutputPath))
			{
				UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "-Output is required.");
				return {};
			}

			// Required target platform
			FString TargetPlatformName;
			if (FParse::Value(CmdLineParams, TEXT("-Platform="), TargetPlatformName))
			{
				if (const ITargetPlatform* TargetPlatform = GetTargetPlatformManagerRef().FindTargetPlatform(TargetPlatformName))
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Display, "Using target platform '{Platform}'", TargetPlatformName);
					Args.TargetPlatform = TargetPlatform;
				}
				else
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Could not find target platfom '{Platform}'.", TargetPlatformName);
					return {};
				}
			}
			else
			{
				UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "-Platform is required.");
				return {};
			}

			bool bSkipCloudCooked = false;
			if (FParse::Bool(CmdLineParams, TEXT("-SkipCloudCookPlugins="), bSkipCloudCooked))
			{
				Args.bWithCloudCookedPlugins = !bSkipCloudCooked;
			}

			return Args;
		}
	};

	FString GetVerseAppDomain()
	{
		FString AppDomain;
		if (!GConfig->GetString(TEXT("Verse"), TEXT("AppDomain"), AppDomain, GGameIni))
		{
			AppDomain = FPaths::Combine(TEXTVIEW("/"), FString(FApp::GetProjectName()) + TEXTVIEW(".com"));
		}
		AppDomain.RemoveFromEnd(TEXTVIEW("/"));
		return AppDomain;
	}

	FString GetAltVerseAppDomain()
	{
		FString AppDomain;
		if (!GConfig->GetString(TEXT("Verse"), TEXT("AltAppDomain"), AppDomain, GGameIni))
		{
			AppDomain = {};
		}
		AppDomain.RemoveFromEnd(TEXTVIEW("/"));
		return AppDomain;
	}

	class FInstallBundleResolver
	{
		TArray<TPair<FString, TArray<FRegexPattern>>> BundleRegexList;
		TMap<FString, FString> RegexMatchCache;

	public:
		FInstallBundleResolver(const TCHAR* IniPlatformName = nullptr)
		{
			FConfigFile MaybeLoadedConfig;
			const FConfigFile* InstallBundleConfig = IniPlatformName ?
				GConfig->FindOrLoadPlatformConfig(MaybeLoadedConfig, *GInstallBundleIni, IniPlatformName) :
				GConfig->FindConfigFile(GInstallBundleIni);

			// We want to load regex even if PlatformChunkID=-1 to make sure we map GFPs that are not packaged
			BundleRegexList = InstallBundleUtil::LoadBundleRegexFromConfig(*InstallBundleConfig);
		}

		FString Resolve(const FStringView& PluginName, const FString& ChunkPattern)
		{
			FString InstallBundleName = UGameFeaturesSubsystem::Get().GetInstallBundleName(PluginName);
			if (InstallBundleName.IsEmpty() && !ChunkPattern.IsEmpty())
			{
				if (FString* CachedInstallBundleName = RegexMatchCache.Find(ChunkPattern))
				{
					InstallBundleName = *CachedInstallBundleName;
				}
				else if (InstallBundleUtil::MatchBundleRegex(BundleRegexList, ChunkPattern, InstallBundleName))
				{
					RegexMatchCache.Add(ChunkPattern, InstallBundleName);
				}
			}

			return InstallBundleName;
		}
	};

	FConfigCacheIni* GetPlatformConfigCacheIni(const FString& IniPlatformName)
	{
#if WITH_EDITOR
		FConfigCacheIni* ConfigCache = FConfigCacheIni::ForPlatform(FName(IniPlatformName));
		if (!ConfigCache)
		{
			UE_LOGFMT(LogGameFeatureVersePathMapper, Warning, "Failed to find config for {PlatformName}", *IniPlatformName);
			ConfigCache = GConfig;
		}
#else
		FConfigCacheIni* ConfigCache = GConfig;
#endif
		return ConfigCache;
	}

	static bool PlatformChunksAreAlwaysResident(const ITargetPlatform* TargetPlatform /*= nullptr*/)
	{
		const FString& IniPlatformName = TargetPlatform ? TargetPlatform->IniPlatformName() : FPlatformProperties::IniPlatformName();
		FConfigCacheIni* ConfigCache = GetPlatformConfigCacheIni(IniPlatformName);

		bool bPlatformAlwaysResident = false;
		if (!ConfigCache->GetBool(TEXT("GameFeaturePlugins"), TEXT("bGFPAreAlwaysResident"), bPlatformAlwaysResident, GInstallBundleIni))
		{
			if (TargetPlatform)
			{
				bPlatformAlwaysResident = TargetPlatform->IsServerOnly() || TargetPlatform->HasEditorOnlyData();
			}
			else
			{
				// DS and cooked editor should always resolve to file protocol for now.
				bPlatformAlwaysResident = IsRunningDedicatedServer() || GIsEditor;
			}
		}

		return bPlatformAlwaysResident;
	}

	static FString GetChunkPatternFormat(const FString& IniPlatformName)
	{
		FConfigCacheIni* ConfigCache = GetPlatformConfigCacheIni(IniPlatformName);

		FString ChunkPatternFormat;
		if (!ConfigCache->GetString(TEXT("GameFeaturePlugins"), TEXT("GFPBundleRegexMatchPatternFormat"), ChunkPatternFormat, GInstallBundleIni))
		{
			ChunkPatternFormat = TEXTVIEW("chunk{Chunk}.pak");
		}

		return ChunkPatternFormat;
	}

	static FString GetChunkPattern(const FString& ChunkPatternFormat, const FString& ChunkName)
	{
		return FString::Format(*ChunkPatternFormat, FStringFormatNamedArguments{ {TEXT("Chunk"), ChunkName} });
	}

	static FString GetChunkPattern(const FString& ChunkPatternFormat, int32 Chunk)
	{
		return FString::Format(*ChunkPatternFormat, FStringFormatNamedArguments{ {TEXT("Chunk"), Chunk} });
	}

	static FString GetDevARPathForPlatform(FStringView PlatformName)
	{
		return FPaths::Combine(
			FPaths::ProjectSavedDir(), 
			TEXTVIEW("Cooked"), 
			PlatformName, 
			FApp::GetProjectName(), 
			TEXTVIEW("Metadata"), 
			TEXTVIEW("DevelopmentAssetRegistry.bin"));
	}

	static FString GetDevARPath(const FArgs& Args)
	{
		if (!Args.DevARPath.IsEmpty())
		{
			return Args.DevARPath;
		}

		if (Args.TargetPlatform)
		{
			return GetDevARPathForPlatform(Args.TargetPlatform->PlatformName());
		}

		return {};
	}

	template<class EnumeratorFunc>
	static TMap<FString, int32> FindGFPChunksImpl(const EnumeratorFunc& Enumerator)
	{
		const IAssetRegistry& AR = IAssetRegistry::GetChecked();

		FARFilter RawFilter;
#if !WITH_EDITORONLY_DATA
		// work-around for in-memory FAssetData not having chunks set
		RawFilter.bIncludeOnlyOnDiskAssets = true;
#endif
		RawFilter.bRecursiveClasses = true;
		RawFilter.ClassPaths.Add(UGameFeatureData::StaticClass()->GetClassPathName());

		FARCompiledFilter Filter;
		AR.CompileFilter(RawFilter, Filter);

		TMap<FString, int32> GFPChunks;

		FNameBuilder PackagePathBuilder;
		auto FindGFDChunks = [&PackagePathBuilder, &GFPChunks](const FAssetData& AssetData) -> bool
		{
			int32 ChunkId = -1;
			if (AssetData.GetChunkIDs().Num() > 0)
			{
				ChunkId = AssetData.GetChunkIDs()[0];
				if (AssetData.GetChunkIDs().Num() > 1)
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Warning, "Multiple Chunks found for {Package}, using chunk {Chunk}", AssetData.PackageName, ChunkId);
				}
			}
			AssetData.PackageName.ToString(PackagePathBuilder);
			if (FStringView(PackagePathBuilder.GetData(), PackagePathBuilder.Len()).StartsWith(TEXT("/Game/Developers")))
			{
				// Ignore "Developers" data
				return true;
			}
			FStringView PackageRoot = FPathViews::GetMountPointNameFromPath(PackagePathBuilder);
			GFPChunks.Emplace(PackageRoot, ChunkId);

			return true;
		};

		Enumerator(Filter, FindGFDChunks);

		// Find any GFPs that don't have content and assign them to chunk0
		const UGameFeaturesSubsystemSettings* GameFeaturesSettings = GetDefault<UGameFeaturesSubsystemSettings>();
		IPluginManager& PluginMan = IPluginManager::Get();
		TArray<TSharedRef<IPlugin>> AllPlugins = PluginMan.GetDiscoveredPlugins();
		for (const TSharedRef<IPlugin>& Plugin : AllPlugins)
		{
			if (Plugin->CanContainContent())
			{
				continue;
			}

			if (GFPChunks.Contains(Plugin->GetName()))
			{
				continue;
			}

			if (!GameFeaturesSettings->IsValidGameFeaturePlugin(Plugin->GetDescriptorFileName()))
			{
				continue;
			}

			GFPChunks.Emplace(Plugin->GetName(), 0);
		}

		return GFPChunks;
	}

	static TMap<FString, int32> FindGFPChunks(const FAssetRegistryState& DevAR)
	{
		return FindGFPChunksImpl([&DevAR](const FARCompiledFilter& Filter, TFunctionRef<bool(const FAssetData&)> Callback)
		{
			DevAR.EnumerateAssets(Filter, {}, Callback, 
				UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnmountedPaths | 
				UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnfilteredArAssets);
		});
	}

	static TMap<FString, int32> FindGFPChunks()
	{
		const IAssetRegistry& AR = IAssetRegistry::GetChecked();
		return FindGFPChunksImpl([&AR](const FARCompiledFilter& Filter, TFunctionRef<bool(const FAssetData&)> Callback)
		{
			AR.EnumerateAssets(Filter, Callback, UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnmountedPaths);
		});
	}

	TArray<FDLCInfo> FindGFPToDLC(const ITargetPlatform* TargetPlatform)
	{
		FConfigFile* InstallBundleConfig = nullptr;
		if (TargetPlatform)
		{
			FConfigFile MaybeLoadedConfig;
			const FString IniPlatformName = TargetPlatform->IniPlatformName();
			InstallBundleConfig = GConfig->FindOrLoadPlatformConfig(MaybeLoadedConfig, *GInstallBundleIni, *IniPlatformName);
		}
		else
		{
			InstallBundleConfig = GConfig->FindConfigFile(GInstallBundleIni);
		}

		TArray<FDLCInfo> FoundDLCInfo;
		const FString DLCInfoSectionPrefix(TEXT("DLCInfo "));
		for (const TPair<FString, FConfigSection>& Pair : *InstallBundleConfig)
		{
			const FString& Section = Pair.Key;
			if (!Section.StartsWith(DLCInfoSectionPrefix))
				continue;

			FString InstallBundleName;
			if (!InstallBundleConfig->GetString(*Section, TEXT("InstallBundleName"), InstallBundleName))
				continue;

			TArray<FString> Plugins;
			if (!InstallBundleConfig->GetArray(*Section, TEXT("Plugins"), Plugins))
				continue;

			FString DLCName = Section.RightChop(DLCInfoSectionPrefix.Len());
			FDLCInfo& NewDLCInfo = FoundDLCInfo.Emplace_GetRef();
			NewDLCInfo.DLCName = MoveTemp(DLCName);
			NewDLCInfo.InstallBundleName = MoveTemp(InstallBundleName);
			NewDLCInfo.Plugins = MoveTemp(Plugins);
		}

		return FoundDLCInfo;
	}

	static bool IsChunkAlwaysResident(TConstArrayView<int32> AlwaysResidentChunks, int32 Chunk)
	{
		return Chunk < 0 || AlwaysResidentChunks.Contains(Chunk);
	}

	// Filter GFPs cooked of out of band
	static bool IsGFPUpluginInBaseBuild(FStringView GFPName)
	{
		// Consider a GFP part of the base build if its plugin was added outside of the
		// GFP statemachine. If there are cases where this doesn't hold, then its probably
		// better to generate an explicit manifest.

		UGameFeaturesSubsystem& GFPSys = UGameFeaturesSubsystem::Get();

		bool bGFPAddedUplugin = false;

		FString GFPURL;
		if (GFPSys.GetPluginURLByName(GFPName, GFPURL))
		{
			GFPSys.GetGameFeatureControlsUPlugin(GFPURL, bGFPAddedUplugin);
		}

		return !bGFPAddedUplugin;
	}

	bool FDepthFirstGameFeatureSorter::Visit(const FName Plugin, TFunctionRef<void(FName, const FString&)> AddOutput)
	{
		const FGameFeaturePluginInfo* MaybePluginInfo = GfpInfoMap.Find(Plugin);
		if (!MaybePluginInfo)
		{
			UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "DepthFirstGameFeatureSorter: could not find {PluginName}", Plugin);
			return false;
		}
		const FGameFeaturePluginInfo& PluginInfo = *MaybePluginInfo;

		// Add a scope here to make sure VisitState isn't used later. It can become invalid if VisitedPlugins is resized
		{
			EVisitState& VisitState = VisitedPlugins.FindOrAdd(Plugin, EVisitState::None);
			if (VisitState == EVisitState::Visiting)
			{
				UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "DepthFirstGameFeatureSorter: Cycle detected in plugin dependencies with {PluginName}", Plugin);
				return false;
			}

			if (VisitState == EVisitState::Visited)
			{
				return true;
			}

			VisitState = EVisitState::Visiting;
		}

		for (const FName DepPlugin : PluginInfo.Dependencies)
		{
			if (!Visit(DepPlugin, AddOutput))
			{
				return false;
			}
		}

		VisitedPlugins.FindChecked(Plugin) = EVisitState::Visited;
		if (bIncludeVirtualNodes || !PluginInfo.GfpUri.IsEmpty()) // An empty URI means this is virtual node that only exists for Verse path resolution
		{
			AddOutput(Plugin, PluginInfo.GfpUri);
		}
		return true;
	}

	bool FDepthFirstGameFeatureSorter::Sort(TFunctionRef<FName()> GetNextRootPlugin, TFunctionRef<void(FName, const FString&)> AddOutput)
	{
		for (FName RootPlugin = GetNextRootPlugin(); !RootPlugin.IsNone(); RootPlugin = GetNextRootPlugin())
		{
			if (!Visit(RootPlugin, AddOutput))
			{
				return false;
			}
		}
		return true;
	}

	bool FDepthFirstGameFeatureSorter::Sort(TConstArrayView<FName> RootPlugins, TFunctionRef<void(FName, const FString&)> AddOutput)
	{
		return Sort(
			[RootPlugins, i = int32(0)]() mutable -> FName
			{
				if (!RootPlugins.IsValidIndex(i))
				{
					return {};
				}
				return RootPlugins[i++];
			},
			AddOutput);
	}

	bool FDepthFirstGameFeatureSorter::Sort(TConstArrayView<FName> RootPlugins, TArray<FName>& OutPlugins)
	{
		return Sort(
			[RootPlugins, i = int32(0)]() mutable -> FName
			{
				if (!RootPlugins.IsValidIndex(i))
				{
					return {};
				}
				return RootPlugins[i++];
			}, 
			[&OutPlugins](FName OutPlugin, const FString& URI)
			{
				OutPlugins.Add(OutPlugin);
			});
	}

	TOptional<FGameFeatureVersePathLookup> BuildLookup(
		const ITargetPlatform* TargetPlatform /*= nullptr*/,
		const FAssetRegistryState* DevAR /*= nullptr*/,
		EBuildLookupOptions Options /*= EBuildLookupOptions::None*/)
	{
		TRACE_CPUPROFILER_EVENT_SCOPE_STR("GameFeatureVersePathMapper::BuildLookup");
		const TMap<FString, int32> GFPChunks = DevAR ? FindGFPChunks(*DevAR) : FindGFPChunks();
		const TArray<FDLCInfo> DLCInfos = FindGFPToDLC(TargetPlatform);
		TSet<FString> PossibleGFPs;
		IPluginManager& PluginMan = IPluginManager::Get();

		FInstallBundleResolver InstallBundleResolver(TargetPlatform ? *TargetPlatform->IniPlatformName() : nullptr);

		const FString AppDomain = GameFeatureVersePathMapper::GetVerseAppDomain();
		const FString GameFeatureRootVersePath = UGameFeatureVersePathMapperCommandlet::GetGameFeatureRootVersePath();
		
		const FString& IniPlatformName = TargetPlatform ? TargetPlatform->IniPlatformName() : FPlatformProperties::IniPlatformName();
		const FString ChunkPatternFormat = GetChunkPatternFormat(IniPlatformName);
		const bool bPlatformChunksAreAlwaysResident = PlatformChunksAreAlwaysResident(TargetPlatform);

		TMap<int32, FString> ChunkIdStringOverride;
		UAssetManager::Get().GetPakChunkIdToStringMapping(IniPlatformName, ChunkIdStringOverride);
		const FString NamedChunkPatternFormat = FString::Printf(TEXT("chunk%c{Chunk}%c.pak"), NAMED_PAK_CHUNK_DELIMITER_CHAR, NAMED_PAK_CHUNK_DELIMITER_CHAR);

		FString TargetPlatformName = TargetPlatform ? TargetPlatform->IniPlatformName() : FPlatformMisc::GetUBTPlatform();
		if (TargetPlatformName.Equals(TEXT("Windows"), ESearchCase::IgnoreCase))
		{
			// legacy change of windows -> win64 as that's how SupportedTargetPlatforms expects windows.
			TargetPlatformName = TEXT("Win64");
		}
		struct ChunkOrBundle
		{
			int32 Chunk = INDEX_NONE;
			FString BundleName;
		};
		TMap<FString, ChunkOrBundle> GFPToChunkOrBundle;
		GFPToChunkOrBundle.Reserve(GFPChunks.Num());
		PossibleGFPs.Reserve(GFPChunks.Num());
		for (const TPair<FString, int32>& Pair : GFPChunks)
		{
			ChunkOrBundle& NewChunkOrBundle = GFPToChunkOrBundle.Add(Pair.Key);
			NewChunkOrBundle.Chunk = Pair.Value;
			PossibleGFPs.Add(Pair.Key);
		}
		
		for (const FDLCInfo& DLC : DLCInfos)
		{
			PossibleGFPs.Reserve(PossibleGFPs.Num() + DLC.Plugins.Num());
			for (const FString& Plugin : DLC.Plugins)
			{
				ChunkOrBundle& NewChunkOrBundle = GFPToChunkOrBundle.FindOrAdd(Plugin);
				if (NewChunkOrBundle.Chunk == 0 || NewChunkOrBundle.Chunk == INDEX_NONE)
				{
					NewChunkOrBundle.BundleName = DLC.InstallBundleName;
				}
				PossibleGFPs.Add(Plugin);
			}
		}

		FGameFeatureVersePathLookup Output;
		for (const TPair<FString, ChunkOrBundle>& Pair : GFPToChunkOrBundle)
		{
			TSharedPtr<IPlugin> Plugin = PluginMan.FindPlugin(Pair.Key);
			if (!Plugin)
			{
				UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Could not find uplugin {PluginName}", Pair.Key);
				continue;
			}

			FStringView PluginNameView(Plugin->GetName());
			FName PluginName(PluginNameView);
			if (EnumHasAnyFlags(Options, EBuildLookupOptions::OnlyBaseBuildPlugins))
			{
				if (!IsGFPUpluginInBaseBuild(PluginNameView))
				{
					continue;
				}
			}

			// Skip plugins that won't be enabled on the platform.
			if (!Plugin->GetDescriptor().SupportsTargetPlatform(TargetPlatformName))
			{
				continue;
			}

			auto PluginIsCloudCooked = [](const TSharedRef<IPlugin>& Plugin) -> bool
				{
					bool bDynamicModule = false;
					FGameFeaturePluginDetails PluginDetails;
					if (UGameFeaturesSubsystem::Get().GetBuiltInGameFeaturePluginDetails(Plugin, PluginDetails))
					{
						const TSharedPtr<FJsonValue>& CookBehavior = PluginDetails.AdditionalMetadata.FindRef(TEXT("CookBehavior"));
						if (CookBehavior.IsValid() && CookBehavior->Type == EJson::Object)
						{
							FString CookType;
							CookBehavior->AsObject()->TryGetStringField(TEXT("Type"), CookType);
							bDynamicModule = CookType.Compare(TEXT("ContentWorker"), ESearchCase::IgnoreCase) == 0;
						}

					}
					return bDynamicModule;
				};
			
			if (!EnumHasAnyFlags(Options, EBuildLookupOptions::WithCloudCookPlugins) && PluginIsCloudCooked(Plugin.ToSharedRef()))
			{
				continue;
			}

			Output.VersePathToGfpMap.Add(FPaths::Combine(GameFeatureRootVersePath, PluginNameView), PluginName);

			// Add a virtual GFP to support plugin specified Verse paths
			if (!Plugin->GetVersePath().IsEmpty() && 
				Plugin->GetVersePath() != AppDomain) // Filter out references to the root path, we don't wan't to allow resolving all content (and we don't register sub-paths)
			{
				// Add a virtual GFP with this Verse path that depends on this GFP
				FName& VirtualGFPName = Output.VersePathToGfpMap.FindOrAdd(Plugin->GetVersePath());
				if (VirtualGFPName.IsNone())
				{
					VirtualGFPName = FName(FStringView(TEXTVIEW("V_") + Plugin->GetVersePath()));
				}

				FGameFeaturePluginInfo& GfpInfo = Output.GfpInfoMap.FindOrAdd(VirtualGFPName);
				GfpInfo.Dependencies.Add(PluginName);
			}

			FGameFeaturePluginInfo& GfpInfo = Output.GfpInfoMap.Add(PluginName);

			const FString DescriptorFileName = FPaths::CreateStandardFilename(Plugin->GetDescriptorFileName());

			const ChunkOrBundle& ChunkOrBundle = Pair.Value;
			FString InstallBundleName;
			if (!ChunkOrBundle.BundleName.IsEmpty())
			{
				InstallBundleName = ChunkOrBundle.BundleName;
			}
			else
			{
				const bool bIsChunkAlwaysResident = bPlatformChunksAreAlwaysResident;
				FString ChunkPattern;
				if (bIsChunkAlwaysResident)
				{
					// pass. ChunkPattern is empty.
				}
				else if (ChunkIdStringOverride.Contains(ChunkOrBundle.Chunk))
				{
					ChunkPattern = GetChunkPattern(NamedChunkPatternFormat, Plugin->GetName());
				}
				else
				{
					ChunkPattern = GetChunkPattern(ChunkPatternFormat, ChunkOrBundle.Chunk);
				}
				InstallBundleName = bIsChunkAlwaysResident ? FString() : InstallBundleResolver.Resolve(PluginNameView, ChunkPattern);
			}

			GfpInfo.GfpUri = (InstallBundleName.IsEmpty()) ?
				UGameFeaturesSubsystem::GetPluginURL_FileProtocol(DescriptorFileName) :
				UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(DescriptorFileName, InstallBundleName);

			for (const FPluginReferenceDescriptor& Dependency : Plugin->GetDescriptor().Plugins)
			{
				// Currently GameFeatureSubsystem only checks bEnabled to determine if it should wait on a dependency, so match that logic here
				if (!Dependency.bEnabled)
				{
					continue;
				}

				if (!PossibleGFPs.Contains(Dependency.Name))
				{
					// Dependency is not a GFP
					continue;
				}

				if (!Dependency.IsSupportedTargetPlatform(TargetPlatformName))
				{
					continue;
				}
				TSharedPtr<IPlugin> DepPlugin = PluginMan.FindPlugin(Dependency.Name);
				if (!DepPlugin)
				{
					UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Could not find uplugin dependency {PluginName}", Dependency.Name);
					continue;
				}
				if (!DepPlugin->GetDescriptor().SupportsTargetPlatform(TargetPlatformName))
				{
					continue;
				}

				if (!EnumHasAnyFlags(Options, EBuildLookupOptions::WithCloudCookPlugins) && PluginIsCloudCooked(DepPlugin.ToSharedRef()))
				{
					continue;
				}

				GfpInfo.Dependencies.Emplace(FStringView(Dependency.Name));
			}
		}

		check(Output.VersePathToGfpMap.Num() == Output.GfpInfoMap.Num());

		return Output;
	}
}

int32 UGameFeatureVersePathMapperCommandlet::Main(const FString& CmdLineParams)
{
	const TOptional<GameFeatureVersePathMapper::FArgs> MaybeArgs = GameFeatureVersePathMapper::FArgs::Parse(*CmdLineParams);
	if (!MaybeArgs)
	{
		// Parse function should print errors
		return 1;
	}
	const GameFeatureVersePathMapper::FArgs& Args = MaybeArgs.GetValue();

	FString DevArPath = GameFeatureVersePathMapper::GetDevARPath(Args);
	if (DevArPath.IsEmpty() && !FPaths::FileExists(DevArPath))
	{
		UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Could not find development asset registry at '{Path}'", DevArPath);
		return 1;
	}

	FAssetRegistryState DevAR;
	if (!FAssetRegistryState::LoadFromDisk(*DevArPath, FAssetRegistryLoadOptions(), DevAR))
	{
		UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Failed to load development asset registry from {Path}", DevArPath);
		return 1;
	}

	GameFeatureVersePathMapper::EBuildLookupOptions BuildOptions = GameFeatureVersePathMapper::EBuildLookupOptions::None;
	if (Args.bWithCloudCookedPlugins)
	{
		BuildOptions |= GameFeatureVersePathMapper::EBuildLookupOptions::WithCloudCookPlugins;
	}
	TOptional<GameFeatureVersePathMapper::FGameFeatureVersePathLookup> MaybeLookup = GameFeatureVersePathMapper::BuildLookup(Args.TargetPlatform, &DevAR, BuildOptions);
	if (!MaybeLookup)
	{
		// BuildLookup will emit errors
		return 1;
	}
	GameFeatureVersePathMapper::FGameFeatureVersePathLookup& Lookup = *MaybeLookup;

	TSharedRef<FJsonObject> OutJsonObject = MakeShared<FJsonObject>();
	{
		{
			// Reversing the VersePathToGfpMap makes it more natural for the registration API
			TMap<FName, TSharedRef<FJsonValueString>> TempGfpVersePathMap;
			TempGfpVersePathMap.Reserve(Lookup.VersePathToGfpMap.Num());
			for (const TPair<FString, FName>& Pair : Lookup.VersePathToGfpMap)
			{
				TempGfpVersePathMap.Emplace(Pair.Value, MakeShared<FJsonValueString>(Pair.Key));
			}

			TSharedRef<FJsonObject> GfpVersePathMap = MakeShared<FJsonObject>();

			// Sort the reversed map in dependency order
			GameFeatureVersePathMapper::FDepthFirstGameFeatureSorter Sorter(Lookup.GfpInfoMap, true /*bIncludeVirtualNodes*/);
			Sorter.Sort(
				[It = TempGfpVersePathMap.CreateConstIterator()]() mutable -> FName
				{
					if (!It)
					{
						return {};
					}
					FName Plugin = It.Key();
					++It;
					return Plugin;
				},
				[&TempGfpVersePathMap, GfpVersePathMap](FName OutPlugin, const FString& OutGfpUri)
				{
					GfpVersePathMap->Values.Add(OutPlugin.ToString(), TempGfpVersePathMap.FindChecked(OutPlugin));
				});

			OutJsonObject->Values.Add(TEXT("GfpVersePathMap"), MakeShared<FJsonValueObject>(GfpVersePathMap));
		}

		{
			TSharedRef<FJsonObject> GfpInfoMap = MakeShared<FJsonObject>();
			for (TPair<FName, GameFeatureVersePathMapper::FGameFeaturePluginInfo>& Pair : Lookup.GfpInfoMap)
			{
				TSharedRef<FJsonObject> GfpInfo = MakeShared<FJsonObject>();
				GfpInfo->Values.Add(TEXT("GfpUri"), MakeShared<FJsonValueString>(MoveTemp(Pair.Value.GfpUri)));

				TArray<TSharedPtr<FJsonValue>> Dependencies;
				Dependencies.Reserve(Pair.Value.Dependencies.Num());
				Algo::Transform(Pair.Value.Dependencies, Dependencies, [](FName Name) { return MakeShared<FJsonValueString>(Name.ToString()); });
				GfpInfo->Values.Add(TEXT("Dependencies"), MakeShared<FJsonValueArray>(MoveTemp(Dependencies)));

				GfpInfoMap->Values.Add(Pair.Key.ToString(), MakeShared<FJsonValueObject>(GfpInfo));
			}

			OutJsonObject->Values.Add(TEXT("GfpInfoMap"), MakeShared<FJsonValueObject>(GfpInfoMap));
		}
	}

	IFileManager::Get().MakeDirectory(*FPaths::GetPath(Args.OutputPath));

	TUniquePtr<FArchive> FileWriter(IFileManager::Get().CreateFileWriter(*Args.OutputPath));
	TSharedRef<TJsonWriter<UTF8CHAR>> JsonWriter = TJsonWriterFactory<UTF8CHAR>::Create(FileWriter.Get());
	if (!FJsonSerializer::Serialize(OutJsonObject, JsonWriter))
	{
		UE_LOGFMT(LogGameFeatureVersePathMapper, Error, "Failed to save output file at {Path}", Args.OutputPath);
		return 1;
	}

	return 0;
}

/*static*/ FString UGameFeatureVersePathMapperCommandlet::GetGameFeatureRootVersePath()
{
	return FPaths::Combine(GameFeatureVersePathMapper::GetVerseAppDomain(), TEXTVIEW("GameFeatures"));
}

GameFeaturesModule.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "Modules/ModuleManager.h"

IMPLEMENT_MODULE(FDefaultModuleImpl, GameFeatures)

GameFeaturesProjectPolicies.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturesProjectPolicies.h"
#include "GameFeaturesSubsystemSettings.h"
#include "GameFeatureData.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "Engine/Engine.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeaturesProjectPolicies)

namespace UE::GameFeatures
{
	static TAutoConsoleVariable<bool> CVarForceAsyncLoad(TEXT("GameFeaturePlugin.ForceAsyncLoad"),
		false,
		TEXT("Enable to force use of async loading even if normally not allowed"));

	const TAutoConsoleVariable<bool>& GetCVarForceAsyncLoad()
	{
		return CVarForceAsyncLoad;
	}
}

void UDefaultGameFeaturesProjectPolicies::InitGameFeatureManager()
{
	UE_LOG(LogGameFeatures, Log, TEXT("Scanning for built-in game feature plugins"));

	auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
	{
		// By default, force all initially loaded plugins to synchronously load, this overrides the behavior of GameFeaturePlugin.AsyncLoad which will be used for later loads
		OutOptions.bForceSyncLoading = true;

		// By default, no plugins are filtered so we expect all built-in dependencies to be created before their parent GFPs
		OutOptions.bLogWarningOnForcedDependencyCreation = true;

		return true;
	};

	UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter);
}

void UDefaultGameFeaturesProjectPolicies::GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const
{
	// By default, load both unless we are a dedicated server or client only cooked build
	bLoadClientData = !IsRunningDedicatedServer();
	bLoadServerData = !IsRunningClientOnly();
}

const TArray<FName> UDefaultGameFeaturesProjectPolicies::GetPreloadBundleStateForGameFeature() const
{
	// By default, use the bundles corresponding to loading mode
	bool bLoadClientData, bLoadServerData;
	GetGameFeatureLoadingMode(bLoadClientData, bLoadServerData);

	TArray<FName> FeatureBundles;
	if (bLoadClientData)
	{
		FeatureBundles.Add(UGameFeaturesSubsystemSettings::LoadStateClient);
	}
	if (bLoadServerData)
	{
		FeatureBundles.Add(UGameFeaturesSubsystemSettings::LoadStateServer);
	}
	return FeatureBundles;
}

bool UGameFeaturesProjectPolicies::IsLoadingStartupPlugins() const
{
	if (GIsRunning && GFrameCounter > 2)
	{
		// Initial loading can take 2 frames
		return false;
	}

	if (IsRunningCommandlet() && GEngine && GEngine->IsInitialized())
	{
		// Commandlets may not tick, so done after initialization
		return false;
	}

	return true;
}

bool UGameFeaturesProjectPolicies::GetGameFeaturePluginURL(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL) const
{
	// It could still be a GFP, but state machine may not have been created for it yet
	// Check if it is a built-in GFP
	const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();
	if (!PluginDescriptorFilename.IsEmpty())
	{
		if (GetDefault<UGameFeaturesSubsystemSettings>()->IsValidGameFeaturePlugin(FPaths::ConvertRelativePathToFull(PluginDescriptorFilename)))
		{
			OutPluginURL = UGameFeaturesSubsystem::GetPluginURL_FileProtocol(PluginDescriptorFilename);
		}
		else
		{
			OutPluginURL = TEXT("");
		}
		return true;
	}
	return false;
}

bool UGameFeaturesProjectPolicies::WillPluginBeCooked(const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails) const
{
	return true;
}

TValueOrError<FString, FString> UGameFeaturesProjectPolicies::ResolvePluginDependency(const FString& PluginURL, const FString& DependencyName, FPluginDependencyDetails& OutDetails) const
{
	OutDetails = {};
	return ResolvePluginDependency(PluginURL, DependencyName);
}

TValueOrError<FString, FString> UGameFeaturesProjectPolicies::ResolvePluginDependency(const FString& PluginURL, const FString& DependencyName) const
{
	FString DependencyURL;
	bool bResolvedDependency = false;

	// Check if UGameFeaturesSubsystem is already aware of it
	if (UGameFeaturesSubsystem::Get().GetPluginURLByName(DependencyName, DependencyURL))
	{
		bResolvedDependency = true;
	}
	// Check if the dependency plugin exists yet (should be true for all built-in plugins)
	else if (TSharedPtr<IPlugin> DependencyPlugin = IPluginManager::Get().FindPlugin(DependencyName))
	{
		bResolvedDependency = GetGameFeaturePluginURL(DependencyPlugin.ToSharedRef(), DependencyURL);
	}

	if (bResolvedDependency)
	{
		return MakeValue(MoveTemp(DependencyURL));
	}

	return MakeError(TEXT("NotFound"));
}

TValueOrError<TArray<EStreamingAssetInstallMode>, FString> UGameFeaturesProjectPolicies::GetStreamingAssetInstallModes(FStringView PluginURL, TConstArrayView<FName> InstallBundleNames) const
{
	TArray<EStreamingAssetInstallMode> InstallModes;
	InstallModes.Reserve(InstallBundleNames.Num());
	while (InstallModes.Num() < InstallBundleNames.Num())
	{
		InstallModes.Emplace(EStreamingAssetInstallMode::Full);
	}

	return MakeValue(MoveTemp(InstallModes));
}

void UGameFeaturesProjectPolicies::ExplicitLoadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate, const bool bActivateGameFeatures)
{
	if (bActivateGameFeatures)
	{
		UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL, CompleteDelegate);
	}
	else
	{
		UGameFeaturesSubsystem::Get().LoadGameFeaturePlugin(PluginURL, CompleteDelegate);
	}
}

bool UGameFeaturesProjectPolicies::AllowAsyncLoad(FStringView /*PluginURL*/) const
{
	return !IsRunningCommandlet() || UE::GameFeatures::CVarForceAsyncLoad.GetValueOnGameThread();
}

FString UGameFeaturesProjectPolicies::GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	return UGameFeatureData::GetInstallBundleName(PluginName, bEvenIfDoesntExist);
}

FString UGameFeaturesProjectPolicies::GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	return UGameFeatureData::GetOptionalInstallBundleName(PluginName, bEvenIfDoesntExist);
}

GameFeaturesSubsystem.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturesSubsystem.h"
#include "Algo/Accumulate.h"
#include "Algo/AllOf.h"
#include "Algo/AnyOf.h"
#include "Algo/TopologicalSort.h"
#include "Algo/Unique.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "BundlePrereqCombinedStatusHelper.h"
#include "Experimental/UnifiedError/UnifiedError.h"
#include "Dom/JsonValue.h"
#include "GameFeaturesSubsystemSettings.h"
#include "GameFeaturesProjectPolicies.h"
#include "GameFeatureData.h"
#include "GameFeaturePluginStateMachine.h"
#include "GameFeatureStateChangeObserver.h"
#include "GameplayTagsManager.h"
#include "Interfaces/IPluginManager.h"
#include "IO/IoStoreOnDemand.h"
#include "Logging/StructuredLog.h"
#include "Misc/App.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/PathViews.h"
#include "Misc/ScopeRWLock.h"
#include "Serialization/JsonSerializer.h"
#include "Stats/StatsMisc.h"
#include "String/ParseTokens.h"
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
#include "Engine/AssetManagerSettings.h"
#include "InstallBundleTypes.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeaturesSubsystem)

DEFINE_LOG_CATEGORY(LogGameFeatures);

FSimpleMulticastDelegate UGameFeaturesSubsystem::OnGameFeaturePolicyPreInit;

namespace UE::GameFeatures
{
	static const FString SubsystemErrorNamespace(TEXT("GameFeaturePlugin.Subsystem."));

	namespace PluginURLStructureInfo
	{
		const TCHAR* OptionAssignOperator = TEXT("=");
		const TCHAR* OptionSeperator = TEXT("?");
		const TCHAR* OptionListSeperator = TEXT(",");
	}

	namespace CommonErrorCodes
	{
		const TCHAR* PluginNotAllowed = TEXT("Plugin_Denied_By_GameSpecificPolicy");
		const TCHAR* PluginFiltered = TEXT("Plugin_Filtered_By_GameSpecificPolicy");
		const TCHAR* PluginDetailsNotFound = TEXT("Plugin_Details_Not_Found");
		const TCHAR* PluginURLNotFound = TEXT("Plugin_URL_Not_Found");
		const TCHAR* DependencyFailedRegister = TEXT("Failed_Dependency_Register");
		const TCHAR* BadURL = TEXT("Bad_URL");
		const TCHAR* UnreachableState = TEXT("State_Currently_Unreachable");

		const TCHAR* CancelAddonCode = TEXT("_Cancel");
	}

#define GAME_FEATURE_PLUGIN_PROTOCOL_PREFIX(inEnum, inString) case EGameFeaturePluginProtocol::inEnum: return inString;
	static const TCHAR* GameFeaturePluginProtocolPrefix(EGameFeaturePluginProtocol Protocol)
	{
		switch (Protocol)
		{
			GAME_FEATURE_PLUGIN_PROTOCOL_LIST(GAME_FEATURE_PLUGIN_PROTOCOL_PREFIX)
		}

		check(false);
		return nullptr;
	}
#undef GAME_FEATURE_PLUGIN_PROTOCOL_PREFIX

	static bool GCachePluginDetails = true;
	static FAutoConsoleVariableRef CVarCachePluginDetails(
		TEXT("GameFeaturePlugin.CachePluginDetails"),
		GCachePluginDetails,
		TEXT("Should use plugin details caching."),
		ECVF_Default);

#if !UE_BUILD_SHIPPING
	static float GBuiltInPluginLoadTimeReportThreshold = 3.0;
	static FAutoConsoleVariableRef CVarBuiltInPluginLoadTimeReportThreshold(
		TEXT("GameFeaturePlugin.BuiltInPluginLoadTimeReportThreshold"),
		GBuiltInPluginLoadTimeReportThreshold,
		TEXT("Built-in plugins that take longer than this amount of time, in seconds, will be reported to the log during startup in non-shipping builds."),
		ECVF_Default);

	static float GBuiltInPluginLoadTimeErrorThreshold = 600;
	static FAutoConsoleVariableRef CVarBuiltInPluginLoadTimeErrorThreshold(
		TEXT("GameFeaturePlugin.BuiltInPluginLoadTimeErrorThreshold"),
		GBuiltInPluginLoadTimeErrorThreshold,
		TEXT("Built-in plugins that take longer than this amount of time, in seconds, will cause an error during startup in non-shipping builds."),
		ECVF_Default);

	static int32 GBuiltInPluginLoadTimeMaxReportCount = 10;
	static FAutoConsoleVariableRef CVarBuiltInPluginLoadTimeMaxReportCount(
		TEXT("GameFeaturePlugin.BuiltInPluginLoadTimeMaxReportCount"),
		GBuiltInPluginLoadTimeMaxReportCount,
		TEXT("When listing worst offenders for built-in plugin load time, show no more than this many plugins, to reduce log spam."),
		ECVF_Default);
#endif // !UE_BUILD_SHIPPING

	static bool bTrimNonStartupEnabledPlugins = true;
	static FAutoConsoleVariableRef CVarTrimNonBuiltInPlugins(
		TEXT("GameFeaturePlugin.TrimNonStartupEnabledPlugins"),
		bTrimNonStartupEnabledPlugins,
		TEXT("When call LoadBuiltInGameFeaturePlugins should only the plugins that were built by this target exe be considered built in."),
		ECVF_Default);

	static FAutoConsoleVariable CVarTrackLoadStats(
		TEXT("GameFeaturePlugin.TrackLoadStats"),
		false,
		TEXT("Track all packages loaded by each GFP. If a package is loaded by multiple GFPs, it will only be ")
		TEXT("attributed to the first one that actually causes the load. Stats can be output with ")
		TEXT("GameFeaturePlugin.PrintLoadStats."));

	TOptional<FString> GetPluginUrlForConsoleCommand(const TArray<FString>& Args, FOutputDevice& Ar)
	{
		TOptional<FString> PluginURL;
		if (Args.Num() > 0)
		{
			EGameFeaturePluginProtocol Protocol = UGameFeaturesSubsystem::GetPluginURLProtocol(Args[0]);
			if (Protocol != EGameFeaturePluginProtocol::Unknown)
			{
				PluginURL.Emplace(Args[0]);
			}
			else
			{
				FString PluginURLStr;
				if (UGameFeaturesSubsystem::Get().GetPluginURLByName(Args[0], /*out*/ PluginURLStr))
				{
					PluginURL.Emplace(MoveTemp(PluginURLStr));
				}
			}
		}

		if (PluginURL)
		{
			Ar.Logf(TEXT("Using URL %s for console command"), *PluginURL.GetValue());
		}
		else
		{
			Ar.Logf(TEXT("Expected a game feature plugin URL or name as an argument"));
		}

		return PluginURL;
	}

#if UE_WITH_PACKAGE_ACCESS_TRACKING
	class FPackageLoadTracker final : public FUObjectArray::FUObjectCreateListener
	{
		struct FStat
		{
			FName Package;
			FName Op;
		};

		/**
		 * Store which packages caused other packages to be loaded. Only tracks loads that occur due to a GFP (direct
		 * and indirect, sync and async). Keys are a package name and values are a list of package names directly loaded
		 * by said package. Indirect loads are gathered by walking the list of loaded packages and querying the map
		 * recursively.
		 */
		TArray<FName> Roots;
		TMap<FName, TArray<FStat>> Stats;
		TArray<FName> UntrackedLoads;

	public:

		FPackageLoadTracker()
		{
			GUObjectArray.AddUObjectCreateListener(this);
		}

		~FPackageLoadTracker()
		{
			GUObjectArray.RemoveUObjectCreateListener(this);
		}

		virtual void OnUObjectArrayShutdown() override
		{
			GUObjectArray.RemoveUObjectCreateListener(this);
		}

		virtual void NotifyUObjectCreated(const UObjectBase* Object, int32 Index) override
		{
			if (Object->GetClass() == UPackage::StaticClass())
			{
				using namespace PackageAccessTracking_Private;

				const UPackage* Package = Cast<UPackage>(Object);
				FString Path = Package->GetPathName();

				// Async loading will cache tracking data when a load is requested and restore it when processing the
				// load later. This means we have tracking for async loads, but it only caches the PackageName and
				// OpName from FTrackedData so we can only rely on those two fields.
				const FTrackedData* Data = FPackageAccessRefScope::GetCurrentThreadAccumulatedData();
				if (Data && !Data->PackageName.IsNone())
				{
					FName LoaderName = Data->PackageName;
					if (TArray<FStat>* LoadedPackages = Stats.Find(LoaderName))
					{
						FName LoadeeName = Object->GetFName();
						LoadedPackages->Add({ LoadeeName, Data->OpName });
						Stats.Add(LoadeeName);
					}
				}
				else
				{
					FName LoadeeName = Object->GetFName();
					UntrackedLoads.Add(LoadeeName);
				}
			}
		}

		void AddRoot(FName PackageName)
		{
			Roots.Add(PackageName);
			Stats.Add(PackageName);
		}

		void PrintLoadStats(bool bShowUntracked) const
		{
			struct Local
			{
				static void VisitPackage(FStringBuilderBase& Buf, const TMap<FName, TArray<FStat>>& Stats, FName Package)
				{
					if (const TArray<FStat>* LoadedList = Stats.Find(Package))
					{
						for (const FStat& Loaded : *LoadedList)
						{
							Buf << TEXT("\n\t\t") << Loaded.Op << TEXT(",");
							int32 Pad = FMath::Max(0, int32(23 - Loaded.Op.GetStringLength()));
							Buf.Appendf(TEXT("%*s"), Pad, TEXT(""));
							Buf << Loaded.Package;
							VisitPackage(Buf, Stats, Loaded.Package);
						}
					}
				}
			};

			TStringBuilder<1024> Buf;
			Buf << TEXT("GameFeaturePlugin Package Load Stats");
			
			for (FName Root : Roots)
			{
				Buf << TEXT("\n\t") << Root << TEXT(": {");
				Local::VisitPackage(Buf, Stats, Root);
				Buf << TEXT("\n\t}");
			}

			if (bShowUntracked)
			{
				Buf << TEXT("\n\tUntracked: {");
				for (FName Untracked : UntrackedLoads)
				{
					Buf << TEXT("\n\t\t") << Untracked;
				}
				Buf << TEXT("\n\t}");
			}
			else
			{
				Buf << TEXT("\n\tUntracked: ") << UntrackedLoads.Num();
			}

			UE_LOG(LogGameFeatures, Display, TEXT("%s"), Buf.ToString());
		}
	};
#else
	class FPackageLoadTracker {};
#endif
}

const FString LexToString(const EBuiltInAutoState BuiltInAutoState)
{
	switch (BuiltInAutoState)
	{
		case EBuiltInAutoState::Invalid:
			return TEXT("Invalid");
		case EBuiltInAutoState::Installed:
			return TEXT("Installed");
		case EBuiltInAutoState::Registered:
			return TEXT("Registered");
		case EBuiltInAutoState::Loaded:
			return TEXT("Loaded");
		case EBuiltInAutoState::Active:
			return TEXT("Active");
		default:
			check(false);
			return TEXT("Unknown");
	}
}

const FString LexToString(const EGameFeatureTargetState GameFeatureTargetState)
{
	switch (GameFeatureTargetState)
	{
		case EGameFeatureTargetState::Installed:
			return TEXT("Installed");
		case EGameFeatureTargetState::Registered:
			return TEXT("Registered"); 
		
		case EGameFeatureTargetState::Loaded:
			return TEXT("Loaded"); 
		case EGameFeatureTargetState::Active:
			return TEXT("Active"); 
		default:
			check(false);
			return TEXT("Unknown");
	}

	static_assert((uint8)EGameFeatureTargetState::Count == 4, "Update LexToString to include new EGameFeatureTargetState");
}

void LexFromString(EGameFeatureTargetState& ValueOut, const TCHAR* StringIn)
{
	//Default value if parsing fails
	ValueOut = EGameFeatureTargetState::Count;

	if (!StringIn || StringIn[0] == '\0')
	{
		ensureAlwaysMsgf(false, TEXT("Invalid empty FString used for EGameFeatureTargetState LexFromString."));
		return;
	}

	for (uint8 EnumIndex = 0; EnumIndex < static_cast<uint8>(EGameFeatureTargetState::Count); ++EnumIndex)
	{
		EGameFeatureTargetState StateToCheck = static_cast<EGameFeatureTargetState>(EnumIndex);
		if (LexToString(StateToCheck).Equals(StringIn, ESearchCase::IgnoreCase))
		{
			ValueOut = StateToCheck;
			return;
		}
	}

	ensureAlwaysMsgf(false, TEXT("Invalid FString { %s } used for EGameFeatureTargetState LexFromString. Does not correspond to any EGameFeatureTargetState!"), StringIn);
}

FGameFeaturePluginIdentifier::FGameFeaturePluginIdentifier(FString PluginURL)
{
	FromPluginURL(MoveTemp(PluginURL));
}

FGameFeaturePluginIdentifier::FGameFeaturePluginIdentifier(FGameFeaturePluginIdentifier&& Other)
	: FGameFeaturePluginIdentifier(MoveTemp(Other.PluginURL))
{
	Other.IdentifyingURLSubset.Reset();
	Other.PluginProtocol = EGameFeaturePluginProtocol::Unknown;
}

FGameFeaturePluginIdentifier& FGameFeaturePluginIdentifier::operator=(FGameFeaturePluginIdentifier&& Other)
{
	FromPluginURL(MoveTemp(Other.PluginURL));
	Other.IdentifyingURLSubset.Reset();
	Other.PluginProtocol = EGameFeaturePluginProtocol::Unknown;
	return *this;
}

void FGameFeaturePluginIdentifier::FromPluginURL(FString PluginURLIn)
{
	//Make sure we have no stale data
	IdentifyingURLSubset.Reset();
	PluginURL = MoveTemp(PluginURLIn);

	if (UGameFeaturesSubsystem::ParsePluginURL(PluginURL, &PluginProtocol, &IdentifyingURLSubset))
	{
		if (!IdentifyingURLSubset.IsEmpty())
		{
			// Plugins must be unique so just use the name as the identifier. This avoids issues with normalizing paths.
			IdentifyingURLSubset = FPathViews::GetCleanFilename(IdentifyingURLSubset);
		}
	}
}

bool FGameFeaturePluginIdentifier::operator==(const FGameFeaturePluginIdentifier& Other) const
{
	return ((PluginProtocol == Other.PluginProtocol) &&
			(IdentifyingURLSubset.Equals(Other.IdentifyingURLSubset, ESearchCase::CaseSensitive)));
}

bool FGameFeaturePluginIdentifier::ExactMatchesURL(const FString& PluginURLIn) const
{
	return GetFullPluginURL().Equals(PluginURLIn, ESearchCase::IgnoreCase);
}

FStringView FGameFeaturePluginIdentifier::GetPluginName() const
{ 
	return FPathViews::GetBaseFilename(IdentifyingURLSubset); 
}

void FGameFeatureStateChangeContext::SetRequiredWorldContextHandle(FName Handle)
{
	WorldContextHandle = Handle;
}

bool FGameFeatureStateChangeContext::ShouldApplyToWorldContext(const FWorldContext& WorldContext) const
{
	if (WorldContextHandle.IsNone())
	{
		return true;
	}
	if (WorldContext.ContextHandle == WorldContextHandle)
	{
		return true;
	}
	return false;
}

bool FGameFeatureStateChangeContext::ShouldApplyUsingOtherContext(const FGameFeatureStateChangeContext& OtherContext) const
{
	if (OtherContext == *this)
	{
		return true;
	}

	// If other context is less restrictive, apply
	if (OtherContext.WorldContextHandle.IsNone())
	{
		return true;
	}

	return false;
}

FSimpleDelegate FGameFeatureDeactivatingContext::PauseDeactivationUntilComplete(FString InPauserTag)
{
	UE_LOG(LogGameFeatures, Display, TEXT("Deactivation of %.*s paused by %s"), PluginName.Len(), PluginName.GetData(), *InPauserTag);

	++NumPausers;
	return FSimpleDelegate::CreateLambda(
		[CompletionCallback = CompletionCallback, PauserTag = MoveTemp(InPauserTag)]() { CompletionCallback(PauserTag); }
	);
}

FSimpleDelegate FGameFeaturePostMountingContext::PauseUntilComplete(FString InPauserTag)
{
	UE_LOG(LogGameFeatures, Display, TEXT("Post-mount of %.*s paused by %s"), PluginName.Len(), PluginName.GetData(), *InPauserTag);

	++NumPausers;
	return FSimpleDelegate::CreateLambda(
		[CompletionCallback = CompletionCallback, PauserTag = MoveTemp(InPauserTag)]() { CompletionCallback(PauserTag); }
	);
}

FInstallBundlePluginProtocolOptions::FInstallBundlePluginProtocolOptions()
	: InstallBundleFlags(EInstallBundleRequestFlags::Defaults)
	, ReleaseInstallBundleFlags(EInstallBundleReleaseRequestFlags::None)
	, bUninstallBeforeTerminate(false)
	, bUserPauseDownload(false)
	, bAllowIniLoading(false)
	, bDoNotDownload(false)
{}

bool FInstallBundlePluginProtocolOptions::operator==(const FInstallBundlePluginProtocolOptions& Other) const
{
	return
		InstallBundleFlags == Other.InstallBundleFlags &&
		bUninstallBeforeTerminate == Other.bUninstallBeforeTerminate &&
		bUserPauseDownload == Other.bUserPauseDownload && 
		bAllowIniLoading == Other.bAllowIniLoading &&
		bDoNotDownload == Other.bDoNotDownload;
}

FGameFeatureProtocolOptions::FGameFeatureProtocolOptions()
	: bForceSyncLoading(false)
	, bBatchProcess(false)
	, bLogWarningOnForcedDependencyCreation(false)
	, bLogErrorOnForcedDependencyCreation(false)
{ 
	SetSubtype<FNull>(); 
}

FGameFeatureProtocolOptions::FGameFeatureProtocolOptions(const FInstallBundlePluginProtocolOptions& InOptions) 
	: TUnion(InOptions) 
	, bForceSyncLoading(false)
	, bLogWarningOnForcedDependencyCreation(false)
	, bLogErrorOnForcedDependencyCreation(false)
{
}

FGameFeatureProtocolOptions::FGameFeatureProtocolOptions(FNull InOptions)
	: bForceSyncLoading(false)
	, bBatchProcess(false)
	, bLogWarningOnForcedDependencyCreation(false)
	, bLogErrorOnForcedDependencyCreation(false)
{
	SetSubtype<FNull>(InOptions);
}

void UGameFeaturesSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	UE_LOG(LogGameFeatures, Log, TEXT("Initializing game features subsystem"));

	// Create the game-specific policy manager
	check(!bInitializedPolicyManager && (GameSpecificPolicies == nullptr));

	const FSoftClassPath& PolicyClassPath = GetDefault<UGameFeaturesSubsystemSettings>()->GameFeaturesManagerClassName;

	UClass* SingletonClass = nullptr;
	if (!PolicyClassPath.IsNull())
	{
		SingletonClass = LoadClass<UGameFeaturesProjectPolicies>(nullptr, *PolicyClassPath.ToString());
	}

	if (SingletonClass == nullptr)
	{
		SingletonClass = UDefaultGameFeaturesProjectPolicies::StaticClass();
	}
		
	GameSpecificPolicies = NewObject<UGameFeaturesProjectPolicies>(this, SingletonClass);
	check(GameSpecificPolicies);

	UAssetManager::CallOrRegister_OnAssetManagerCreated(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnAssetManagerCreated));

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("ListGameFeaturePlugins"),
		TEXT("Prints game features plugins and their current state to log. (options: [-activeonly] [-csv])"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateUObject(this, &ThisClass::ListGameFeaturePlugins),
		ECVF_Default);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("LoadGameFeaturePlugin"),
		TEXT("Loads and activates a game feature plugin by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL.GetValue(), FGameFeaturePluginLoadComplete());
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("DeactivateGameFeaturePlugin"),
		TEXT("Deactivates a game feature plugin by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL.GetValue(), FGameFeaturePluginLoadComplete());
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("UnloadGameFeaturePlugin"),
		TEXT("Unloads a game feature plugin by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().UnloadGameFeaturePlugin(PluginURL.GetValue());
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("UnloadAndKeepRegisteredGameFeaturePlugin"),
		TEXT("Unloads a game feature plugin by PluginName or URL but keeps it registered"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().UnloadGameFeaturePlugin(PluginURL.GetValue(), true);
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("ReleaseGameFeaturePlugin"),
		TEXT("Releases a game feature plugin's InstallBundle data by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().ReleaseGameFeaturePlugin(PluginURL.GetValue());
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("CancelGameFeaturePlugin"),
		TEXT("Cancel any state changes for a game feature plugin by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().CancelGameFeatureStateChange(PluginURL.GetValue());
			}
		}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("TerminateGameFeaturePlugin"),
		TEXT("Terminates a game feature plugin by PluginName or URL"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
		{
			if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
			{
				UGameFeaturesSubsystem::Get().TerminateGameFeaturePlugin(PluginURL.GetValue());
			}
		}),
		ECVF_Cheat);

#if !UE_BUILD_SHIPPING
	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("EnableDebugGameFeatureState"),
		TEXT("Trigger a debug breakpoint if when the state of the gameplay feature changes"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
			{
				if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
				{
					UGameFeaturesSubsystem::Get().SetPluginDebugStateEnabled(PluginURL.GetValue(), true);
				}
			}),
		ECVF_Cheat);

	IConsoleManager::Get().RegisterConsoleCommand(
		TEXT("DisableDebugGameFeatureState"),
		TEXT("Turn off triggering a debug breakpoint if when the state of the gameplay feature changes"),
		FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*, FOutputDevice& Ar)
			{
				if (TOptional<FString> PluginURL = UE::GameFeatures::GetPluginUrlForConsoleCommand(Args, Ar))
				{
					UGameFeaturesSubsystem::Get().SetPluginDebugStateEnabled(PluginURL.GetValue(), false);
				}
			}),
		ECVF_Cheat);
#endif

#if UE_WITH_PACKAGE_ACCESS_TRACKING
	if (UE::GameFeatures::CVarTrackLoadStats->GetBool())
	{
		PackageLoadTracker = MakeUnique<UE::GameFeatures::FPackageLoadTracker>();
		IConsoleManager::Get().RegisterConsoleCommand(
			TEXT("GameFeaturePlugin.PrintLoadStats"),
			TEXT("Print a list of all packages loaded by each GFP. If a package is loaded by multiple GFPs, it will only be")
			TEXT("attributed to the first one that actually causes the load")
			TEXT("\nUsage: GameFeaturePlugin.PrintLoadStats [-ShowUntracked]")
			TEXT("\n-ShowUntracked - Print a list of packages that were loaded, but don't have tracking to know why"),
			FConsoleCommandWithArgsDelegate::CreateLambda([&Tracker = PackageLoadTracker](const TArray<FString>& Args)
			{
				bool bShowUntracked = Args.Contains(TEXT("-ShowUntracked"));
				Tracker->PrintLoadStats(bShowUntracked);
			})
		);
	}
#endif

	GetExplanationForUnavailablePackageDelegateHandle = IPluginManager::Get().GetExplanationForUnavailablePackageWithPluginInfoDelegate().AddUObject(this, &UGameFeaturesSubsystem::GetExplanationForUnavailablePackage);
	IPluginManager::Get().OnPluginUnmounted().AddWeakLambda(this, [this] (const IPlugin& Plugin) { PruneCachedGameFeaturePluginDetails(/* doesnt use URL */ FString(""), Plugin.GetDescriptorFileName()); });
	IPluginManager::Get().OnPluginEdited().AddWeakLambda(this, [this](IPlugin& Plugin) { PruneCachedGameFeaturePluginDetails(/* doesnt use URL */ FString(""), Plugin.GetDescriptorFileName()); });

#if WITH_EDITOR
	GameFeatureDataExternalAssetsPathCache = MakeUnique<FGameFeatureDataExternalAssetsPathCache>();
#endif
}

void UGameFeaturesSubsystem::Deinitialize()
{
	UE_LOG(LogGameFeatures, Log, TEXT("Shutting down game features subsystem"));
	IPluginManager::Get().GetExplanationForUnavailablePackageWithPluginInfoDelegate().Remove(GetExplanationForUnavailablePackageDelegateHandle);
	GetExplanationForUnavailablePackageDelegateHandle.Reset();

	if ((GameSpecificPolicies != nullptr) && bInitializedPolicyManager)
	{
		GameSpecificPolicies->ShutdownGameFeatureManager();
	}
	GameSpecificPolicies = nullptr;
	bInitializedPolicyManager = false;

	if (TickHandle.IsValid())
	{
		FTSTicker::RemoveTicker(MoveTemp(TickHandle));
		TickHandle.Reset();
	}

#if WITH_EDITOR
	GameFeatureDataExternalAssetsPathCache.Reset();
#endif
}

void UGameFeaturesSubsystem::OnAssetManagerCreated()
{
	check(!bInitializedPolicyManager && (GameSpecificPolicies != nullptr));

	// Make sure the game has the appropriate asset manager configuration or we won't be able to load game feature data assets
	FPrimaryAssetId DummyGameFeatureDataAssetId(UGameFeatureData::StaticClass()->GetFName(), NAME_None);
	FPrimaryAssetRules GameDataRules = UAssetManager::Get().GetPrimaryAssetRules(DummyGameFeatureDataAssetId);
	if (GameDataRules.IsDefault())
	{
		const bool bHasProject = FApp::HasProjectName(); // Only error when we have a UE project loaded, as otherwise there won't be any GFPs to load
		UE_CLOG(bHasProject, LogGameFeatures, Error, TEXT("Asset manager settings do not include a rule for assets of type %s, which is required for game feature plugins to function"), *UGameFeatureData::StaticClass()->GetName());
	}

	// Give system that want to add an observer before we start to mount plugins a chance to do so.
	OnGameFeaturePolicyPreInit.Broadcast();

	// Create the game-specific policy
	UE_LOG(LogGameFeatures, Verbose, TEXT("Initializing game features policy (type %s)"), *GameSpecificPolicies->GetClass()->GetName());
	bInitializedPolicyManager = true; // Set before calling InitGameFeatureManager() because InitGameFeatureManager may load GFPs
	GameSpecificPolicies->InitGameFeatureManager();
}

bool UGameFeaturesSubsystem::IsPluginAllowed(const FString& PluginURL, FString* OutReason) const
{
	ensureMsgf(bInitializedPolicyManager, TEXT("Attemting to load plugin [%s] before GameFeaturesSubsystem is ready!"), *PluginURL);
	return bInitializedPolicyManager && GameSpecificPolicies->IsPluginAllowed(PluginURL, OutReason);
}

TSharedPtr<FStreamableHandle> UGameFeaturesSubsystem::LoadGameFeatureData(const FString& GameFeatureToLoad, bool bStartStalled /*= false*/)
{
	return UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(
		FSoftObjectPath(GameFeatureToLoad), 
		FStreamableDelegate(), 
		FStreamableManager::DefaultAsyncLoadPriority, 
		false, 
		bStartStalled, 
		TEXT("LoadGameFeatureData"));
}

void UGameFeaturesSubsystem::UnloadGameFeatureData(const UGameFeatureData* GameFeatureToUnload)
{
	UAssetManager& LocalAssetManager = UAssetManager::Get();
	LocalAssetManager.UnloadPrimaryAsset(GameFeatureToUnload->GetPrimaryAssetId());
}

void UGameFeaturesSubsystem::AddGameFeatureToAssetManager(const UGameFeatureData* GameFeatureToAdd, const FString& PluginName, TArray<FName>& OutNewPrimaryAssetTypes)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(GFP_AddToAssetManager);
	check(GameFeatureToAdd);
	FString PluginRootPath = TEXT("/") + PluginName + TEXT("/");
	UAssetManager& LocalAssetManager = UAssetManager::Get();
	IAssetRegistry& LocalAssetRegistry = LocalAssetManager.GetAssetRegistry();

	LocalAssetManager.PushBulkScanning();

	// Add the GameFeatureData itself to the primary asset list
#if WITH_EDITOR
	// In the editor, we may not have scanned the FAssetData yet if during startup, but that is fine because we can gather bundles from the object itself, so just create the FAssetData from the object
	LocalAssetManager.RegisterSpecificPrimaryAsset(GameFeatureToAdd->GetPrimaryAssetId(), FAssetData(GameFeatureToAdd));
#else
	// In non-editor, the asset bundle data is compiled out, so it must be gathered from the asset registry instead
	LocalAssetManager.RegisterSpecificPrimaryAsset(GameFeatureToAdd->GetPrimaryAssetId(), LocalAssetRegistry.GetAssetByObjectPath(FSoftObjectPath(GameFeatureToAdd), true));
#endif // WITH_EDITOR

	// @TODO: HACK - There is no guarantee that the plugin mount point was added before the initial asset scan.
	// If not, ScanPathsForPrimaryAssets will fail to find primary assets without a syncronous scan.
	// A proper fix for this would be to handle all the primary asset discovery internally ins the asset manager 
	// instead of doing it here.
	// We just mounted the folder that contains these primary assets and the editor background scan may not
	// not be finished by the time this is called, but a rescan will happen later in OnAssetRegistryFilesLoaded 
	// as long as LocalAssetRegistry.IsLoadingAssets() is true.
	const bool bForceSynchronousScan = !LocalAssetRegistry.IsGathering();

	for (FPrimaryAssetTypeInfo TypeInfo : GameFeatureToAdd->GetPrimaryAssetTypesToScan())
	{
		for (FDirectoryPath& Path : TypeInfo.GetDirectories())
		{
			// Convert plugin-relative paths to full package paths
			FixPluginPackagePath(Path.Path, PluginRootPath, false);
		}

		// This function also fills out runtime data on the copy
		if (!LocalAssetManager.ShouldScanPrimaryAssetType(TypeInfo))
		{
			continue;
		}

		FPrimaryAssetTypeInfo ExistingAssetTypeInfo;
		const bool bAlreadyExisted = LocalAssetManager.GetPrimaryAssetTypeInfo(FPrimaryAssetType(TypeInfo.PrimaryAssetType), /*out*/ ExistingAssetTypeInfo);
		LocalAssetManager.ScanPathsForPrimaryAssets(TypeInfo.PrimaryAssetType, TypeInfo.AssetScanPaths, TypeInfo.AssetBaseClassLoaded, TypeInfo.bHasBlueprintClasses, TypeInfo.bIsEditorOnly, bForceSynchronousScan);

		if (!bAlreadyExisted)
		{
			OutNewPrimaryAssetTypes.Add(TypeInfo.PrimaryAssetType);

			// If we did not previously scan anything for a primary asset type that is in our config, try to reuse the cook rules from the config instead of the one in the gamefeaturedata, which should not be modifying cook rules
			const FPrimaryAssetTypeInfo* ConfigTypeInfo = LocalAssetManager.GetSettings().PrimaryAssetTypesToScan.FindByPredicate([&TypeInfo](const FPrimaryAssetTypeInfo& PATI) -> bool { return PATI.PrimaryAssetType == TypeInfo.PrimaryAssetType; });
			if (ConfigTypeInfo)
			{
				LocalAssetManager.SetPrimaryAssetTypeRules(TypeInfo.PrimaryAssetType, ConfigTypeInfo->Rules);
			}
			else
			{
				LocalAssetManager.SetPrimaryAssetTypeRules(TypeInfo.PrimaryAssetType, TypeInfo.Rules);
			}
		}
	}
	LocalAssetManager.PopBulkScanning();

	const UAssetManagerSettings& Settings = LocalAssetManager.GetSettings();
	for (const FPrimaryAssetRulesCustomOverride& Override : Settings.CustomPrimaryAssetRules)
	{
		if (Override.FilterDirectory.Path.StartsWith(PluginRootPath))
		{
			LocalAssetManager.ApplyCustomPrimaryAssetRulesOverride(Override);
		}
	}
}

void UGameFeaturesSubsystem::RemoveGameFeatureFromAssetManager(const UGameFeatureData* GameFeatureToRemove, const FString& PluginName, const TArray<FName>& AddedPrimaryAssetTypes)
{
	check(GameFeatureToRemove);
	FString PluginRootPath = TEXT("/") + PluginName + TEXT("/");
	UAssetManager& LocalAssetManager = UAssetManager::Get();

	for (FPrimaryAssetTypeInfo TypeInfo : GameFeatureToRemove->GetPrimaryAssetTypesToScan())
	{
		if (AddedPrimaryAssetTypes.Contains(TypeInfo.PrimaryAssetType))
		{
			LocalAssetManager.RemovePrimaryAssetType(TypeInfo.PrimaryAssetType);
			continue;
		}

		for (FDirectoryPath& Path : TypeInfo.GetDirectories())
		{
			FixPluginPackagePath(Path.Path, PluginRootPath, false);
		}

		// This function also fills out runtime data on the copy
		if (!LocalAssetManager.ShouldScanPrimaryAssetType(TypeInfo))
		{
			continue;
		}

		LocalAssetManager.RemoveScanPathsForPrimaryAssets(TypeInfo.PrimaryAssetType, TypeInfo.AssetScanPaths, TypeInfo.AssetBaseClassLoaded, TypeInfo.bHasBlueprintClasses, TypeInfo.bIsEditorOnly);
	}
}

void UGameFeaturesSubsystem::ForEachGameFeature(TFunctionRef<void(FGameFeatureInfo&&)> Visitor) const
{
	for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
	{
		if (UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value())
		{
			FGameFeatureInfo GameFeatureInfo = { GFSM->GetPluginName(), GFSM->GetPluginURL(), GFSM->WasLoadedAsBuiltIn(), GFSM->GetCurrentState() };
			Visitor(MoveTemp(GameFeatureInfo));
		}
	}
}

void UGameFeaturesSubsystem::AddObserver(UObject* Observer)
{
	//deprecated, previous default behaviour
	AddObserver(Observer, EObserverPluginStateUpdateMode::CurrentAndFuture);
}

void UGameFeaturesSubsystem::AddObserver(UObject* Observer, EObserverPluginStateUpdateMode UpdateMode)
{
	QUICK_SCOPE_CYCLE_COUNTER(UGameFeaturesSubsystem_AddObserver);

	check(Observer);
	IGameFeatureStateChangeObserver* Interface = Cast<IGameFeatureStateChangeObserver>(Observer);
	if (ensureAlwaysMsgf(Interface != nullptr, TEXT("Observers must implement the IGameFeatureStateChangeObserver interface.")))
	{
		Observers.AddUnique(Observer);

		if (UpdateMode == EObserverPluginStateUpdateMode::CurrentAndFuture)
		{
			// Push the current state of all known game features to the new observer
			for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
			{
				if (UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value())
				{
					if (const UGameFeatureData* GameFeatureData = GFSM->GetGameFeatureDataForRegisteredPlugin(false))
					{
						const FString& PluginName = GFSM->GetPluginName();
						const FString& PluginURL = GFSM->GetPluginURL();

						Interface->OnGameFeatureRegistering(GameFeatureData, PluginName, PluginURL);

						if (GFSM->GetCurrentState() >= EGameFeaturePluginState::Loaded)
						{
							Interface->OnGameFeatureLoading(GameFeatureData, PluginURL);
						}

						if (GFSM->GetCurrentState() >= EGameFeaturePluginState::Active)
						{
							Interface->OnGameFeatureActivating(GameFeatureData, PluginURL);
							Interface->OnGameFeatureActivated(GameFeatureData, PluginURL);
						}
					}
				}
			}
		}
	}
}

void UGameFeaturesSubsystem::RemoveObserver(UObject* Observer)
{
	check(Observer);
	Observers.RemoveSingleSwap(Observer);
}

FString UGameFeaturesSubsystem::GetPluginURL_FileProtocol(const FString& PluginDescriptorPath)
{
	return UE::GameFeatures::GameFeaturePluginProtocolPrefix(EGameFeaturePluginProtocol::File) + PluginDescriptorPath;
}

FString UGameFeaturesSubsystem::GetPluginURL_FileProtocol(const FString& PluginDescriptorPath, TArrayView<const TPair<FString, FString>> AdditionalOptions)
{
	FString Path;
	Path += UE::GameFeatures::GameFeaturePluginProtocolPrefix(EGameFeaturePluginProtocol::File);
	Path += PluginDescriptorPath;
	if (AdditionalOptions.Num() > 0)
	{
		Path += UE::GameFeatures::PluginURLStructureInfo::OptionSeperator;
		Path += FString::JoinBy(AdditionalOptions, UE::GameFeatures::PluginURLStructureInfo::OptionSeperator,
			[](const TPair<FString, FString>& OptionPair)
			{
				return OptionPair.Key + UE::GameFeatures::PluginURLStructureInfo::OptionAssignOperator + OptionPair.Value;
			});
	}
	return Path;
}

FString UGameFeaturesSubsystem::GetPluginFilename_FileProtocol(const FString& PluginUrlFileProtocol)
{
	const TCHAR* Prefix = UE::GameFeatures::GameFeaturePluginProtocolPrefix(EGameFeaturePluginProtocol::File);
	FStringView PrefixView(Prefix);
	FString Result;
	if (ensureAlwaysMsgf(PluginUrlFileProtocol.StartsWith(Prefix), TEXT("Unexpected protocol for %s"), *PluginUrlFileProtocol))
	{
		Result = PluginUrlFileProtocol.RightChop(PrefixView.Len());
	}
	return Result;
}

void UGameFeaturesSubsystem::GetExplanationForUnavailablePackage(const FString& UnavailablePackage, IPlugin* PluginIfFound, FStringBuilderBase& InOutExplanation)
{
#if WITH_EDITOR
	if (PluginIfFound)
	{
		if (FString* Explanation = UnmountedPluginNameToExplanation.Find(PluginIfFound->GetName()))
		{
			InOutExplanation.Appendf(TEXT("\nUGameFeaturesSubsystem: Explanation for not mounting plugin %s: %s"), *PluginIfFound->GetFriendlyName(), **Explanation);
		}
	}
	else
	{
		FString ContentDirName = FPackageName::SplitPackageNameRoot(UnavailablePackage, nullptr).GetData();
		if (FString* Explanation = UnmountedPluginNameToExplanation.Find(ContentDirName))
		{
			InOutExplanation.Appendf(TEXT("\nUGameFeaturesSubsystem: Explanation for not mounting plugin %s: %s"), *ContentDirName, **Explanation);
		}
	}
#endif
}

FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, const FInstallBundlePluginProtocolMetaData& ProtocolMetadata, TArrayView<const TPair<FString, FString>> AdditionalOptions = {})
{
	ensure(ProtocolMetadata.InstallBundles.Num() > 0);
	FString Path;
	Path += UE::GameFeatures::GameFeaturePluginProtocolPrefix(EGameFeaturePluginProtocol::InstallBundle);
	Path += PluginName;
	Path += ProtocolMetadata.ToString();
	if (AdditionalOptions.Num() > 0)
	{
		Path += UE::GameFeatures::PluginURLStructureInfo::OptionSeperator;
		Path += FString::JoinBy(AdditionalOptions, UE::GameFeatures::PluginURLStructureInfo::OptionSeperator,
			[](const TPair<FString, FString>& OptionPair)
			{
				return OptionPair.Key + UE::GameFeatures::PluginURLStructureInfo::OptionAssignOperator + OptionPair.Value;
			});
	}

	return Path;
}

FString UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(const FString& PluginName, TArrayView<const FString> BundleNames)
{
	FInstallBundlePluginProtocolMetaData ProtocolMetadata;
	for (const FString& BundleName : BundleNames)
	{
		ProtocolMetadata.InstallBundles.Emplace(BundleName);
	}
	return ::GetPluginURL_InstallBundleProtocol(PluginName, ProtocolMetadata);
}

FString UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(const FString& PluginName, const FString& BundleName)
{
	return GetPluginURL_InstallBundleProtocol(PluginName, MakeArrayView(&BundleName, 1));
}

FString UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(const FString& PluginName, const TArrayView<const FName> BundleNames)
{
	return GetPluginURL_InstallBundleProtocol(PluginName, BundleNames, TArrayView<const TPair<FString, FString>>());
}

FString UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(const FString& PluginName, FName BundleName)
{
	return GetPluginURL_InstallBundleProtocol(PluginName, MakeArrayView(&BundleName, 1));
}

FString UGameFeaturesSubsystem::GetPluginURL_InstallBundleProtocol(const FString& PluginName, TArrayView<const FName> BundleNames, TArrayView<const TPair<FString, FString>> AdditionalOptions)
{
	FInstallBundlePluginProtocolMetaData ProtocolMetadata;
	ProtocolMetadata.InstallBundles.Append(BundleNames.GetData(), BundleNames.Num());
	return ::GetPluginURL_InstallBundleProtocol(PluginName, ProtocolMetadata, AdditionalOptions);
}

EGameFeaturePluginProtocol UGameFeaturesSubsystem::GetPluginURLProtocol(FStringView PluginURL)
{
	for (EGameFeaturePluginProtocol Protocol : TEnumRange<EGameFeaturePluginProtocol>())
	{
		if (UGameFeaturesSubsystem::IsPluginURLProtocol(PluginURL, Protocol))
		{
			return Protocol;
		}
	}
	return EGameFeaturePluginProtocol::Unknown;
}

bool UGameFeaturesSubsystem::IsPluginURLProtocol(FStringView PluginURL, EGameFeaturePluginProtocol PluginProtocol)
{
	return PluginURL.StartsWith(UE::GameFeatures::GameFeaturePluginProtocolPrefix(PluginProtocol));
}

bool UGameFeaturesSubsystem::ParsePluginURL(FStringView PluginURL, EGameFeaturePluginProtocol* OutProtocol /*= nullptr*/, FStringView* OutPath /*= nullptr*/, FStringView* OutOptions /*= nullptr*/)
{
	FStringView Path;
	FStringView Options;
	EGameFeaturePluginProtocol PluginProtocol = UGameFeaturesSubsystem::GetPluginURLProtocol(PluginURL);

	if (ensureAlwaysMsgf(PluginProtocol != EGameFeaturePluginProtocol::Unknown && PluginProtocol != EGameFeaturePluginProtocol::Count,
		TEXT("Invalid PluginProtocol in PluginURL %.*s"), PluginURL.Len(), PluginURL.GetData()))
	{
		int32 PluginProtocolLen = FCString::Strlen(UE::GameFeatures::GameFeaturePluginProtocolPrefix(PluginProtocol));
		int32 FirstOptionIndex = UE::String::FindFirst(PluginURL, UE::GameFeatures::PluginURLStructureInfo::OptionSeperator, ESearchCase::IgnoreCase);

		//If we don't have any options, then the Path is just our entire URL except the protocol string
		if (FirstOptionIndex == INDEX_NONE)
		{
			Path = PluginURL.RightChop(PluginProtocolLen);
		}
		//The Path will be the string between the end of the protocol string and before the first option
		else
		{
			const int32 IdentifierCharCount = (FirstOptionIndex - PluginProtocolLen);
			Path = PluginURL.Mid(PluginProtocolLen, IdentifierCharCount);
			Options = PluginURL.RightChop(FirstOptionIndex);
		}

		if (ensureAlwaysMsgf(Path.EndsWith(TEXTVIEW(".uplugin")), TEXT("Invalid path in PluginURL %.*s"), PluginURL.Len(), PluginURL.GetData()))
		{
			if (OutProtocol)
			{
				*OutProtocol = PluginProtocol;
			}

			if (OutPath)
			{
				*OutPath = Path;
			}

			if (OutOptions)
			{
				*OutOptions = Options;
			}

			return true;
		}
	}

	return false;
}

namespace GameFeaturesSubsystem
{
	static bool SplitOption(FStringView OptionPair, FStringView& OutOptionName, FStringView& OutOptionValue)
	{
		int32 TokenCount = 0;
		FStringView OptionName;
		FStringView OptionValue;
		UE::String::ParseTokens(OptionPair, UE::GameFeatures::PluginURLStructureInfo::OptionAssignOperator, 
		[&TokenCount, &OptionName, &OptionValue](FStringView Token)
		{
			++TokenCount;
			switch (TokenCount)
			{
			case 1:
				OptionName = Token;
				break;
			case 2:
				OptionValue = Token;
				break;
			}
		});

		const bool bSuccess = TokenCount == 2;
		if (bSuccess)
		{
			OutOptionName = OptionName;
			OutOptionValue = OptionValue;
		}

		return bSuccess;
	}

	struct FParsePluginURLOptionsFilter
	{
		EGameFeatureURLOptions OptionsFlags;
		TConstArrayView<FStringView> AdditionalOptions;
	};

	static bool ParsePluginURLOptions(FStringView URLOptionsString, const FParsePluginURLOptionsFilter* OptionsFilter,
		TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output)
	{
		enum class EParseState : uint8
		{
			First,
			OK,
			Error
		};

		//Parse through our URLOptions. The first option won't appear until after the first seperator.
		//We don't care what comes before the first seperator
		EParseState ParseState = EParseState::First;
		UE::String::ParseTokens(URLOptionsString, UE::GameFeatures::PluginURLStructureInfo::OptionSeperator, 
		[OptionsFilter, Output, &ParseState](FStringView Token)
		{
			if (ParseState == EParseState::Error)
			{
				return;
			}

			if (ParseState == EParseState::First)
			{
				ParseState = EParseState::OK;
				return;
			}

			FStringView OptionName;
			FStringView OptionValue;
			if (!GameFeaturesSubsystem::SplitOption(Token, OptionName, OptionValue))
			{
				ParseState = EParseState::Error;
				return;
			}

			EGameFeatureURLOptions OptionEnum = EGameFeatureURLOptions::None;

			if (!OptionsFilter || OptionsFilter->OptionsFlags != EGameFeatureURLOptions::None)
			{
				LexFromString(OptionEnum, OptionName);
			}

			if (!OptionsFilter || EnumHasAnyFlags(OptionsFilter->OptionsFlags, OptionEnum) || OptionsFilter->AdditionalOptions.Contains(OptionName))
			{
				UE::String::ParseTokens(OptionValue, UE::GameFeatures::PluginURLStructureInfo::OptionListSeperator,
				[Output, OptionEnum, OptionName](FStringView ListToken)
				{
					Output(OptionEnum, OptionName, ListToken);
				});
			}
		});

		return ParseState == EParseState::OK;
	}
}

bool UGameFeaturesSubsystem::ParsePluginURLOptions(FStringView URLOptionsString,
	TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output)
{
	return GameFeaturesSubsystem::ParsePluginURLOptions(URLOptionsString, nullptr, Output);
}

bool UGameFeaturesSubsystem::ParsePluginURLOptions(FStringView URLOptionsString, EGameFeatureURLOptions OptionsFlags,
	TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output)
{
	const GameFeaturesSubsystem::FParsePluginURLOptionsFilter OptionsFilter{ OptionsFlags, {} };
	return GameFeaturesSubsystem::ParsePluginURLOptions(URLOptionsString, &OptionsFilter, Output);
}

bool UGameFeaturesSubsystem::ParsePluginURLOptions(FStringView URLOptionsString, TConstArrayView<FStringView> AdditionalOptions,
	TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output)
{
	const GameFeaturesSubsystem::FParsePluginURLOptionsFilter OptionsFilter{ EGameFeatureURLOptions::None, AdditionalOptions };
	return GameFeaturesSubsystem::ParsePluginURLOptions(URLOptionsString, &OptionsFilter, Output);
}

bool UGameFeaturesSubsystem::ParsePluginURLOptions(FStringView URLOptionsString, EGameFeatureURLOptions OptionsFlags, TConstArrayView<FStringView> AdditionalOptions,
	TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output)
{
	const GameFeaturesSubsystem::FParsePluginURLOptionsFilter OptionsFilter{ OptionsFlags, AdditionalOptions };
	return GameFeaturesSubsystem::ParsePluginURLOptions(URLOptionsString, &OptionsFilter, Output);
}

void UGameFeaturesSubsystem::OnGameFeatureTerminating(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Terminating, PluginIdentifier, &PluginName);

	if (!PluginName.IsEmpty())
	{
		// Unmap plugin name to plugin URL
		GameFeaturePluginNameToPathMap.Remove(PluginName);
	}
}

void UGameFeaturesSubsystem::OnGameFeatureCheckingStatus(const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::CheckingStatus, PluginIdentifier);
}

void UGameFeaturesSubsystem::OnGameFeatureStatusKnown(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	// Map plugin name to plugin URL
	if (ensure(!GameFeaturePluginNameToPathMap.Contains(PluginName)))
	{
		GameFeaturePluginNameToPathMap.Add(PluginName, PluginIdentifier.GetFullPluginURL());
	}
}

void UGameFeaturesSubsystem::OnGameFeaturePredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Predownloading, PluginIdentifier, &PluginName);
}

void UGameFeaturesSubsystem::OnGameFeaturePostPredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::PostPredownloading, PluginIdentifier, &PluginName);
}

void UGameFeaturesSubsystem::OnGameFeatureDownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Downloading, PluginIdentifier, &PluginName);
}

void UGameFeaturesSubsystem::OnGameFeatureReleasing(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Releasing, PluginIdentifier, &PluginName);
}

void UGameFeaturesSubsystem::OnGameFeaturePreMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePreMountingContext& Context)
{
	CallbackObservers(EObserverCallback::PreMounting, PluginIdentifier, &PluginName, /*GameFeatureData=*/nullptr, &Context);
}

void UGameFeaturesSubsystem::OnGameFeaturePostMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePostMountingContext& Context)
{
	CallbackObservers(EObserverCallback::PostMounting, PluginIdentifier, &PluginName, /*GameFeatureData=*/nullptr, &Context);
}

void UGameFeaturesSubsystem::OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Registering, PluginIdentifier, &PluginName, GameFeatureData);

	for (UGameFeatureAction* Action : GameFeatureData->GetActions())
	{
		if (Action != nullptr)
		{
			Action->OnGameFeatureRegistering();
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeatureUnregistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Unregistering, PluginIdentifier, &PluginName, GameFeatureData);

#if !WITH_EDITOR
	check(GameFeatureData);
#else
	if (GameFeatureData) // In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
#endif
	{
		for (UGameFeatureAction* Action : GameFeatureData->GetActions())
		{
			if (Action != nullptr)
			{
				Action->OnGameFeatureUnregistering();
			}
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeatureLoading(const UGameFeatureData* GameFeatureData, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Loading, PluginIdentifier, nullptr, GameFeatureData);

	for (UGameFeatureAction* Action : GameFeatureData->GetActions())
	{
		if (Action != nullptr)
		{
			Action->OnGameFeatureLoading();
		}
	}
}
void UGameFeaturesSubsystem::OnGameFeatureUnloading(const UGameFeatureData* GameFeatureData, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	CallbackObservers(EObserverCallback::Unloading, PluginIdentifier, nullptr, GameFeatureData);

#if !WITH_EDITOR
	check(GameFeatureData);
#else
	if (GameFeatureData) // In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
#endif
	{
		for (UGameFeatureAction* Action : GameFeatureData->GetActions())
		{
			if (Action != nullptr)
			{
				Action->OnGameFeatureUnloading();
			}
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeatureActivating(const UGameFeatureData* GameFeatureData, const FString& PluginName, FGameFeatureActivatingContext& Context, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnActivating_CallbackObservers);
		CallbackObservers(EObserverCallback::Activating, PluginIdentifier, &PluginName, GameFeatureData);
	}

	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnActivating_CallbackActions);
		for (UGameFeatureAction* Action : GameFeatureData->GetActions())
		{
			if (Action != nullptr)
			{
				Action->OnGameFeatureActivating(Context);
			}
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeatureActivated(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnActivated_CallbackObservers);
		CallbackObservers(EObserverCallback::Activated, PluginIdentifier, &PluginName, GameFeatureData);
	}

	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnActivated_CallbackActions);
		for (UGameFeatureAction* Action : GameFeatureData->GetActions())
		{
			if (Action != nullptr)
			{
				Action->OnGameFeatureActivated();
			}
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeatureDeactivating(const UGameFeatureData* GameFeatureData, const FString& PluginName, FGameFeatureDeactivatingContext& Context, const FGameFeaturePluginIdentifier& PluginIdentifier)
{
#if !WITH_EDITOR
	check(GameFeatureData);
#else
	if (GameFeatureData) // In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
#endif
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnDeactivating_CallbackObservers);
		CallbackObservers(EObserverCallback::Deactivating, PluginIdentifier, &PluginName, GameFeatureData, &Context);
	}
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_OnDeactivating_OnGameFeatureDeactivating);
		for (UGameFeatureAction* Action : GameFeatureData->GetActions())
		{
			if (Action != nullptr)
			{
				Action->OnGameFeatureDeactivating(Context);
			}
		}
	}
}

void UGameFeaturesSubsystem::OnGameFeaturePauseChange(const FGameFeaturePluginIdentifier& PluginIdentifier, const FString& PluginName, FGameFeaturePauseStateChangeContext& Context)
{
	CallbackObservers(EObserverCallback::PauseChanged, PluginIdentifier, &PluginName, nullptr, &Context);
}

const UGameFeatureData* UGameFeaturesSubsystem::GetDataForStateMachine(UGameFeaturePluginStateMachine* GFSM) const
{
	return GFSM->GetGameFeatureDataForActivePlugin();
}

const UGameFeatureData* UGameFeaturesSubsystem::GetRegisteredDataForStateMachine(UGameFeaturePluginStateMachine* GFSM) const
{
	return GFSM->GetGameFeatureDataForRegisteredPlugin();
}

void UGameFeaturesSubsystem::GetGameFeatureDataForActivePlugins(TArray<const UGameFeatureData*>& OutActivePluginFeatureDatas)
{
	for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
	{
		if (UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value())
		{
			if (const UGameFeatureData* GameFeatureData = GFSM->GetGameFeatureDataForActivePlugin())
			{
				OutActivePluginFeatureDatas.Add(GameFeatureData);
			}
		}
	}
}

const UGameFeatureData* UGameFeaturesSubsystem::GetGameFeatureDataForActivePluginByURL(const FString& PluginURL)
{
	if (UGameFeaturePluginStateMachine* GFSM = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return GFSM->GetGameFeatureDataForActivePlugin();
	}

	return nullptr;
}

const UGameFeatureData* UGameFeaturesSubsystem::GetGameFeatureDataForRegisteredPluginByURL(const FString& PluginURL, bool bCheckForRegistering /*= false*/)
{
	if (UGameFeaturePluginStateMachine* GFSM = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return GFSM->GetGameFeatureDataForRegisteredPlugin(bCheckForRegistering);
	}

	return nullptr;
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginInstalled(const FString& PluginURL) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return StateMachine->GetCurrentState() >= EGameFeaturePluginState::Installed;
	}
	return false;
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginMounted(const FString& PluginURL) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return StateMachine->GetCurrentState() > EGameFeaturePluginState::Mounting;
	}
	return false;
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginRegistered(const FString& PluginURL, bool bCheckForRegistering /*= false*/) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		const EGameFeaturePluginState CurrentState = StateMachine->GetCurrentState();

		return StateMachine->GetCurrentState() >= EGameFeaturePluginState::Registered || (bCheckForRegistering && CurrentState == EGameFeaturePluginState::Registering);
	}
	return false;
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginLoaded(const FString& PluginURL) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return StateMachine->GetCurrentState() >= EGameFeaturePluginState::Loaded;
	}
	return false;
}

bool UGameFeaturesSubsystem::WasGameFeaturePluginLoadedAsBuiltIn(const FString& PluginURL) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		return StateMachine->WasLoadedAsBuiltIn();
	}
	return false;
}

void UGameFeaturesSubsystem::LoadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	LoadGameFeaturePlugin(PluginURL, FGameFeatureProtocolOptions(), CompleteDelegate);
}

void UGameFeaturesSubsystem::LoadGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	const bool bIsPluginAllowed = IsPluginAllowed(PluginURL);
	if (!bIsPluginAllowed)
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginNotAllowed)));
		return;
	}

	UGameFeaturePluginStateMachine* StateMachine = FindOrCreateGameFeaturePluginStateMachine(PluginURL, ProtocolOptions);

	if (!StateMachine->IsRunning() && StateMachine->GetCurrentState() == EGameFeaturePluginState::Active)
	{
		// TODO: Resolve the activated case here, this is needed because in a PIE environment the plugins
		// are not sandboxed, and we need to do simulate a successful activate call in order run GFP systems 
		// on whichever Role runs second between client and server.

		// Refire the observer for Activated and do nothing else.
		CallbackObservers(EObserverCallback::Activating, StateMachine->GetPluginIdentifier(), &StateMachine->GetPluginName(), StateMachine->GetGameFeatureDataForActivePlugin());
	}

	if (ShouldUpdatePluginProtocolOptions(StateMachine, ProtocolOptions))
	{
		const UE::GameFeatures::FResult Result = UpdateGameFeatureProtocolOptions(StateMachine, ProtocolOptions);
		if (Result.HasError())
		{
			CompleteDelegate.ExecuteIfBound(Result);
			return;
		}
	}

	ChangeGameFeatureDestination(StateMachine, ProtocolOptions, FGameFeaturePluginStateRange(EGameFeaturePluginState::Loaded, EGameFeaturePluginState::Active), CompleteDelegate);
}

void UGameFeaturesSubsystem::LoadGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate)
{
	struct FLoadContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsLoaded CompleteDelegate;

		int32 NumPluginsLoaded = 0;
		bool bPushedTagsBroadcast = false;

		FLoadContext()
		{
			if (!IsEngineExitRequested())
			{
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
			else if(UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
			{
				TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
		}

		~FLoadContext()
		{
			if (bPushedTagsBroadcast)
			{
				if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PopDeferOnGameplayTagTreeChangedBroadcast();
				}
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FLoadContext> LoadContext = MakeShared<FLoadContext>();
	LoadContext->CompleteDelegate = CompleteDelegate;

	LoadContext->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		LoadContext->Results.Add(PluginURL, MakeError("Pending"));
	}

	const int32 NumPluginsToLoad = PluginURLs.Num();
	UE_LOG(LogGameFeatures, Log, TEXT("Loading %i GFPs"), NumPluginsToLoad);

	for (const FString& PluginURL : PluginURLs)
	{
		LoadGameFeaturePlugin(PluginURL, ProtocolOptions, FGameFeaturePluginChangeStateComplete::CreateLambda([LoadContext, PluginURL](const UE::GameFeatures::FResult& Result)
		{
			LoadContext->Results.Add(PluginURL, Result);
			++LoadContext->NumPluginsLoaded;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Loading %i GFPs"), LoadContext->NumPluginsLoaded);
		}));
	}
}

void UGameFeaturesSubsystem::RegisterGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	RegisterGameFeaturePlugin(PluginURL, FGameFeatureProtocolOptions(), CompleteDelegate);
}

void UGameFeaturesSubsystem::RegisterGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	FString PluginDisallowedReason;
	const bool bIsPluginAllowed = IsPluginAllowed(PluginURL, &PluginDisallowedReason);
	if (!bIsPluginAllowed)
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginNotAllowed),
			FText::Format(NSLOCTEXT("GameFeatures", "PluginDisallowedText", "Plugin disallowed reason: {0}"), FText::FromString(PluginDisallowedReason))));
		return;
	}

	UGameFeaturePluginStateMachine* StateMachine = FindOrCreateGameFeaturePluginStateMachine(PluginURL, ProtocolOptions);

	if (!StateMachine->IsRunning() && StateMachine->GetCurrentState() == EGameFeaturePluginState::Active)
	{
		// TODO: Resolve the activated case here, this is needed because in a PIE environment the plugins
		// are not sandboxed, and we need to do simulate a successful activate call in order run GFP systems 
		// on whichever Role runs second between client and server.

		// Refire the observer for Activated and do nothing else.
		CallbackObservers(EObserverCallback::Activating, StateMachine->GetPluginIdentifier(), &StateMachine->GetPluginName(), StateMachine->GetGameFeatureDataForActivePlugin());
	}

	if (ShouldUpdatePluginProtocolOptions(StateMachine, ProtocolOptions))
	{
		const UE::GameFeatures::FResult Result = UpdateGameFeatureProtocolOptions(StateMachine, ProtocolOptions);
		if (Result.HasError())
		{
			CompleteDelegate.ExecuteIfBound(Result);
			return;
		}
	}

	ChangeGameFeatureDestination(StateMachine, ProtocolOptions, FGameFeaturePluginStateRange(EGameFeaturePluginState::Registered, EGameFeaturePluginState::Active), CompleteDelegate);
}

void UGameFeaturesSubsystem::RegisterGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate)
{
	struct FLoadContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsLoaded CompleteDelegate;

		int32 NumPluginsLoaded = 0;
		bool bPushedTagsBroadcast = false;

		FLoadContext()
		{
			if (!IsEngineExitRequested())
			{
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
			else if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
			{
				TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
		}

		~FLoadContext()
		{
			if (bPushedTagsBroadcast)
			{
				if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PopDeferOnGameplayTagTreeChangedBroadcast();
				}
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FLoadContext> LoadContext = MakeShared<FLoadContext>();
	LoadContext->CompleteDelegate = CompleteDelegate;

	LoadContext->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		LoadContext->Results.Add(PluginURL, MakeError("Pending"));
	}

	const int32 NumPluginsToLoad = PluginURLs.Num();
	UE_LOG(LogGameFeatures, Log, TEXT("Registering %i GFPs"), NumPluginsToLoad);

	for (const FString& PluginURL : PluginURLs)
	{
		RegisterGameFeaturePlugin(PluginURL, ProtocolOptions, FGameFeaturePluginChangeStateComplete::CreateLambda([LoadContext, PluginURL](const UE::GameFeatures::FResult& Result)
			{
				LoadContext->Results.Add(PluginURL, Result);
				++LoadContext->NumPluginsLoaded;
				UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Registering %i GFPs"), LoadContext->NumPluginsLoaded);
			}));
	}
}

void UGameFeaturesSubsystem::LoadAndActivateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	ChangeGameFeatureTargetState(PluginURL, EGameFeatureTargetState::Active, CompleteDelegate);
}

void UGameFeaturesSubsystem::LoadAndActivateGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate)
{
	ChangeGameFeatureTargetState(PluginURL, ProtocolOptions, EGameFeatureTargetState::Active, CompleteDelegate);
}

void UGameFeaturesSubsystem::LoadAndActivateGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate)
{
	ChangeGameFeatureTargetState(PluginURLs, ProtocolOptions, EGameFeatureTargetState::Active, CompleteDelegate);
}

void UGameFeaturesSubsystem::ChangeGameFeatureTargetState(const FString& PluginURL, EGameFeatureTargetState TargetState, const FGameFeaturePluginChangeStateComplete& CompleteDelegate)
{
	ChangeGameFeatureTargetState(PluginURL, FGameFeatureProtocolOptions(), TargetState, CompleteDelegate);
}

void UGameFeaturesSubsystem::ChangeGameFeatureTargetState(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, EGameFeatureTargetState TargetState, const FGameFeaturePluginChangeStateComplete& CompleteDelegate)
{
	TRACE_CPUPROFILER_EVENT_SCOPE_STR("UGameFeaturesSubsystem::ChangeGameFeatureTargetState");
	EGameFeaturePluginState TargetPluginState = EGameFeaturePluginState::MAX;

	switch (TargetState)
	{
	case EGameFeatureTargetState::Installed:	TargetPluginState = EGameFeaturePluginState::Installed;		break;
	case EGameFeatureTargetState::Registered:	TargetPluginState = EGameFeaturePluginState::Registered;	break;
	case EGameFeatureTargetState::Loaded:		TargetPluginState = EGameFeaturePluginState::Loaded;		break;
	case EGameFeatureTargetState::Active:		TargetPluginState = EGameFeaturePluginState::Active;		break;
	}

	// Make sure we have coverage on all values of EGameFeatureTargetState
	static_assert(std::underlying_type<EGameFeatureTargetState>::type(EGameFeatureTargetState::Count) == 4, "");
	check(TargetPluginState != EGameFeaturePluginState::MAX);


	FString PluginDisallowedReason;
	const bool bIsPluginAllowed = IsPluginAllowed(PluginURL, &PluginDisallowedReason);

	UGameFeaturePluginStateMachine* StateMachine = nullptr;
	if (!bIsPluginAllowed)
	{
		StateMachine = FindGameFeaturePluginStateMachine(PluginURL);
		if (!StateMachine)
		{
			UE_LOG(LogGameFeatures, Log, TEXT("Cannot create GFP State Machine: Plugin not allowed %s"), *PluginURL);

			CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginNotAllowed),
				FText::Format(NSLOCTEXT("GameFeatures", "PluginDisallowedText", "Plugin disallowed reason: {0}"), FText::FromString(PluginDisallowedReason))));
			return;
		}
	}
	else
	{
		StateMachine = FindOrCreateGameFeaturePluginStateMachine(PluginURL, ProtocolOptions);
	}
	
	check(StateMachine);

	if (!bIsPluginAllowed)
	{
		if (TargetPluginState > StateMachine->GetCurrentState() || TargetPluginState > StateMachine->GetDestination())
		{
			UE_LOG(LogGameFeatures, Log, TEXT("Cannot change game feature target state: Plugin not allowed %s"), *PluginURL);

			CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginNotAllowed),
				FText::Format(NSLOCTEXT("GameFeatures", "PluginDisallowedText", "Plugin disallowed reason: {0}"), FText::FromString(PluginDisallowedReason))));
			return;
		}
	}

	if (TargetState == EGameFeatureTargetState::Active &&
		!StateMachine->IsRunning() && 
		StateMachine->GetCurrentState() == TargetPluginState)
	{
		// TODO: Resolve the activated case here, this is needed because in a PIE environment the plugins
		// are not sandboxed, and we need to do simulate a successful activate call in order run GFP systems 
		// on whichever Role runs second between client and server.

		// Refire the observer for Activated and do nothing else.
		CallbackObservers(EObserverCallback::Activating, StateMachine->GetPluginIdentifier(), &StateMachine->GetPluginName(), StateMachine->GetGameFeatureDataForActivePlugin());
	}
	
	if (ShouldUpdatePluginProtocolOptions(StateMachine, ProtocolOptions))
	{
		const UE::GameFeatures::FResult Result = UpdateGameFeatureProtocolOptions(StateMachine, ProtocolOptions);
		if (Result.HasError())
		{
			CompleteDelegate.ExecuteIfBound(Result);
			return;
		}
	}
	
	ChangeGameFeatureDestination(StateMachine, ProtocolOptions, FGameFeaturePluginStateRange(TargetPluginState), CompleteDelegate);
}

void UGameFeaturesSubsystem::ChangeGameFeatureTargetState(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, EGameFeatureTargetState TargetState, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate)
{
	struct FLoadContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsLoaded CompleteDelegate;

		int32 NumPluginsLoaded = 0;
		bool bPushedTagsBroadcast = false;

		FLoadContext()
		{
			if (!IsEngineExitRequested())
			{
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
			else if(UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
			{
				TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
		}

		~FLoadContext()
		{
			if (bPushedTagsBroadcast)
			{
				if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PopDeferOnGameplayTagTreeChangedBroadcast();
				}
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FLoadContext> LoadContext = MakeShared<FLoadContext>();
	LoadContext->CompleteDelegate = CompleteDelegate;

	LoadContext->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		LoadContext->Results.Add(PluginURL, MakeError("Pending"));
	}

	const int32 NumPluginsToLoad = PluginURLs.Num();
	UE_LOG(LogGameFeatures, Log, TEXT("Transitioning (%s) %i GFPs"), *LexToString(TargetState), NumPluginsToLoad);

	for (const FString& PluginURL : PluginURLs)
	{
		ChangeGameFeatureTargetState(PluginURL, ProtocolOptions, TargetState, FGameFeaturePluginChangeStateComplete::CreateLambda([LoadContext, PluginURL, TargetState](const UE::GameFeatures::FResult& Result)
		{
			LoadContext->Results.Add(PluginURL, Result);
			++LoadContext->NumPluginsLoaded;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Transitioning (%s) %i GFPs"), *LexToString(TargetState), LoadContext->NumPluginsLoaded);
		}));
	}
}

UE::GameFeatures::FResult UGameFeaturesSubsystem::UpdateGameFeatureProtocolOptions(const FString& PluginURL, const FGameFeatureProtocolOptions& NewOptions, bool* bOutDidUpdate /*= nullptr*/)
{
	UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL);
	return UpdateGameFeatureProtocolOptions(StateMachine, NewOptions, bOutDidUpdate);
}

UE::GameFeatures::FResult UGameFeaturesSubsystem::UpdateGameFeatureProtocolOptions(UGameFeaturePluginStateMachine* StateMachine, const FGameFeatureProtocolOptions& NewOptions, bool* bOutDidUpdate /*= nullptr*/)
{
	if (bOutDidUpdate)
	{
		*bOutDidUpdate = false;
	}

	if (!StateMachine)
	{
		return MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL);
	}

	bool bUpdated = false;
	UE::GameFeatures::FResult Result = StateMachine->TryUpdatePluginProtocolOptions(NewOptions, bUpdated);
	if (bOutDidUpdate)
	{
		*bOutDidUpdate = bUpdated;
	}

	return Result;
}

bool UGameFeaturesSubsystem::ShouldUpdatePluginProtocolOptions(const UGameFeaturePluginStateMachine* StateMachine, const FGameFeatureProtocolOptions& NewOptions)
{
	if (NewOptions.HasSubtype<FNull>())
	{
		return false;
	}

	if (!StateMachine)
	{
		return false;
	}
	
	//Make sure our StateMachine isn't in terminal, don't want to update Terminal plugins
	if (TerminalGameFeaturePluginStateMachines.Contains(StateMachine) || (StateMachine->GetCurrentState() == EGameFeaturePluginState::Terminal))
	{
		return false;
	}

	if (StateMachine->GetProtocolOptions() == NewOptions)
	{
		return false;
	}
	
	return true;
}

namespace UE::GameFeatures
{
	static float CombineInstallProgress(float InstallProgress, float AssetDependencyProgress)
	{
		// Assumuption that most of the progress will be from asset dependencies in this case
		// For this to be more accurate we'd need to figure out the actual sizes during 
		// EGameFeaturePluginState::CheckingStatus but this is most likely good enough
		return 0.2f * InstallProgress + 0.8f * AssetDependencyProgress;
	}
}

bool UGameFeaturesSubsystem::GetGameFeaturePluginInstallPercent(const FString& PluginURL, float& Install_Percent) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		if (StateMachine->IsStatusKnown() && StateMachine->IsAvailable())
		{
			const FGameFeaturePluginStateInfo& StateInfo = StateMachine->GetCurrentStateInfo();

			float InstallProgress = 0.0f;
			if (StateInfo.State == EGameFeaturePluginState::Downloading)
			{
				InstallProgress = StateInfo.Progress;
			}
			else if (StateInfo.State >= EGameFeaturePluginState::Installed)
			{
				InstallProgress = 1.0f;
			}

			if (!StateMachine->HasAssetStreamingDependencies())
			{
				Install_Percent = InstallProgress;
				return true;
			}

			float AssetDependencyProgress = 0.0f;
			if (StateInfo.State == EGameFeaturePluginState::AssetDependencyStreaming)
			{
				AssetDependencyProgress = StateInfo.Progress;
			}
			else if(StateInfo.State >= EGameFeaturePluginState::Registering)
			{
				AssetDependencyProgress = 1.0f;
			}

			Install_Percent = UE::GameFeatures::CombineInstallProgress(InstallProgress, AssetDependencyProgress);
			return true;
		}
	}
	return false;
}

bool UGameFeaturesSubsystem::GetGameFeaturePluginInstallPercent(TConstArrayView<FString> PluginURLs, float& Install_Percent) const
{
	float TotalInstallPercent = 0;
	int32 NumFound = 0;

	for (const FString& URL : PluginURLs)
	{
		float SingleInstallPercent = 0;
		if (GetGameFeaturePluginInstallPercent(URL, SingleInstallPercent))
		{
			TotalInstallPercent += SingleInstallPercent;
			++NumFound;
		}
	}

	if (NumFound > 0)
	{
		Install_Percent = TotalInstallPercent / NumFound;
		return true;
	}

	return false;
}

bool UGameFeaturesSubsystem::DoesGameFeaturePluginNeedUpdate(const FString& PluginURL) const
{
	TArray<FName> InstallBundles;
	const bool bParseSuccess = UGameFeaturesSubsystem::ParsePluginURLOptions(PluginURL, EGameFeatureURLOptions::Bundles,
		[&InstallBundles](EGameFeatureURLOptions Option, FStringView OptionName, FStringView OptionValue)
		{
			check(Option == EGameFeatureURLOptions::Bundles);
			InstallBundles.Emplace(OptionValue);
		});

	if (InstallBundles.IsEmpty())
	{
		return false;
	}

	TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
	TValueOrError<FInstallBundleCombinedInstallState, EInstallBundleResult> InstallStateResult = BundleManager->GetInstallStateSynchronous(InstallBundles, false);
	if (InstallStateResult.HasError())
	{
		UE_LOG(LogGameFeatures, Error, TEXT("Failed to get install state for PluginURL %s : Error reason %s"), *PluginURL, LexToString(InstallStateResult.GetError()));
		return false;
	}

	return InstallStateResult.GetValue().GetAnyBundleHasState(EInstallBundleInstallState::NeedsUpdate);
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginActive(const FString& PluginURL, bool bCheckForActivating /*= false*/) const
{
	if (const UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		const EGameFeaturePluginState CurrentState = StateMachine->GetCurrentState();

		return CurrentState == EGameFeaturePluginState::Active || (bCheckForActivating && CurrentState == EGameFeaturePluginState::Activating);
	}

	return false;
}

bool UGameFeaturesSubsystem::IsGameFeaturePluginActiveByName(FStringView PluginName, bool bCheckForActivating /*= false*/) const
{
	FString PluginURLName;
	if (GetPluginURLByName(PluginName, PluginURLName))
	{
		if (!IsGameFeaturePluginActive(PluginURLName, bCheckForActivating))
		{
			return false;
		}
	}
	else
	{
		return false;
	}

	return true;
}

void UGameFeaturesSubsystem::DeactivateGameFeaturePlugin(const FString& PluginURL)
{
	DeactivateGameFeaturePlugin(PluginURL, FGameFeaturePluginDeactivateComplete());
}

void UGameFeaturesSubsystem::DeactivateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginDeactivateComplete& CompleteDelegate)
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		ChangeGameFeatureDestination(StateMachine, FGameFeaturePluginStateRange(EGameFeaturePluginState::Terminal, EGameFeaturePluginState::Loaded), CompleteDelegate);
	}
	else
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL)));
	}
}

void UGameFeaturesSubsystem::UnloadGameFeaturePlugin(const FString& PluginURL, bool bKeepRegistered /*= false*/)
{
	UnloadGameFeaturePlugin(PluginURL, FGameFeaturePluginUnloadComplete(), bKeepRegistered);
}

void UGameFeaturesSubsystem::UnloadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUnloadComplete& CompleteDelegate, bool bKeepRegistered /*= false*/)
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		EGameFeaturePluginState TargetPluginState = bKeepRegistered ? EGameFeaturePluginState::Registered : EGameFeaturePluginState::Installed;
		ChangeGameFeatureDestination(StateMachine, FGameFeaturePluginStateRange(EGameFeaturePluginState::Terminal, TargetPluginState), CompleteDelegate);
	}
	else
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL)));
	}
}

void UGameFeaturesSubsystem::ReleaseGameFeaturePlugin(const FString& PluginURL)
{
	ReleaseGameFeaturePlugin(PluginURL, FGameFeaturePluginReleaseComplete());
}

void UGameFeaturesSubsystem::ReleaseGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginReleaseComplete& CompleteDelegate)
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		ChangeGameFeatureDestination(StateMachine, FGameFeaturePluginStateRange(EGameFeaturePluginState::Terminal, EGameFeaturePluginState::StatusKnown), CompleteDelegate);
	}
	else
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL)));
	}
}

void UGameFeaturesSubsystem::ReleaseGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginsReleased& CompleteDelegate)
{
	struct FReleaseContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsReleased CompleteDelegate;
		int32 NumPluginsReleased = 0;
		bool bDeferGCAfterUnload = false;
		bool bPushedTagsBroadcast = false;

		FReleaseContext()
		{
			if (!IsEngineExitRequested())
			{
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				IPluginManager::Get().SuppressPluginUnloadGC();
				bDeferGCAfterUnload = true;
				bPushedTagsBroadcast = true;
			}
			else if(UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
			{
				TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
		}

		~FReleaseContext()
		{
			if (bDeferGCAfterUnload)
			{
				IPluginManager::Get().ResumePluginUnloadGC();
			}
			if (bPushedTagsBroadcast)
			{
				UGameplayTagsManager::Get().PopDeferOnGameplayTagTreeChangedBroadcast();
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FReleaseContext> LoadContext = MakeShared<FReleaseContext>();
	LoadContext->CompleteDelegate = CompleteDelegate;

	LoadContext->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		LoadContext->Results.Add(PluginURL, MakeError("Pending"));
	}

	for (const FString& PluginURL : PluginURLs)
	{
		ReleaseGameFeaturePlugin(PluginURL, FGameFeaturePluginReleaseComplete::CreateLambda([LoadContext, PluginURL](const UE::GameFeatures::FResult& Result)
		{
			LoadContext->Results[PluginURL] = Result;
			++LoadContext->NumPluginsReleased;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Releasing %i/%i GFPs"), LoadContext->NumPluginsReleased, LoadContext->Results.Num());
		}));
	}
}

void UGameFeaturesSubsystem::UninstallGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUninstallComplete& CompleteDelegate /*= FGameFeaturePluginUninstallComplete()*/)
{
	UninstallGameFeaturePlugin(PluginURL, FGameFeatureProtocolOptions(), CompleteDelegate);
}

void UGameFeaturesSubsystem::UninstallGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& InProtocolOptions, const FGameFeaturePluginUninstallComplete& CompleteDelegate /*= FGameFeaturePluginUninstallComplete()*/)
{
	// FindOrCreate so that we can make sure we uninstall data for plugins that were installed on a previous application run
	// but have not yet been requested on this application run and so are not yet in the plugin list but might have data on disk
	// to uninstall
	UGameFeaturePluginStateMachine* StateMachine = FindOrCreateGameFeaturePluginStateMachine(PluginURL, InProtocolOptions);
	check(StateMachine);

	// We may need to update our ProtocolOptions to force certain metadata changes to facilitate this uninstall
	FGameFeatureProtocolOptions ProtocolOptions = StateMachine->GetProtocolOptions();

	// InstallBundle Protocol GameFeatures may need to change their metadata to force this uninstall
	if (StateMachine->GetPluginIdentifier().GetPluginProtocol() == EGameFeaturePluginProtocol::InstallBundle)
	{
		// It's possible that ParseURL hasn't been called yet so setup options here if needed.
		if (!ProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>())
		{
			ensureMsgf(ProtocolOptions.HasSubtype<FNull>(), TEXT("Protocol options type is incorrect for URL %s"), *PluginURL);
			ProtocolOptions.SetSubtype<FInstallBundlePluginProtocolOptions>();
		}

		// Need to force on bUninstallBeforeTerminate if it wasn't already set to on in our Metadata
		FInstallBundlePluginProtocolOptions& InstallBundleOptions = ProtocolOptions.GetSubtype<FInstallBundlePluginProtocolOptions>();
		if (!InstallBundleOptions.bUninstallBeforeTerminate)
		{
			InstallBundleOptions.bUninstallBeforeTerminate = true;
		}
	}

	// Weird flow here because we need to do a few tasks asynchronously
	// 1) Update Protocol Options   -->   2) Call to set destination to Uninstall --> 3) After we get to Uninstall go to Terminate

	// FIRST:
	// If we need to update our ProtocolOptions, do that first before starting the Uninstall. This allows us to update
	// options that might be important on the way to Terminal if they are changed. EX: FInstallBundlePluginProtocolMetaData::bUninstallBeforeTerminate
	if (ShouldUpdatePluginProtocolOptions(StateMachine, ProtocolOptions))
	{
		const UE::GameFeatures::FResult Result = UpdateGameFeatureProtocolOptions(StateMachine, ProtocolOptions);
		if (Result.HasError())
		{
			CompleteDelegate.ExecuteIfBound(Result);
			return;
		}
	}

	// SECOND:
	// Kick off the Uninstall destination after updating our options if necessary
	ChangeGameFeatureDestination(StateMachine, ProtocolOptions, FGameFeaturePluginStateRange(EGameFeaturePluginState::Uninstalled),
		FGameFeaturePluginTerminateComplete::CreateWeakLambda(this, [this, PluginURL, CompleteDelegate](const UE::GameFeatures::FResult& Result)
		{
			// THIRD:
			// Kick off the actual Terminate after we successfully transition to Uninstalled state
			if (Result.HasValue())
			{
				TerminateGameFeaturePlugin(PluginURL, CompleteDelegate);
			}
			//If we failed just bubble error up
			else
			{
				CompleteDelegate.ExecuteIfBound(Result);
			}
		}));
}

void UGameFeaturesSubsystem::TerminateGameFeaturePlugin(const FString& PluginURL)
{
	TerminateGameFeaturePlugin(PluginURL, FGameFeaturePluginTerminateComplete());
}

void UGameFeaturesSubsystem::TerminateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginTerminateComplete& CompleteDelegate)
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		ChangeGameFeatureDestination(StateMachine, FGameFeaturePluginStateRange(EGameFeaturePluginState::Terminal), CompleteDelegate);
	}
	else
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL)));
	}
}

void UGameFeaturesSubsystem::TerminateGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginsTerminated& CompleteDelegate)
{
	struct FContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsTerminated CompleteDelegate;

		int32 NumPluginsTerminated = 0;
		bool bPushedTagsBroadcast = false;

		FContext()
		{
			if (!IsEngineExitRequested())
			{
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
			else if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
			{
				TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
		}

		~FContext()
		{
			if (bPushedTagsBroadcast)
			{
				if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PopDeferOnGameplayTagTreeChangedBroadcast();
				}
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FContext> Context = MakeShared<FContext>();
	Context->CompleteDelegate = CompleteDelegate;

	Context->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		Context->Results.Add(PluginURL, MakeError("Pending"));
	}

	const int32 NumPlugins = PluginURLs.Num();
	UE_LOG(LogGameFeatures, Log, TEXT("Terminating %i GFPs"), NumPlugins);

	for (const FString& PluginURL : PluginURLs)
	{
		TerminateGameFeaturePlugin(PluginURL, FGameFeaturePluginTerminateComplete::CreateLambda([Context, PluginURL](const UE::GameFeatures::FResult& Result)
		{
			Context->Results.Add(PluginURL, Result);
			++Context->NumPluginsTerminated;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Terminating %i GFPs"), Context->NumPluginsTerminated);
		}));
	}
}

void UGameFeaturesSubsystem::CancelGameFeatureStateChange(const FString& PluginURL)
{
	CancelGameFeatureStateChange(PluginURL, FGameFeaturePluginChangeStateComplete());
}

void UGameFeaturesSubsystem::CancelGameFeatureStateChange(const FString& PluginURL, const FGameFeaturePluginChangeStateComplete& CompleteDelegate)
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		const bool bCancelPending = StateMachine->TryCancel(FGameFeatureStateTransitionCanceled::CreateWeakLambda(this, [CompleteDelegate](UGameFeaturePluginStateMachine* Machine)
		{
			CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeValue()));
		}));

		if (!bCancelPending)
		{
			CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeValue()));
		}
	}
	else
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::BadURL)));
	}
}

void UGameFeaturesSubsystem::CancelGameFeatureStateChange(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginChangeStateComplete& CompleteDelegate)
{
	struct FContext
	{
		TMap<FString, UE::GameFeatures::FResult> Results;
		FMultipleGameFeaturePluginsLoaded CompleteDelegate;

		int32 NumPluginsCanceled = 0;

		~FContext()
		{
			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FContext> CancelContext = MakeShared<FContext>();
	CancelContext->CompleteDelegate = CompleteDelegate;

	CancelContext->Results.Reserve(PluginURLs.Num());
	for (const FString& PluginURL : PluginURLs)
	{
		CancelContext->Results.Add(PluginURL, MakeError("Pending"));
	}

	UE_LOG(LogGameFeatures, Log, TEXT("Canceling %i GFP transitions"), PluginURLs.Num());

	for (const FString& PluginURL : PluginURLs)
	{
		CancelGameFeatureStateChange(PluginURL, FGameFeaturePluginChangeStateComplete::CreateLambda([CancelContext, PluginURL](const UE::GameFeatures::FResult& Result)
		{
			CancelContext->Results.Add(PluginURL, Result);
			++CancelContext->NumPluginsCanceled;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished canceling %i GFP transitions"), CancelContext->NumPluginsCanceled);
		}));
	}
}

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter, const FGameFeaturePluginLoadComplete& CompleteDelegate /*= FGameFeaturePluginLoadComplete()*/)
{
	UE_SCOPED_ENGINE_ACTIVITY(TEXT("Loading GameFeaturePlugin %s"), *Plugin->GetName());

#if WITH_EDITOR
	auto AddUnmountedPluginExplination = [this, &Plugin](const UE::GameFeatures::FResult& Result)
	{
		UnmountedPluginNameToExplanation.FindOrAdd(Plugin->GetName()) = UE::GameFeatures::ToString(Result);
		return Result;
	};
#else
	auto AddUnmountedPluginExplination = [](const UE::GameFeatures::FResult& Result)
	{
		return Result;
	};
#endif // WITH_EDITOR

	UAssetManager::Get().PushBulkScanning();
	ON_SCOPE_EXIT
	{
		UAssetManager::Get().PopBulkScanning();
	};

	const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();

	FString PluginURL;
	FGameFeaturePluginDetails PluginDetails;

	if (PluginDescriptorFilename.IsEmpty())
	{
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginDetailsNotFound)));
	}
	else if (!GetDefault<UGameFeaturesSubsystemSettings>()->IsValidGameFeaturePlugin(FPaths::ConvertRelativePathToFull(PluginDescriptorFilename)))
	{
		// Not a GFP, trivial success
		CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeValue()));
	}
	else if (!FPaths::FileExists(PluginDescriptorFilename) || !GetGameFeaturePluginDetailsInternal(PluginDescriptorFilename, PluginDetails))
	{
		CompleteDelegate.ExecuteIfBound(AddUnmountedPluginExplination(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginDetailsNotFound)));
	}
	else if (!GetBuiltInGameFeaturePluginURL(Plugin, PluginURL))
	{
		CompleteDelegate.ExecuteIfBound(AddUnmountedPluginExplination(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginURLNotFound)));
	}
	else if (FString PluginDisallowedReason; !IsPluginAllowed(PluginURL, &PluginDisallowedReason))
	{
		CompleteDelegate.ExecuteIfBound(
			AddUnmountedPluginExplination(
				UE::GameFeatures::FResult(
					MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginNotAllowed),
					FText::Format(NSLOCTEXT("GameFeatures", "PluginDisallowedText", "Plugin disallowed reason: {0}"), FText::FromString(PluginDisallowedReason))
				)
			)
		);
	}
	else
	{
		FBuiltInGameFeaturePluginBehaviorOptions BehaviorOptions;
		const bool bShouldProcess = AdditionalFilter(Plugin->GetDescriptorFileName(), PluginDetails, BehaviorOptions);
		if (!bShouldProcess)
		{
			CompleteDelegate.ExecuteIfBound(AddUnmountedPluginExplination(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::PluginFiltered)));
		}
		else
		{
			FGameFeatureProtocolOptions ProtocolOptions;
			if (UGameFeaturesSubsystem::GetPluginURLProtocol(PluginURL) == EGameFeaturePluginProtocol::InstallBundle)
			{
				FInstallBundlePluginProtocolOptions InstallBundleOptions;
				InstallBundleOptions.bAllowIniLoading = true;
				InstallBundleOptions.bDoNotDownload = BehaviorOptions.bDoNotDownload;
				ProtocolOptions = FGameFeatureProtocolOptions(InstallBundleOptions);
			}
			ProtocolOptions.bForceSyncLoading = BehaviorOptions.bForceSyncLoading;
			ProtocolOptions.bLogWarningOnForcedDependencyCreation = BehaviorOptions.bLogWarningOnForcedDependencyCreation;
			ProtocolOptions.bLogErrorOnForcedDependencyCreation = BehaviorOptions.bLogErrorOnForcedDependencyCreation;
			ProtocolOptions.bBatchProcess = BehaviorOptions.bBatchProcess;

			bool bFoundExisting = false;
			UGameFeaturePluginStateMachine* StateMachine = FindOrCreateGameFeaturePluginStateMachine(PluginURL, ProtocolOptions, &bFoundExisting);
			if (bFoundExisting && ShouldUpdatePluginProtocolOptions(StateMachine, ProtocolOptions))
			{
				const UE::GameFeatures::FResult Result = UpdateGameFeatureProtocolOptions(StateMachine, ProtocolOptions);
				if (Result.HasError())
				{
					CompleteDelegate.ExecuteIfBound(Result);
					return;
				}
			}

			EBuiltInAutoState InitialAutoState = (BehaviorOptions.AutoStateOverride != EBuiltInAutoState::Invalid) ? 
				BehaviorOptions.AutoStateOverride : PluginDetails.BuiltInAutoState;
			if (InitialAutoState < EBuiltInAutoState::Registered && IsRunningCookCommandlet())
			{
				UE_LOG(LogGameFeatures, Display, TEXT("%s will be set to Registered for cooking"), *Plugin->GetName());
				InitialAutoState = EBuiltInAutoState::Registered;
			}
			const EGameFeaturePluginState DestinationState = ConvertInitialFeatureStateToTargetState(InitialAutoState);

			StateMachine->SetWasLoadedAsBuiltIn();

			// If we're already at the destination or beyond, don't transition back
			FGameFeaturePluginStateRange Destination(DestinationState, EGameFeaturePluginState::Active);
			ChangeGameFeatureDestination(StateMachine, ProtocolOptions, Destination,
				FGameFeaturePluginChangeStateComplete::CreateWeakLambda(this, [this, StateMachine, Destination, CompleteDelegate](const UE::GameFeatures::FResult& Result)
				{
					LoadBuiltInGameFeaturePluginComplete(Result, StateMachine, Destination);
					CompleteDelegate.ExecuteIfBound(Result);
				}));
		}
	}
}

#if UE_BUILD_SHIPPING
class FBuiltInPluginLoadTimeTracker {};
class FBuiltInPluginLoadTimeTrackerScope
{
public:
	FORCEINLINE FBuiltInPluginLoadTimeTrackerScope(FBuiltInPluginLoadTimeTracker& InTracker, const TSharedRef<IPlugin>& Plugin) {};
};
#else // !UE_BUILD_SHIPPING
class FBuiltInPluginLoadTimeTracker
{
public:
	FBuiltInPluginLoadTimeTracker()
	{
		StartTime = FPlatformTime::Seconds();
	}

	~FBuiltInPluginLoadTimeTracker()
	{
		double TotalLoadTime = FPlatformTime::Seconds() - StartTime;
		UE_LOG(LogGameFeatures, Display, TEXT("Total built in plugin load time %.4fs"), TotalLoadTime);

		if (PluginLoadTimes.Num() > 0)
		{
			UE_LOG(LogGameFeatures, Display, TEXT("There were %d built in plugins that took longer than %.4fs to load. Listing worst offenders."), PluginLoadTimes.Num(), UE::GameFeatures::GBuiltInPluginLoadTimeReportThreshold);
			const int32 NumToReport = FMath::Min(PluginLoadTimes.Num(), UE::GameFeatures::GBuiltInPluginLoadTimeMaxReportCount);
			Algo::Sort(PluginLoadTimes, [](const TPair<FString, double>& A, TPair<FString, double>& B) { return A.Value > B.Value; });
			for (int32 PluginIdx = 0; PluginIdx < NumToReport; ++PluginIdx)
			{
				const TPair<FString, double>& Plugin = PluginLoadTimes[PluginIdx];
				double LoadTime = Plugin.Value;
				if (LoadTime >= UE::GameFeatures::GBuiltInPluginLoadTimeErrorThreshold)
				{
					UE_LOG(LogGameFeatures, Warning, TEXT("%s took %.4f seconds to load. Something was done to significantly increase the load time of this plugin and it is now well outside what is acceptable. Reduce the load time to much less than %.4f seconds. Ideally, reduce the load time to less than %.4f seconds."),
						*Plugin.Key, Plugin.Value, UE::GameFeatures::GBuiltInPluginLoadTimeErrorThreshold, UE::GameFeatures::GBuiltInPluginLoadTimeReportThreshold);
				}
				else
				{
					UE_LOG(LogGameFeatures, Display, TEXT("  %.4fs\t%s"), Plugin.Value, *Plugin.Key);
				}
			}
		}
	}

	void ReportPlugin(const FString& PluginName, double LoadTime)
	{
		PluginLoadTimes.Emplace(PluginName, LoadTime);
	}

private:
	TArray<TPair<FString, double>> PluginLoadTimes;
	double StartTime;
};

class FBuiltInPluginLoadTimeTrackerScope
{
public:
	FBuiltInPluginLoadTimeTrackerScope(FBuiltInPluginLoadTimeTracker& InTracker, const TSharedRef<IPlugin>& Plugin)
		: Tracker(InTracker), PluginName(Plugin->GetName()), StartTime(FPlatformTime::Seconds())
	{}

	~FBuiltInPluginLoadTimeTrackerScope()
	{
		double LoadTime = FPlatformTime::Seconds() - StartTime;
		if (LoadTime >= UE::GameFeatures::GBuiltInPluginLoadTimeReportThreshold)
		{
			Tracker.ReportPlugin(PluginName, LoadTime);
		}
	}

private:
	FBuiltInPluginLoadTimeTracker& Tracker;
	FString PluginName;
	double StartTime;
};

#endif // !UE_BUILD_SHIPPING

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugins(FBuiltInPluginAdditionalFilters AdditionalFilter, const FBuiltInGameFeaturePluginsLoaded& InCompleteDelegate /*= FBuiltInGameFeaturePluginsLoaded()*/)
{
	FBuiltInPluginAdditionalFilters_Copyable NullCopyable;
	LoadBuiltInGameFeaturePluginsInternal(AdditionalFilter, NullCopyable, 0, InCompleteDelegate);
}

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugins_Amortized(const FBuiltInPluginAdditionalFilters_Copyable& AdditionalFilter_Copyable, int32 AmortizeRate, const FBuiltInGameFeaturePluginsLoaded& InCompleteDelegate /*= FBuiltInGameFeaturePluginsLoaded()*/)
{
	LoadBuiltInGameFeaturePluginsInternal(AdditionalFilter_Copyable, AdditionalFilter_Copyable, AmortizeRate, InCompleteDelegate);
}

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePluginsInternal(FBuiltInPluginAdditionalFilters AdditionalFilter, const FBuiltInPluginAdditionalFilters_Copyable& AdditionalFilter_Copyable, int32 AmortizeRate, const FBuiltInGameFeaturePluginsLoaded& InCompleteDelegate /*= FBuiltInGameFeaturePluginsLoaded()*/)
{
	TRACE_CPUPROFILER_EVENT_SCOPE_STR("UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugins");

	struct FLoadContext
	{
		FScopeLogTime ScopeLogTime{TEXT("BuiltInGameFeaturePlugins loaded."), nullptr, FConditionalScopeLogTime::ScopeLog_Seconds};

		TArray<TSharedRef<IPlugin>> AmortizedPluginsRemaining;
		TMap<FString, UE::GameFeatures::FResult> Results;
		FBuiltInGameFeaturePluginsLoaded CompleteDelegate;

		int32 NumPluginsLoaded = 0;
		int32 PluginAmortizeRate = 0;
		bool bPushedTagsBroadcast = false;
		bool bPushedAssetBulkScanning = false;

		FLoadContext()
		{
			if (!IsEngineExitRequested())
			{
				UAssetManager::Get().PushBulkScanning();
				bPushedAssetBulkScanning = true;
				UGameplayTagsManager::Get().PushDeferOnGameplayTagTreeChangedBroadcast();
				bPushedTagsBroadcast = true;
			}
			else
			{
				if(UAssetManager* AssetManager =  UAssetManager::GetIfInitialized())
				{
					AssetManager->PushBulkScanning();
					bPushedAssetBulkScanning = true;
				}
				if(UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PushDeferOnGameplayTagTreeChangedBroadcast();
					bPushedTagsBroadcast = true;
				}
			}
		}

		~FLoadContext()
		{
			if (bPushedTagsBroadcast)
			{
				if (UGameplayTagsManager* TagsManager = UGameplayTagsManager::GetIfAllocated())
				{
					TagsManager->PopDeferOnGameplayTagTreeChangedBroadcast();
				}
			}
			if (bPushedAssetBulkScanning)
			{
				if(UAssetManager* AssetManager =  UAssetManager::GetIfInitialized())
				{
					AssetManager->PopBulkScanning();
				}
			}

			CompleteDelegate.ExecuteIfBound(Results);
		}
	};
	TSharedRef<FLoadContext> LoadContext = MakeShared<FLoadContext>();
	LoadContext->CompleteDelegate = InCompleteDelegate;

	FBuiltInPluginLoadTimeTracker PluginLoadTimeTracker;
	TArray<TSharedRef<IPlugin>> EnabledPlugins = IPluginManager::Get().GetEnabledPlugins();

	if (UE::GameFeatures::bTrimNonStartupEnabledPlugins)
	{
		const TSet<FString>& CompiledInPlugins = IPluginManager::Get().GetPluginsEnabledAtStartup();
		EnabledPlugins.RemoveAllSwap([&CompiledInPlugins](const TSharedRef<IPlugin>& Plugin)
			{
				const bool bRemove = !CompiledInPlugins.Contains(Plugin->GetName());
				return bRemove;
			});
	}

	LoadContext->Results.Reserve(EnabledPlugins.Num());
	for (const TSharedRef<IPlugin>& Plugin : EnabledPlugins)
	{
		LoadContext->Results.Add(Plugin->GetName(), MakeError("Pending"));
	}

	const int32 NumPluginsToLoad = EnabledPlugins.Num();
	UE_LOG(LogGameFeatures, Log, TEXT("Loading %i builtins. %s"), NumPluginsToLoad, AmortizeRate > 0 ? TEXT("Amortized") : TEXT("Not Amortized"));

	// Sort the plugins so we can more accurately track how long it takes to load rather than have inconsistent dependency timings.
	TArray<TSharedRef<IPlugin>> Dependencies;
	auto GetPluginDependencies =
		[&Dependencies](TSharedRef<IPlugin> CurrentPlugin)
	{
		IPluginManager& PluginManager = IPluginManager::Get();
		Dependencies.Reset();
		
		const FPluginDescriptor& Desc = CurrentPlugin->GetDescriptor();
		for (const FPluginReferenceDescriptor& Dependency : Desc.Plugins)
		{
			if (Dependency.bEnabled)
			{
				if (TSharedPtr<IPlugin> FoundPlugin = PluginManager.FindEnabledPlugin(Dependency.Name))
				{
					Dependencies.Add(FoundPlugin.ToSharedRef());
				}
			}
		}
		return Dependencies;
	};

	Algo::TopologicalSort(EnabledPlugins, GetPluginDependencies);

	auto ProcessGameFeaturePlugin = [this](const TSharedRef<IPlugin>& PluginToProcess, const TSharedRef<FLoadContext>& LoadContextToProcess, const FBuiltInPluginAdditionalFilters& AdditionalFilterForProcess)
	{
		LoadBuiltInGameFeaturePlugin(PluginToProcess, AdditionalFilterForProcess, FGameFeaturePluginLoadComplete::CreateLambda([LoadContextToProcess, PluginToProcess](const UE::GameFeatures::FResult& Result)
		{
			LoadContextToProcess->Results.Add(PluginToProcess->GetName(), Result);
			++LoadContextToProcess->NumPluginsLoaded;
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Finished Loading %i builtins"), LoadContextToProcess->NumPluginsLoaded);
		}));
	};

	if (AmortizeRate > 0)
	{
		LoadContext->PluginAmortizeRate = AmortizeRate;
		LoadContext->AmortizedPluginsRemaining = EnabledPlugins;
		// Note that we are using AdditionalFilter_Copyable here. We need to make a copy of thus function so that the captured properties in lambdas are not destroyed
		FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this, [this, LoadContext, AdditionalFilter_Copyable, ProcessGameFeaturePlugin](float)
		{
			if (GameSpecificPolicies)
			{
				int32 NumPluginsThisFrame = FMath::Min(LoadContext->PluginAmortizeRate, LoadContext->AmortizedPluginsRemaining.Num());
				
				for (int32 PluginIdx = 0; PluginIdx < NumPluginsThisFrame; ++PluginIdx)
				{
					const TSharedRef<IPlugin>& Plugin = LoadContext->AmortizedPluginsRemaining[PluginIdx];
					ProcessGameFeaturePlugin(Plugin, LoadContext, AdditionalFilter_Copyable);
					
				}

				LoadContext->AmortizedPluginsRemaining.RemoveAt(0, NumPluginsThisFrame);
			}
			return GameSpecificPolicies && LoadContext->AmortizedPluginsRemaining.Num() > 0;
		}));
	}
	else
	{
		for (const TSharedRef<IPlugin>& Plugin : EnabledPlugins)
		{
			FBuiltInPluginLoadTimeTrackerScope TrackerScope(PluginLoadTimeTracker, Plugin);
			ProcessGameFeaturePlugin(Plugin, LoadContext, AdditionalFilter);
		}
	}
}

bool UGameFeaturesSubsystem::GetPluginURLByName(FStringView PluginName, FString& OutPluginURL) const
{
	if (const FString* PluginURL = GameFeaturePluginNameToPathMap.FindByHash(GetTypeHash(PluginName), PluginName))
	{
		OutPluginURL = *PluginURL;
		return true;
	}

	return false;
}

bool UGameFeaturesSubsystem::GetPluginURLForBuiltInPluginByName(const FString& PluginName, FString& OutPluginURL) const
{
	return GetPluginURLByName(PluginName, OutPluginURL);
}

FString UGameFeaturesSubsystem::GetPluginFilenameFromPluginURL(const FString& PluginURL) const
{
	FString PluginFilename;
	const UGameFeaturePluginStateMachine* GFSM = FindGameFeaturePluginStateMachine(PluginURL);
	if (GFSM == nullptr || !GFSM->GetPluginFilename(PluginFilename))
	{
		UE_LOG(LogGameFeatures, Error, TEXT("UGameFeaturesSubsystem could not get the plugin path from the plugin URL. URL:%s "), *PluginURL);
	}
	return PluginFilename;
}

FString UGameFeaturesSubsystem::GetPluginNameFromPluginURL(const FString& PluginURL) const
{
	const UGameFeaturePluginStateMachine* GFSM = FindGameFeaturePluginStateMachine(PluginURL);
	if (GFSM == nullptr)
	{
		UE_LOG(LogGameFeatures, Error, TEXT("UGameFeaturesSubsystem could not get the plugin name from the plugin URL. URL:%s "), *PluginURL);
		return FString();
	}
	return GFSM->GetPluginName();
}

void UGameFeaturesSubsystem::FixPluginPackagePath(FString& PathToFix, const FString& PluginRootPath, bool bMakeRelativeToPluginRoot)
{
	if (bMakeRelativeToPluginRoot)
	{
		// This only modifies paths starting with the root
		PathToFix.RemoveFromStart(PluginRootPath);
	}
	else
	{
		if (!FPackageName::IsValidLongPackageName(PathToFix))
		{
			PathToFix = PluginRootPath / PathToFix;
		}
	}
}

void UGameFeaturesSubsystem::GetLoadedGameFeaturePluginFilenamesForCooking(TArray<FString>& OutLoadedPluginFilenames) const
{
	for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
	{
		UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value();
		if (GFSM && GFSM->GetCurrentState() > EGameFeaturePluginState::Installed)
		{
			FString PluginFilename;
			if (GFSM->GetPluginFilename(PluginFilename))
			{
				OutLoadedPluginFilenames.Add(PluginFilename);
			}
		}
	}
}

EGameFeaturePluginState UGameFeaturesSubsystem::GetPluginState(const FString& PluginURL) const
{
	FGameFeaturePluginIdentifier PluginIdentifier(PluginURL);
	return GetPluginState(PluginIdentifier);
}

EGameFeaturePluginState UGameFeaturesSubsystem::GetPluginState(FGameFeaturePluginIdentifier PluginIdentifier) const
{
	if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginIdentifier))
	{
		return StateMachine->GetCurrentState();
	}
	else
	{
		return EGameFeaturePluginState::UnknownStatus;
	}
}

bool UGameFeaturesSubsystem::GetGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL, FGameFeaturePluginDetails& OutPluginDetails) const
{
	return GetBuiltInGameFeaturePluginURL(Plugin, OutPluginURL) && GetBuiltInGameFeaturePluginDetails(Plugin, OutPluginDetails);
}

bool UGameFeaturesSubsystem::GetBuiltInGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL, FGameFeaturePluginDetails& OutPluginDetails) const
{
	return GetBuiltInGameFeaturePluginURL(Plugin, OutPluginURL) && GetBuiltInGameFeaturePluginDetails(Plugin, OutPluginDetails);
}

bool UGameFeaturesSubsystem::GetBuiltInGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, struct FGameFeaturePluginDetails& OutPluginDetails) const
{
	const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();
	// Make sure you are in a game feature plugins folder. All GameFeaturePlugins are rooted in a GameFeatures folder.
	if (!PluginDescriptorFilename.IsEmpty() && GetDefault<UGameFeaturesSubsystemSettings>()->IsValidGameFeaturePlugin(FPaths::ConvertRelativePathToFull(PluginDescriptorFilename)) && FPaths::FileExists(PluginDescriptorFilename))
	{
		return GetGameFeaturePluginDetailsInternal(PluginDescriptorFilename, OutPluginDetails);
	}

	return false;
}


bool UGameFeaturesSubsystem::GetBuiltInGameFeaturePluginURL(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL) const
{
	// @TODO: this problematic because it assumes file protocol.
	// Ideally this would work with any protocol, but for current uses cases the exact protocol doesn't seem to matter.

	const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();
	// Make sure you are in a game feature plugins folder. All GameFeaturePlugins are rooted in a GameFeatures folder.
	if (!PluginDescriptorFilename.IsEmpty() && GetDefault<UGameFeaturesSubsystemSettings>()->IsValidGameFeaturePlugin(FPaths::ConvertRelativePathToFull(PluginDescriptorFilename)) && FPaths::FileExists(PluginDescriptorFilename))
	{
		const FString PluginName = Plugin->GetName();
		bool bFoundPluginURL = GetPluginURLByName(PluginName, OutPluginURL);
		if (!bFoundPluginURL)
		{
			bFoundPluginURL = GameSpecificPolicies->GetGameFeaturePluginURL(Plugin, OutPluginURL);
		}
		return bFoundPluginURL;
	}

	return false;
}

bool UGameFeaturesSubsystem::GetGameFeaturePluginDetails(const FString& PluginURL, FGameFeaturePluginDetails& OutPluginDetails) const
{
	FStringView PluginPath;
	if (UGameFeaturesSubsystem::ParsePluginURL(PluginURL, nullptr, &PluginPath))
	{
		return GetGameFeaturePluginDetailsInternal(FString(PluginPath), OutPluginDetails);
	}

	return false;
}

bool UGameFeaturesSubsystem::GetGameFeatureControlsUPlugin(const FString& PluginURL, bool& OutGameFeatureControlsUPlugin) const
{
	if (UGameFeaturePluginStateMachine* Machine = FindGameFeaturePluginStateMachine(PluginURL))
	{
		OutGameFeatureControlsUPlugin = Machine->GetProperties().bAddedPluginToManager;
		return true;
	}

	return false;
}

bool UGameFeaturesSubsystem::GetGameFeaturePluginDetailsInternal(const FString& PluginDescriptorFilename, FGameFeaturePluginDetails& OutPluginDetails) const
{
	TRACE_CPUPROFILER_EVENT_SCOPE(GFP_GetPluginDetails);

	check(GameSpecificPolicies);
	if (!GameSpecificPolicies->ShouldReadPluginDetails(PluginDescriptorFilename))
	{
		return false;
	}

	// GFPs are implemented with a plugin so FPluginReferenceDescriptor doesn't know anything about them.
	// Need a better way of storing GFP specific plugin data...

	if (UE::GameFeatures::GCachePluginDetails)
	{
		UE::TReadScopeLock ReadLock(CachedGameFeaturePluginDetailsLock);

		if (FCachedGameFeaturePluginDetails* ExistingDetails = CachedPluginDetailsByFilename.Find(PluginDescriptorFilename))
		{
			OutPluginDetails = ExistingDetails->Details;
			return true;
		}
	}

	TSharedPtr<FJsonObject> ObjectPtr;
	{
#if WITH_EDITOR
		// In the editor we already have the plugin JSON cached
		TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(FPathViews::GetBaseFilename(PluginDescriptorFilename));
		if (!Plugin.IsValid())
		{
			if (IPluginManager::Get().AddToPluginsList(PluginDescriptorFilename))
			{
				Plugin = IPluginManager::Get().FindPlugin(FPathViews::GetBaseFilename(PluginDescriptorFilename));
			}
		}

		if (Plugin)
		{
			ObjectPtr = Plugin->GetDescriptorJson();
		}
		else
#endif // WITH_EDITOR
		{
			bool TryWithNative = true;
			if (FPluginDescriptor::CustomPluginDescriptorReaderDelegate.IsBound())
			{
				TryWithNative = false;
				FText FailReason; 
				bool Success = FPluginDescriptor::CustomPluginDescriptorReaderDelegate.Execute(*PluginDescriptorFilename, &FailReason, ObjectPtr, TryWithNative);
				if (!Success && !TryWithNative)
				{
					UE_LOG(LogGameFeatures, Error, TEXT("Failed to read plugin descriptor %s. Reason: %s"), *PluginDescriptorFilename, *FailReason.ToString());
					return false;
				}
			}

			if (TryWithNative)
			{
				// Read the file to a string
				FString FileContents;
				if (!FFileHelper::LoadFileToString(FileContents, *PluginDescriptorFilename))
				{
					UE_LOG(LogGameFeatures, Error, TEXT("UGameFeaturesSubsystem could not load plugin descriptor. Failed to read file. File:%s Error:%d"), *PluginDescriptorFilename, FPlatformMisc::GetLastError());
					return false;
				}

				// Deserialize a JSON object from the string	
				TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(FileContents);
				if (!FJsonSerializer::Deserialize(Reader, ObjectPtr) || !ObjectPtr.IsValid())
				{
					UE_LOG(LogGameFeatures, Error, TEXT("UGameFeaturesSubsystem could not load plugin descriptor. Json invalid. File:%s. Error:%s"), *PluginDescriptorFilename, *Reader->GetErrorMessage());
					return false;
				}
			}
		}
	}

	// Read the properties
	// Hotfixable. If it is not specified, then we assume it is
	OutPluginDetails.bHotfixable = true;
	ObjectPtr->TryGetBoolField(TEXTVIEW("Hotfixable"), OutPluginDetails.bHotfixable);

	// Determine the initial plugin state
	OutPluginDetails.BuiltInAutoState = DetermineBuiltInInitialFeatureState(ObjectPtr, PluginDescriptorFilename);

	// Read any additional metadata the policy might want to consume (e.g., a release version number)
	for (const FString& ExtraKey : GetDefault<UGameFeaturesSubsystemSettings>()->AdditionalPluginMetadataKeys)
	{
		TSharedPtr<FJsonValue> Field = ObjectPtr->TryGetField(ExtraKey);
		if (Field.IsValid())
		{
			OutPluginDetails.AdditionalMetadata.Add(ExtraKey, Field);
		}
		else
		{
			OutPluginDetails.AdditionalMetadata.Add(ExtraKey, MakeShared<FJsonValueString>(TEXT("")));
		}
	}

	// Parse plugin dependencies
	const TArray<TSharedPtr<FJsonValue>>* PluginsArray = nullptr;
	ObjectPtr->TryGetArrayField(TEXTVIEW("Plugins"), PluginsArray);
	if (PluginsArray)
	{
		const FStringView NameField = TEXTVIEW("Name");
		const FStringView EnabledField = TEXTVIEW("Enabled");
		const FStringView ActivateField = TEXTVIEW("Activate");
		for (const TSharedPtr<FJsonValue>& PluginElement : *PluginsArray)
		{
			if (!PluginElement)
			{
				continue;
			}

			const TSharedPtr<FJsonObject>* ElementObjectPtr = nullptr;
			PluginElement->TryGetObject(ElementObjectPtr);
			if (!ElementObjectPtr || !ElementObjectPtr->IsValid())
			{
				continue;
			}
			const TSharedPtr<FJsonObject>& ElementObject = *ElementObjectPtr;

			FString DependencyName;
			ElementObject->TryGetStringField(NameField, DependencyName);
			if (DependencyName.IsEmpty())
			{
				UE_LOG(LogGameFeatures, Error, TEXT("Error parsing dependency name in %s! Invalid JSON data!"), *PluginDescriptorFilename);
				continue;
			}

			bool bElementEnabled = false;
			ElementObject->TryGetBoolField(EnabledField, bElementEnabled);
			if (!bElementEnabled)
			{
				UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Skipping adding dependency %s in %s. Plugin is disabled."), *DependencyName, *PluginDescriptorFilename);
				continue;
			}

			//Have to get Activate from JSON as it's unique to GFP and not in the PluginManager
			bool bElementActivate = false;
			ElementObject->TryGetBoolField(ActivateField, bElementActivate);

			FGameFeaturePluginReferenceDetails& RefDetails = OutPluginDetails.PluginDependencies.Emplace_GetRef();
			RefDetails.PluginName = MoveTemp(DependencyName);
			RefDetails.bShouldActivate = bElementActivate;
		}
	}

	if (UE::GameFeatures::GCachePluginDetails)
	{
		UE::TWriteScopeLock WriteLock(CachedGameFeaturePluginDetailsLock);
		CachedPluginDetailsByFilename.Add(PluginDescriptorFilename, FCachedGameFeaturePluginDetails(OutPluginDetails));
	}

	return true;
}

void UGameFeaturesSubsystem::PruneCachedGameFeaturePluginDetails(const FString& PluginURL, const FString& PluginDescriptorFilename) const
{
	UE::TWriteScopeLock WriteLock(CachedGameFeaturePluginDetailsLock);
	CachedPluginDetailsByFilename.Remove(PluginDescriptorFilename);
}

struct FGameFeaturePluginPredownloadContext : public FGameFeaturePluginPredownloadHandle
{
	struct FGFPData
	{
		FInstallBundlePluginProtocolMetaData ProtocolMetadata;
		TArray<EStreamingAssetInstallMode> BundleInstallModes;
	};

	static constexpr const FStringView PredownloadErrorNamespace = TEXTVIEW("GameFeaturePlugin.Predownload.");

	UE::GameFeatures::FResult Result = MakeValue();

	FTSTicker::FDelegateHandle TickHandle;

	TMap<FGameFeaturePluginIdentifier, FGFPData> GFPs;

	TArray<FName> PendingBundleDownloads;

	FInstallBundleCombinedProgressTracker ProgressTracker{ false /*bAutoTick*/};

	TUniqueFunction<void(const UE::GameFeatures::FResult&)> OnComplete;
	TUniqueFunction<void(float)> OnProgress;

	TArray<UE::IoStore::FOnDemandContentHandle> IoStoreContentHandles;
	TArray<UE::IoStore::FOnDemandInstallRequest> IoStoreInstallRequests;
	TArray<UE::IoStore::FOnDemandInstallProgress> IoStoreProgress;
	TOptional<UE::UnifiedError::FError> IoStoreError;
	int32 IoStorePendingInstalls = 0;

	float Progress = 0.0f;

	UE::FMutex IoStoreMutex;

	bool bHasAssetDependencies = false;
	bool bIsComplete = false;
	bool bCanceled = false;

	virtual ~FGameFeaturePluginPredownloadContext() override
	{
		Cleanup();
	}

	virtual bool IsComplete() const override
	{
		return bIsComplete;
	}

	virtual const UE::GameFeatures::FResult& GetResult() const override
	{
		return Result;
	}

	virtual float GetProgress() const override
	{
		return Progress;
	}

	virtual void Cancel() override
	{
		bCanceled = true;

		if (PendingBundleDownloads.Num() > 0)
		{
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
			if (BundleManager)
			{
				BundleManager->CancelUpdateContent(PendingBundleDownloads);
			}
		}

		UE::TUniqueLock Lock(IoStoreMutex);
		CancelIoStoreRequests();
	}

	bool Tick(float /*dt*/)
	{
		UpdateProgress();

		return true;
	}

	void Cleanup()
	{
		if (TickHandle.IsValid())
		{
			FTSTicker::RemoveTicker(TickHandle);
		}		
		IInstallBundleManager::InstallBundleCompleteDelegate.RemoveAll(this);
		IInstallBundleManager::PausedBundleDelegate.RemoveAll(this);
	}

	void SetComplete()
	{
		ReleaseBundlesIfPossible();

		bIsComplete = true;
		if (OnComplete)
		{
			OnComplete(Result);
		}
	}

	void SetComplete(UE::GameFeatures::FResult&& InResult)
	{
		ReleaseBundlesIfPossible();

		Result = MoveTemp(InResult);
		bIsComplete = true;
		if (OnComplete)
		{
			OnComplete(Result);
		}
	}

	void SetCompleteCanceled()
	{
		ReleaseBundlesIfPossible();

		Result = MakeError(FString::Printf(TEXT("%.*s%s"),
			PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
			TEXT("Canceled")));
		bIsComplete = true;
		if (OnComplete)
		{
			OnComplete(Result);
		}
	}

	void Start(TConstArrayView<FString> PluginURLs)
	{
		if (bCanceled)
		{
			SetCompleteCanceled();
			return;
		}

		for (const FString& URL : PluginURLs)
		{
			if (UGameFeaturesSubsystem::GetPluginURLProtocol(URL) != EGameFeaturePluginProtocol::InstallBundle)
			{
				// Only support install bundle protocol for downloading right now
				continue;
			}

			TValueOrError<FInstallBundlePluginProtocolMetaData, FString> MaybeInstallBundleOptions = FInstallBundlePluginProtocolMetaData::FromString(URL);
			if (MaybeInstallBundleOptions.HasError())
			{
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to parse URL {URL}", ("URL", URL));
				UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
					PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
					*MaybeInstallBundleOptions.GetError()));
				SetComplete(MoveTemp(ErrorResult));
				return;
			}

			GFPs.Emplace(URL, MaybeInstallBundleOptions.StealValue());
		}

		if (GFPs.Num() == 0)
		{
			SetComplete(MakeValue());
			return;
		}

		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
		if (!BundleManager)
		{
			UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed, no Install Bundle Manager found.");
			UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
				PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
				TEXT("BundleManager_Null")));
			SetComplete(MoveTemp(ErrorResult));
			return;
		}

		UGameFeaturesSubsystem& GFPSubSys = UGameFeaturesSubsystem::Get();

		bool bAllBundlesUpToDate = true; 
		TArray<FName> BundlesToInstall;
		for (TPair<FGameFeaturePluginIdentifier, FGFPData>& Pair : GFPs)
		{
			const FGameFeaturePluginIdentifier& GFPIdentifier = Pair.Key;
			FGFPData& GFPData = Pair.Value;

			UGameFeaturePluginStateMachine* Machine = GFPSubSys.FindGameFeaturePluginStateMachine(GFPIdentifier);
			if (Machine && Machine->GetDestination() < EGameFeaturePluginState::Installed)
			{
				// Existing machine exists and wants to be uninstalled, can't precache
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed because a GFP is unloading, GFP: {GFP}", ("GFP", Machine->GetPluginName()));
				UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
					PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
					TEXT("GFPUnloading")));
				SetComplete(MoveTemp(ErrorResult));
				return;
			}

			GFPSubSys.OnGameFeaturePredownloading(FString(GFPIdentifier.GetPluginName()), GFPIdentifier);

			const bool bAddDependencies = true;
			TValueOrError<FInstallBundleCombinedInstallState, EInstallBundleResult> MaybeInstallState = 
				BundleManager->GetInstallStateSynchronous(GFPData.ProtocolMetadata.InstallBundles, bAddDependencies);
			if (MaybeInstallState.HasError())
			{
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed, failed to get install state for {GFP}", ("GFP", GFPIdentifier.GetPluginName()));
				UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
					PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
					LexToString(MaybeInstallState.GetError())));
				SetComplete(MoveTemp(ErrorResult));
				return;
			}

			const FInstallBundleCombinedInstallState& InstallState = MaybeInstallState.GetValue();
			const bool bIsAvailable = Algo::AllOf(GFPData.ProtocolMetadata.InstallBundles,
				[&InstallState](FName BundleName) { return InstallState.IndividualBundleStates.Contains(BundleName); });

			if (!bIsAvailable)
			{
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed, unvailable {GFP}", ("GFP", GFPIdentifier.GetPluginName()));
				UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
					PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
					TEXT("Plugin_Unavailable")));
				SetComplete(MoveTemp(ErrorResult));
				return;
			}

			GFPData.ProtocolMetadata.InstallBundlesWithAssetDependencies = InstallState.BundlesWithIoStoreOnDemand.Array();
			if (!GFPData.ProtocolMetadata.InstallBundlesWithAssetDependencies.IsEmpty())
			{
				TValueOrError<TArray<EStreamingAssetInstallMode>, FString> MaybeInstallModes =
					UGameFeaturesSubsystem::Get().GetPolicy().GetStreamingAssetInstallModes(
						GFPIdentifier.GetFullPluginURL(), GFPData.ProtocolMetadata.InstallBundlesWithAssetDependencies);

				if (MaybeInstallModes.HasError())
				{
					UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed, no install modes for {GFP}", ("GFP", GFPIdentifier.GetPluginName()));
					UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
						PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
						*MaybeInstallModes.GetError()));
					SetComplete(MoveTemp(ErrorResult));
					return;
				}

				GFPData.BundleInstallModes = MaybeInstallModes.StealValue();
				check(GFPData.BundleInstallModes.Num() == GFPData.ProtocolMetadata.InstallBundlesWithAssetDependencies.Num());
			}

			bHasAssetDependencies = bHasAssetDependencies || !InstallState.BundlesWithIoStoreOnDemand.IsEmpty();

			bAllBundlesUpToDate = bAllBundlesUpToDate && 
				InstallState.BundlesWithIoStoreOnDemand.IsEmpty() && // For now, just assume that any IAD data is not up to date
				InstallState.GetAllBundlesHaveState(EInstallBundleInstallState::UpToDate);

			// Update metadata with fully expanded dependency list. This can only be done after all bundles are known to be available,
			// otherwise unavailable bundles in the URL could be stripped from the list.
			GFPData.ProtocolMetadata.InstallBundles.Empty(InstallState.IndividualBundleStates.Num());
			InstallState.IndividualBundleStates.GetKeys(GFPData.ProtocolMetadata.InstallBundles);

			// Its ok to have duplicates in this list
			BundlesToInstall.Append(GFPData.ProtocolMetadata.InstallBundles);
		}

		// Early out if everything is up to date already. This helps avoid enquing UI dialogs for content that doesn't actually need to be downloaded
		if (bAllBundlesUpToDate)
		{
			SetComplete(MakeValue());
			return;
		}

		BundleManager->GetContentState(BundlesToInstall, EInstallBundleGetContentStateFlags::None, false,
			FInstallBundleGetContentStateDelegate::CreateLambda(
				[Context = SharedThis(this)](FInstallBundleCombinedContentState BundleContentState)
				{ Context->OnGotContentState(MoveTemp(BundleContentState)); }
			)
		);
	}

	void OnGotContentState(FInstallBundleCombinedContentState BundleContentState)
	{
		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

		if (BundleContentState.GetAllBundlesHaveState(EInstallBundleInstallState::UpToDate))
		{
			OnAllInstallBundlesCompleted();
			return;
		}

		if (bCanceled)
		{
			SetCompleteCanceled();
			return;
		}

		TArray<FName> BundlesToInstall;
		for (const TPair<FGameFeaturePluginIdentifier, FGFPData>& Pair : GFPs)
		{
			const FGFPData& GFPData = Pair.Value;
			BundlesToInstall.Append(GFPData.ProtocolMetadata.InstallBundles);
		}

		EInstallBundleRequestFlags InstallFlags = EInstallBundleRequestFlags::Defaults | EInstallBundleRequestFlags::SkipMount;
		TValueOrError<FInstallBundleRequestInfo, EInstallBundleResult> MaybeRequestInfo = BundleManager->RequestUpdateContent(BundlesToInstall, InstallFlags);

		if (MaybeRequestInfo.HasError())
		{
			UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to request content, Error: {Error}", ("Error", LexToString(MaybeRequestInfo.GetError())));
			UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
				PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
				LexToString(MaybeRequestInfo.GetError())));
			SetComplete(MoveTemp(ErrorResult));
			return;
		}

		FInstallBundleRequestInfo RequestInfo = MaybeRequestInfo.StealValue();
		if (RequestInfo.BundlesEnqueued.Num() == 0)
		{
			OnAllInstallBundlesCompleted();
			return;
		}

		PendingBundleDownloads = MoveTemp(RequestInfo.BundlesEnqueued);

		ProgressTracker.SetBundlesToTrackFromContentState(BundleContentState, PendingBundleDownloads);

		// Start ticking
		TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateSP(this, &FGameFeaturePluginPredownloadContext::Tick));

		IInstallBundleManager::InstallBundleCompleteDelegate.AddLambda(
			[Context = SharedThis(this)](FInstallBundleRequestResultInfo BundleResult)
			{ Context->OnInstallBundleCompleted(MoveTemp(BundleResult)); });
		// TODO: handle pause?  Just cancel? This should only be relevent for cell connections
		// IInstallBundleManager::PausedBundleDelegate.AddRaw(this, &FGameFeaturePluginState_Downloading::OnInstallBundlePaused);
	}

	void OnInstallBundleCompleted(FInstallBundleRequestResultInfo BundleResult)
	{
		if (!PendingBundleDownloads.Contains(BundleResult.BundleName))
		{
			return;
		}

		PendingBundleDownloads.RemoveSwap(BundleResult.BundleName);

		if (Result.HasValue() && BundleResult.Result != EInstallBundleResult::OK)
		{
			if (BundleResult.OptionalErrorCode.IsEmpty())
			{
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to install {Bundle}, Error: {Error}",
					("Bundle", BundleResult.BundleName), ("Error", LexToString(BundleResult.Result)));
			}
			else
			{
				UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to install {Bundle}, Error: {Error}",
					("Bundle", BundleResult.BundleName), ("Error", BundleResult.OptionalErrorCode));
			}

			//Use OptionalErrorCode and/or OptionalErrorText if available
			const FString ErrorCodeEnding = (BundleResult.OptionalErrorCode.IsEmpty()) ? LexToString(BundleResult.Result) : BundleResult.OptionalErrorCode;
			FText ErrorText = BundleResult.OptionalErrorCode.IsEmpty() ? UE::GameFeatures::CommonErrorCodes::GetErrorTextForBundleResult(BundleResult.Result) : BundleResult.OptionalErrorText;
			Result = UE::GameFeatures::FResult(
				MakeError(FString::Printf(TEXT("%.*s%s"), PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(), *ErrorCodeEnding)),
				MoveTemp(ErrorText)
			);

			// Cancel remaining downloads
			TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();
			BundleManager->CancelUpdateContent(PendingBundleDownloads);
		}

		if (PendingBundleDownloads.Num() > 0)
		{
			return;
		}

		// Delay call. We don't want to possibly release bundles from within the complete callback.
		FTSTicker::GetCoreTicker().AddTicker(
			FTickerDelegate::CreateLambda([Context = SharedThis(this)](float)
			{
				Context->OnAllInstallBundlesCompleted();
				return false;
			})
		);
	}

	void OnAllInstallBundlesCompleted()
	{
		if (Result.HasError() || !bHasAssetDependencies)
		{
			SetComplete();
			Cleanup();
		}

		UE::IoStore::IOnDemandIoStore* IoStore = UE::IoStore::TryGetOnDemandIoStore();
		if (!IoStore)
		{
			UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to get IoStoreOnDemand module!");
			UE::GameFeatures::FResult ErrorResult = MakeError(FString::Printf(TEXT("%.*s%s"),
				PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(),
				TEXT("IoStoreOnDemand.ModuleNotFound")));
			SetComplete(MoveTemp(ErrorResult));
			Cleanup();
			return;
		}

		// Create a single list of all IAD assets to install
		TMap<FName, EStreamingAssetInstallMode> InstallBundlesWithAssetDependencies;
		for (TPair<FGameFeaturePluginIdentifier, FGFPData>& Pair : GFPs)
		{
			FGFPData& GFPData = Pair.Value;

			for (int i = 0; const FName BundleName : GFPData.ProtocolMetadata.InstallBundlesWithAssetDependencies)
			{
				const EStreamingAssetInstallMode GfpInstallMode = GFPData.BundleInstallModes[i++];
			
				// Merge install modes from all GFPs that references the bundle
				EStreamingAssetInstallMode& BundleInstallMode = 
					InstallBundlesWithAssetDependencies.FindOrAdd(BundleName, GfpInstallMode);
				if (BundleInstallMode == EStreamingAssetInstallMode::GfpRequiredOnly && GfpInstallMode == EStreamingAssetInstallMode::Full)
				{
					BundleInstallMode = EStreamingAssetInstallMode::Full;
				}
			}
		}

		IoStorePendingInstalls = InstallBundlesWithAssetDependencies.Num();
		IoStoreContentHandles.SetNum(IoStorePendingInstalls);
		IoStoreInstallRequests.SetNum(IoStorePendingInstalls);
		IoStoreProgress.SetNum(IoStorePendingInstalls);
		for (int i = 0; const TPair<FName, EStreamingAssetInstallMode>& Pair : InstallBundlesWithAssetDependencies)
		{
			const FName InstallBundle = Pair.Key;
			const EStreamingAssetInstallMode InstallMode = Pair.Value;

			FNameBuilder Debugname(InstallBundle);
			Debugname.Append(TEXTVIEW("/deps"));
			IoStoreContentHandles[i] = UE::IoStore::FOnDemandContentHandle::Create(Debugname);

			UE::IoStore::FOnDemandInstallArgs InstallArgs;
			InstallArgs.MountId = InstallBundle.ToString();
			if (InstallMode == EStreamingAssetInstallMode::GfpRequiredOnly)
			{
				InstallArgs.TagSets.Emplace(TEXTVIEW("required"));
			}
			InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::InstallSoftReferences;
			if (UE::GameFeatures::CVarAllowMissingOnDemandDependencies.GetValueOnGameThread())
			{
				InstallArgs.Options |= UE::IoStore::EOnDemandInstallOptions::AllowMissingDependencies;
			}

			InstallArgs.ContentHandle = IoStoreContentHandles[i];

			UE::TUniqueLock Lock(IoStoreMutex);

			IoStoreInstallRequests[i] = IoStore->Install(MoveTemp(InstallArgs),
				// On Complete
				[this, Context = SharedThis(this), i](UE::IoStore::FOnDemandInstallResult&& OnDemandInstallResult)
				{
					UE::TUniqueLock Lock(IoStoreMutex);

					if (!OnDemandInstallResult.IsOk() && !IoStoreError.IsSet())
					{
						IoStoreError = MoveTemp(OnDemandInstallResult.Error);
						CancelIoStoreRequests();
					}

					--IoStorePendingInstalls;
					if (IoStorePendingInstalls == 0)
					{
						ExecuteOnGameThread(UE_SOURCE_LOCATION, [Context]
						{
							Context->OnAllIoStoreRequestsCompleted();
						});
					}
				},
				// On Progress
				[this, Context = SharedThis(this), i](const UE::IoStore::FOnDemandInstallProgress& InProgress)
				{
					UE::TUniqueLock Lock(IoStoreMutex);
					IoStoreProgress[i] = InProgress;
				}
			);

			++i;
		}
		
	}

	void OnAllIoStoreRequestsCompleted()
	{
		// Don't need to take the mutex here, all the requests should be done

		if (bCanceled)
		{
			SetCompleteCanceled();
		}
		else if (IoStoreError.IsSet())
		{
			UE_LOGFMT(LogGameFeatures, Error, "GFP Predownload failed to install iostore on demand assets, Error: {Error}", IoStoreError.GetValue());

			const FText ErrorMessage	= IoStoreError.GetValue().GetErrorMessage();
			FString ErrorCode			= FString(IoStoreError.GetValue().GetModuleIdAndErrorCodeString());

			ErrorCode.ReplaceCharInline(TEXT(' '), TEXT('_'), ESearchCase::CaseSensitive);
			UE::GameFeatures::FResult ErrorResult(MakeError(
				FString::Printf(TEXT("%.*s%s"), PredownloadErrorNamespace.Len(), PredownloadErrorNamespace.GetData(), *ErrorCode)),
				ErrorMessage);	
			SetComplete(MoveTemp(ErrorResult));
		}
		else
		{
			SetComplete();
		}

		Cleanup();
	}

	void UpdateProgress()
	{
		ProgressTracker.ForceTick();
		const float InstallBundleProgress = ProgressTracker.GetCurrentCombinedProgress().ProgressPercent;

		if (!bHasAssetDependencies)
		{
			Progress = InstallBundleProgress;
			return;
		}

		float IoStoreRelativeProgress;
		{
			UE::TUniqueLock Lock(IoStoreMutex);

#if 0
			// This approach doesn't work becuse we don't get initial progress for a request until the request is started
			// so the total size changes as requests are processed which causes progress backsliding
			const UE::IoStore::FOnDemandInstallProgress SumIoStoreProgress = Algo::Accumulate(
				IoStoreProgress,
				UE::IoStore::FOnDemandInstallProgress(),
				&UE::IoStore::FOnDemandInstallProgress::Combine);

			IoStoreRelativeProgress = SumIoStoreProgress.GetRelativeProgress();
#else
			// TODO: Just use flat weighting for now, but this could be improved by weighting by install size
			const float SumIoStoreProgress = Algo::TransformAccumulate(
				IoStoreProgress, 
				&UE::IoStore::FOnDemandInstallProgress::GetRelativeProgress, 
				0.0f);

			IoStoreRelativeProgress = IoStoreProgress.IsEmpty() ? 0.0f : (SumIoStoreProgress / IoStoreProgress.Num());
#endif
		}
		
		Progress = UE::GameFeatures::CombineInstallProgress(InstallBundleProgress, IoStoreRelativeProgress);

		if (OnProgress)
		{
			OnProgress(Progress);
		}
	}

	void CancelIoStoreRequests()
	{
		for (UE::IoStore::FOnDemandInstallRequest& InstallRequest : IoStoreInstallRequests)
		{
			InstallRequest.Cancel();
		}
	}

	// Predownload shouldn't pin any cached bundles so release them now
	void ReleaseBundlesIfPossible()
	{
		// Don't need to do anything special with content handles. GFPs will keep their own handles internally.
		IoStoreContentHandles.Empty();

		UGameFeaturesSubsystem& GFPSubSys = UGameFeaturesSubsystem::Get();
		TSharedPtr<IInstallBundleManager> BundleManager = IInstallBundleManager::GetPlatformInstallBundleManager();

		TArray<FName> ReleaseList;
		TSet<FName> KeepList;
		for (const TPair<FString, TObjectPtr<UGameFeaturePluginStateMachine>>& Pair : GFPSubSys.GameFeaturePluginStateMachines)
		{
			UGameFeaturePluginStateMachine* Machine = Pair.Value;
			if (Machine &&
				Machine->GetCurrentState() > EGameFeaturePluginState::StatusKnown &&
				Machine->GetCurrentState() != EGameFeaturePluginState::Releasing)
			{
				const FGameFeatureProtocolMetadata& ProtocolMetaData = Machine->GetProtocolMetadata();
				if (ProtocolMetaData.HasSubtype<FInstallBundlePluginProtocolMetaData>())
				{
					const FInstallBundlePluginProtocolMetaData& ProtocolData = ProtocolMetaData.GetSubtype<FInstallBundlePluginProtocolMetaData>();
					KeepList.Append(ProtocolData.InstallBundles);
				}
			}
		}

		for (const TPair<FGameFeaturePluginIdentifier, FGFPData>& Pair : GFPs)
		{
			const FGameFeaturePluginIdentifier& GFPIdentifier = Pair.Key;
			const FGFPData& GFPData = Pair.Value;

			UGameFeaturePluginStateMachine* Machine = GFPSubSys.FindGameFeaturePluginStateMachine(GFPIdentifier);
			if (Machine &&
				Machine->GetCurrentState() > EGameFeaturePluginState::StatusKnown &&
				Machine->GetCurrentState() != EGameFeaturePluginState::Releasing)
			{
				// A machine is using the bundles, don't release
				KeepList.Append(GFPData.ProtocolMetadata.InstallBundles);
				continue;
			}

			// Only send this if a GFP is not using the bundle
			GFPSubSys.OnGameFeaturePostPredownloading(FString(GFPIdentifier.GetPluginName()), GFPIdentifier);

			ReleaseList.Append(GFPData.ProtocolMetadata.InstallBundles);
		}

		BundleManager->RequestReleaseContent(ReleaseList, EInstallBundleReleaseRequestFlags::None, KeepList.Array());
	}
};

TSharedRef<FGameFeaturePluginPredownloadHandle> UGameFeaturesSubsystem::PredownloadGameFeaturePlugins(TConstArrayView<FString> PluginURLs, TUniqueFunction<void(const UE::GameFeatures::FResult&)> OnComplete /*= nullptr*/, TUniqueFunction<void(float)> OnProgress /*= nullptr*/)
{
	TSharedRef<FGameFeaturePluginPredownloadContext> Context = MakeShared<FGameFeaturePluginPredownloadContext>();
	Context->OnComplete = MoveTemp(OnComplete);
	Context->OnProgress = MoveTemp(OnProgress);
	Context->Start(PluginURLs);

	return Context;
}

UGameFeaturePluginStateMachine* UGameFeaturesSubsystem::FindGameFeaturePluginStateMachine(const FString& PluginURL) const
{
	FGameFeaturePluginIdentifier FindPluginIdentifier(PluginURL);
	return FindGameFeaturePluginStateMachine(FindPluginIdentifier);
}

UGameFeaturePluginStateMachine* UGameFeaturesSubsystem::FindGameFeaturePluginStateMachine(const FGameFeaturePluginIdentifier& PluginIdentifier) const
{
	const FStringView ShortUrl = PluginIdentifier.GetIdentifyingString();

	TObjectPtr<UGameFeaturePluginStateMachine> const* ExistingStateMachine = 
		GameFeaturePluginStateMachines.FindByHash(GetTypeHash(PluginIdentifier.GetIdentifyingString()), PluginIdentifier.GetIdentifyingString());
	if (ExistingStateMachine)
	{
		EGameFeaturePluginProtocol ExpectedProtocol = (*ExistingStateMachine)->GetPluginIdentifier().GetPluginProtocol();
		if (ensureMsgf(ExpectedProtocol == PluginIdentifier.GetPluginProtocol(), TEXT("Expected protocol %s for %.*s"), UE::GameFeatures::GameFeaturePluginProtocolPrefix(ExpectedProtocol), ShortUrl.Len(), ShortUrl.GetData()))
		{
			UE_LOG(LogGameFeatures, VeryVerbose, TEXT("FOUND GameFeaturePlugin using PluginIdentifier:%.*s for PluginURL:%s"), ShortUrl.Len(), ShortUrl.GetData(), *PluginIdentifier.GetFullPluginURL());
			return *ExistingStateMachine;
		}
	}
	UE_LOG(LogGameFeatures, VeryVerbose, TEXT("NOT FOUND GameFeaturePlugin using PluginIdentifier:%.*s for PluginURL:%s"), ShortUrl.Len(), ShortUrl.GetData(), *PluginIdentifier.GetFullPluginURL());

	return nullptr;
}

// Note: ProtocolOptions is not defaulted here. Any API call that could create a state machine should allow the user to pass ProtocolOptions to initialize the machine.
// It is acceptable that user passes null options. 
UGameFeaturePluginStateMachine* UGameFeaturesSubsystem::FindOrCreateGameFeaturePluginStateMachine(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, bool* bOutFoundExisting /*= nullptr*/)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(GFP_FindOrCreateStateMachine);
	FGameFeaturePluginIdentifier PluginIdentifier(PluginURL);
	TObjectPtr<UGameFeaturePluginStateMachine> const* ExistingStateMachine =
		GameFeaturePluginStateMachines.FindByHash(GetTypeHash(PluginIdentifier.GetIdentifyingString()), PluginIdentifier.GetIdentifyingString());

	if (bOutFoundExisting)
	{
		*bOutFoundExisting = !!ExistingStateMachine;
	}

	if (ExistingStateMachine)
	{
		// In this case, still return the existing machine, even if the protocol doesn't match. This function should never return null.
		// There can only be one active instance of any machine.
		EGameFeaturePluginProtocol ExpectedProtocol = (*ExistingStateMachine)->GetPluginIdentifier().GetPluginProtocol();
		ensureMsgf(ExpectedProtocol == PluginIdentifier.GetPluginProtocol(), TEXT("Expected protocol %s for %.*s"), UE::GameFeatures::GameFeaturePluginProtocolPrefix(ExpectedProtocol), PluginIdentifier.GetIdentifyingString().Len(), PluginIdentifier.GetIdentifyingString().GetData());

		UE_LOG(LogGameFeatures, VeryVerbose, TEXT("Found GameFeaturePlugin StateMachine using Identifier:%.*s from PluginURL:%s"), PluginIdentifier.GetIdentifyingString().Len(), PluginIdentifier.GetIdentifyingString().GetData(), *PluginURL);
		return *ExistingStateMachine;
	}

	UE_LOG(LogGameFeatures, Verbose, TEXT("Creating GameFeaturePlugin StateMachine using Identifier:%.*s from PluginURL:%s"), PluginIdentifier.GetIdentifyingString().Len(), PluginIdentifier.GetIdentifyingString().GetData(), *PluginURL);

#if UE_WITH_PACKAGE_ACCESS_TRACKING
	if (PackageLoadTracker)
	{
		FName PluginName = FName(PluginIdentifier.GetPluginName());
		PackageLoadTracker->AddRoot(PluginName);
	}
#endif

	UGameFeaturePluginStateMachine* NewStateMachine = NewObject<UGameFeaturePluginStateMachine>(this);
	GameFeaturePluginStateMachines.Add(FString(PluginIdentifier.GetIdentifyingString()), NewStateMachine);
	NewStateMachine->InitStateMachine(MoveTemp(PluginIdentifier), ProtocolOptions);

	return NewStateMachine;
}

void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePluginComplete(const UE::GameFeatures::FResult& Result, UGameFeaturePluginStateMachine* Machine, FGameFeaturePluginStateRange RequestedDestination)
{
	check(Machine);
	if (Result.HasValue())
	{
		checkf(RequestedDestination.Contains(Machine->GetCurrentState()), TEXT("Game feature '%s': Ending state %s is not in expected range [%s, %s]"), 
			*Machine->GetGameFeatureName(), 
			*UE::GameFeatures::ToString(Machine->GetCurrentState()), 
			*UE::GameFeatures::ToString(RequestedDestination.MinState), 
			*UE::GameFeatures::ToString(RequestedDestination.MaxState));
	}
	else
	{
		SetExplanationForNotMountingPlugin(Machine->GetPluginURL(), UE::GameFeatures::ToString(Result));
	}
}

void UGameFeaturesSubsystem::ChangeGameFeatureDestination(UGameFeaturePluginStateMachine* Machine, const FGameFeaturePluginStateRange& StateRange, FGameFeaturePluginChangeStateComplete CompleteDelegate)
{
	ChangeGameFeatureDestination(Machine, FGameFeatureProtocolOptions(), StateRange, CompleteDelegate);
}

void UGameFeaturesSubsystem::ChangeGameFeatureDestination(UGameFeaturePluginStateMachine* Machine, const FGameFeatureProtocolOptions& InProtocolOptions, const FGameFeaturePluginStateRange& StateRange, FGameFeaturePluginChangeStateComplete CompleteDelegate)
{
	const bool bSetDestination = Machine->SetDestination(StateRange,
		FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::ChangeGameFeatureTargetStateComplete, CompleteDelegate));

	if (bSetDestination)
	{
		UE_LOG(LogGameFeatures, Verbose, TEXT("ChangeGameFeatureDestination: Set Game Feature %s Destination State to [%s, %s]"), *Machine->GetGameFeatureName(), *UE::GameFeatures::ToString(StateRange.MinState), *UE::GameFeatures::ToString(StateRange.MaxState));
	}
	else
	{
		FGameFeaturePluginStateRange CurrDestination = Machine->GetDestination();
		UE_LOG(LogGameFeatures, Display, TEXT("ChangeGameFeatureDestination: Attempting to cancel transition for Game Feature %s. Desired [%s, %s]. Current [%s, %s]"), 
			*Machine->GetGameFeatureName(), 
			*UE::GameFeatures::ToString(StateRange.MinState), *UE::GameFeatures::ToString(StateRange.MaxState),
			*UE::GameFeatures::ToString(CurrDestination.MinState), *UE::GameFeatures::ToString(CurrDestination.MaxState));

		// Try canceling any current transition, then retry
		auto OnCanceled = [this, InProtocolOptions, StateRange, CompleteDelegate](UGameFeaturePluginStateMachine* Machine) mutable
		{
			// Special case for terminal state since it cannot be exited, we need to make a new machine
			if (Machine->GetCurrentState() == EGameFeaturePluginState::Terminal)
			{
				UGameFeaturePluginStateMachine* NewMachine = FindOrCreateGameFeaturePluginStateMachine(Machine->GetPluginURL(), InProtocolOptions);
				checkf(NewMachine != Machine, TEXT("Game Feature Plugin %s should have already been removed from subsystem!"), *Machine->GetPluginURL());
				Machine = NewMachine;
			}

			// Now that the transition has been canceled, retry reaching the desired destination
			const bool bSetDestination = Machine->SetDestination(StateRange,
				FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::ChangeGameFeatureTargetStateComplete, CompleteDelegate));

			if (!ensure(bSetDestination))
			{
				UE_LOG(LogGameFeatures, Warning, TEXT("ChangeGameFeatureDestination: Failed to set Game Feature %s Destination State to [%s, %s]"), *Machine->GetGameFeatureName(), *UE::GameFeatures::ToString(StateRange.MinState), *UE::GameFeatures::ToString(StateRange.MaxState));

				CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::UnreachableState)));
			}
			else
			{
				UE_LOG(LogGameFeatures, Display, TEXT("ChangeGameFeatureDestination: OnCanceled, set Game Feature %s Destination State to [%s, %s]"), *Machine->GetGameFeatureName(), *UE::GameFeatures::ToString(StateRange.MinState), *UE::GameFeatures::ToString(StateRange.MaxState));
			}
		};

		const bool bCancelPending = Machine->TryCancel(FGameFeatureStateTransitionCanceled::CreateWeakLambda(this, MoveTemp(OnCanceled)));
		if (!ensure(bCancelPending))
		{
			UE_LOG(LogGameFeatures, Warning, TEXT("ChangeGameFeatureDestination: Failed to cancel Game Feature %s"), *Machine->GetGameFeatureName());

			CompleteDelegate.ExecuteIfBound(UE::GameFeatures::FResult(MakeError(UE::GameFeatures::SubsystemErrorNamespace + UE::GameFeatures::CommonErrorCodes::UnreachableState + UE::GameFeatures::CommonErrorCodes::CancelAddonCode)));
		}
	}
}

void UGameFeaturesSubsystem::ChangeGameFeatureTargetStateComplete(UGameFeaturePluginStateMachine* Machine, const UE::GameFeatures::FResult& Result, FGameFeaturePluginChangeStateComplete CompleteDelegate)
{
#if WITH_EDITOR
	if (!Result.HasError() && Machine->GetCurrentState() > EGameFeaturePluginState::Mounting)
	{
		UnmountedPluginNameToExplanation.Remove(Machine->GetPluginName());
	}
#endif
	CompleteDelegate.ExecuteIfBound(Result);
}

void UGameFeaturesSubsystem::BeginTermination(UGameFeaturePluginStateMachine* Machine)
{
	check(IsValid(Machine));
	check(Machine->GetCurrentState() == EGameFeaturePluginState::Terminal);

	FStringView Identifer = Machine->GetPluginIdentifier().GetIdentifyingString();

	UE_LOG(LogGameFeatures, Verbose, TEXT("BeginTermination of GameFeaturePlugin. Identifier:%.*s URL:%s"), Identifer.Len(), Identifer.GetData(), *(Machine->GetPluginURL()));
	GameFeaturePluginStateMachines.RemoveByHash(GetTypeHash(Identifer), Identifer);
	TerminalGameFeaturePluginStateMachines.Add(Machine);
}

void UGameFeaturesSubsystem::FinishTermination(UGameFeaturePluginStateMachine* Machine)
{
	UE_LOG(LogGameFeatures, Verbose, TEXT("FinishTermination of GameFeaturePlugin. Identifier:%.*s URL:%s"), Machine->GetPluginIdentifier().GetIdentifyingString().Len(), Machine->GetPluginIdentifier().GetIdentifyingString().GetData(), *(Machine->GetPluginURL()));
	TerminalGameFeaturePluginStateMachines.RemoveSwap(Machine);
}

bool UGameFeaturesSubsystem::FindOrCreatePluginDependencyStateMachines(const FString& PluginURL, const FGameFeaturePluginStateMachineProperties& InStateProperties, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines)
{
	const FString& PluginFilename = InStateProperties.PluginInstalledFilename;
	const FGameFeatureProtocolOptions InDepProtocolOptions = InStateProperties.RecycleProtocolOptions();
    const EGameFeaturePluginProtocol InProtocol = UGameFeaturesSubsystem::GetPluginURLProtocol(PluginURL);
	const bool bWarnOnDepCreation = InStateProperties.ProtocolOptions.bLogWarningOnForcedDependencyCreation;
	const bool bErrorOnDepCreation = InStateProperties.ProtocolOptions.bLogErrorOnForcedDependencyCreation;

	FGameFeaturePluginDetails Details;
	if (GetGameFeaturePluginDetailsInternal(PluginFilename, Details))
	{
		for (const FGameFeaturePluginReferenceDetails& PluginDependency : Details.PluginDependencies)
		{
			const FString& DependencyName = PluginDependency.PluginName;
			FPluginDependencyDetails DependencyDetails;
			TValueOrError<FString, FString> DependencyURLInfo = GameSpecificPolicies->ResolvePluginDependency(PluginURL, DependencyName, DependencyDetails);
			if (DependencyURLInfo.HasError())
			{
				FString ErrorMessage = FString::Printf(TEXT("Game feature plugin '%s' has unknown dependency '%s' [%s]."), *PluginFilename, *DependencyName, *DependencyURLInfo.GetError());

				SetExplanationForNotMountingPlugin(PluginURL, ErrorMessage);

				if (DependencyDetails.bFailIfNotFound)
				{
					// This plugin is known to be a hard dependency so fail early before something harder to debug happens.
					UE_LOG(LogGameFeatures, Error, TEXT("%s"), *ErrorMessage);
					return false;
				}
				else
				{
					// Don't actually return false here as we want to still be able to progress in the case of 
					// things like an editor plugin being included as a dependency in the client. We can't tell from just 
					// the reference if a plugin is not enabled for this build target.
					UE_LOG(LogGameFeatures, Log, TEXT("%s"), *ErrorMessage);
					continue;
				}
			}

			const FString& DependencyURL = DependencyURLInfo.GetValue();

			// Dependency may not be a GFP and so will have an empty URL but not have an error
			if (DependencyURL.IsEmpty())
			{
				continue;
			}

			// Inherit dep protocol options if possible
			FGameFeatureProtocolOptions DepProtocolOptions;
			EGameFeaturePluginProtocol DepProtocol = UGameFeaturesSubsystem::GetPluginURLProtocol(DependencyURL);
			if (DepProtocol == EGameFeaturePluginProtocol::InstallBundle)
            {
                if (InDepProtocolOptions.HasSubtype<FInstallBundlePluginProtocolOptions>())
                {
                    DepProtocolOptions = InDepProtocolOptions;
                }
                else if (InProtocol == EGameFeaturePluginProtocol::File)
                {
                    FInstallBundlePluginProtocolOptions InstallBundleOptions;
                    InstallBundleOptions.bAllowIniLoading = true;
                    DepProtocolOptions = FGameFeatureProtocolOptions(InstallBundleOptions);
                }
            }
            // Always propogate non-protocol specific flags
            DepProtocolOptions.bForceSyncLoading = InDepProtocolOptions.bForceSyncLoading;
            DepProtocolOptions.bLogWarningOnForcedDependencyCreation = InDepProtocolOptions.bLogWarningOnForcedDependencyCreation;
            DepProtocolOptions.bLogErrorOnForcedDependencyCreation = InDepProtocolOptions.bLogErrorOnForcedDependencyCreation;

			bool bFoundExisting = false;
			UGameFeaturePluginStateMachine* ResolvedDependency = FindOrCreateGameFeaturePluginStateMachine(DependencyURL, DepProtocolOptions, &bFoundExisting);
			check(ResolvedDependency);

			if (!bFoundExisting)
			{
				// Propogate bWasLoadedAsBuiltInGameFeaturePlugin
				if (InStateProperties.bWasLoadedAsBuiltInGameFeaturePlugin)
				{
					ResolvedDependency->SetWasLoadedAsBuiltIn();
				}

				// Note: Given that LoadBuiltInGameFeaturePlugins does a topological sort, we don't expect to hit this path for built-ins
				if (bWarnOnDepCreation)
				{
					if (InStateProperties.bWasLoadedAsBuiltInGameFeaturePlugin)
					{
						UE_LOGFMT(LogGameFeatures, Warning, "GFP dependency {Dep} was forcibly created by {Parent}, Game specific policies may be incorrectly filtering this dependency.",
							("Dep", ResolvedDependency->GetPluginIdentifier().GetIdentifyingString()), ("Parent", InStateProperties.PluginIdentifier.GetIdentifyingString()));
					}
					else
					{
						UE_LOGFMT(LogGameFeatures, Warning, "GFP dependency {Dep} was unexpectedly forcibly created by {Parent}",
							("Dep", ResolvedDependency->GetPluginIdentifier().GetIdentifyingString()), ("Parent", InStateProperties.PluginIdentifier.GetIdentifyingString()));
					}
				}
				else if (bErrorOnDepCreation)
				{
					if (InStateProperties.bWasLoadedAsBuiltInGameFeaturePlugin)
					{
						UE_LOGFMT(LogGameFeatures, Error, "GFP dependency {Dep} was forcibly created by {Parent}, Game specific policies may be incorrectly filtering this dependency.",
							("Dep", ResolvedDependency->GetPluginIdentifier().GetIdentifyingString()), ("Parent", InStateProperties.PluginIdentifier.GetIdentifyingString()));
					}
					else
					{
						UE_LOGFMT(LogGameFeatures, Error, "GFP dependency {Dep} was unexpectedly forcibly created by {Parent}",
							("Dep", ResolvedDependency->GetPluginIdentifier().GetIdentifyingString()), ("Parent", InStateProperties.PluginIdentifier.GetIdentifyingString()));
					}
				}
			}

			OutDependencyMachines.Add(ResolvedDependency);
		}

		Algo::Sort(OutDependencyMachines);
		OutDependencyMachines.SetNum(Algo::Unique(OutDependencyMachines));
		return true;
	}

	return false;
}

bool UGameFeaturesSubsystem::FindPluginDependencyStateMachinesToActivate(const FString& PluginURL, const FString& PluginFilename, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines) const
{
	const bool bResult = EnumeratePluginDependenciesWithShouldActivate(PluginURL, PluginFilename, [this, &OutDependencyMachines](const FString& DependencyName, const FString& DependencyURL) {
		UGameFeaturePluginStateMachine* Dependency = FindGameFeaturePluginStateMachine(DependencyURL);
		if (Dependency)
		{
			OutDependencyMachines.Add(Dependency);
			return true;
		}
		//Expect to find all valid dependencies and activate them, so error if not found
		else
		{
			UE_LOG(LogGameFeatures, Error, TEXT("FindPluginDependencyStateMachinesToActivate failed to find plugin state machine for %s using URL %s"), *DependencyName, *DependencyURL);
			return false;
		}
	});
	Algo::Sort(OutDependencyMachines);
	OutDependencyMachines.SetNum(Algo::Unique(OutDependencyMachines));

	return bResult;
}

bool UGameFeaturesSubsystem::FindPluginDependencyStateMachinesToDeactivate(const FString& PluginURL, const FString& PluginFilename, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines) const
{
	const bool bResult = EnumeratePluginDependenciesWithShouldActivate(PluginURL, PluginFilename, [this, &OutDependencyMachines](const FString& DependencyName, const FString& DependencyURL) {
		UGameFeaturePluginStateMachine* Dependency = FindGameFeaturePluginStateMachine(DependencyURL);
		if (Dependency)
		{
			OutDependencyMachines.Add(Dependency);
		}
		else
		{
			// Depenedency may have been fully terminated which is considered deactivated already.
			UE_LOG(LogGameFeatures, Log, TEXT("FindPluginDependencyStateMachinesToDeactivate unable to find plugin state machine for %s using URL %s"), *DependencyName, *DependencyURL);
		}
		return true;
	});
	Algo::Sort(OutDependencyMachines);
	OutDependencyMachines.SetNum(Algo::Unique(OutDependencyMachines));

	return bResult;
}

template <typename CallableT>
bool UGameFeaturesSubsystem::EnumeratePluginDependenciesWithShouldActivate(const FString& PluginURL, const FString& PluginFilename, CallableT Callable) const
{
	FGameFeaturePluginDetails Details;
	if (GetGameFeaturePluginDetailsInternal(PluginFilename, Details))
	{
		for (const FGameFeaturePluginReferenceDetails& PluginDependency : Details.PluginDependencies)
		{
			if (PluginDependency.bShouldActivate)
			{
				const FString& DependencyName = PluginDependency.PluginName;
				TValueOrError<FString, FString> DependencyURLInfo = GameSpecificPolicies->ResolvePluginDependency(PluginURL, DependencyName);
				if (DependencyURLInfo.HasError())
				{
					UE_LOG(LogGameFeatures, Error, TEXT("Failure to resolve dependency %s [%s] for parent plugin url: %s"), *DependencyName, *DependencyURLInfo.GetError(), *PluginURL);
					return false;
				}

				const FString& DependencyURL = DependencyURLInfo.GetValue();

				// Dependency may not be a GFP and so will have an empty URL but not have an error
				if (DependencyURL.IsEmpty())
				{
					continue;
				}

				if (!Callable(DependencyName, DependencyURL))
				{
					return false;
				}
			}
		}
		return true;
	}
	else
	{
		return false;
	}
}

void UGameFeaturesSubsystem::ListGameFeaturePlugins(const TArray<FString>& Args, UWorld* InWorld, FOutputDevice& Ar)
{
	const bool bActiveOnly = Args.ContainsByPredicate([](const FString& Arg) { return Arg.Compare(TEXT("-ACTIVEONLY"), ESearchCase::IgnoreCase) == 0; });
	const bool bCsv = Args.ContainsByPredicate([](const FString& Arg) { return Arg.Compare(TEXT("-CSV"), ESearchCase::IgnoreCase) == 0; });

	FString PlatformName = FPlatformMisc::GetCPUBrand().TrimStartAndEnd();
	Ar.Logf(TEXT("Listing Game Feature Plugins...(%s)"), *PlatformName);
	if (bCsv)
	{
		Ar.Logf(TEXT(",Plugin,State"));
	}

	// create a copy for sorting
	TArray<typename decltype(GameFeaturePluginStateMachines)::ValueType> StateMachines;
	GameFeaturePluginStateMachines.GenerateValueArray(StateMachines);

	// Alphasort
	StateMachines.Sort([](const UGameFeaturePluginStateMachine& A, const UGameFeaturePluginStateMachine& B) { return A.GetGameFeatureName().Compare(B.GetGameFeatureName()) < 0; });

	int32 PluginCount = 0;
	for (UGameFeaturePluginStateMachine* GFSM : StateMachines)
	{
		if (!GFSM)
		{
			continue;
		}

		if (bActiveOnly && GFSM->GetCurrentState() != EGameFeaturePluginState::Active)
		{
			continue;
		}

		if (bCsv)
		{
			Ar.Logf(TEXT(",%s,%s"), *GFSM->GetGameFeatureName(), *UE::GameFeatures::ToString(GFSM->GetCurrentState()));
		}
		else
		{
			Ar.Logf(TEXT("%s (%s)"), *GFSM->GetGameFeatureName(), *UE::GameFeatures::ToString(GFSM->GetCurrentState()));
		}
		++PluginCount;
	}

	Ar.Logf(TEXT("Total Game Feature Plugins: %d"), PluginCount);
}

void UGameFeaturesSubsystem::CallbackObservers(EObserverCallback CallbackType, const FGameFeaturePluginIdentifier& PluginIdentifier,
	const FString* PluginName /*= nullptr*/, 
	const UGameFeatureData* GameFeatureData /*= nullptr*/, 
	FGameFeatureStateChangeContext* StateChangeContext /*= nullptr*/)
{
	static_assert(std::underlying_type<EObserverCallback>::type(EObserverCallback::Count) == 16, "Update UGameFeaturesSubsystem::CallbackObservers to handle added EObserverCallback");

	// Protect against modifying the observer list during iteration
	TArray<UObject*> LocalObservers(Observers);

	switch (CallbackType)
	{
	case EObserverCallback::CheckingStatus:
	{
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureCheckingStatus(PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Terminating:
	{
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureTerminating(PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Predownloading:
	{
		check(PluginName);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeaturePredownloading(*PluginName, PluginIdentifier);
		}
		break;
	}
	case EObserverCallback::PostPredownloading:
	{
		check(PluginName);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeaturePostPredownloading(*PluginName, PluginIdentifier);
		}
		break;
	}
	case EObserverCallback::Downloading:
	{
		check(PluginName);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureDownloading(*PluginName, PluginIdentifier);
		}
		break;
	}
	case EObserverCallback::Releasing:
	{
		check(PluginName);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureReleasing(*PluginName, PluginIdentifier);
		}
		break;
	}
	case EObserverCallback::PreMounting:
	{
		check(PluginName);
		check(StateChangeContext);
		FGameFeaturePreMountingContext* PreMountingContext = static_cast<FGameFeaturePreMountingContext*>(StateChangeContext);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeaturePreMounting(*PluginName, PluginIdentifier, *PreMountingContext);
		}
		break;
	}
	case EObserverCallback::PostMounting:
	{
		check(PluginName);
		check(StateChangeContext);
		FGameFeaturePostMountingContext* PostMountingContext = static_cast<FGameFeaturePostMountingContext*>(StateChangeContext);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeaturePostMounting(*PluginName, PluginIdentifier, *PostMountingContext);
		}
		break;
	}
	case EObserverCallback::Registering:
	{
		check(PluginName);
		check(GameFeatureData);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureRegistering(GameFeatureData, *PluginName, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Unregistering:
	{
		check(PluginName);
#if !WITH_EDITOR
		// In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
		check(GameFeatureData);
#endif
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureUnregistering(GameFeatureData, *PluginName, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Loading:
	{
		check(GameFeatureData);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureLoading(GameFeatureData, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Unloading:
	{
#if !WITH_EDITOR
		// In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
		check(GameFeatureData);
#endif
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureUnloading(GameFeatureData, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Activating:
	{
		check(GameFeatureData);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureActivating(GameFeatureData, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Activated:
	{
		check(GameFeatureData);
		for (UObject* Observer : LocalObservers)
		{
			CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureActivated(GameFeatureData, PluginIdentifier.GetFullPluginURL());
		}
		break;
	}
	case EObserverCallback::Deactivating:
	{
#if !WITH_EDITOR
		// In the editor the GameFeatureData asset can be force deleted, otherwise it should exist
		check(GameFeatureData);
#endif
		check(StateChangeContext);
		FGameFeatureDeactivatingContext* DeactivatingContext = static_cast<FGameFeatureDeactivatingContext*>(StateChangeContext);
		if (ensureAlwaysMsgf(DeactivatingContext, TEXT("Invalid StateChangeContext supplied! Could not cast to FGameFeaturePauseStateChangeContext*!")))
		{
			for (UObject* Observer : LocalObservers)
			{
				CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeatureDeactivating(GameFeatureData, *DeactivatingContext, PluginIdentifier.GetFullPluginURL());
			}
		}
		break;
	}
	case EObserverCallback::PauseChanged:
	{
		check(PluginName);
		check(StateChangeContext);
		FGameFeaturePauseStateChangeContext* PauseChangeContext = static_cast<FGameFeaturePauseStateChangeContext*>(StateChangeContext);
		if (ensureAlwaysMsgf(PauseChangeContext, TEXT("Invalid StateChangeContext supplied! Could not cast to FGameFeaturePauseStateChangeContext*!")))
		{
			for (UObject* Observer : LocalObservers)
			{
				CastChecked<IGameFeatureStateChangeObserver>(Observer)->OnGameFeaturePauseChange(PluginIdentifier.GetFullPluginURL(), *PluginName, *PauseChangeContext);
			}
		}
		break;
	}
	default:
		UE_LOG(LogGameFeatures, Fatal, TEXT("Unkown EObserverCallback!"));
	}
}

void UGameFeaturesSubsystem::RegisterRunningStateMachine(UGameFeaturePluginStateMachine* GFPSM)
{
	check(!RunningStateMachines.Contains(GFPSM));
	RunningStateMachines.Add(GFPSM);
}

void UGameFeaturesSubsystem::UnregisterRunningStateMachine(UGameFeaturePluginStateMachine* GFPSM)
{
	verify(RunningStateMachines.Remove(GFPSM) == 1);
}

FDelegateHandle UGameFeaturesSubsystem::AddBatchingRequest(EGameFeaturePluginState State, FGameFeaturePluginRequestUpdateStateMachine UpdateDelegate)
{
	// Adding first?
	if (BatchProcessingFences.IsEmpty())
	{
		EnableTick();
	}

	FGameFeatureBatchProcessingFence& Fence = BatchProcessingFences.FindOrAdd(State);
	return Fence.NotifyUpdateStateMachines.Add(UpdateDelegate);
}

void UGameFeaturesSubsystem::CancelBatchingRequest(EGameFeaturePluginState State, FDelegateHandle DelegateHandle)
{
	FGameFeatureBatchProcessingFence& Fence = BatchProcessingFences[State];
	Fence.NotifyUpdateStateMachines.Remove(DelegateHandle);
}

void UGameFeaturesSubsystem::EnableTick()
{
	if (!TickHandle.IsValid())
	{
		TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UGameFeaturesSubsystem::Tick));
		UE_LOG(LogGameFeatures, Verbose, TEXT("UGameFeaturesSubsystem enabled tick [InFlightTransitions:%d]"), RunningStateMachines.Num());
	}
}

void UGameFeaturesSubsystem::DisableTick()
{
	if (TickHandle.IsValid())
	{
		TickHandle.Reset();
		UE_LOG(LogGameFeatures, Verbose, TEXT("UGameFeaturesSubsystem disabled tick [InFlightTransitions:%d]"), RunningStateMachines.Num());
	}
}

bool UGameFeaturesSubsystem::Tick(float DeltaTime)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(GFP_GameFeaturesSubsystem_Tick);

	bool bContinueTicking = TickBatchProcessing();
	if (!bContinueTicking)
	{
		DisableTick();
		return false;
	}

	return true;
}

bool UGameFeaturesSubsystem::TickBatchProcessing()
{
	TRACE_CPUPROFILER_EVENT_SCOPE(GFP_GameFeaturesSubsystem_TickFences);

	// No fences?
	if (BatchProcessingFences.IsEmpty())
	{
		return false;
	}

	// Find the closest fence to process.
	EGameFeaturePluginState CurrentFence = EGameFeaturePluginState::MAX;
	for (std::underlying_type<EGameFeaturePluginState>::type iState = 0;
		iState < EGameFeaturePluginState::MAX;
		++iState)
	{
		if (BatchProcessingFences.Contains((EGameFeaturePluginState)iState))
		{
			CurrentFence = (EGameFeaturePluginState)(iState);
			break;
		}
	}

	check(CurrentFence != EGameFeaturePluginState::MAX);

	// Check for in flight transitions on state machines to be included in fence
	TArray<UGameFeaturePluginStateMachine*> AtFence;
	AtFence.Reserve(RunningStateMachines.Num());
	bool bAreAnyGFPSMsPendingAtFence = false;
	for (UGameFeaturePluginStateMachine* GFPSM : RunningStateMachines)
	{
		if (GFPSM->GetProperties().IsWaitingForBatchProcessing() && GFPSM->GetCurrentState() == CurrentFence)
		{
			AtFence.Add(GFPSM);
		}
		if (GFPSM->GetDestination().MinState >= CurrentFence && GFPSM->GetCurrentState() < CurrentFence)
		{
			bAreAnyGFPSMsPendingAtFence = true;
			break;
		}
	}

	if (!bAreAnyGFPSMsPendingAtFence)
	{
		TRACE_CPUPROFILER_EVENT_SCOPE(GFP_GameFeaturesSubsystem_FenceComplete);

		FGameFeatureBatchProcessingFence& Fence = BatchProcessingFences[CurrentFence];

		UGameFeaturePluginStateMachine::BatchProcess(CurrentFence, AtFence);

		Fence.NotifyUpdateStateMachines.Broadcast();

		BatchProcessingFences.Remove(CurrentFence);

		return BatchProcessingFences.IsEmpty();
	}

	return true;
}

TSet<FString> UGameFeaturesSubsystem::GetActivePluginNames() const
{
	TSet<FString> ActivePluginNames;

	for (const TPair<FString, TObjectPtr<UGameFeaturePluginStateMachine>>& Pair : GameFeaturePluginStateMachines)
	{
		UGameFeaturePluginStateMachine* StateMachine = Pair.Value;
		if (StateMachine->GetCurrentState() == EGameFeaturePluginState::Active &&
			StateMachine->GetDestination().Contains(EGameFeaturePluginState::Active))
		{
			ActivePluginNames.Add(StateMachine->GetPluginName());
		}
	}

	return ActivePluginNames;
}

namespace GameFeaturesSubsystem
{ 
	static bool IsContentWithinActivePlugin(const FString& InObjectOrPackagePath, const TSet<FString>& ActivePluginNames)
	{
		// Look for the first slash beyond the first one we start with.
		const int32 RootEndIndex = InObjectOrPackagePath.Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, 1);

		const FString ObjectPathRootName = InObjectOrPackagePath.Mid(1, RootEndIndex - 1);

		if (ActivePluginNames.Contains(ObjectPathRootName))
		{
			return true;
		}

		return false;
	}
}

void UGameFeaturesSubsystem::FilterInactivePluginAssets(TArray<FAssetIdentifier>& AssetsToFilter) const
{
	AssetsToFilter.RemoveAllSwap([ActivePluginNames = GetActivePluginNames()](const FAssetIdentifier& Asset) 
	{
		return !GameFeaturesSubsystem::IsContentWithinActivePlugin(Asset.PackageName.ToString(), ActivePluginNames);
	});
}

void UGameFeaturesSubsystem::FilterInactivePluginAssets(TArray<FAssetData>& AssetsToFilter) const
{
	AssetsToFilter.RemoveAllSwap([ActivePluginNames = GetActivePluginNames()](const FAssetData& Asset) 
	{
		return !GameFeaturesSubsystem::IsContentWithinActivePlugin(Asset.GetObjectPathString(), ActivePluginNames);
	});
}

EBuiltInAutoState UGameFeaturesSubsystem::DetermineBuiltInInitialFeatureState(TSharedPtr<FJsonObject> Descriptor, const FString& ErrorContext)
{
	EBuiltInAutoState InitialState = EBuiltInAutoState::Invalid;

	FString InitialFeatureStateStr;
	if (Descriptor->TryGetStringField(TEXT("BuiltInInitialFeatureState"), InitialFeatureStateStr))
	{
		if (InitialFeatureStateStr == TEXT("Installed"))
		{
			InitialState = EBuiltInAutoState::Installed;
		}
		else if (InitialFeatureStateStr == TEXT("Registered"))
		{
			InitialState = EBuiltInAutoState::Registered;
		}
		else if (InitialFeatureStateStr == TEXT("Loaded"))
		{
			InitialState = EBuiltInAutoState::Loaded;
		}
		else if (InitialFeatureStateStr == TEXT("Active"))
		{
			InitialState = EBuiltInAutoState::Active;
		}
		else
		{
			if (!ErrorContext.IsEmpty())
			{
				UE_LOG(LogGameFeatures, Error, TEXT("Game feature '%s' has an unknown value '%s' for BuiltInInitialFeatureState (expected Installed, Registered, Loaded, or Active); defaulting to Active."), *ErrorContext, *InitialFeatureStateStr);
			}
			InitialState = EBuiltInAutoState::Active;
		}
	}
	else
	{
		// BuiltInAutoRegister. Default to true. If this is a built in plugin, should it be registered automatically (set to false if you intent to load late with LoadAndActivateGameFeaturePlugin)
		bool bBuiltInAutoRegister = true;
		Descriptor->TryGetBoolField(TEXT("BuiltInAutoRegister"), bBuiltInAutoRegister);

		// BuiltInAutoLoad. Default to true. If this is a built in plugin, should it be loaded automatically (set to false if you intent to load late with LoadAndActivateGameFeaturePlugin)
		bool bBuiltInAutoLoad = true;
		Descriptor->TryGetBoolField(TEXT("BuiltInAutoLoad"), bBuiltInAutoLoad);

		// The cooker will need to activate the plugin so that assets can be scanned properly
		bool bBuiltInAutoActivate = true;
		Descriptor->TryGetBoolField(TEXT("BuiltInAutoActivate"), bBuiltInAutoActivate);

		InitialState = EBuiltInAutoState::Installed;
		if (bBuiltInAutoRegister)
		{
			InitialState = EBuiltInAutoState::Registered;
			if (bBuiltInAutoLoad)
			{
				InitialState = EBuiltInAutoState::Loaded;
				if (bBuiltInAutoActivate)
				{
					InitialState = EBuiltInAutoState::Active;
				}
			}
		}

		if (!ErrorContext.IsEmpty())
		{
			//@TODO: Increase severity to a warning after changing existing features
			UE_LOG(LogGameFeatures, Log, TEXT("Game feature '%s' has no BuiltInInitialFeatureState key, using legacy BuiltInAutoRegister(%d)/BuiltInAutoLoad(%d)/BuiltInAutoActivate(%d) values to arrive at initial state."),
				*ErrorContext,
				bBuiltInAutoRegister ? 1 : 0,
				bBuiltInAutoLoad ? 1 : 0,
				bBuiltInAutoActivate ? 1 : 0);
		}
	}

	return InitialState;
}

EGameFeaturePluginState UGameFeaturesSubsystem::ConvertInitialFeatureStateToTargetState(EBuiltInAutoState AutoState)
{
	EGameFeaturePluginState InitialState;
	switch (AutoState)
	{
	default:
	case EBuiltInAutoState::Invalid:
		InitialState = EGameFeaturePluginState::UnknownStatus;
		break;
	case EBuiltInAutoState::Installed:
		InitialState = EGameFeaturePluginState::Installed;
		break;
	case EBuiltInAutoState::Registered:
		InitialState = EGameFeaturePluginState::Registered;
		break;
	case EBuiltInAutoState::Loaded:
		InitialState = EGameFeaturePluginState::Loaded;
		break;
	case EBuiltInAutoState::Active:
		InitialState = EGameFeaturePluginState::Active;
		break;
	}
	return InitialState;
}

void UGameFeaturesSubsystem::GetPluginsToCook(TSet<FString>& OutPlugins)
{
	// Command line parameter -CookPlugins.
	static TArray<FString> PluginsList = []()
	{
		TArray<FString> ReturnList;
		FString CookPluginsStr;
		if (FParse::Value(FCommandLine::Get(), TEXT("CookPlugins="), CookPluginsStr, false))
		{
			// check if it's a filename
			if (CookPluginsStr.EndsWith(".txt"))
			{
				const bool bSuccess = FFileHelper::LoadFileToStringWithLineVisitor(*CookPluginsStr, [&ReturnList](FStringView Line)
				{
					ReturnList.Add(FString(Line));
				});
				ensureMsgf(bSuccess, TEXT("Failed to read - %s"), *CookPluginsStr);
			}
			else
			{
				CookPluginsStr.ParseIntoArray(ReturnList, TEXT(","));
			}
		}

		return ReturnList;
	}();
	
	OutPlugins.Append(PluginsList);	
}

bool UGameFeaturesSubsystem::GetPluginDebugStateEnabled(const FString& PluginUrl)
{
#if !UE_BUILD_SHIPPING
	return DebugStateChangedForPlugins.Contains(PluginUrl);
#else
	return false;
#endif
}
void UGameFeaturesSubsystem::SetPluginDebugStateEnabled(const FString& PluginUrl, bool bEnabled)
{
#if !UE_BUILD_SHIPPING
	if (bEnabled)
	{
		DebugStateChangedForPlugins.Add(PluginUrl);
	}
	else
	{
		DebugStateChangedForPlugins.Remove(PluginUrl);
	}
#endif
}

FString UGameFeaturesSubsystem::GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	if (GameSpecificPolicies)
	{
		return GameSpecificPolicies->GetInstallBundleName(PluginName, bEvenIfDoesntExist);
	}
	return UGameFeatureData::GetInstallBundleName(PluginName, bEvenIfDoesntExist);
}

FString UGameFeaturesSubsystem::GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist /*= false*/)
{
	if (GameSpecificPolicies)
	{
		return GameSpecificPolicies->GetOptionalInstallBundleName(PluginName, bEvenIfDoesntExist);
	}
	return UGameFeatureData::GetOptionalInstallBundleName(PluginName, bEvenIfDoesntExist);
}

void UGameFeaturesSubsystem::SetExplanationForNotMountingPlugin(const FString& PluginURL, const FString& Explanation)
{
#if WITH_EDITOR
	FGameFeaturePluginIdentifier Identifier(PluginURL);
	FStringView PluginName = Identifier.GetPluginName();
	if (!PluginName.IsEmpty())
	{
		UnmountedPluginNameToExplanation.FindOrAdd(FString(PluginName)) = Explanation;
	}
#endif
}

GameFeaturesSubsystemSettings.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturesSubsystemSettings.h"
#include "Misc/Paths.h"
#include "Misc/PathViews.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeaturesSubsystemSettings)

const FName UGameFeaturesSubsystemSettings::LoadStateClient(TEXT("Client"));
const FName UGameFeaturesSubsystemSettings::LoadStateServer(TEXT("Server"));

UGameFeaturesSubsystemSettings::UGameFeaturesSubsystemSettings()
{
	PRAGMA_DISABLE_DEPRECATION_WARNINGS
	BuiltInGameFeaturePluginsFolder = FPaths::ConvertRelativePathToFull(FPaths::ProjectPluginsDir() + TEXT("GameFeatures/"));
	PRAGMA_ENABLE_DEPRECATION_WARNINGS
}

bool UGameFeaturesSubsystemSettings::IsValidGameFeaturePlugin(const FString& PluginDescriptorFilename) const
{
	// Build the cache of game feature plugin folders the first time this is called
	static struct FBuiltInGameFeaturePluginsFolders
	{
		FBuiltInGameFeaturePluginsFolders()
		{
			const FPaths::EGetExtensionDirsFlags ExtensionFlags =
				FPaths::EGetExtensionDirsFlags::WithBase |
				FPaths::EGetExtensionDirsFlags::WithRestricted;

			// Get all the existing game feature paths
			TArray<FString> RelativePaths = FPaths::GetExtensionDirs(
				FPaths::ProjectDir(), FPaths::Combine(TEXT("Plugins"), TEXT("GameFeatures")), ExtensionFlags);

			// The base directory may not exist yet, add it if empty
			if (RelativePaths.IsEmpty())
			{
				RelativePaths.Add(FPaths::Combine(FPaths::ProjectDir(), TEXT("Plugins"), TEXT("GameFeatures")));
			}

			BuiltInGameFeaturePluginsFolders.Reserve(2 * RelativePaths.Num());
			for (FString& BuiltInFolder : RelativePaths)
			{
				BuiltInFolder /= TEXT(""); // Add trailing slash if needed
				BuiltInGameFeaturePluginsFolders.Add(FPaths::ConvertRelativePathToFull(BuiltInFolder));
				BuiltInGameFeaturePluginsFolders.Add(MoveTemp(BuiltInFolder));
			}
		}

		TArray<FString> BuiltInGameFeaturePluginsFolders;
	} Lazy;

	// Check to see if the filename is rooted in a game feature plugin folder
	for (const FString& BuiltInFolder : Lazy.BuiltInGameFeaturePluginsFolders)
	{
		if (FPathViews::IsParentPathOf(BuiltInFolder, PluginDescriptorFilename))
		{
			return true;
		}
	}

	return false;
}


GameFeaturePluginTestHelper.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR

#include "GameFeaturePluginTestsHelper.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/FileManager.h"

FString GeneratePluginString(const TArray<FGameFeatureDependsProperties>& Depends)
{
	TStringBuilder<512> Out;
	for (const FGameFeatureDependsProperties& Depend : Depends)
	{
		Out += FString::Printf(TEXT(R"(
		{
			"Name": "%s",
			"Enabled": true,
			"Activate": %s,
			"Optional": true
		},

		)"), *Depend.PluginName, Depend.ShouldActivate == EShouldActivate::No ? TEXT("false") : TEXT("true"));
	}

	return *Out;
}

bool CreateGameFeaturePlugin(FGameFeatureProperties Properties, FString& OutPluginURL)
{
	FString PluginPath = FPaths::ProjectPluginsDir() / TEXT("GameFeatures") / Properties.PluginName;
	FString UPluginPath = PluginPath / (Properties.PluginName + TEXT(".uplugin"));

	// if one was already created here, delete it before making a new one
	if (FPaths::DirectoryExists(PluginPath))
	{
		IFileManager::Get().DeleteDirectory(*PluginPath, /* bEnsureExists */ false, /* bDeleteEntireTree */ true);
	}

	IFileManager::Get().MakeDirectory(*PluginPath, /* bCreateTree */ true);

	FString PluginDependsString = GeneratePluginString(Properties.Depends);
	FString PluginDetials = FString::Printf(TEXT(R"(
	{
		"FileVersion": 3,
		"Version": 1,
		"VersionName": "1.0",
		"FriendlyName": "%s",
		"Description": "Generated GFP for Testing",
		"Category": "GFPTesting",
		"CreatedBy": "Automated",
		"CreatedByURL": "",
		"DocsURL": "",
		"MarketplaceURL": "",
		"SupportURL": "",
		"EnabledByDefault": false,
		"CanContainContent": false,
		"IsBetaVersion": false,
		"IsExperimentalVersion": false,
		"Installed": false,
		"ExplicitlyLoaded": true,
		"BuiltInInitialFeatureState": "%s",
		"Plugins": [%s]
	})"), *Properties.PluginName, *LexToString(Properties.BuiltinAutoState), *PluginDependsString);

	bool bSavedFile = FFileHelper::SaveStringToFile(PluginDetials, *UPluginPath);

	OutPluginURL = UGameFeaturesSubsystem::GetPluginURL_FileProtocol(IFileManager::Get().ConvertToRelativePath(*UPluginPath));
	return bSavedFile;
}

#endif

GameFeaturePluginTests.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR

#include "Misc/AutomationTest.h"
#include "Misc/ScopeExit.h"
#include "Tests/AutomationCommon.h"
#include "Tests/AutomationEditorCommon.h"

#include "GameFeaturePluginOperationResult.h"
#include "GameFeaturePluginStateMachine.h"
#include "GameFeaturePluginTestsHelper.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeatureTypes.h"

DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForTrue, bool*, bVariableToWaitFor);
bool FWaitForTrue::Update()
{
    return *bVariableToWaitFor;
}

DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FExecuteFunction, TFunction<bool()>, Function);
bool FExecuteFunction::Update()
{
	return Function();
}

EGameFeaturePluginState ConvertTargetStateToPluginState(const EGameFeatureTargetState TargetState)
{
	switch (TargetState)
	{
		case EGameFeatureTargetState::Installed:
			return EGameFeaturePluginState::Installed;
		case EGameFeatureTargetState::Registered:
			return EGameFeaturePluginState::Registered;
		case EGameFeatureTargetState::Loaded:
			return EGameFeaturePluginState::Loaded;
		case EGameFeatureTargetState::Active:
			return EGameFeaturePluginState::Active;
		default:
			break;
	}

	return EGameFeaturePluginState::MAX;
}

class FTestGameFeaturePluginBase : public FAutomationTestBase
{
public:
	FTestGameFeaturePluginBase(const FString& InName, const bool bInComplexTask)
		: FAutomationTestBase(InName, bInComplexTask)
	{
	}

	~FTestGameFeaturePluginBase()
	{
	}

	bool IsPluginInPluginStateRange(const FGameFeaturePluginStateRange PluginStateRange, const FString& PluginURL)
	{
		EGameFeaturePluginState CurrentPluginState = UGameFeaturesSubsystem::Get().GetPluginState(PluginURL);

		return PluginStateRange.Contains(CurrentPluginState);
	}

	void LatentTestPluginState(const EGameFeatureTargetState PluginTargetState, const FString& PluginURL)
	{
		LatentTestPluginState(FGameFeaturePluginStateRange(ConvertTargetStateToPluginState(PluginTargetState)), PluginURL);
	}

	void LatentTestPluginState(const FGameFeaturePluginStateRange PluginStateRange, const FString& PluginURL)
	{
		ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this, PluginStateRange, PluginURL]
		{
			TestTrue(FString::Printf(TEXT("Plugin %s in %s state, expected plugin state in range (%s, %s)"),
				*PluginURL, *UE::GameFeatures::ToString(UGameFeaturesSubsystem::Get().GetPluginState(PluginURL)), *UE::GameFeatures::ToString(PluginStateRange.MinState), *UE::GameFeatures::ToString(PluginStateRange.MaxState)),
				IsPluginInPluginStateRange(PluginStateRange, PluginURL));

			return true;
		}));
	}

	void LatentTestTransitionGFP(const EGameFeatureTargetState TargetState, const FString& PluginURL)
	{
		ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this, TargetState, PluginURL]
		{
			*bAsyncCommandComplete = false;

			UGameFeaturesSubsystem::Get().ChangeGameFeatureTargetState(PluginURL, TargetState,
				FGameFeaturePluginChangeStateComplete::CreateLambda([this, TargetState, PluginURL](const UE::GameFeatures::FResult& Result)
				{
					*bAsyncCommandComplete = true;
					TestFalse(FString::Printf(TEXT("Failed to transition to %s: error: %s"), *LexToString(TargetState), *UE::GameFeatures::ToString(Result)),
						Result.HasError());
				}
			));

			return true;
		}));

		ADD_LATENT_AUTOMATION_COMMAND(FWaitForTrue(&*bAsyncCommandComplete));

		LatentTestPluginState(TargetState, PluginURL);
	}

	void LatentCheckInitialPluginState()
	{
		// Check we are somewhere between uninited, and uninstalled for the first time we check this and after we restore the plugin state
		// depending on the initial state as well as deactivating/terminating the plugin we should be in the Terminal or UnknownStatus node
		LatentTestPluginState(FGameFeaturePluginStateRange(EGameFeaturePluginState::Uninitialized, EGameFeaturePluginState::Uninstalled), GFPFileURL);
	}

	void LatentRestorePluginState()
	{
		ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
		{
			*bAsyncCommandComplete = false;

			// We are in an uninstalled/terminal/not setup state. Dont try to Deactivate/Terminate when we are not Activated/Installed
			if (IsPluginInPluginStateRange(FGameFeaturePluginStateRange(EGameFeaturePluginState::Uninitialized, EGameFeaturePluginState::Uninstalled), GFPFileURL))
			{
				*bAsyncCommandComplete = true;
				return true;
			}

			UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(GFPFileURL,
				FGameFeaturePluginReleaseComplete::CreateLambda([this](const UE::GameFeatures::FResult& Result)
				{
					*bAsyncCommandComplete = true;
					TestFalse(FString::Printf(TEXT("Failed to deactivate plugin, error: %s"), *UE::GameFeatures::ToString(Result)),
						Result.HasError());
				}
			));

			return true;
		}));
		ADD_LATENT_AUTOMATION_COMMAND(FWaitForTrue(&*bAsyncCommandComplete));

		ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
		{
			*bAsyncCommandComplete = false;

			// We are in an uninstalled/terminal/not setup state. Dont try to Deactivate/Terminate when we are not Activated/Installed
			if (IsPluginInPluginStateRange(FGameFeaturePluginStateRange(EGameFeaturePluginState::Uninitialized, EGameFeaturePluginState::Uninstalled), GFPFileURL))
			{
				*bAsyncCommandComplete = true;
				return true;
			}

			UGameFeaturesSubsystem::Get().TerminateGameFeaturePlugin(GFPFileURL,
				FGameFeaturePluginReleaseComplete::CreateLambda([this](const UE::GameFeatures::FResult& Result)
				{
					*bAsyncCommandComplete = true;
					TestFalse(FString::Printf(TEXT("Failed to terminate plugin, error: %s"), *UE::GameFeatures::ToString(Result)),
						Result.HasError());
				}
			));

			return true;
		}));
		ADD_LATENT_AUTOMATION_COMMAND(FWaitForTrue(&*bAsyncCommandComplete));
	}

	// For now hard-coded into EngineTest area but can always be adjusted later
	const FString GFPPluginPath = TEXT("../../../EngineTest/Plugins/GameFeatures/GameFeatureEngineTestC/GameFeatureEngineTestC.uplugin");
	const FString GFPFileURL = FString(TEXT("file:")) + GFPPluginPath;
	TSharedRef<bool> bAsyncCommandComplete = MakeShared<bool>(false);
};

IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FGameFeatureSubsystemTestChangeState, FTestGameFeaturePluginBase, "GameFeaturePlugin.Subsystem.ChangeTargetState", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter);
bool FGameFeatureSubsystemTestChangeState::RunTest(const FString& Parameters)
{
	// Ensure if the test was canceled we restore the plugin back to an deactivated/terminated state
	LatentRestorePluginState();
	LatentCheckInitialPluginState();
	ON_SCOPE_EXIT
	{
		LatentRestorePluginState();
	};

	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, GFPFileURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, GFPFileURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Loaded, GFPFileURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Active, GFPFileURL);

    return true;
}

IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FGameFeatureSubsystemTestUninstall, FTestGameFeaturePluginBase, "GameFeaturePlugin.Subsystem.FilePluginProtocol", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter);
bool FGameFeatureSubsystemTestUninstall::RunTest(const FString& Parameters)
{
	// Ensure if the test was canceled we restore the plugin back to an deactivated/terminated state
	LatentRestorePluginState();
	LatentCheckInitialPluginState();
	ON_SCOPE_EXIT
	{
		LatentRestorePluginState();
	};

	// Get us into an installed state so we can query info about the GFP
	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, GFPFileURL);

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		EGameFeaturePluginProtocol FilePluginProtocol = UGameFeaturesSubsystem::Get().GetPluginURLProtocol(GFPFileURL);
		if (!TestEqual(FString::Printf(TEXT("Expected PluginProtocol to be File but was %i"), (int)FilePluginProtocol),
				FilePluginProtocol, EGameFeaturePluginProtocol::File))
		{
			return true;
		}

		if (!TestTrue(TEXT("Expected PluginProtocol to be File but was not"),
				UGameFeaturesSubsystem::Get().IsPluginURLProtocol(GFPFileURL, EGameFeaturePluginProtocol::File)))
		{
			return true;
		}

		EGameFeaturePluginProtocol PluginProtocol;
		FStringView PluginPath;
		if (!TestTrue(TEXT("Failed to parse plugin URL"),
				UGameFeaturesSubsystem::Get().ParsePluginURL(GFPFileURL, &PluginProtocol, &PluginPath)))
		{
			return true;
		}

		if (!TestEqual(FString::Printf(TEXT("Expected PluginProtocol to be File but was %i"), (int)PluginProtocol),
				PluginProtocol, EGameFeaturePluginProtocol::File))
		{
			return true;
		}

		if (!TestEqual(FString::Printf(TEXT("Expected parsed PluginPath %s to equal %s"), PluginPath.GetData(), *GFPPluginPath),
			PluginPath, GFPPluginPath))
		{
			return true;
		}

		return true;
	}));

	return true;
}

IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FGameFeatureSubsystemTestGetGameFeatureData, FTestGameFeaturePluginBase, "GameFeaturePlugin.Subsystem.GetGameFeatureData", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter);
bool FGameFeatureSubsystemTestGetGameFeatureData::RunTest(const FString& Parameters)
{
	// Ensure if the test was canceled we restore the plugin back to an deactivated/terminated state
	LatentRestorePluginState();
	LatentCheckInitialPluginState();
	ON_SCOPE_EXIT
	{
		LatentRestorePluginState();
	};

	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, GFPFileURL);

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		const UGameFeatureData* GameFeatureData = UGameFeaturesSubsystem::Get().GetGameFeatureDataForRegisteredPluginByURL(GFPFileURL);
		TestNull(TEXT("GameFeatureData is not NULL, GFP is only in the Installed state and should not have any GameFeatureData"), GameFeatureData);

		return true;
	}));

	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, GFPFileURL);

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		const UGameFeatureData* GameFeatureData = UGameFeaturesSubsystem::Get().GetGameFeatureDataForRegisteredPluginByURL(GFPFileURL);
		TestNotNull(TEXT("GameFeatureData is NULL, but the GFP should have a valid GameFeatureData"), GameFeatureData);

		return true;
	}));

	LatentTestTransitionGFP(EGameFeatureTargetState::Active, GFPFileURL);

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		const UGameFeatureData* GameFeatureData = UGameFeaturesSubsystem::Get().GetGameFeatureDataForActivePluginByURL(GFPFileURL);
		TestNotNull(TEXT("GameFeatureData is NULL, but the GFP should have a valid GameFeatureData"), GameFeatureData);

		return true;
	}));

	return true;
}

// This test is testing that non-compiled in plugins do not get marked as built in once they are loaded through external APIs
// To see the test fail set GameFeaturePlugin.TrimNonStartupEnabledPlugins=false, which will go back to the old way the plugin system would handle new plugins not set as built in
IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FGameFeatureSubsystemTestNonBuiltinPluginDoesntConvertToBuiltinPlugin, FTestGameFeaturePluginBase, "GameFeaturePlugin.Subsystem.NonBuiltinPluginDoesntConvertToBuiltinPlugin", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter);
bool FGameFeatureSubsystemTestNonBuiltinPluginDoesntConvertToBuiltinPlugin::RunTest(const FString& Parameters)
{
	// Ensure if the test was canceled we restore the plugin back to an deactivated/terminated state
	LatentRestorePluginState();
	LatentCheckInitialPluginState();
	ON_SCOPE_EXIT
	{
		LatentRestorePluginState();
	};

	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, GFPFileURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, GFPFileURL);

	// Test we get to registered, installed -> mounted which will get our plugin in the enabled/mounted state
	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		TestFalse(FString::Printf(TEXT("WasGameFeaturePluginLoadedAsBuiltIn on GFP %s to be false but was true"), *GFPFileURL),
			UGameFeaturesSubsystem::Get().WasGameFeaturePluginLoadedAsBuiltIn(GFPFileURL));
		return true;
	}));

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		*bAsyncCommandComplete = false;

		auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
		{
			return true;
		};

		UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter,
			FBuiltInGameFeaturePluginsLoaded::CreateLambda([this](const TMap<FString, UE::GameFeatures::FResult>& Results)
			{
				*bAsyncCommandComplete = true;
				for (const TPair<FString, UE::GameFeatures::FResult>& Result : Results)
				{
					TestFalse(FString::Printf(TEXT("Failed to LoadBuiltInGameFeaturePlugins on %s error: %s"), *Result.Get<0>(), *UE::GameFeatures::ToString(Result.Get<1>())),
						Result.Get<1>().HasError());
				}
			}
		));

		return true;
	}));
	ADD_LATENT_AUTOMATION_COMMAND(FWaitForTrue(&*bAsyncCommandComplete));

	ADD_LATENT_AUTOMATION_COMMAND(FExecuteFunction([this]
	{
		TestFalse(FString::Printf(TEXT("WasGameFeaturePluginLoadedAsBuiltIn on GFP %s to be false but was true"), *GFPFileURL),
			UGameFeaturesSubsystem::Get().WasGameFeaturePluginLoadedAsBuiltIn(GFPFileURL));
		return true;
	}));

	return true;
}


/**
 * This is a test that will fail currently:
 *   Create GFPs A -> C, D and B -> C, D then activate A and B
 *   move B to registered causing C, D to transition to Loaded (since they are ShouldActive)
 *     expect:
 *       C, D to stay active since A is still active and depends on that
 *     result:
 *       C, D become deactiving/loaded since B we downgraded to registered, which deactivates its depends
 *
 *   TODO this is simply testing our CreatePlugin logic for now, Leaving on even though it doesnt test the failure at the bottom!
 */
IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(FGameFeatureSubsystemTestCreatePlugin, FTestGameFeaturePluginBase, "GameFeaturePlugin.Subsystem.DeactivatePreventsDependsDowngradeIfRefCount", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter);
bool FGameFeatureSubsystemTestCreatePlugin::RunTest(const FString& Parameters)
{
	FGameFeatureProperties PropertiesA;
	PropertiesA.PluginName = TEXT("TestingA");
	PropertiesA.BuiltinAutoState = EGameFeatureTargetState::Installed;
	PropertiesA.Depends = { FGameFeatureDependsProperties{ TEXT("TestingD"), EShouldActivate::Yes }, FGameFeatureDependsProperties{ TEXT("TestingC"), EShouldActivate::Yes } };

	FString PluginAURL;
	CreateGameFeaturePlugin(PropertiesA, PluginAURL);

	FGameFeatureProperties PropertiesB;
	PropertiesB.PluginName = TEXT("TestingB");
	PropertiesB.BuiltinAutoState = EGameFeatureTargetState::Installed;
	PropertiesB.Depends = { FGameFeatureDependsProperties{ TEXT("TestingD"), EShouldActivate::Yes }, FGameFeatureDependsProperties{ TEXT("TestingC"), EShouldActivate::Yes } };

	FString PluginBURL;
	CreateGameFeaturePlugin(PropertiesB, PluginBURL);

	FGameFeatureProperties PropertiesC;
	PropertiesC.PluginName = TEXT("TestingC");
	PropertiesC.BuiltinAutoState = EGameFeatureTargetState::Installed;

	FString PluginCURL;
	CreateGameFeaturePlugin(PropertiesC, PluginCURL);

	FGameFeatureProperties PropertiesD;
	PropertiesD.PluginName = TEXT("TestingD");
	PropertiesD.BuiltinAutoState = EGameFeatureTargetState::Installed;

	FString PluginDURL;
	CreateGameFeaturePlugin(PropertiesD, PluginDURL);

	// make sure they are all installed
	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, PluginCURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, PluginDURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, PluginAURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Installed, PluginBURL);

	// register A, and B which will pull in C, D to registered
	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, PluginAURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, PluginBURL);
	LatentTestPluginState(EGameFeatureTargetState::Registered, PluginCURL);
	LatentTestPluginState(EGameFeatureTargetState::Registered, PluginDURL);

	// activate A, and B which will pull in C, D to active
	LatentTestTransitionGFP(EGameFeatureTargetState::Active, PluginAURL);
	LatentTestTransitionGFP(EGameFeatureTargetState::Active, PluginBURL);
	LatentTestPluginState(EGameFeatureTargetState::Active, PluginCURL);
	LatentTestPluginState(EGameFeatureTargetState::Active, PluginDURL);

	// drop A to registered, which currently drops C, D to Loaded which is a bug
	LatentTestTransitionGFP(EGameFeatureTargetState::Registered, PluginAURL);

	/* TODO remove this once we have ref counting working, and stop deactiving C and D here
	LatentTestPluginState(EGameFeaturePluginState::Active, PluginCURL);
	LatentTestPluginState(EGameFeaturePluginState::Active, PluginDURL);
	*/

	return true;
}

#endif // WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR

GameFeaturePluginTestsHelper.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeaturesSubsystem.h"

enum class EShouldActivate : uint8
{
	No,
	Yes
};

struct FGameFeatureDependsProperties
{
	FString PluginName;
	EShouldActivate ShouldActivate = EShouldActivate::No;
};

struct FGameFeatureProperties
{
	FString PluginName;
	EGameFeatureTargetState	BuiltinAutoState = EGameFeatureTargetState::Installed;
	TArray<FGameFeatureDependsProperties> Depends;
};

bool CreateGameFeaturePlugin(FGameFeatureProperties Properties, FString& OutPluginURL);

GameFeatureAction.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.generated.h"

#define UE_API GAMEFEATURES_API

class UGameFeatureData;
struct FGameFeatureActivatingContext;
struct FGameFeatureDeactivatingContext;
struct FAssetBundleData;

/** Represents an action to be taken when a game feature is activated */
UCLASS(MinimalAPI, DefaultToInstanced, EditInlineNew, Abstract)
class UGameFeatureAction : public UObject
{
	GENERATED_BODY()

public:
	UE_API virtual UGameFeatureData* GetGameFeatureData() const;

	/** Called when the object owning the action is registered for possible activation, this is called even if a feature never activates */
	virtual void OnGameFeatureRegistering() {}

	/** Called to unregister an action, it will not be activated again without being registered again */
	virtual void OnGameFeatureUnregistering() {}
	
	/** Called to indicate that a feature is being loaded for activation in the near future */
	virtual void OnGameFeatureLoading() {}

	/** Called to indicate that a feature is being unloaded */
	virtual void OnGameFeatureUnloading() {}

	/** Called when the feature is actually applied */
	UE_API virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context);

	/** Older-style activation function with no context, called by base class if context version is not overridden */
	virtual void OnGameFeatureActivating() {}

	/** Called when the feature is fully active */
	virtual void OnGameFeatureActivated() {}

	/** Called when game feature is deactivated, it may be activated again in the near future */
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) {}

	/** Returns whether the action game feature plugin is registered or not. */
	UE_API bool IsGameFeaturePluginRegistered(bool bCheckForRegistering = false) const;

	/** Returns whether the action game feature plugin is active or not. */
	UE_API bool IsGameFeaturePluginActive(bool bCheckForActivating = false) const;

#if WITH_EDITORONLY_DATA
	virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) {}
#endif
};

#undef UE_API

GameFeatureAction_AddActorFactory.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"
#include "GameFeatureAction_AddActorFactory.generated.h"

class UActorComponent;
class UActorFactory;

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddActorFactory

/**
 * GameFeatureAction to add an actor factory when this plugin registers.
 * Useful for factories that might load BP classes within a plugin which might not have been discovered yet.
 */
UCLASS(MinimalAPI, meta=(DisplayName="Add Actor Factory"), Config=Engine)
class UGameFeatureAction_AddActorFactory final : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~Start of UGameFeatureAction interface
	virtual void OnGameFeatureRegistering() override;
	virtual void OnGameFeatureUnregistering() override;
	//~End of UGameFeatureAction interface

#if WITH_EDITOR

	/** UObject overrides */
	virtual void PostRename(UObject* OldOuter, const FName OldName) override;
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
	/** ~UObject overrides */
#endif // WITH_EDITOR
	
protected:
#if WITH_EDITORONLY_DATA
	/**
	 * The actor factory class to add once this plugin registers
	 * Actor factories should be setup with bShouldAutoRegister so that they do not register during engine boot.
	 */
	UPROPERTY(EditAnywhere, Category = "Actor Factory")
	TSoftClassPtr<UObject> ActorFactory;

	TWeakObjectPtr<UObject> AddedFactory;
#endif

	void AddActorFactory();
	void RemoveActorFactory();
};

GameFeatureAction_AddCheats.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"

#include "GameFeatureAction_AddCheats.generated.h"

class UCheatManager;
class UCheatManagerExtension;
template <typename T> class TSubclassOf;

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddCheats

/**
 * Adds cheat manager extensions to the cheat manager for each player
 */
UCLASS(MinimalAPI, meta=(DisplayName="Add Cheats"))
class UGameFeatureAction_AddCheats final : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~UGameFeatureAction interface
	virtual void OnGameFeatureActivating() override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~End of UGameFeatureAction interface

	//~UObject interface
#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
	//~End of UObject interface

private:
	void OnCheatManagerCreated(UCheatManager* CheatManager);
	void SpawnCheatManagerExtension(UCheatManager* CheatManager, const TSubclassOf<UCheatManagerExtension>& CheatManagerClass);

public:
	/** Cheat managers to setup for the game feature plugin */
	UPROPERTY(EditAnywhere, Category="Cheats")
	TArray<TSoftClassPtr<UCheatManagerExtension>> CheatManagers;

	UPROPERTY(EditAnywhere, Category="Cheats")
	bool bLoadCheatManagersAsync;

private:
	FDelegateHandle CheatManagerRegistrationHandle;

	UPROPERTY(Transient)
	TArray<TWeakObjectPtr<UCheatManagerExtension>> SpawnedCheatManagers;

	bool bIsActive = false;
};

GameFeatureAction_AddChunkOverride.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"
#include "GameFeatureAction_AddChunkOverride.generated.h"

class UActorComponent;

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddChunkOverride

/**
 * Used to cook assets from a GFP into a specified chunkId.
 * This can be useful when individually cooking GFPs for iteration or splitting up a packaged
 * game into smaller downloadable chunks.
 */
UCLASS(MinimalAPI, meta=(DisplayName="Add Chunk Override"), Config=Engine)
class UGameFeatureAction_AddChunkOverride final : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~Start of UGameFeatureAction interface
	virtual void OnGameFeatureRegistering() override;
	virtual void OnGameFeatureUnregistering() override;
	//~End of UGameFeatureAction interface

	DECLARE_DELEGATE_RetVal_OneParam(bool, FShouldAddChunkOverride, const UGameFeatureData*);
	/**
	 * Optionally bound delegate to determine when to add the chunk override.
	 * When bound this will be checked before attempting to add the chunk override.
	 * Bound delegates should return true if the GameFeatureData should have a chunk id overriden; otherwise, false.
	 */
	GAMEFEATURES_API static FShouldAddChunkOverride ShouldAddChunkOverride;

#if WITH_EDITOR
	/**
	 * Given the package name will check if this is a package from a GFP that we want to assign a specific chunk to.
	 * returns the override chunk for this package if one is set.
	 * 
	 * Should be used in combination with overriding UAssetManager::GetPackageChunkIds so that you are able to reassign a startup package.
	 * This can be necessary to reassign startup packages such as the GameFeatureData asset.
	 */
	GAMEFEATURES_API static TOptional<int32> GetChunkForPackage(const FString& PackageName);

	GAMEFEATURES_API static TArray<FString> GetPluginNameFromChunkID(int32 ChunkID);

	/** UObject overrides */
	virtual void PostRename(UObject* OldOuter, const FName OldName) override;
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
	/** ~UObject overrides */

	bool ShouldOverrideChunk() const { return bShouldOverrideChunk; }

	int32 GetChunkID() const { return ChunkId; };

	GAMEFEATURES_API static int32 GetLowestAllowedChunkId();
#endif // WITH_EDITOR
	
private:
#if WITH_EDITORONLY_DATA
	/**
	 * Should this GFP have their packages cooked into the specified ChunkId
	 */
	UPROPERTY(EditAnywhere, Category = "Asset Management")
	bool bShouldOverrideChunk = false;

	/**
	 * What ChunkId to place the packages inside for this particular GFP.
	 */
	UPROPERTY(EditAnywhere, Category = "Asset Management", meta=(EditCondition="bShouldOverrideChunk"))
	int32 ChunkId = -1;

	/**
	 * What Chunk we are parented to.
	 * This is used by the ChunkDependencyInfo for when mutiple chunk overrides might conflict requiring assets to be pulled into a lower chunk
	 */
	UPROPERTY(EditAnywhere, Category = "Asset Management", meta=(EditCondition="bShouldOverrideChunk"))
	int32 ParentChunk = 10;

	/**
	 * Config defined value for what is the smallest chunk index the autogeneration code can generate.
	 * If autogeneration produces a chunk index lower than this value users will need to manually define the chunk index this GFP will cook into.
	 */
	UPROPERTY(config)
	int32 LowestAllowedChunkIndexForAutoGeneration = INDEX_NONE;
#endif

	void AddChunkIdOverride();
	void RemoveChunkIdOverride();

#if WITH_EDITOR
	/**
	 * Attempts to generate a unique int32 id for the given plugin based on the name of the plugin.
	 * returns -1 if a unique name couldn't be generated with consideration to other plugins that have an override id.
	 */
	int32 GenerateUniqueChunkId() const;
#endif // WITH_EDITOR
};

GameFeatureAction_AddComponents.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Engine/EngineBaseTypes.h"
#include "GameFeatureAction.h"
#include "GameFeaturesSubsystem.h"
#include "UObject/ObjectKey.h"

#include "GameFeatureAction_AddComponents.generated.h"

#define UE_API GAMEFEATURES_API

class AActor;
class UActorComponent;
class UGameFrameworkComponentManager;
class UGameInstance;
struct FComponentRequestHandle;
struct FWorldContext;

enum class EGameFrameworkAddComponentFlags : uint8;

// Description of a component to add to a type of actor when this game feature is enabled
// (the actor class must be game feature aware, it does not happen magically)
//@TODO: Write more documentation here about how to make an actor game feature / modular gameplay aware
USTRUCT()
struct FGameFeatureComponentEntry
{
	GENERATED_BODY()

	UE_API FGameFeatureComponentEntry();

	// The base actor class to add a component to
	UPROPERTY(EditAnywhere, Category="Components", meta=(AllowAbstract="True"))
	TSoftClassPtr<AActor> ActorClass;

	// The component class to add to the specified type of actor
	UPROPERTY(EditAnywhere, Category="Components")
	TSoftClassPtr<UActorComponent> ComponentClass;
	
	// Should this component be added for clients
	UPROPERTY(EditAnywhere, Category="Components")
	uint8 bClientComponent:1;

	// Should this component be added on servers
	UPROPERTY(EditAnywhere, Category="Components")
	uint8 bServerComponent:1;

	// Observe these rules when adding the component, if any
	UPROPERTY(EditAnywhere, Category = "Components", meta = (Bitmask, BitmaskEnum = "/Script/ModularGameplay.EGameFrameworkAddComponentFlags"))
	uint8 AdditionFlags;
};	

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddComponents

/**
 * Adds actor<->component spawn requests to the component manager
 *
 * @see UGameFrameworkComponentManager
 */
UCLASS(MinimalAPI, meta = (DisplayName = "Add Components"))
class UGameFeatureAction_AddComponents final : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~UGameFeatureAction interface
	virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
#if WITH_EDITORONLY_DATA
	virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) override;
#endif
	//~End of UGameFeatureAction interface

	//~UObject interface
#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
	//~End of UObject interface

	/** List of components to add to gameplay actors when this game feature is enabled */
	UPROPERTY(EditAnywhere, Category="Components", meta=(TitleProperty="{ActorClass} -> {ComponentClass}"))
	TArray<FGameFeatureComponentEntry> ComponentList;

private:
	struct FContextHandles
	{
		FDelegateHandle GameInstanceStartHandle;
		TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequestHandles;
	};

	struct FGameInstanceData
	{
		// NetMode of the current world, used to determine if client/server components should be requested
		ENetMode WorldNetMode = ENetMode::NM_MAX;

		// May contain null entries, this array is parallel to ComponentList so that client/server components
		// can be added or removed in-place when the world changes
		TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequestHandles;
	};

	struct FActivationContextData
	{
		// Map of GameInstance names to component data for it's world
		TMap<FObjectKey, FGameInstanceData> GameInstanceDataMap;
	};

	void AddToWorld(const FWorldContext& WorldContext, FContextHandles& Handles);

	void HandleGameInstanceStart(UGameInstance* GameInstance, FGameFeatureStateChangeContext ChangeContext);

	void HandleGameInstanceStart_NewWorldTracking(UGameInstance* GameInstance);
	void HandleGameInstanceWorldChanged(UGameInstance* GameInstance, UWorld* OldWorld, UWorld* NewWorld);
	void AddGameInstanceForActivation(TNotNull<UGameInstance*> GameInstance, FActivationContextData& ActivationContextData);
	void UpdateComponentsOnManager(TNotNull<UWorld*> World, TNotNull<UGameFrameworkComponentManager*> Manager, FGameInstanceData& ComponentData);
	TSharedPtr<FComponentRequestHandle> AddComponentRequest(TNotNull<UWorld*> World, TNotNull<UGameFrameworkComponentManager*> Manager, const FGameFeatureComponentEntry& Entry);

	TMap<FGameFeatureStateChangeContext, FContextHandles> ContextHandles;

	FDelegateHandle GameInstanceStartHandle;
	FDelegateHandle GameInstanceWorldChangedHandle;
	TMap<FGameFeatureStateChangeContext, FActivationContextData> ActivationContextDataMap;
	
};

#undef UE_API

GameFeatureAction_AddWPContent.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"

#include "GameFeatureAction_AddWPContent.generated.h"

#define UE_API GAMEFEATURES_API

class UGameFeatureData;
class IPlugin;
class FContentBundleClient;
class UContentBundleDescriptor;

/**
 *
 */
UCLASS(MinimalAPI, meta = (DisplayName = "Add World Partition Content (Content Bundle)"))
class UGameFeatureAction_AddWPContent : public UGameFeatureAction
{
	GENERATED_UCLASS_BODY()

public:
	//~ Begin UGameFeatureAction interface
	UE_API virtual void OnGameFeatureRegistering() override;
	UE_API virtual void OnGameFeatureUnregistering() override;
	UE_API virtual void OnGameFeatureActivating() override;
	UE_API virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~ End UGameFeatureAction interface

	const UContentBundleDescriptor* GetContentBundleDescriptor() const { return ContentBundleDescriptor; }

private:
	UPROPERTY(VisibleAnywhere, Category = ContentBundle)
	TObjectPtr<UContentBundleDescriptor> ContentBundleDescriptor;

	TSharedPtr<FContentBundleClient> ContentBundleClient;
};

#undef UE_API

GameFeatureAction_AddWorldPartitionContent.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"
#include "GameFeatureAction_AddWorldPartitionContent.generated.h"

#define UE_API GAMEFEATURES_API

class UExternalDataLayerAsset;

/**
 *
 */
UCLASS(MinimalAPI, meta = (DisplayName = "Add World Partition Content"))
class UGameFeatureAction_AddWorldPartitionContent : public UGameFeatureAction
{
	GENERATED_UCLASS_BODY()

public:
	//~ Begin UGameFeatureAction interface
	UE_API virtual void OnGameFeatureRegistering() override;
	UE_API virtual void OnGameFeatureUnregistering() override;
	UE_API virtual void OnGameFeatureActivating() override;
	UE_API virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~ End UGameFeatureAction interface

#if WITH_EDITOR
	//~ Begin UObject interface
	UE_API virtual void PreEditChange(FProperty* PropertyThatWillChange) override;
	UE_API virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
	UE_API virtual void PreEditUndo() override;
	UE_API virtual void PostEditUndo() override;
	//~ End UObject interface

	const TObjectPtr<const UExternalDataLayerAsset>& GetExternalDataLayerAsset() const { return ExternalDataLayerAsset; }
#endif

private:
#if WITH_EDITOR
	UE_API void OnExternalDataLayerAssetChanged(const UExternalDataLayerAsset* OldAsset, const UExternalDataLayerAsset* NewAsset);
#endif

	/** Used to detect changes on the Data Layer Asset in the action. */
	TWeakObjectPtr<const UExternalDataLayerAsset> PreEditChangeExternalDataLayerAsset;
	TWeakObjectPtr<const UExternalDataLayerAsset> PreEditUndoExternalDataLayerAsset;

	/** External Data Layer used by this action. */
	UPROPERTY(EditAnywhere, Category = DataLayer)
	TObjectPtr<const UExternalDataLayerAsset> ExternalDataLayerAsset;

#if WITH_EDITORONLY_DATA
	/** Only used when converting from UGameFeatureAction_AddWPContent */
	UPROPERTY()
	FGuid ConvertedContentBundleGuid;
#endif

	friend class UGameFeatureActionConvertContentBundleWorldPartitionBuilder;
};

#undef UE_API

GameFeatureAction_AudioActionBase.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "AudioDeviceHandle.h"
#include "GameFeatureAction.h"
#include "GameFeaturesSubsystem.h"

#include "GameFeatureAction_AudioActionBase.generated.h"

#define UE_API GAMEFEATURES_API

/**
 * Base class for GameFeatureActions that affect the audio engine
 */
UCLASS(MinimalAPI, Abstract)
class UGameFeatureAction_AudioActionBase : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~ Begin of UGameFeatureAction interface
	UE_API virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
	UE_API virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~ End of UGameFeatureAction interface

protected:
	/** Handle to delegate callbacks */
	FDelegateHandle DeviceCreatedHandle;
	FDelegateHandle DeviceDestroyedHandle;

	UE_API void OnDeviceCreated(Audio::FDeviceId InDeviceId);
	UE_API void OnDeviceDestroyed(Audio::FDeviceId InDeviceId);

	virtual void AddToDevice(const FAudioDeviceHandle& AudioDeviceHandle) {}
	virtual void RemoveFromDevice(const FAudioDeviceHandle& AudioDeviceHandle) {}
};

#undef UE_API

GameFeatureAction_DataRegistry.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"
#include "GameFeatureAction_DataRegistry.generated.h"

class UDataRegistry;

/** Specifies a list of Data Registries to load and initialize with this feature */
UCLASS(MinimalAPI, meta = (DisplayName = "Add Data Registry"))
class UGameFeatureAction_DataRegistry : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	virtual void OnGameFeatureRegistering() override;
	virtual void OnGameFeatureUnregistering() override;
	virtual void OnGameFeatureActivating() override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;

	/** If true, we should load the registry at registration time instead of activation time */
	virtual bool ShouldPreloadAtRegistration();

#if WITH_EDITORONLY_DATA
	virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) override;
#endif

	//~UObject interface
#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
	//~End of UObject interface

private:
	/** List of registry assets to load and initialize */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	TArray<TSoftObjectPtr<UDataRegistry> > RegistriesToAdd;

	/** If true, this will preload the registries when the feature is registered in the editor to support the editor pickers */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	bool bPreloadInEditor;

	/** If true, this will preload the registries when the feature is registered whilst a commandlet is running */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	bool bPreloadInCommandlets;
};

GameFeatureAction_DataRegistrySource.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "GameFeatureAction.h"
#include "GameFeatureAction_DataRegistrySource.generated.h"

class UCurveTable;
class UDataTable;

/** Defines which source assets to add and conditions for adding */
USTRUCT()
struct FDataRegistrySourceToAdd
{
	GENERATED_BODY()

	FDataRegistrySourceToAdd()
		: AssetPriority(0)
		, bClientSource(false)
		, bServerSource(false)
	{}

	/** Name of the registry to add to */
	UPROPERTY(EditAnywhere, Category="Registry Data")
	FName RegistryToAddTo;

	/** Priority to use when adding to the registry.  Higher priorities are searched first */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	int32 AssetPriority;

	/** Should this component be added for clients */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	uint8 bClientSource : 1;

	/** Should this component be added on servers */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	uint8 bServerSource : 1;

	/** Link to the data table to add to the registry */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	TSoftObjectPtr<UDataTable> DataTableToAdd;

	/** Link to the curve table to add to the registry */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	TSoftObjectPtr<UCurveTable> CurveTableToAdd;
};

/** Specifies a list of source assets to add to Data Registries when this feature is activated */
UCLASS(MinimalAPI, meta = (DisplayName = "Add Data Registry Source"))
class UGameFeatureAction_DataRegistrySource : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	virtual void OnGameFeatureRegistering() override;
	virtual void OnGameFeatureUnregistering() override;
	virtual void OnGameFeatureActivating() override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;

	/** If true, we should load the sources at registration time instead of activation time */
	virtual bool ShouldPreloadAtRegistration();

#if WITH_EDITORONLY_DATA
	virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) override;
#endif

	//~UObject interface
#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
	//~End of UObject interface

#if WITH_EDITOR
	/** Used by an editor tool to programmatically register a new source */
	GAMEFEATURES_API void AddSource(const FDataRegistrySourceToAdd& NewSource);
#endif

private:
	/** List of sources to add when this feature is activated */
	UPROPERTY(EditAnywhere, Category = "Registry Data", meta=(TitleProperty="RegistryToAddTo"))
	TArray<FDataRegistrySourceToAdd> SourcesToAdd;

	/** If true, this will preload the sources when the feature is registered in the editor to support the editor pickers */
	UPROPERTY(EditAnywhere, Category = "Registry Data")
	bool bPreloadInEditor;
};

GameFeatureData.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Engine/DataAsset.h"
#include "GameFeatureAction.h"
#include "Engine/ExternalObjectAndActorDependencyGatherer.h" // For IExternalAssetPathsProvider

#include "GameFeatureData.generated.h"

#define UE_API GAMEFEATURES_API

class FConfigFile;
struct FPrimaryAssetTypeInfo;
struct FExternalDataLayerUID;

struct FAssetData;

#if WITH_EDITOR

class FPathTree;
class FGameFeatureDataExternalAssetsPathCache : public IExternalAssetPathsProvider
{
	TMultiMap<FName, FName> PerLevelAssetDirectories;
	TMap<FName, FString> GameFeatureDataAssetsToMountPoint;
	TSet<FName> AllLevels;
	bool bCacheIsUpToDate = false;
	FDelegateHandle OnPathAddedDelegateHandle;

public:
		
	FGameFeatureDataExternalAssetsPathCache();
	virtual ~FGameFeatureDataExternalAssetsPathCache();

	void OnPathsAdded(TConstArrayView<FStringView>);
	void UpdateCache(const FUpdateCacheContext& Context) override;
	TArray<FName> GetPathsForPackage(FName LevelPath) override;
};
#endif

/** Data related to a game feature, a collection of code and content that adds a separable discrete feature to the game */
UCLASS(MinimalAPI)
class UGameFeatureData : public UPrimaryDataAsset
{
	GENERATED_BODY()

public:
	/** Method to get where the primary assets should scanned from in the plugin hierarchy */
	virtual const TArray<FPrimaryAssetTypeInfo>& GetPrimaryAssetTypesToScan() const { return PrimaryAssetTypesToScan; }

#if WITH_EDITOR
	virtual TArray<FPrimaryAssetTypeInfo>& GetPrimaryAssetTypesToScan() { return PrimaryAssetTypesToScan; }
	UE_API virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
	static UE_API void GetDependencyDirectoriesFromAssetData(const FAssetData& AssetData, TArray<FString>& OutDependencyDirectories);

	//~Begin deprecation
	UE_DEPRECATED(5.4, "Implement the version that takes FAssetRegistryTagsContext instead.")
	UE_API virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const override;
	UE_DEPRECATED(5.4, "GetContentBundleGuidsAssetRegistryTag is deprecated")
	static FName GetContentBundleGuidsAssetRegistryTag() { return NAME_None; }
	UE_DEPRECATED(5.4, "GetContentBundleGuidsFromAsset is deprecated, use GetDependencyDirectoriesFromAssetData")
	static void GetContentBundleGuidsFromAsset(const FAssetData& Asset, TArray<FGuid>& OutContentBundleGuids) {}
	//~End deprecation
#endif //if WITH_EDITOR

	/** Method to process the base ini file for the plugin during loading */
	static UE_API void InitializeBasePluginIniFile(const FString& PluginInstalledFilename);

	/** Method to process ini files for the plugin during activation */
	static UE_API void InitializeHierarchicalPluginIniFiles(const FString& PluginInstalledFilename);
	static UE_API void InitializeHierarchicalPluginIniFiles(const TArrayView<FString>& PluginInstalledFilenames);

	UFUNCTION(BlueprintCallable, Category = "GameFeature")
	static UE_API void GetPluginName(const UGameFeatureData* GFD, FString& PluginName);

	UE_API void GetPluginName(FString& PluginName) const;

	/** Returns whether the game feature plugin is registered or not. */
	UE_API bool IsGameFeaturePluginRegistered(bool bCheckForRegistering = false) const;

	/** Returns whether the game feature plugin is active or not. */
	UE_API bool IsGameFeaturePluginActive(bool bCheckForActivating = false) const;

	/**
	 * Returns the install bundle name if one exists for this plugin.
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	static UE_API FString GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);

	/**
	 * Returns the optional install bundle name if one exists for this plugin.
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	static UE_API FString GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);

public:
	//~UPrimaryDataAsset interface
#if WITH_EDITORONLY_DATA
	UE_API virtual void UpdateAssetBundleData() override;
#endif
	//~End of UPrimaryDataAsset interface

	//~UObject interface
#if WITH_EDITOR
	UE_API virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
	//~End of UObject interface

	const TArray<UGameFeatureAction*>& GetActions() const { return Actions; }

#if WITH_EDITOR
	TArray<TObjectPtr<UGameFeatureAction>>& GetMutableActionsInEditor() { return Actions; }
#endif

private:
#if WITH_EDITOR
	UFUNCTION()
	UE_API TArray<UClass*> GetDisallowedActions() const;
#endif

	/** Internal helper function to reload config data on objects as a result of a plugin INI being loaded */
	static UE_API void ReloadConfigs(FConfigFile& PluginConfig);

protected:

	/** List of actions to perform as this game feature is loaded/activated/deactivated/unloaded */
	UPROPERTY(EditDefaultsOnly, Instanced, Category="Game Feature | Actions", meta = (GetDisallowedClasses = "GetDisallowedActions"))
	TArray<TObjectPtr<UGameFeatureAction>> Actions;

	/** List of asset types to scan at startup */
	UPROPERTY(EditAnywhere, Category="Game Feature | Asset Manager", meta=(TitleProperty="PrimaryAssetType"))
	TArray<FPrimaryAssetTypeInfo> PrimaryAssetTypesToScan;
};

#undef UE_API

GameFeatureOptionalContentInstaller.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "BundlePrereqCombinedStatusHelper.h"
#include "GameFeatureStateChangeObserver.h"
#include "HAL/IConsoleManager.h"
#include "InstallBundleManagerInterface.h"
#include "GameFeatureOptionalContentInstaller.generated.h"

namespace UE::GameFeatures
{
	struct FResult;
}

enum class EInstallBundleReleaseRequestFlags : uint32;

/** 
 * Utilty class to install GFP optional paks (usually containing optional mips) in sync with GFP content installs.
 * NOTE: This only currently supports LRU cached install bundles. It would need UI callbacks and additional support 
 * for free space checks and progress tracking to fully support non-LRU GFPs.
 */
UCLASS(MinimalAPI)
class UGameFeatureOptionalContentInstaller : public UObject, public IGameFeatureStateChangeObserver
{
	GENERATED_BODY()

public:
	static GAMEFEATURES_API TMulticastDelegate<void(const FString& PluginName, const UE::GameFeatures::FResult&)> OnOptionalContentInstalled;
	static GAMEFEATURES_API TMulticastDelegate<void()> OnOptionalContentInstallStarted;
	static GAMEFEATURES_API TMulticastDelegate<void(const bool bInstallSuccessful)> OnOptionalContentInstallFinished;

public:
	virtual void BeginDestroy() override;

	void GAMEFEATURES_API Init(
		TUniqueFunction<TArray<FName>(FString)> GetOptionalBundlePredicate,
		TUniqueFunction<TArray<FName>(FString)> GetOptionalKeepBundlePredicate = [](auto) { return TArray<FName>{}; });

	void GAMEFEATURES_API Enable(bool bEnable);

	void GAMEFEATURES_API UninstallContent();

	void GAMEFEATURES_API EnableCellularDownloading(bool bEnable);
	
	bool GAMEFEATURES_API HasOngoingInstalls() const;

	/**
	 * Returns the total progress (between 0 and 1) of all the optional bundles currently being installed.
	 * Returns 0 while the progress tracker is starting which happens the first time it is called while bundles are being installed.
	 */
	float GAMEFEATURES_API GetAllInstallsProgress();

private:
	bool UpdateContent(const FString& PluginName, bool bIsPredownload);

	void OnContentInstalled(FInstallBundleRequestResultInfo InResult, FString PluginName);

	void ReleaseContent(const FString& PluginName, EInstallBundleReleaseRequestFlags Flags = EInstallBundleReleaseRequestFlags::None);

	void OnEnabled();
	void OnDisabled();

	bool IsEnabled() const;

	void OnCVarsChanged();

	void StartTotalProgressTracker();

	// IGameFeatureStateChangeObserver Interface
	virtual void OnGameFeaturePredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) override;
	virtual void OnGameFeatureDownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) override;
    virtual void OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL) override;
	virtual void OnGameFeatureReleasing(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) override;

private:
	struct FGFPInstall
	{
		FDelegateHandle CallbackHandle;
		TArray<FName> BundlesEnqueued;
		bool bIsPredownload = false;
	};

private:
	TUniqueFunction<TArray<FName>(FString)> GetOptionalBundlePredicate;
	TUniqueFunction<TArray<FName>(FString)> GetOptionalKeepBundlePredicate;
	TSharedPtr<IInstallBundleManager> BundleManager;
	TSet<FString> RelevantGFPs;
	TMap<FString, FGFPInstall> ActiveGFPInstalls;
	TOptional<FInstallBundleCombinedProgressTracker> TotalProgressTracker;
	static const FName GFOContentRequestName;

	/** Delegate handle for a console variable sink */
	FConsoleVariableSinkHandle CVarSinkHandle;

	bool bEnabled = false;
	bool bEnabledCVar = false;
	bool bAllowCellDownload = false;
};

GameFeaturePluginOperationResult.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Internationalization/Text.h"
#include "Templates/ValueOrError.h"

#define UE_API GAMEFEATURES_API

enum class EInstallBundleResult : uint32;
enum class EInstallBundleReleaseResult : uint32;

namespace UE::GameFeatures
{
	//Type used to determine if our FResult is actually an error or success (and hold the error code)
	using FErrorCodeType = TValueOrError<void, FString>;

	/** Struct wrapper on a combined TValueOrError ErrorCode and FText OptionalErrorText that can provide an FString error code and
		if set more detailed FText OptionalErrorText information for the results of a GameFeaturesPlugin operation. */
	struct FResult
	{
		/** Error Code representing the error that occurred */
		FErrorCodeType ErrorCode;

		/** Optional Localized error description to bubble up to the user if one was generated */
		FText OptionalErrorText;

		/** Quick functions that just pass through to the ErrorCode */
		bool HasValue() const { return ErrorCode.HasValue(); }
		bool HasError() const { return ErrorCode.HasError(); }
		FString GetError() const { return ErrorCode.GetError(); }
		FString StealError() { return ErrorCode.StealError(); }

		UE_API FResult(FErrorCodeType ErrorCodeIn, FText ErrorTextIn);

		/** Explicit constructor for times we want to create an FResult directly from an FErrorCodeType and not through MakeValue or MakeError.
			Explicit to avoid any confusion with the following templated constructors. */
		UE_API explicit FResult(FErrorCodeType ErrorCodeIn);

		/** Template Conversion Constructor to allow us to initialize from TValueOrError MakeValue
			This is needed because of how TValueOrError implements MakeValue through the same templated approach with TValueOrError_ValueProxy. */
		template <typename... ArgTypes>
		FResult(TValueOrError_ValueProxy<ArgTypes...>&& ValueProxyIn)
			: ErrorCode(MoveTemp(ValueProxyIn))
			, OptionalErrorText()
		{
		}

		/** Template Conversion Constructor to allow us to initialize from TValueOrError MakeError 
			This is needed because of how TValueOrError implements MakeError through the same templated approach with TValueOrError_ErrorProxy. */
		template <typename... ArgTypes>
		FResult(TValueOrError_ErrorProxy<ArgTypes...>&& ErrorProxyIn)
			: ErrorCode(MoveTemp(ErrorProxyIn))
			, OptionalErrorText()
		{
		}

	private:
		//No default constructor as we want to force you to always specify at the minimum 
		//if the FResult is an error or not through a supplied FErrorCodeType
		FResult() = delete;
	};

	GAMEFEATURES_API FString ToString(const FResult& Result);

	namespace CommonErrorCodes
	{
		const FText& GetErrorTextForBundleResult(EInstallBundleResult ErrorResult);
		const FText& GetErrorTextForReleaseResult(EInstallBundleReleaseResult ErrorResult);

		const FText& GetGenericFatalError();
		const FText& GetGenericConnectionError();
		const FText& GetGenericMountError();
		const FText& GetGenericReleaseResult();
	}
}

#undef UE_API

GameFeatureStateChangeObserver.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "UObject/Interface.h"
#include "GameFeatureStateChangeObserver.generated.h"

class UGameFeatureData;
struct FGameFeaturePluginIdentifier;
struct FGameFeaturePreMountingContext;
struct FGameFeaturePostMountingContext;
struct FGameFeatureDeactivatingContext;
struct FGameFeaturePauseStateChangeContext;

UINTERFACE(MinimalAPI)
class UGameFeatureStateChangeObserver : public UInterface
{
	GENERATED_BODY()
};

/**
 * This class is meant to be overridden in your game to handle game-specific reactions to game feature plugins
 * being mounted or unmounted
 *
 * Generally you should prefer to use UGameFeatureAction instances on your game feature data asset instead of
 * this, especially if any data is involved
 *
 * If you do use these, create them in your UGameFeaturesProjectPolicies subclass and register them via
 * AddObserver / RemoveObserver on UGameFeaturesSubsystem
 */
class IGameFeatureStateChangeObserver
{
	GENERATED_BODY()

public:

	// Invoked when going from the UnknownStatus state to the CheckingStatus state
	virtual void OnGameFeatureCheckingStatus(const FString& PluginURL) {}

	// Invoked prior to terminating a game feature plugin
	virtual void OnGameFeatureTerminating(const FString& PluginURL) {}

	// Invoked when content begins installing via predownload
	virtual void OnGameFeaturePredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) {}

	// Invoked when pre-downloading content has finished.
	virtual void OnGameFeaturePostPredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) {}

	// Invoked when content begins installing
	virtual void OnGameFeatureDownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) {}

	// Invoked when content is released (the point it at which it is safe to remove it)
	virtual void OnGameFeatureReleasing(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier) {}

	// Invoked prior to mounting a plugin (but after its install bundles become available, if any)
	virtual void OnGameFeaturePreMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePreMountingContext& Context) {}

	// Invoked at the end of the plugin mounting phase (whether it was successfully mounted or not)
	virtual void OnGameFeaturePostMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePostMountingContext& Context) {}

	// Invoked after a game feature plugin has been registered
	virtual void OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL) {}

	// Invoked prior to unregistering a game feature plugin
	virtual void OnGameFeatureUnregistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FString& PluginURL) {}

	// Invoked in the early stages of the game feature plugin loading phase
	virtual void OnGameFeatureLoading(const UGameFeatureData* GameFeatureData, const FString& PluginURL) {}

	// Invoked after a game feature plugin is unloaded
	virtual void OnGameFeatureUnloading(const UGameFeatureData* GameFeatureData, const FString& PluginURL) {}

	// Invoked prior to activating a game feature plugin
	virtual void OnGameFeatureActivating(const UGameFeatureData* GameFeatureData, const FString& PluginURL) {}

	// Invoked after a game feature plugin is activated
	virtual void OnGameFeatureActivated(const UGameFeatureData* GameFeatureData, const FString& PluginURL) {}

	// Invoked prior to deactivating a game feature plugin
	virtual void OnGameFeatureDeactivating(const UGameFeatureData* GameFeatureData, FGameFeatureDeactivatingContext& Context, const FString& PluginURL) {}

	/** Called whenever a GameFeature State either pauses or resumes work without transitioning out of that state.
		EX: Downloading paused due to a users cellular data settings or the user taking a pause action. We
		may not yet transition to a download error, but want a way to observe this behavior. */
	virtual void OnGameFeaturePauseChange(const FString& PluginURL, const FString& PluginName, FGameFeaturePauseStateChangeContext& Context) {}
};

GameFeatureTypes.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Containers/StringFwd.h"
#include "Misc/EnumClassFlags.h"
#include "Misc/EnumRange.h"

class FString;

/** 
  GFP States 
  Desitination states must be fully ordered, Transistion and Error states should be in between the Destination states they transition to/from.
  See the state chart in GameFeaturePluginStateMachine.h for reference.
*/
#define GAME_FEATURE_PLUGIN_STATE_LIST(XSTATE)	\
	XSTATE(Uninitialized,					NSLOCTEXT("GameFeatures", "UninitializedStateDisplayName", "Uninitialized"))									/* Unset. Not yet been set up. */ \
	XSTATE(Terminal,						NSLOCTEXT("GameFeatures", "TerminalStateDisplayName", "Terminal"))												/* Final State before removal of the state machine. */ \
	XSTATE(UnknownStatus,					NSLOCTEXT("GameFeatures", "UnknownStatusStateDisplayName", "UnknownStatus"))									/* Initialized, but the only thing known is the URL to query status. */ \
	XSTATE(Uninstalled,						NSLOCTEXT("GameFeatures", "UninstalledStateDisplayName", "Uninstalled"))										/* All installed data for this plugin has now been uninstalled from local storage (i.e the hard drive) */ \
	XSTATE(Uninstalling,					NSLOCTEXT("GameFeatures", "UninstallingStateDisplayName", "Uninstalling"))										/* Transition state between StatusKnown -> Terminal for any plugin that can have data that needs to have local data uninstalled. */ \
	XSTATE(ErrorUninstalling,				NSLOCTEXT("GameFeatures", "ErrorUninstallingStateDisplayName", "ErrorUninstalling"))							/* Error state for Uninstalling -> Terminal transition. */  \
	XSTATE(CheckingStatus,					NSLOCTEXT("GameFeatures", "CheckingStatusStateDisplayName", "CheckingStatus"))									/* Transition state UnknownStatus -> StatusKnown. The status is in the process of being queried. */ \
	XSTATE(ErrorCheckingStatus,				NSLOCTEXT("GameFeatures", "ErrorCheckingStatusStateDisplayName", "ErrorCheckingStatus"))						/* Error state for UnknownStatus -> StatusKnown transition. */ \
	XSTATE(ErrorUnavailable,				NSLOCTEXT("GameFeatures", "ErrorUnavailableStateDisplayName", "ErrorUnavailable"))								/* Error state for UnknownStatus -> StatusKnown transition. */ \
	XSTATE(StatusKnown,						NSLOCTEXT("GameFeatures", "StatusKnownStateDisplayName", "StatusKnown"))										/* The plugin's information is known, but no action has taken place yet. */ \
	XSTATE(Releasing,						NSLOCTEXT("GameFeatures", "ReleasingStateDisplayName", "Releasing"))											/* Transition State for Installed -> StatusKnown. Releases local data from any relevant caches. */ \
	XSTATE(ErrorManagingData,				NSLOCTEXT("GameFeatures", "ErrorManagingDataStateDisplayName", "ErrorManagingData"))							/* Error state for Installed -> StatusKnown and StatusKnown -> Installed transitions. */ \
	XSTATE(Downloading,						NSLOCTEXT("GameFeatures", "DownloadingStateDisplayName", "Downloading"))										/* Transition state StatusKnown -> Installed. In the process of adding to local storage. */ \
	XSTATE(Installed,						NSLOCTEXT("GameFeatures", "InstalledStateDisplayName", "Installed"))											/* The plugin is in local storage (i.e. it is on the hard drive) */ \
	XSTATE(ErrorMounting,					NSLOCTEXT("GameFeatures", "ErrorMountingStateDisplayName", "ErrorMounting"))									/* Error state for Installed -> Registered and Registered -> Installed transitions. */ \
	XSTATE(ErrorWaitingForDependencies,		NSLOCTEXT("GameFeatures", "ErrorWaitingForDependenciesStateDisplayName", "ErrorWaitingForDependencies"))		/* Error state for Installed -> Registered and Registered -> Installed transitions. */ \
	XSTATE(ErrorRegistering,				NSLOCTEXT("GameFeatures", "ErrorRegisteringDisplayName", "ErrorRegistering"))									/* Error state for Installed -> Registered and Registered -> Installed transitions. */ \
	XSTATE(WaitingForDependencies,			NSLOCTEXT("GameFeatures", "WaitingForDependenciesStateDisplayName", "WaitingForDependencies"))					/* Transition state Installed -> Registered. In the process of loading code/content for all dependencies into memory. */ \
	XSTATE(AssetDependencyStreamOut,		NSLOCTEXT("GameFeatures", "AssetDependencyStreamOutDisplayName", "AssetDependencyStreamOut"))					/* Transition state Registered -> Installed. In the process of streaming out individual assets from dependencies. */ \
	XSTATE(ErrorAssetDependencyStreaming,	NSLOCTEXT("GameFeatures", "ErrorAssetDependencyStreamingStateDisplayName", "ErrorAssetDependencyStreaming"))	/* Error state for Installed -> Registered and Registered -> Installed transitions. */ \
	XSTATE(AssetDependencyStreaming,		NSLOCTEXT("GameFeatures", "AssetDependencyStreamingDisplayName", "AssetDependencyStreaming"))					/* Transition state Installed -> Registered. In the process of streaming individual assets from dependencies. */ \
	XSTATE(Unmounting,						NSLOCTEXT("GameFeatures", "UnmountingStateDisplayName", "Unmounting"))											/* Transition state Registered -> Installed. The content file(s) (i.e. pak file) for the plugin is unmounting. */ \
	XSTATE(Mounting,						NSLOCTEXT("GameFeatures", "MountingStateDisplayName", "Mounting"))												/* Transition state Installed -> Registered. The content files(s) (i.e. pak file) for the plugin is getting mounted. */ \
	XSTATE(Unregistering,					NSLOCTEXT("GameFeatures", "UnregisteringStateDisplayName", "Unregistering"))									/* Transition state Registered -> Installed. Cleaning up data gathered in Registering. */ \
	XSTATE(Registering,						NSLOCTEXT("GameFeatures", "RegisteringStateDisplayName", "Registering"))										/* Transition state Installed -> Registered. Discovering assets in the plugin, but not loading them, except a few for discovery reasons. */ \
	XSTATE(Registered,						NSLOCTEXT("GameFeatures", "RegisteredStateDisplayName", "Registered"))											/* The assets in the plugin are known, but have not yet been loaded, except a few for discovery reasons. */ \
	XSTATE(ErrorLoading,					NSLOCTEXT("GameFeatures", "ErrorLoadingDisplayName", "ErrorLoading"))											/* Error state for Loading -> Loaded transition */ \
	XSTATE(Unloading,						NSLOCTEXT("GameFeatures", "UnloadingStateDisplayName", "Unloading"))											/* Transition state Loaded -> Registered. In the process of removing code/content from memory. */ \
	XSTATE(Loading,							NSLOCTEXT("GameFeatures", "LoadingStateDisplayName", "Loading"))												/* Transition state Registered -> Loaded. In the process of loading code/content into memory. */ \
	XSTATE(Loaded,							NSLOCTEXT("GameFeatures", "LoadedStateDisplayName", "Loaded"))													/* The plugin is loaded into memory, but not registered with game systems and active. */ \
	XSTATE(ErrorActivatingDependencies,		NSLOCTEXT("GameFeatures", "ErrorActivatingDependenciesStateDisplayName", "ErrorActivatingDependencies"))		/* Error state for Registered -> Active transition. */ \
	XSTATE(ActivatingDependencies,			NSLOCTEXT("GameFeatures", "ActivatingDependenciesStateDisplayName", "ActivatingDependencies"))					/* Transition state Registered -> Active. In the process of selectively activating dependencies.*/ \
	XSTATE(Deactivating,					NSLOCTEXT("GameFeatures", "DeactivatingStateDisplayName", "Deactivating"))										/* Transition state Active -> Loaded. Currently unregistering with game systems. */ \
	XSTATE(ErrorDeactivatingDependencies,	NSLOCTEXT("GameFeatures", "ErrorDeactivatingDependenciesStateDisplayName", "DeactivatingDependencies"))			/* Error state for Active -> Loaded transition. */ \
	XSTATE(DeactivatingDependencies,		NSLOCTEXT("GameFeatures", "DeactivatingDependenciesStateDisplayName", "DeactivatingDependencies"))				/* Transition state Active -> Loaded. In the process of selectively deactivating dependencies.*/ \
	XSTATE(Activating,						NSLOCTEXT("GameFeatures", "ActivatingStateDisplayName", "Activating"))											/* Transition state Loaded -> Active. Currently registering plugin code/content with game systems. */ \
	XSTATE(Active,							NSLOCTEXT("GameFeatures", "ActiveStateDisplayName", "Active"))													/* Plugin is fully loaded and active. It is affecting the game.  */ 

#define GAME_FEATURE_PLUGIN_STATE_ENUM(inEnum, inText) inEnum,
namespace GameFeaturePluginStatePrivate
{
	enum EGameFeaturePluginState : uint8
	{
		GAME_FEATURE_PLUGIN_STATE_LIST(GAME_FEATURE_PLUGIN_STATE_ENUM)
		MAX
	};
}
using EGameFeaturePluginState = GameFeaturePluginStatePrivate::EGameFeaturePluginState;
#undef GAME_FEATURE_PLUGIN_STATE_ENUM

namespace UE::GameFeatures
{
	GAMEFEATURES_API FString ToString(EGameFeaturePluginState InType);
}

/** GFP Protocols */
#define GAME_FEATURE_PLUGIN_PROTOCOL_LIST(XPROTO)	\
	XPROTO(File,			TEXT("file:"))			\
	XPROTO(InstallBundle,	TEXT("installbundle:"))	\
	XPROTO(Unknown,			TEXT(""))

#define GAME_FEATURE_PLUGIN_PROTOCOL_ENUM(inEnum, inString) inEnum,
enum class EGameFeaturePluginProtocol : uint8
{
	GAME_FEATURE_PLUGIN_PROTOCOL_LIST(GAME_FEATURE_PLUGIN_PROTOCOL_ENUM)
	Count
};
#undef GAME_FEATURE_PLUGIN_PROTOCOL_ENUM

ENUM_RANGE_BY_COUNT(EGameFeaturePluginProtocol, EGameFeaturePluginProtocol::Count);

/** GFP URL Options */
#define GAME_FEATURE_PLUGIN_URL_OPTIONS_LIST(XOPTION)	\
	XOPTION(Bundles,	(1 << 0))

#define GAME_FEATURE_PLUGIN_URL_OPTIONS_ENUM(inEnum, inVal) inEnum = inVal,
#define GAME_FEATURE_PLUGIN_URL_OPTIONS_ALL(inEnum, inVal) | inEnum
enum class EGameFeatureURLOptions : uint8
{
	None,
	GAME_FEATURE_PLUGIN_URL_OPTIONS_LIST(GAME_FEATURE_PLUGIN_URL_OPTIONS_ENUM)
	All = None GAME_FEATURE_PLUGIN_URL_OPTIONS_LIST(GAME_FEATURE_PLUGIN_URL_OPTIONS_ALL)
};
#undef GAME_FEATURE_PLUGIN_URL_OPTIONS_ALL
#undef GAME_FEATURE_PLUGIN_URL_OPTIONS_ENUM

ENUM_CLASS_FLAGS(EGameFeatureURLOptions);

const TCHAR* LexToString(EGameFeatureURLOptions);
void LexFromString(EGameFeatureURLOptions& ValueOut, const FStringView& StringIn);

GameFeatureTypesFwd.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreTypes.h"

class FString;
enum class EGameFeaturePluginProtocol : uint8;

namespace GameFeaturePluginStatePrivate
{
	enum EGameFeaturePluginState : uint8;
}
using EGameFeaturePluginState = GameFeaturePluginStatePrivate::EGameFeaturePluginState;

namespace UE::GameFeatures
{
	GAMEFEATURES_API FString ToString(EGameFeaturePluginState InType);
}

enum class EGameFeatureURLOptions : uint8;

GameFeatureVersePathMapperCommandlet.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Commandlets/Commandlet.h"
#include "Misc/Crc.h"
#include "GameFeatureVersePathMapperCommandlet.generated.h"

class FAssetRegistryState;

namespace GameFeatureVersePathMapper
{
	/** Case sensitive hashing function for TMap */
	template <typename ValueType>
	struct FCaseSensitiveKeyMapFuncs : BaseKeyFuncs<ValueType, FString, /*bInAllowDuplicateKeys*/false>
	{
		static inline const FString& GetSetKey(const TPair<FString, ValueType>& Element)
		{
			return Element.Key;
		}
		static inline bool Matches(const FString& A, const FString& B)
		{
			return A.Equals(B, ESearchCase::CaseSensitive);
		}
		static inline uint32 GetKeyHash(const FString& Key)
		{
			return FCrc::StrCrc32(*Key);
		}
	};

	struct FGameFeaturePluginInfo
	{
		FString GfpUri;
		TArray<FName> Dependencies;
	};

	struct FGameFeatureVersePathLookup
	{
		TMap<FString, FName, FDefaultSetAllocator, GameFeatureVersePathMapper::FCaseSensitiveKeyMapFuncs<FName>> VersePathToGfpMap;
		TMap<FName, FGameFeaturePluginInfo> GfpInfoMap;
	};

	struct FDLCInfo
	{
		FString DLCName;
		FString InstallBundleName;
		TArray<FString> Plugins;
	};

	GAMEFEATURES_API TArray<FDLCInfo> FindGFPToDLC(const ITargetPlatform* TargetPlatform);

	GAMEFEATURES_API FString GetVerseAppDomain();
	GAMEFEATURES_API FString GetAltVerseAppDomain();

	/**
	 * Finds plugin dependencies and returns them in dependency order (reverse topological sort order)
	 */
	class FDepthFirstGameFeatureSorter
	{
	private:
		enum class EVisitState : uint8
		{
			None,
			Visiting,
			Visited
		};

		const TMap<FName, FGameFeaturePluginInfo>& GfpInfoMap;
		TMap<FName, EVisitState> VisitedPlugins;

		bool bIncludeVirtualNodes = false;

		bool Visit(FName Plugin, TFunctionRef<void(FName, const FString&)> AddOutput);

	public:
		/**
		 * Constructor
		 * @Param InGfpInfoMap Map containing plugin dependencies (FDepthFirstGameFeatureSorter points to this map, it is not copied)
		 */
		FDepthFirstGameFeatureSorter(const TMap<FName, FGameFeaturePluginInfo>& InGfpInfoMap, bool bInIncludeVirtualNodes = false) 
			: GfpInfoMap(InGfpInfoMap) 
			, bIncludeVirtualNodes(bInIncludeVirtualNodes)
		{}

		// @TODO: Allow passing a callback to fetch dependencies?

		/**
		 * Find and sort all dependencies
		 * @Param GetNextRootPlugin function that iterates plugins in the root set, returning None after the final plugin in the set.
		 * @Param AddOutput callback to receive roots and dependencies, called in dependency order
		 * @Return false if there is an error or a cyclic dependency is discovered
		 */
		GAMEFEATURES_API bool Sort(TFunctionRef<FName()> GetNextRootPlugin, TFunctionRef<void(FName, const FString&)> AddOutput);

		/**
		 * Find and sort all dependencies
		 * @Param RootPlugins set of root plugins
		 * @Param AddOutput callback to receive roots and dependencies, called in dependency order
		 * @Return false if there is an error or a cyclic dependency is discovered
		 */
		GAMEFEATURES_API bool Sort(TConstArrayView<FName> RootPlugins, TFunctionRef<void(FName, const FString&)> AddOutput);
		bool Sort(FName RootPlugin, TFunctionRef<void(FName, const FString&)> AddOutput) { return Sort(MakeArrayView(&RootPlugin, 1), AddOutput); }

		/**
		 * Find and sort all dependencies
		 * @Param RootPlugins set of root plugins
		 * @Param OutPlugins roots and dependencies in dependency order
		 * @Return false if there is an error or a cyclic dependency is discovered
		 */
		GAMEFEATURES_API bool Sort(TConstArrayView<FName> RootPlugins, TArray<FName>& OutPlugins);
		bool Sort(FName RootPlugin, TArray<FName>& OutPlugins) { return Sort(MakeArrayView(&RootPlugin, 1), OutPlugins); }
	};

	enum class EBuildLookupOptions : uint32
	{
		None = 0,
		OnlyBaseBuildPlugins = 1 << 0, // Only include GFPs that are intrinsic to the current target
		WithCloudCookPlugins = 1 << 1, // Include GFPs with CookBehavior.Type=ContentWorker
	};
	ENUM_CLASS_FLAGS(EBuildLookupOptions)

	/**
	 * Build a FGameFeatureVersePathLookup that can be used to map Verse paths to Game Feature URIs
	 * @Param TargetPlatform If set, uses the corresponding platform config
	 * @Param DevAR If set, use the specified development asset registry state instead of the global asset registry
	 * @Return false if there is an error or a cyclic dependency is discovered
	 */
	GAMEFEATURES_API TOptional<FGameFeatureVersePathLookup> BuildLookup(
		const ITargetPlatform* TargetPlatform = nullptr, 
		const FAssetRegistryState* DevAR = nullptr, 
		EBuildLookupOptions Options = EBuildLookupOptions::None);
}

UCLASS(config = Editor)
class UGameFeatureVersePathMapperCommandlet : public UCommandlet
{
	GENERATED_BODY()

public:
	virtual int32 Main(const FString& CmdLineParams) override;

	GAMEFEATURES_API static FString GetGameFeatureRootVersePath();
};

GameFeaturesProjectPolicies.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "UObject/PrimaryAssetId.h"
#include "GameFeaturesSubsystem.h"

#include "GameFeaturesProjectPolicies.generated.h"

#define UE_API GAMEFEATURES_API

class UGameFeatureData;

namespace UE::GameFeatures
{
	UE_API extern const TAutoConsoleVariable<bool>& GetCVarForceAsyncLoad();
}

struct FPluginDependencyDetails
{
	bool bFailIfNotFound = false;
};

enum class EStreamingAssetInstallMode : uint8
{
	GfpRequiredOnly, // only stream in data required for the GFP to load
	Full, // stream in all data
};

// This class allows project-specific rules to be implemented for game feature plugins.
// Create a subclass and choose it in Project Settings .. Game Features
UCLASS(MinimalAPI)
class UGameFeaturesProjectPolicies : public UObject
{
	GENERATED_BODY()

public:
	// Called when the game feature manager is initialized
	virtual void InitGameFeatureManager() { }

	// Called when the game feature manager is shut down
	virtual void ShutdownGameFeatureManager() { }

	// Called to determined the expected state of a plugin under the WhenLoading conditions.
	UE_API virtual bool WillPluginBeCooked(const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails) const;

	// Called when a game feature plugin enters the Loading state to determine additional assets to load
	virtual TArray<FPrimaryAssetId> GetPreloadAssetListForGameFeature(const UGameFeatureData* GameFeatureToLoad, bool bIncludeLoadedAssets = false) const { return TArray<FPrimaryAssetId>(); }

	// Returns the bundle state to use for assets returned by GetPreloadAssetListForGameFeature()
	// See the Asset Manager documentation for more information about asset bundles
	virtual const TArray<FName> GetPreloadBundleStateForGameFeature() const { return TArray<FName>(); }

	// Called to determine if this should be treated as a client, server, or both for data preloading
	// Actions can use this to decide what to load at runtime
	virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const { bLoadClientData = true; bLoadServerData = true; }

	// Called to determine if we are still during engine startup, which can modify loading behavior.
	// This defaults to true for the first few frames of a normal game or editor, but can be overridden.
	UE_API virtual bool IsLoadingStartupPlugins() const;

	// Called to determine the plugin URL for a given known Plugin. Can be used if the policy wants to deliver non file based URLs.
	UE_API virtual bool GetGameFeaturePluginURL(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL) const;

	UE_DEPRECATED(5.6, "Replaced with IsPluginAllowed(PluginURL, OutReason).")
	virtual bool IsPluginAllowed(const FString& PluginURL) const { return true; }
	// Called to determine if a plugin is allowed to be loaded or not
	// (e.g., when doing a fast cook a game might want to disable some or all game feature plugins)
	virtual bool IsPluginAllowed(const FString& PluginURL, FString* OutReason) const 
	{ 
		if (OutReason)
		{
			OutReason = {};
		}
		PRAGMA_DISABLE_DEPRECATION_WARNINGS
		return IsPluginAllowed(PluginURL); 
		PRAGMA_ENABLE_DEPRECATION_WARNINGS
	}

	// Return true if a uplugin's details should be read and false if it should be skipped. Skipped plugins will not be processed as GFPs and skipped as though they didn't exist. Useful to limit the number of uplugin files opened for perf reasons
	virtual bool ShouldReadPluginDetails(const FString& PluginDescriptorFilename) const { return true; }

	// Called to resolve plugin dependencies, will successfully return an empty string if a dependency is not a GFP.
	// This may be called with file protocol for built-in plugins in some cases, even if a different protocol is used at runtime.
	// returns The dependency URL or an error if the dependency could not be resolved
	UE_API virtual TValueOrError<FString, FString> ResolvePluginDependency(const FString& PluginURL, const FString& DependencyName, FPluginDependencyDetails& OutDetails) const;
	UE_API virtual TValueOrError<FString, FString> ResolvePluginDependency(const FString& PluginURL, const FString& DependencyName) const;

	// Called to resolve install bundles for streaming asset dependencies
	UE_DEPRECATED(5.6, "Use GetStreamingAssetInstallModes instead of creating new bundles for streaming assets.")
	virtual TValueOrError<TArray<FName>, FString> GetStreamingAssetInstallBundles(FStringView PluginURL) const { return MakeValue(); }

	// Called to resolve install modes for streaming asset dependencies
	// Return a streaming asset install mode for each install bundle
	UE_API virtual TValueOrError<TArray<EStreamingAssetInstallMode>, FString> GetStreamingAssetInstallModes(FStringView PluginURL, TConstArrayView<FName> InstallBundleNames) const;

	// Called by code that explicitly wants to load a specific plugin
	// (e.g., when using a fast cook a game might want to allow explicitly loaded game feature plugins)
	UE_API virtual void ExplicitLoadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate, const bool bActivateGameFeatures);

	// Called to determine if async loading is allowed
	UE_API virtual bool AllowAsyncLoad(FStringView PluginURL) const;

	/**
	 * Returns the install bundle name if one exists for this plugin.
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	UE_API virtual FString GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);

	/**
	 * Returns the optional install bundle name if one exists for this plugin.
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	UE_API virtual FString GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);
};

// This is a default implementation that immediately processes all game feature plugins the based on
// their BuiltInAutoRegister, BuiltInAutoLoad, and BuiltInAutoActivate settings.
//
// It will be used if no project-specific policy is set in Project Settings .. Game Features
UCLASS(MinimalAPI)
class UDefaultGameFeaturesProjectPolicies : public UGameFeaturesProjectPolicies
{
	GENERATED_BODY()

public:
	//~UGameFeaturesProjectPolicies interface
	UE_API virtual void InitGameFeatureManager() override;
	UE_API virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;
	UE_API virtual const TArray<FName> GetPreloadBundleStateForGameFeature() const override;
	//~End of UGameFeaturesProjectPolicies interface
};

#undef UE_API

GameFeaturesSubsystem.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Containers/Union.h"
#include "Delegates/Delegate.h"
#include "Engine/Engine.h"
#include "GameFeatureTypesFwd.h"
#include "Misc/TransactionallySafeRWLock.h"

#include "GameFeaturesSubsystem.generated.h"

#define UE_API GAMEFEATURES_API

namespace UE::GameFeatures
{
	class FPackageLoadTracker;
	struct FResult;
}

class UGameFeaturePluginStateMachine;
class IGameFeatureStateChangeObserver;
struct FStreamableHandle;
struct FAssetIdentifier;
class UGameFeatureData;
class UGameFeaturesProjectPolicies;
class IPlugin;
class FJsonObject;
struct FWorldContext;
struct FGameFeaturePluginStateRange;
struct FGameFeaturePluginStateMachineProperties;
struct FInstallBundleReleaseRequestInfo;
enum class EInstallBundleResult : uint32;
enum class EInstallBundleRequestFlags : uint32;
enum class EInstallBundleReleaseRequestFlags : uint32;

/** Holds static global information about how our PluginURLs are structured */
namespace UE::GameFeatures
{
	namespace PluginURLStructureInfo
	{
		/** Character used to denote what value is being assigned to the option before it */
		extern GAMEFEATURES_API const TCHAR* OptionAssignOperator;

		/** Character used to separate options on the URL. Used between each assigned value and the next Option name. */
		extern GAMEFEATURES_API const TCHAR* OptionSeperator;

		/** Character used to separate lists of values for a single option. Used between each entry in the list. */
		extern GAMEFEATURES_API const TCHAR* OptionListSeperator;
	};

	namespace CommonErrorCodes
	{
		extern const TCHAR* DependencyFailedRegister;
	};
};

/** 
 * Struct that determines if game feature action state changes should be applied for cases where there are multiple worlds or contexts.
 * The default value means to apply to all possible objects. This can be safely copied and used for later querying.
 */
struct FGameFeatureStateChangeContext
{
public:

	/** Sets a specific world context handle to limit changes to */
	UE_API void SetRequiredWorldContextHandle(FName Handle);

	/** Sees if the specific world context matches the application rules */
	UE_API bool ShouldApplyToWorldContext(const FWorldContext& WorldContext) const;

	/** True if events bound using this context should apply when using other context */
	UE_API bool ShouldApplyUsingOtherContext(const FGameFeatureStateChangeContext& OtherContext) const;

	/** Check if this has the exact same state change application rules */
	inline bool operator==(const FGameFeatureStateChangeContext& OtherContext) const
	{
		if (OtherContext.WorldContextHandle == WorldContextHandle)
		{
			return true;
		}

		return false;
	}

	/** Allow this to be used as a map key */
	inline friend uint32 GetTypeHash(const FGameFeatureStateChangeContext& OtherContext)
	{
		return GetTypeHash(OtherContext.WorldContextHandle);
	}

private:
	/** Specific world context to limit changes to, if none then it will apply to all */
	FName WorldContextHandle;
};

/** Context that provides extra information for activating a game feature */
struct FGameFeatureActivatingContext : public FGameFeatureStateChangeContext
{
public:
	//@TODO: Add rules specific to activation when required

private:

	friend struct FGameFeaturePluginState_Activating;
};

/** Context that provides extra information for deactivating a game feature, will use the same change context rules as the activating context */
struct FGameFeatureDeactivatingContext : public FGameFeatureStateChangeContext
{
public:
	UE_DEPRECATED(5.2, "Use tagged version instead")
	FSimpleDelegate PauseDeactivationUntilComplete()
	{
		return PauseDeactivationUntilComplete(TEXT("Unknown(Deprecated)"));
	}

	// Call this if your observer has an asynchronous action to complete as part of shutdown, and invoke the returned delegate when you are done (on the game thread!)
	GAMEFEATURES_API FSimpleDelegate PauseDeactivationUntilComplete(FString InPauserTag);

	UE_DEPRECATED(5.2, "Use tagged version instead")
	FGameFeatureDeactivatingContext(FSimpleDelegate&& InCompletionDelegate)
		: PluginName(TEXTVIEW("Unknown(Deprecated)"))
		, CompletionCallback([CompletionDelegate = MoveTemp(InCompletionDelegate)](FStringView) { CompletionDelegate.ExecuteIfBound(); })
	{
	}

	FGameFeatureDeactivatingContext(FStringView InPluginName, TFunction<void(FStringView InPauserTag)>&& InCompletionCallback)
		: PluginName(InPluginName)
		, CompletionCallback(MoveTemp(InCompletionCallback))
	{
	}

	int32 GetNumPausers() const { return NumPausers; }
private:
	FStringView PluginName;
	TFunction<void(FStringView InPauserTag)> CompletionCallback;
	int32 NumPausers = 0;

	friend struct FGameFeaturePluginState_Deactivating;
};

/** Context that provides extra information for a game feature changing its pause state */
struct FGameFeaturePauseStateChangeContext : public FGameFeatureStateChangeContext
{
public:
	FGameFeaturePauseStateChangeContext(FString PauseStateNameIn, FString PauseReasonIn, bool bIsPausedIn)
		: PauseStateName(MoveTemp(PauseStateNameIn))
		, PauseReason(MoveTemp(PauseReasonIn))
		, bIsPaused(bIsPausedIn)
	{
	}

	/** Returns true if the State has paused or false if it is resuming */
	bool IsPaused() const { return bIsPaused; }

	/** Returns an FString description of why the state has paused work. */
	const FString& GetPauseReason() const { return PauseReason; }

	/** Returns an FString description of what state has issued the pause change */
	const FString& GetPausingStateName() const { return PauseStateName; }

private:
	FString PauseStateName;
	FString PauseReason;
	bool bIsPaused = false;
};


/** Context that provides extra information prior to mounting a plugin */
struct FGameFeaturePreMountingContext : public FGameFeatureStateChangeContext
{
public:
	bool bOpenPluginShaderLibrary = true;

private:

	friend struct FGameFeaturePluginState_Mounting;
};

/** Context that allows pausing prior to transitioning out of the mounting state */
struct FGameFeaturePostMountingContext : public FGameFeatureStateChangeContext
{
public:
	// Call this if your observer has an asynchronous action to complete prior to transitioning out of the mounting state
	// and invoke the returned delegate when you are done (on the game thread!)
	GAMEFEATURES_API FSimpleDelegate PauseUntilComplete(FString InPauserTag);

	FGameFeaturePostMountingContext(FStringView InPluginName, TFunction<void(FStringView InPauserTag)>&& InCompletionCallback)
		: PluginName(InPluginName)
		, CompletionCallback(MoveTemp(InCompletionCallback))
	{}

	int32 GetNumPausers() const { return NumPausers; }

private:
	FStringView PluginName;
	TFunction<void(FStringView InPauserTag)> CompletionCallback;
	int32 NumPausers = 0;

	friend struct FGameFeaturePluginState_Mounting;
};

GAMEFEATURES_API DECLARE_LOG_CATEGORY_EXTERN(LogGameFeatures, Log, All);
/** Notification that a game feature plugin install/register/load/unload has finished */
DECLARE_DELEGATE_OneParam(FGameFeaturePluginChangeStateComplete, const UE::GameFeatures::FResult& /*Result*/);

/** A request to update the state machine and process states */
DECLARE_DELEGATE(FGameFeaturePluginRequestUpdateStateMachine);
DECLARE_MULTICAST_DELEGATE(FNotifyGameFeaturePluginRequestUpdateStateMachine)

using FGameFeaturePluginLoadComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginDeactivateComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginUnloadComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginReleaseComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginUninstallComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginTerminateComplete = FGameFeaturePluginChangeStateComplete;
using FGameFeaturePluginUpdateProtocolComplete = FGameFeaturePluginChangeStateComplete;

using FMultipleGameFeaturePluginChangeStateComplete = TDelegate<void(const TMap<FString, UE::GameFeatures::FResult>& Results)>;

using FBuiltInGameFeaturePluginsLoaded = FMultipleGameFeaturePluginChangeStateComplete;
using FMultipleGameFeaturePluginsLoaded = FMultipleGameFeaturePluginChangeStateComplete;
using FMultipleGameFeaturePluginsReleased = FMultipleGameFeaturePluginChangeStateComplete;
using FMultipleGameFeaturePluginsTerminated = FMultipleGameFeaturePluginChangeStateComplete;

enum class EBuiltInAutoState : uint8
{
	Invalid,
	Installed,
	Registered,
	Loaded,
	Active
};
const FString GAMEFEATURES_API LexToString(const EBuiltInAutoState BuiltInAutoState);

UENUM(BlueprintType)
enum class EGameFeatureTargetState : uint8
{
	Installed,
	Registered,
	Loaded,
	Active,
	Count	UMETA(Hidden)
};
const FString GAMEFEATURES_API LexToString(const EGameFeatureTargetState GameFeatureTargetState);
void GAMEFEATURES_API LexFromString(EGameFeatureTargetState& Value, const TCHAR* StringIn);

struct FGameFeaturePluginReferenceDetails
{
	FString PluginName;
	bool bShouldActivate = false;
};

struct FGameFeaturePluginDetails
{
	TArray<FGameFeaturePluginReferenceDetails> PluginDependencies;
	TMap<FString, TSharedPtr<class FJsonValue>> AdditionalMetadata;
	bool bHotfixable = false;
	EBuiltInAutoState BuiltInAutoState = EBuiltInAutoState::Invalid;
};

struct FBuiltInGameFeaturePluginBehaviorOptions
{
	EBuiltInAutoState AutoStateOverride = EBuiltInAutoState::Invalid;

	/** Force this GFP to load synchronously even if async loading is allowed */
	bool bForceSyncLoading = false;

	/** Batch process GFPs if/when possible (could be used when processing multiple plugins)*/
	bool bBatchProcess = false;

	/** Disallows downloading, useful for conditionally loading content only if it's already been installed */
	bool bDoNotDownload = false;

	/** Log Warning if loading this GFP forces dependencies to be created, useful for catching GFP load filtering bugs */
	bool bLogWarningOnForcedDependencyCreation = false;

	/** Log Error if loading this GFP forces dependencies to be created, useful for catching GFP load filtering bugs */
	bool bLogErrorOnForcedDependencyCreation = false;
};

struct FGameFeaturePluginAsyncHandle : public TSharedFromThis<FGameFeaturePluginAsyncHandle>
{
	virtual ~FGameFeaturePluginAsyncHandle() {}
	virtual bool IsComplete() const = 0;
	virtual const UE::GameFeatures::FResult& GetResult() const = 0;
	virtual float GetProgress() const = 0;
	virtual void Cancel() = 0;
};

/** Handle to track a GFP predownload */
struct FGameFeaturePluginPredownloadHandle : public FGameFeaturePluginAsyncHandle
{
};

/** Struct used to transform a GameFeaturePlugin URL into something that can uniquely identify the GameFeaturePlugin
    without including any transient data being passed in through the URL */
USTRUCT()
struct FGameFeaturePluginIdentifier
{
	GENERATED_BODY()

	FGameFeaturePluginIdentifier() = default;
	UE_API explicit FGameFeaturePluginIdentifier(FString PluginURL);

	FGameFeaturePluginIdentifier(const FGameFeaturePluginIdentifier& Other)
		: FGameFeaturePluginIdentifier(Other.PluginURL)
	{}

	UE_API FGameFeaturePluginIdentifier(FGameFeaturePluginIdentifier&& Other);

	FGameFeaturePluginIdentifier& operator=(const FGameFeaturePluginIdentifier& Other)
	{
		FromPluginURL(Other.PluginURL);
		return *this;
	}

	UE_API FGameFeaturePluginIdentifier& operator=(FGameFeaturePluginIdentifier&& Other);

	/** Used to determine if 2 FGameFeaturePluginIdentifiers are referencing the same GameFeaturePlugin.
		Only matching on Identifying information instead of all the optional bundle information */
	UE_API bool operator==(const FGameFeaturePluginIdentifier& Other) const;

	/** Function that fills out IdentifyingURLSubset from the given PluginURL */
	UE_API void FromPluginURL(FString PluginURL);

	/** Returns true if this FGameFeaturePluginIdentifier exactly matches the given PluginURL.
		To match exactly all information in the PluginURL has to match and not just the IdentifyingURLSubset */
	UE_API bool ExactMatchesURL(const FString& PluginURL) const;

	EGameFeaturePluginProtocol GetPluginProtocol() const { return PluginProtocol; }

	/** Returns the Identifying information used for this Plugin. It is a subset of the URL used to create it.*/
	FStringView GetIdentifyingString() const { return IdentifyingURLSubset; }

	/** Returns the name of the plugin */
	UE_API FStringView GetPluginName() const;

	/** Get the Full PluginURL used to originally construct this identifier */
	const FString& GetFullPluginURL() const { return PluginURL; }

	friend inline uint32 GetTypeHash(const FGameFeaturePluginIdentifier& PluginIdentifier)
	{
		return GetTypeHash(PluginIdentifier.IdentifyingURLSubset);
	}

private:
	/** Full PluginURL used to originally construct this identifier */
	FString PluginURL;

	/** The part of the URL that can be used to uniquely identify this plugin without any transient data */
	FStringView IdentifyingURLSubset;

	/** The protocol used in the URL for this GameFeaturePlugin URL */
	EGameFeaturePluginProtocol PluginProtocol;

	//Friend class so that it can access parsed URL data from under the hood
	friend struct FGameFeaturePluginStateMachineProperties;
};

USTRUCT()
struct FInstallBundlePluginProtocolOptions
{
	GENERATED_BODY()

	UE_API FInstallBundlePluginProtocolOptions();

	PRAGMA_DISABLE_DEPRECATION_WARNINGS
	FInstallBundlePluginProtocolOptions(const FInstallBundlePluginProtocolOptions&) = default;
	FInstallBundlePluginProtocolOptions& operator=(const FInstallBundlePluginProtocolOptions&) = default;
	PRAGMA_ENABLE_DEPRECATION_WARNINGS

	/** EInstallBundleRequestFlags utilized during the download/install by InstallBundleManager */
	EInstallBundleRequestFlags InstallBundleFlags;

	/** EInstallBundleReleaseRequestFlags utilized during our release and uninstall states */
	UE_DEPRECATED(5.6, "Release flags are now applied internally and no longer need to be explicitly set.")
	EInstallBundleReleaseRequestFlags ReleaseInstallBundleFlags;

	/** If we want to attempt to uninstall InstallBundle data installed by this plugin before terminating */
	bool bUninstallBeforeTerminate : 1;

	/** If we want to set the Downloading state to pause because of user interaction */
	bool bUserPauseDownload : 1;

	/** Allow the GFP to load INI files, should only be allowed for trusted content */
	bool bAllowIniLoading : 1;

	/** Disallows downloading, useful for conditionally loading content only if it's already been installed */
	bool bDoNotDownload : 1;

	UE_API bool operator==(const FInstallBundlePluginProtocolOptions& Other) const;
};

struct FGameFeatureProtocolOptions : public TUnion<FInstallBundlePluginProtocolOptions, FNull>
{
	GAMEFEATURES_API FGameFeatureProtocolOptions();
	GAMEFEATURES_API explicit FGameFeatureProtocolOptions(const FInstallBundlePluginProtocolOptions& InOptions);
	GAMEFEATURES_API explicit FGameFeatureProtocolOptions(FNull InOptions);

	bool operator==(const FGameFeatureProtocolOptions& Other) const
	{
		return TUnion<FInstallBundlePluginProtocolOptions, FNull>::operator==(Other) &&
			bForceSyncLoading == Other.bForceSyncLoading &&
			bBatchProcess == Other.bBatchProcess &&
			bLogWarningOnForcedDependencyCreation == Other.bLogWarningOnForcedDependencyCreation &&
			bLogErrorOnForcedDependencyCreation == Other.bLogErrorOnForcedDependencyCreation;
	}

	/** Force this GFP to load synchronously even if async loading is allowed */
	bool bForceSyncLoading : 1;

	/** Batch process GFPs if/when possible (could be used when processing multiple plugins)*/
	bool bBatchProcess : 1;

	/** Log Warning if loading this GFP forces dependencies to be created, useful for catching GFP load filtering bugs */
	bool bLogWarningOnForcedDependencyCreation : 1;

	/** Log Error if loading this GFP forces dependencies to be created, useful for catching GFP load filtering bugs */
	bool bLogErrorOnForcedDependencyCreation : 1;
};

// some important information about a gamefeature
struct FGameFeatureInfo
{
	FString Name;
	FString URL;
	bool bLoadedAsBuiltIn;
	EGameFeaturePluginState CurrentState;
};

/** The manager subsystem for game features */
UCLASS(MinimalAPI)
class UGameFeaturesSubsystem : public UEngineSubsystem
{
	GENERATED_BODY()

public:
	//~UEngineSubsystem interface
	UE_API virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	UE_API virtual void Deinitialize() override;
	//~End of UEngineSubsystem interface

	static UGameFeaturesSubsystem& Get() { return *GEngine->GetEngineSubsystem<UGameFeaturesSubsystem>(); }

	UE_API static FSimpleMulticastDelegate OnGameFeaturePolicyPreInit;

public:
	/** Loads the specified game feature data and its bundles */
	static UE_API TSharedPtr<FStreamableHandle> LoadGameFeatureData(const FString& GameFeatureToLoad, bool bStartStalled = false);
	static UE_API void UnloadGameFeatureData(const UGameFeatureData* GameFeatureToUnload);

	UE_DEPRECATED(5.7, "Use AddObserver with EObserverPluginStateUpdateMode parameter")
	UE_API void AddObserver(UObject* Observer);

	enum class EObserverPluginStateUpdateMode
	{
		FutureOnly, //Only call the observer with future plugin state changes
		CurrentAndFuture, //Also update the observer with the current plugin state at add time - note, if project has a lot of plugins this can be expensive
	};

	//Add Observer to be notified when plugin states change. Observer must implement IGameFeatureStateChangeObserver and be non-null
	UE_API void AddObserver(UObject* Observer, const EObserverPluginStateUpdateMode UpdateMode);
	UE_API void RemoveObserver(UObject* Observer);

	UE_API void ForEachGameFeature(TFunctionRef<void(FGameFeatureInfo&&)> Visitor) const;

	/**
	 * Calls the compile-time lambda on each active game feature data of the specified type
	 * @param GameFeatureDataType       The kind of data required
	 */
	template<class GameFeatureDataType, typename Func>
	void ForEachActiveGameFeature(Func InFunc) const
	{
		for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
		{
			if (UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value())
			{
				if (const GameFeatureDataType* GameFeatureData = Cast<const GameFeatureDataType>(GetDataForStateMachine(GFSM)))
				{
					InFunc(GameFeatureData);
				}
			}
		}
	}

	/**
	 * Calls the compile-time lambda on each registered game feature data of the specified type
	 * @param GameFeatureDataType       The kind of data required
	 */
	template<class GameFeatureDataType, typename Func>
	void ForEachRegisteredGameFeature(Func InFunc) const
	{
		for (auto StateMachineIt = GameFeaturePluginStateMachines.CreateConstIterator(); StateMachineIt; ++StateMachineIt)
		{
			if (UGameFeaturePluginStateMachine* GFSM = StateMachineIt.Value())
			{
				if (const GameFeatureDataType* GameFeatureData = Cast<const GameFeatureDataType>(GetRegisteredDataForStateMachine(GFSM)))
				{
					InFunc(GameFeatureData);
				}
			}
		}
	}

public:
	/** Construct a 'file:' Plugin URL using from the PluginDescriptorPath */
	static UE_API FString GetPluginURL_FileProtocol(const FString& PluginDescriptorPath);
	static UE_API FString GetPluginURL_FileProtocol(const FString& PluginDescriptorPath, TArrayView<const TPair<FString, FString>> AdditionalOptions);
	
	/** Get the file path portion of a file protocol URL*/
	static UE_API FString GetPluginFilename_FileProtocol(const FString& PluginUrlFileProtocol);

	/** Construct a 'installbundle:' Plugin URL using from the PluginName and required install bundles */
	static UE_API FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, TArrayView<const FString> BundleNames);
	static UE_API FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, const FString& BundleName);
	static UE_API FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, TArrayView<const FName> BundleNames);
	static UE_API FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, FName BundleName);
	static UE_API FString GetPluginURL_InstallBundleProtocol(const FString& PluginName, TArrayView<const FName> BundleNames, TArrayView<const TPair<FString, FString>> AdditionalOptions);

	/** Returns the plugin protocol for the specified URL */
	static UE_API EGameFeaturePluginProtocol GetPluginURLProtocol(FStringView PluginURL);

	/** Tests whether the plugin URL is the specified protocol */
	static UE_API bool IsPluginURLProtocol(FStringView PluginURL, EGameFeaturePluginProtocol PluginProtocol);

	/** Parse the plugin URL into subparts */
	static UE_API bool ParsePluginURL(FStringView PluginURL, EGameFeaturePluginProtocol* OutProtocol = nullptr, FStringView* OutPath = nullptr, FStringView* OutOptions = nullptr);

	/** Parse options from a plugin URL or the options subpart of the plugin URL */
	static UE_API bool ParsePluginURLOptions(FStringView URLOptionsString,
		TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output);
	static UE_API bool ParsePluginURLOptions(FStringView URLOptionsString, EGameFeatureURLOptions OptionsFlags,
		TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output);
	static UE_API bool ParsePluginURLOptions(FStringView URLOptionsString, TConstArrayView<FStringView> AdditionalOptions,
		TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output);
	static UE_API bool ParsePluginURLOptions(FStringView URLOptionsString, EGameFeatureURLOptions OptionsFlags, TConstArrayView<FStringView> AdditionalOptions,
		TFunctionRef<void(EGameFeatureURLOptions Option, FStringView OptionString, FStringView OptionValue)> Output);


public:
	/** Returns all the active plugins GameFeatureDatas */
	UE_API void GetGameFeatureDataForActivePlugins(TArray<const UGameFeatureData*>& OutActivePluginFeatureDatas);

	/** Returns the game feature data for an active plugin specified by PluginURL */
	UE_API const UGameFeatureData* GetGameFeatureDataForActivePluginByURL(const FString& PluginURL);

	/** Returns the game feature data for a registered plugin specified by PluginURL */
	UE_API const UGameFeatureData* GetGameFeatureDataForRegisteredPluginByURL(const FString& PluginURL, bool bCheckForRegistering = false);

	/** Determines if a plugin is in the Installed state (or beyond) */
	UE_API bool IsGameFeaturePluginInstalled(const FString& PluginURL) const;

	/** Determines if a plugin is beyond the Mounting state */
	UE_API bool IsGameFeaturePluginMounted(const FString& PluginURL) const;

	/** Determines if a plugin is in the Registered state (or beyond) */
	UE_API bool IsGameFeaturePluginRegistered(const FString& PluginURL, bool bCheckForRegistering = false) const;

	/** Determines if a plugin is in the Loaded state (or beyond) */
	UE_API bool IsGameFeaturePluginLoaded(const FString& PluginURL) const;

	/** Was this game feature plugin loaded using the LoadBuiltInGameFeaturePlugin path */
	UE_API bool WasGameFeaturePluginLoadedAsBuiltIn(const FString& PluginURL) const;

	/** Loads a single game feature plugin. */
	UE_API void LoadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void LoadGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void LoadGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate);

	/** Registers a single game feature plugin. */
	UE_API void RegisterGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void RegisterGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void RegisterGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate);

	/** Loads a single game feature plugin and activates it. */
	UE_API void LoadAndActivateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void LoadAndActivateGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginLoadComplete& CompleteDelegate);
	UE_API void LoadAndActivateGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate);

	/** Changes the target state of a game feature plugin */
	UE_API void ChangeGameFeatureTargetState(const FString& PluginURL, EGameFeatureTargetState TargetState, const FGameFeaturePluginChangeStateComplete& CompleteDelegate);
	UE_API void ChangeGameFeatureTargetState(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, EGameFeatureTargetState TargetState, const FGameFeaturePluginChangeStateComplete& CompleteDelegate);
	UE_API void ChangeGameFeatureTargetState(TConstArrayView<FString> PluginURLs, const FGameFeatureProtocolOptions& ProtocolOptions, EGameFeatureTargetState TargetState, const FMultipleGameFeaturePluginsLoaded& CompleteDelegate);

	/** Changes the protocol options of a game feature plugin. Useful to change any options data such as settings flags */
	UE_API UE::GameFeatures::FResult UpdateGameFeatureProtocolOptions(const FString& PluginURL, const FGameFeatureProtocolOptions& NewOptions, bool* bOutDidUpdate = nullptr);

	/** Gets the Install_Percent for single game feature plugin if it is active. */
	UE_API bool GetGameFeaturePluginInstallPercent(const FString& PluginURL, float& Install_Percent) const;
	UE_API bool GetGameFeaturePluginInstallPercent(TConstArrayView<FString> PluginURLs, float& Install_Percent) const;

	/** Determines if a plugin is in the Active state.*/
	UE_API bool IsGameFeaturePluginActive(const FString& PluginURL, bool bCheckForActivating = false) const;

	/** Determines if a plugin is in the Active state.*/
	UE_API bool IsGameFeaturePluginActiveByName(FStringView PluginName, bool bCheckForActivating = false) const;

	/** Determines if a plugin is up to date or needs an update. Returns true if an update is available.*/
	UE_API bool DoesGameFeaturePluginNeedUpdate(const FString& PluginURL) const;

	/** Deactivates the specified plugin */
	UE_API void DeactivateGameFeaturePlugin(const FString& PluginURL);
	UE_API void DeactivateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginDeactivateComplete& CompleteDelegate);

	/** Unloads the specified game feature plugin. */
	UE_API void UnloadGameFeaturePlugin(const FString& PluginURL, bool bKeepRegistered = false);
	UE_API void UnloadGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUnloadComplete& CompleteDelegate, bool bKeepRegistered = false);

	/** Releases any game data stored for this GameFeaturePlugin. Does not uninstall data and it will remain on disk. */
	UE_API void ReleaseGameFeaturePlugin(const FString& PluginURL);
	UE_API void ReleaseGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginReleaseComplete& CompleteDelegate);
	UE_API void ReleaseGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginsReleased& CompleteDelegate);

	/** Uninstalls any game data stored for this GameFeaturePlugin and terminates the GameFeaturePlugin.
		If the given PluginURL is not found this will create a GameFeaturePlugin first and attempt to run it through the uninstall flow.
		This allows for the uninstalling of data that was installed on previous runs of the application where we haven't yet requested the
		GameFeaturePlugin that we would like to uninstall data for on this run. */
	UE_API void UninstallGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUninstallComplete& CompleteDelegate = FGameFeaturePluginUninstallComplete());
	UE_API void UninstallGameFeaturePlugin(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginUninstallComplete& CompleteDelegate = FGameFeaturePluginUninstallComplete());

	/** Terminate the GameFeaturePlugin and remove all associated plugin tracking data. */
	UE_API void TerminateGameFeaturePlugin(const FString& PluginURL);
	UE_API void TerminateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginTerminateComplete& CompleteDelegate);
	UE_API void TerminateGameFeaturePlugin(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginsTerminated& CompleteDelegate);
	
	/** Attempt to cancel any state change. Calls back when cancelation is complete. Any other pending callbacks will be called with a canceled error. */
	UE_API void CancelGameFeatureStateChange(const FString& PluginURL);
	UE_API void CancelGameFeatureStateChange(const FString& PluginURL, const FGameFeaturePluginChangeStateComplete& CompleteDelegate);
	UE_API void CancelGameFeatureStateChange(TConstArrayView<FString> PluginURLs, const FMultipleGameFeaturePluginChangeStateComplete& CompleteDelegate);

	/**
	 * If the specified plugin is known by the game feature system, returns the URL used to identify it
	 * @return true if the plugin exists, false if it was not found
	 */
	UE_API bool GetPluginURLByName(FStringView PluginName, FString& OutPluginURL) const;

	/** If the specified plugin is a built-in plugin, return the URL used to identify it. Returns true if the plugin exists, false if it was not found */
	UE_DEPRECATED(5.1, "Use GetPluginURLByName instead")
	UE_API bool GetPluginURLForBuiltInPluginByName(const FString& PluginName, FString& OutPluginURL) const;

	/** Get the plugin path from the plugin URL */
	UE_API FString GetPluginFilenameFromPluginURL(const FString& PluginURL) const;

	/** Get the plugin name from the plugin URL */
	UE_API FString GetPluginNameFromPluginURL(const FString& PluginURL) const;

	/** Fixes a package path/directory to either be relative to plugin root or not. Paths relative to different roots will not be modified */
	static UE_API void FixPluginPackagePath(FString& PathToFix, const FString& PluginRootPath, bool bMakeRelativeToPluginRoot);

	/** Returns the game-specific policy for managing game feature plugins */
	template <typename T = UGameFeaturesProjectPolicies>
	T& GetPolicy() const
	{
		ensureMsgf(bInitializedPolicyManager, TEXT("Attemting to get policy before GameFeaturesSubsystem is ready!"));
		return *CastChecked<T>(GameSpecificPolicies, ECastCheckedType::NullChecked);
	}

	typedef TFunctionRef<bool(const FString& PluginFilename, const FGameFeaturePluginDetails& Details, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions)> FBuiltInPluginAdditionalFilters;
	typedef TFunction<bool(const FString& PluginFilename, const FGameFeaturePluginDetails& Details, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions)> FBuiltInPluginAdditionalFilters_Copyable;

	/** Loads a built-in game feature plugin if it passes the specified filter */
	UE_API void LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter, const FGameFeaturePluginLoadComplete& CompleteDelegate = FGameFeaturePluginLoadComplete());

	/** Loads all built-in game feature plugins that pass the specified filters */
	UE_API void LoadBuiltInGameFeaturePlugins(FBuiltInPluginAdditionalFilters AdditionalFilter, const FBuiltInGameFeaturePluginsLoaded& CompleteDelegate = FBuiltInGameFeaturePluginsLoaded());

	/** Loads all built-in game feature plugins that pass the specified filters, split over multiple frames processing only AmortizeRate plugins per frame if greater than 0. Note that AdditionalFilter must be a TFunction and not a TFunctionRef since it will be used in future ticks */
	UE_API void LoadBuiltInGameFeaturePlugins_Amortized(const FBuiltInPluginAdditionalFilters_Copyable& AdditionalFilter_Copyable, int32 AmortizeRate, const FBuiltInGameFeaturePluginsLoaded& CompleteDelegate = FBuiltInGameFeaturePluginsLoaded());
private:
	void LoadBuiltInGameFeaturePluginsInternal(FBuiltInPluginAdditionalFilters AdditionalFilter, const FBuiltInPluginAdditionalFilters_Copyable& AdditionalFilter_Copyable, int32 AmortizeRate, const FBuiltInGameFeaturePluginsLoaded& CompleteDelegate = FBuiltInGameFeaturePluginsLoaded());
public:

	/** Returns the list of plugin filenames that have progressed beyond installed. Used in cooking to determine which will be cooked. */
	//@TODO: GameFeaturePluginEnginePush: Might not be general enough for engine level, TBD
	UE_API void GetLoadedGameFeaturePluginFilenamesForCooking(TArray<FString>& OutLoadedPluginFilenames) const;

	/** Removes assets that are in plugins we know to be inactive.  Order is not maintained. */
	UE_API void FilterInactivePluginAssets(TArray<FAssetIdentifier>& AssetsToFilter) const;

	/** Removes assets that are in plugins we know to be inactive.  Order is not maintained. */
	UE_API void FilterInactivePluginAssets(TArray<FAssetData>& AssetsToFilter) const;

	/** Returns the current state of the state machine for the specified plugin URL */
	UE_API EGameFeaturePluginState GetPluginState(const FString& PluginURL) const;

	/** Returns the current state of the state machine for the specified plugin PluginIdentifier */
	UE_API EGameFeaturePluginState GetPluginState(FGameFeaturePluginIdentifier PluginIdentifier) const;

	/** Gets relevant properties out of a uplugin file */
	UE_DEPRECATED(5.4, "Use GetBuiltInGameFeaturePluginDetails instead")
	UE_API bool GetGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL, struct FGameFeaturePluginDetails& OutPluginDetails) const;

	/** Gets relevant properties out of a uplugin file. Should only be used for built-in GFPs */
	UE_DEPRECATED(5.5, "Use non-PluginURL version of GetBuiltInGameFeaturePluginDetails and GetBuiltInGameFeaturePluginPath instead")
	UE_API bool GetBuiltInGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL, struct FGameFeaturePluginDetails& OutPluginDetails) const;

	/** Gets relevant properties out of a uplugin file. Should only be used for built-in GFPs */
	UE_API bool GetBuiltInGameFeaturePluginDetails(const TSharedRef<IPlugin>& Plugin, struct FGameFeaturePluginDetails& OutPluginDetails) const;

	/** Gets the URL for the given plugin, applying game-specific policies where appropriate. Should only be used for built-in GFPs */
	UE_API bool GetBuiltInGameFeaturePluginURL(const TSharedRef<IPlugin>& Plugin, FString& OutPluginURL) const;

	/** Gets relevant properties out of a uplugin file if it's installed */
	UE_API bool GetGameFeaturePluginDetails(const FString& PluginURL, struct FGameFeaturePluginDetails& OutPluginDetails) const;

	/** 
	 * Sets OutGameFeatureControlsUPlugin to true if the uplugin was added by a GFP as opposed to existing independent of the GFP subsystem.
	 */
	UE_API bool GetGameFeatureControlsUPlugin(const FString& PluginURL, bool& OutGameFeatureControlsUPlugin) const;

	UE_API bool IsPluginAllowed(const FString& PluginURL, FString* OutReason = nullptr) const;

	/** 
	 * Pre-install any required game feature data, which can be useful for larger payloads. 
	 * This does not instantiate any GFP although it is safe to do so before this finishes. 
	 * This doesn't not resolve any dependencies, PluginURLs should contain all dependencies.
	 */
	UE_API TSharedRef<FGameFeaturePluginPredownloadHandle> PredownloadGameFeaturePlugins(TConstArrayView<FString> PluginURLs, 
		TUniqueFunction<void(const UE::GameFeatures::FResult&)> OnComplete = nullptr, TUniqueFunction<void(float)> OnProgress = nullptr);
	friend struct FGameFeaturePluginPredownloadContext;

	/** Determine the initial feature state for a built-in plugin */
	static UE_API EBuiltInAutoState DetermineBuiltInInitialFeatureState(TSharedPtr<FJsonObject> Descriptor, const FString& ErrorContext);

	static UE_API EGameFeaturePluginState ConvertInitialFeatureStateToTargetState(EBuiltInAutoState InitialState);

	/** Used during a DLC cook to determine which plugins should be cooked */
	static UE_API void GetPluginsToCook(TSet<FString>& OutPlugins);

	UE_API bool GetPluginDebugStateEnabled(const FString& PluginUrl);
	UE_API void SetPluginDebugStateEnabled(const FString& PluginUrl, bool bEnabled);

	/**
	 * Returns the install bundle name if one exists for this plugin. Can be overridden by the policy provider.
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	UE_API FString GetInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);

	/**
	 * Returns the optional install bundle name if one exists for this plugin. Can be overridden by the policy provider
	 * @param - PluginName - the name of the GameFeaturePlugin we want to get a bundle for. Should be the same name as the .uplugin file
	 * @param - bEvenIfDoesntExist - when true will return the name of bundle we are looking for without checking if it exists or not.
	 */
	UE_API FString GetOptionalInstallBundleName(FStringView PluginName, bool bEvenIfDoesntExist = false);

private:
	UE_API TSet<FString> GetActivePluginNames() const;

	UE_API void OnGameFeatureTerminating(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Terminal;

	UE_API void OnGameFeatureCheckingStatus(const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_UnknownStatus;

	UE_API void OnGameFeatureStatusKnown(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifierL);
	friend struct FGameFeaturePluginState_CheckingStatus;

	UE_API void OnGameFeaturePredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	UE_API void OnGameFeaturePostPredownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);

	UE_API void OnGameFeatureDownloading(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Downloading;

	UE_API void OnGameFeatureReleasing(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Releasing;

	UE_API void OnGameFeaturePreMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePreMountingContext& Context);
	UE_API void OnGameFeaturePostMounting(const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier, FGameFeaturePostMountingContext& Context);
	friend struct FGameFeaturePluginState_Mounting;

	UE_API void OnGameFeatureRegistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Registering;

	UE_API void OnGameFeatureUnregistering(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Unregistering;

	UE_API void OnGameFeatureActivating(const UGameFeatureData* GameFeatureData, const FString& PluginName, FGameFeatureActivatingContext& Context, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Activating;

	UE_API void OnGameFeatureActivated(const UGameFeatureData* GameFeatureData, const FString& PluginName, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Active;

	UE_API void OnGameFeatureDeactivating(const UGameFeatureData* GameFeatureData, const FString& PluginName, FGameFeatureDeactivatingContext& Context, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Deactivating;

	UE_API void OnGameFeatureLoading(const UGameFeatureData* GameFeatureData, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Loading;

	UE_API void OnGameFeatureUnloading(const UGameFeatureData* GameFeatureData, const FGameFeaturePluginIdentifier& PluginIdentifier);
	friend struct FGameFeaturePluginState_Unloading;

	UE_API void OnGameFeaturePauseChange(const FGameFeaturePluginIdentifier& PluginIdentifier, const FString& PluginName, FGameFeaturePauseStateChangeContext& Context);
	friend struct FGameFeaturePluginState_Deactivating;

	UE_API void OnAssetManagerCreated();

	/** Scans for assets specified in the game feature data */
	static UE_API void AddGameFeatureToAssetManager(const UGameFeatureData* GameFeatureToAdd, const FString& PluginName, TArray<FName>& OutNewPrimaryAssetTypes);

	static UE_API void RemoveGameFeatureFromAssetManager(const UGameFeatureData* GameFeatureToRemove, const FString& PluginName, const TArray<FName>& AddedPrimaryAssetTypes);

	// Provide additional causal information when a package is unavailable for load
	UE_API void GetExplanationForUnavailablePackage(const FString& SkippedPackage, IPlugin* PluginIfFound, FStringBuilderBase& InOutExplanation);

private:

	UE_API bool ShouldUpdatePluginProtocolOptions(const UGameFeaturePluginStateMachine* StateMachine, const FGameFeatureProtocolOptions& NewOptions);
	UE_API UE::GameFeatures::FResult UpdateGameFeatureProtocolOptions(UGameFeaturePluginStateMachine* StateMachine, const FGameFeatureProtocolOptions& NewOptions, bool* bOutDidUpdate = nullptr);

	UE_API const UGameFeatureData* GetDataForStateMachine(UGameFeaturePluginStateMachine* GFSM) const;
	UE_API const UGameFeatureData* GetRegisteredDataForStateMachine(UGameFeaturePluginStateMachine* GFSM) const;

	/** Gets relevant properties out of a uplugin file */
	UE_API bool GetGameFeaturePluginDetailsInternal(const FString& PluginDescriptorFilename, struct FGameFeaturePluginDetails& OutPluginDetails) const;

	/** Prunes any cached GFP details */
	UE_API void PruneCachedGameFeaturePluginDetails(const FString& PluginURL, const FString& PluginDescriptorFilename) const;
	friend struct FGameFeaturePluginState_Unmounting;
	friend struct FBaseDataReleaseGameFeaturePluginState;

	/** Gets the state machine associated with the specified URL */
	UE_API UGameFeaturePluginStateMachine* FindGameFeaturePluginStateMachine(const FString& PluginURL) const;

	/** Gets the state machine associated with the specified PluginIdentifier */
	UE_API UGameFeaturePluginStateMachine* FindGameFeaturePluginStateMachine(const FGameFeaturePluginIdentifier& PluginIdentifier) const;

	/** Gets the state machine associated with the specified URL, creates it if it doesnt exist */
	UE_API UGameFeaturePluginStateMachine* FindOrCreateGameFeaturePluginStateMachine(const FString& PluginURL, const FGameFeatureProtocolOptions& ProtocolOptions, bool* bOutFoundExisting = nullptr);

	/** Notification that a game feature has finished loading, and whether it was successful */
	UE_API void LoadBuiltInGameFeaturePluginComplete(const UE::GameFeatures::FResult& Result, UGameFeaturePluginStateMachine* Machine, FGameFeaturePluginStateRange RequestedDestination);

	/** 
	 * Sets a new destination state. Will attempt to cancel the current transition if the new destination is incompatible with the current destination 
	 * Note: In the case that the existing machine is terminal, a new one will need to be created. In that case ProtocolOptions will be used for the new machine.
	 */
	UE_API void ChangeGameFeatureDestination(UGameFeaturePluginStateMachine* Machine, const FGameFeaturePluginStateRange& StateRange, FGameFeaturePluginChangeStateComplete CompleteDelegate);
	UE_API void ChangeGameFeatureDestination(UGameFeaturePluginStateMachine* Machine, const FGameFeatureProtocolOptions& ProtocolOptions, const FGameFeaturePluginStateRange& StateRange, FGameFeaturePluginChangeStateComplete CompleteDelegate);

	/** Generic notification that calls the Complete delegate without broadcasting anything else.*/
	UE_API void ChangeGameFeatureTargetStateComplete(UGameFeaturePluginStateMachine* Machine, const UE::GameFeatures::FResult& Result, FGameFeaturePluginChangeStateComplete CompleteDelegate);

	UE_API void BeginTermination(UGameFeaturePluginStateMachine* Machine);
	UE_API void FinishTermination(UGameFeaturePluginStateMachine* Machine);
	friend class UGameFeaturePluginStateMachine;

	/** Handler for when a state machine requests its dependencies. Returns false if the dependencies could not be read */
	UE_API bool FindOrCreatePluginDependencyStateMachines(const FString& PluginURL, const FGameFeaturePluginStateMachineProperties& InStateProperties, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines);
	template <typename> friend struct FTransitionDependenciesGameFeaturePluginState;
	friend struct FWaitingForDependenciesTransitionPolicy;

	UE_API bool FindPluginDependencyStateMachinesToActivate(const FString& PluginURL, const FString& PluginFilename, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines) const;
	friend struct FActivatingDependenciesTransitionPolicy;

	UE_API bool FindPluginDependencyStateMachinesToDeactivate(const FString& PluginURL, const FString& PluginFilename, TArray<UGameFeaturePluginStateMachine*>& OutDependencyMachines) const;
	friend struct FDeactivatingDependenciesTransitionPolicy;

	template <typename CallableT>
	bool EnumeratePluginDependenciesWithShouldActivate(const FString& PluginURL, const FString& PluginFilename, CallableT Callable) const;

	/** Handle 'ListGameFeaturePlugins' console command */
	UE_API void ListGameFeaturePlugins(const TArray<FString>& Args, UWorld* InWorld, FOutputDevice& Ar);

	UE_API void SetExplanationForNotMountingPlugin(const FString& PluginURL, const FString& Explanation);

	enum class EObserverCallback
	{
		CheckingStatus,
		Terminating,
		Predownloading,
		PostPredownloading,
		Downloading,
		Releasing,
		PreMounting,
		PostMounting,
		Registering,
		Unregistering,
		Loading,
		Unloading,
		Activating,
		Activated,
		Deactivating,
		PauseChanged,
		Count
	};

	UE_API void CallbackObservers(EObserverCallback CallbackType, const FGameFeaturePluginIdentifier& PluginIdentifier,
		const FString* PluginName = nullptr, 
		const UGameFeatureData* GameFeatureData = nullptr, 
		FGameFeatureStateChangeContext* StateChangeContext = nullptr);

	/** Registers a state machine that is in transition and running */
	UE_API void RegisterRunningStateMachine(UGameFeaturePluginStateMachine* GFPSM);

	/** Unregisters a state machine that was in transition is no longer running */
	UE_API void UnregisterRunningStateMachine(UGameFeaturePluginStateMachine* GFPSM);

	/** Adds a batching request for a given state so we can start listening for when state machines have arrived at a fence */
	UE_API FDelegateHandle AddBatchingRequest(EGameFeaturePluginState State, FGameFeaturePluginRequestUpdateStateMachine UpdateDelegate);

	/** Cancels an existing batching request */
	UE_API void CancelBatchingRequest(EGameFeaturePluginState State, FDelegateHandle DelegateHandle);

	UE_API void EnableTick();
	UE_API void DisableTick();
	UE_API bool Tick(float DeltaTime);

	UE_API bool TickBatchProcessing();

private:
	/** The list of all game feature plugin state machine objects */
	UPROPERTY(Transient)
	TMap<FString, TObjectPtr<UGameFeaturePluginStateMachine>> GameFeaturePluginStateMachines;

	/** The tick handle if currently registered for a tick */
	FTSTicker::FDelegateHandle TickHandle;

	/** State machine currently in transition, used to limit search space when checking a batch processing fence or similar */
	TArray<UGameFeaturePluginStateMachine*> RunningStateMachines;

	/** Active fences */
	struct FGameFeatureBatchProcessingFence
	{
		FNotifyGameFeaturePluginRequestUpdateStateMachine NotifyUpdateStateMachines;
	};
	TMap<EGameFeaturePluginState, FGameFeatureBatchProcessingFence> BatchProcessingFences;

	/** Game feature plugin state machine objects that are being terminated. Used to prevent GC until termination is complete. */
	UPROPERTY(Transient)
	TArray<TObjectPtr<UGameFeaturePluginStateMachine>> TerminalGameFeaturePluginStateMachines;

	TMap<FString, FString> GameFeaturePluginNameToPathMap;

	struct FCachedGameFeaturePluginDetails
	{
		FGameFeaturePluginDetails Details;
		FCachedGameFeaturePluginDetails() = default;
		FCachedGameFeaturePluginDetails(const FGameFeaturePluginDetails& InDetails) : Details(InDetails) {}
	};
	mutable TMap<FString, FCachedGameFeaturePluginDetails> CachedPluginDetailsByFilename;
	mutable FTransactionallySafeRWLock CachedGameFeaturePluginDetailsLock;

	UPROPERTY()
	TArray<TObjectPtr<UObject>> Observers;

	UPROPERTY(Transient)
	TObjectPtr<UGameFeaturesProjectPolicies> GameSpecificPolicies;

	TUniquePtr<class UE::GameFeatures::FPackageLoadTracker> PackageLoadTracker;

#if WITH_EDITOR
	// When we decide not to mount a plugin, we can store an explanation here so that if we later attempt to load an asset from it we can tell the user why it's not available
	TMap<FString, FString> UnmountedPluginNameToExplanation;

	TUniquePtr<class FGameFeatureDataExternalAssetsPathCache> GameFeatureDataExternalAssetsPathCache;
#endif

#if !UE_BUILD_SHIPPING
	TSet<FString> DebugStateChangedForPlugins;
#endif
	FDelegateHandle GetExplanationForUnavailablePackageDelegateHandle;

	bool bInitializedPolicyManager = false;
};

#undef UE_API

GameFeaturesSubsystemSettings.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Engine/DeveloperSettings.h"
#include "GameFeaturesSubsystemSettings.generated.h"

#define UE_API GAMEFEATURES_API

/** Settings for the Game Features framework */
UCLASS(MinimalAPI, config=Game, defaultconfig, meta = (DisplayName = "Game Features"))
class UGameFeaturesSubsystemSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	UE_API UGameFeaturesSubsystemSettings();

	/** State/Bundle to always load on clients */
	static UE_API const FName LoadStateClient;

	/** State/Bundle to always load on dedicated server */
	static UE_API const FName LoadStateServer;

	/** Name of a singleton class to spawn as the game feature project policy. If empty, it will spawn the default one (UDefaultGameFeaturesProjectPolicies) */
	UPROPERTY(config, EditAnywhere, Category=DefaultClasses, meta=(MetaClass="/Script/GameFeatures.GameFeaturesProjectPolicies", DisplayName="Game Feature Project Policy Class", ConfigRestartRequired=true))
	FSoftClassPath GameFeaturesManagerClassName;

	/** List of plugins that are forcibly enabled (e.g., via a hotfix) */
	UPROPERTY(config, EditAnywhere, Category = GameFeatures)
	TArray<FString> EnabledPlugins;

	/** List of plugins that are forcibly disabled (e.g., via a hotfix) */
	UPROPERTY(config, EditAnywhere, Category=GameFeatures)
	TArray<FString> DisabledPlugins;

	/** List of metadata (additional keys) to try parsing from the .uplugin to provide to FGameFeaturePluginDetails */
	UPROPERTY(config, EditAnywhere, Category=GameFeatures)
	TArray<FString> AdditionalPluginMetadataKeys;

	UE_DEPRECATED(5.0, "Use IsValidGameFeaturePlugin() instead")
	FString BuiltInGameFeaturePluginsFolder;

public:
	// Returns true if the specified (normalized or full) path is a game feature plugin
	UE_API bool IsValidGameFeaturePlugin(const FString& PluginDescriptorFilename) const;

};

#undef UE_API

GameFeaturesEditor.Build.cs

// Copyright Epic Games, Inc. All Rights Reserved.

namespace UnrealBuildTool.Rules
{
	public class GameFeaturesEditor : ModuleRules
	{
        public GameFeaturesEditor(ReadOnlyTargetRules Target) : base(Target)
		{
			PublicDependencyModuleNames.AddRange(
				new string[]
                {
                    "Core",
                    "CoreUObject",
					"GameFeatures",
					"UnrealEd",
				}
			);

			PrivateDependencyModuleNames.AddRange(
				new string[]
				{
					"AssetTools",
					"AssetRegistry",
					"DataLayerEditor",
					"DataValidation",
					"DeveloperSettings",
					"Engine",
					"ModularGameplay",
					"EditorSubsystem",
					"Projects",
					"EditorFramework",
					"Slate",
					"SlateCore",
					"PropertyEditor",
					"SharedSettingsWidgets",
					"Json"
				}
			);
		}
	}
}

GameFeatureActionConvertContentBundleWorldPartitionBuilder.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureActionConvertContentBundleWorldPartitionBuilder.h"
#include "GameFeatureAction_AddWorldPartitionContent.h"
#include "GameFeatureAction_AddWPContent.h"
#include "WorldPartition/ContentBundle/ContentBundleWorldSubsystem.h"
#include "WorldPartition/ContentBundle/ContentBundleEditor.h"
#include "WorldPartition/ContentBundle/ContentBundleDescriptor.h"
#include "WorldPartition/ContentBundle/ContentBundleClient.h"
#include "WorldPartition/DataLayer/ExternalDataLayerAsset.h"
#include "WorldPartition/DataLayer/ExternalDataLayerInstance.h"
#include "WorldPartition/DataLayer/ExternalDataLayerManager.h"
#include "DataLayer/DataLayerEditorSubsystem.h"
#include "DataLayer/ExternalDataLayerFactory.h"
#include "UObject/StrongObjectPtr.h"
#include "UObject/MetaData.h"
#include "Commandlets/Commandlet.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeatureData.h"
#include "PackageSourceControlHelper.h"
#include "SourceControlHelpers.h"
#include "Misc/PathViews.h"
#include "AssetSelection.h"
#include "PackageTools.h"
#include "IAssetTools.h"
#include "Algo/Find.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(GameFeatureActionConvertContentBundleWorldPartitionBuilder)

DEFINE_LOG_CATEGORY_STATIC(LogConvertContentBundleBuilder, All, All);

UGameFeatureActionConvertContentBundleWorldPartitionBuilder::UGameFeatureActionConvertContentBundleWorldPartitionBuilder(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
	, bReportOnly(false)
	, bRemoveContentBundleAction(false)
{}

bool UGameFeatureActionConvertContentBundleWorldPartitionBuilder::PreRun(UWorld* World, FPackageSourceControlHelper& PackageHelper)
{
	TArray<FString> Tokens, Switches;
	TMap<FString, FString> CommandLineParams;
	UCommandlet::ParseCommandLine(*GetBuilderArgs(), Tokens, Switches, CommandLineParams);

	FString const* DestFolderPtr = CommandLineParams.Find(TEXT("DestinationFolder"));
	DestinationFolder = DestFolderPtr ? *DestFolderPtr : FString();
	bReportOnly = Switches.Contains(TEXT("ReportOnly"));
	bRemoveContentBundleAction = Switches.Contains(TEXT("RemoveContentBundleAction"));

	if (FString const* ContentBundlesToConvertString = CommandLineParams.Find(TEXT("ContentBundles")))
	{
		if ((*ContentBundlesToConvertString).ParseIntoArray(ContentBundlesToConvert, TEXT("+")) == 0)
		{
			UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to parse ContentBundles argument '%s'."), **ContentBundlesToConvertString);
			return false;
		}
	}

	return true;
}

bool UGameFeatureActionConvertContentBundleWorldPartitionBuilder::RunInternal(UWorld* World, const FCellInfo& InCellInfo, FPackageSourceControlHelper& PackageHelper)
{
	TStrongObjectPtr<UExternalDataLayerFactory> ExternalDataLayerFactory(NewObject<UExternalDataLayerFactory>(GetTransientPackage()));

	UContentBundleManager* ContentBundleManager = World->ContentBundleManager;
	if (!ContentBundleManager)
	{
		UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("World %s does not have content bundles."), *World->GetName());
		return false;
	}

	TArray<TSharedPtr<FContentBundleEditor>> ContentBundles;
	if (!ContentBundleManager->GetEditorContentBundle(ContentBundles))
	{
		UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("World %s does not have content bundles."), *World->GetName());
		return false;
	}

	UExternalDataLayerManager* ExternalDataLayerManager = UExternalDataLayerManager::GetExternalDataLayerManager(World);
	if (!ExternalDataLayerManager)
	{
		UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("World %s does not have an ExternalDataLayerManager"), *World->GetName());
		return false;
	}

	UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Found %d Content bundles in World %s"), ContentBundles.Num(), *World->GetName());

	TArray<TSharedPtr<FContentBundleEditor>> ContentBundlesToProcess;
	if (!ContentBundlesToConvert.IsEmpty())
	{
		for (TSharedPtr<FContentBundleEditor>& ContentBundle : ContentBundles)
		{
			if (!ContentBundlesToConvert.Contains(ContentBundle->GetDisplayName()))
			{
				UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Skipping %s: Not in the list of content bundles to convert."), *ContentBundle->GetDisplayName());
				continue;
			}
			ContentBundlesToProcess.Add(ContentBundle);
		}
	}
	else
	{
		ContentBundlesToProcess = ContentBundles;
	}

	bool bIsSuccess = true;
	for (TSharedPtr<FContentBundleEditor>& ContentBundle : ContentBundlesToProcess)
	{
		TSet<UPackage*> PackagesToSave;
		TSet<UPackage*> PackagesToDelete;

		TSharedPtr<FContentBundleClient> ContentBundleClient = ContentBundle->GetClient().Pin();
		if (!ContentBundleClient.IsValid())
		{
			UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to access Client of Content Bundle %s"), *ContentBundle->GetDisplayName());
			bIsSuccess = false;
			continue;
		}

		if (ContentBundleClient->GetState() == EContentBundleClientState::Registered)
		{
			if (const FContentBundleBase* CB = ContentBundleManager->GetContentBundle(World, ContentBundle->GetDescriptor()->GetGuid()))
			{
				if (CB->GetStatus() != EContentBundleStatus::ContentInjected)
				{
					UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Requesting forced injection of Content Bundle %s for conversion purposes."), *ContentBundle->GetDisplayName());
					ContentBundleClient->RequestContentInjection();
				}
			}
		}

		if (const FContentBundleBase* CB = ContentBundleManager->GetContentBundle(World, ContentBundle->GetDescriptor()->GetGuid()))
		{
			if (CB->GetStatus() != EContentBundleStatus::ContentInjected)
			{
				UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Skipping %s: Not injected in this world."), *ContentBundle->GetDisplayName());
				continue;
			}
		}

		UActorDescContainerInstance* ContentBundleContainerInstance = ContentBundle->GetActorDescContainerInstance().Get();
		const UContentBundleDescriptor* ContentBundleDescriptor = ContentBundle->GetDescriptor();
		UGameFeatureAction_AddWPContent* OldAddWPContentAction = ContentBundleDescriptor->GetTypedOuter<UGameFeatureAction_AddWPContent>();
		UGameFeatureData* GameFeatureData = ContentBundleDescriptor->GetTypedOuter<UGameFeatureData>();
		if (!OldAddWPContentAction || !GameFeatureData || !ContentBundleContainerInstance)
		{
			UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Skipping invalid Content Bundle %s"), *ContentBundle->GetDisplayName());
			bIsSuccess = false;
			continue;
		}

		if (ContentBundleContainerInstance->IsEmpty())
		{
			UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Skipping conversion : Empty Content Bundle %s"), *ContentBundle->GetDisplayName());
			SkippedEmptyContentBundles.Add(ContentBundle);
			continue;
		}

		// Find or create EDL Asset for this Content Bundle
		UExternalDataLayerAsset* ExternalDataLayerAsset = GetOrCreateExternalDataLayerAsset(ContentBundleDescriptor, ExternalDataLayerFactory.Get(), PackagesToSave);
		UE_CLOG(ExternalDataLayerAsset, LogConvertContentBundleBuilder, Log, TEXT("Converting Content Bundle %s using External Data Layer Asset %s."), *ContentBundle->GetDisplayName(), *ExternalDataLayerAsset->GetPathName());
		if (!ExternalDataLayerAsset)
		{
			UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to retrieve or create External Data Layer Asset for Content Bundle %s"), *ContentBundle->GetDisplayName());
			bIsSuccess = false;
			continue;
		}

		// Allow injection of External Data Layer (only used by this builder)
		UExternalDataLayerEngineSubsystem::FForcedExternalDataLayerInjectionKey ForcedInjectionKey(World, ExternalDataLayerAsset);
		UExternalDataLayerEngineSubsystem::Get().ForcedAllowInjection.Add(ForcedInjectionKey);
		ON_SCOPE_EXIT { UExternalDataLayerEngineSubsystem::Get().ForcedAllowInjection.Remove(ForcedInjectionKey); };

		// Find existing GameFeatureAction_AddWorldPartitionContent for this EDL Asset
		UGameFeatureAction* const* ExistingAddWorldPartitionContent = Algo::FindByPredicate(GameFeatureData->GetActions(), [ExternalDataLayerAsset](UGameFeatureAction* Action) { return Action && Action->IsA<UGameFeatureAction_AddWorldPartitionContent>() && Cast<UGameFeatureAction_AddWorldPartitionContent>(Action)->GetExternalDataLayerAsset() == ExternalDataLayerAsset; });
		UGameFeatureAction_AddWorldPartitionContent* AddWorldPartitionContent = ExistingAddWorldPartitionContent ? Cast<UGameFeatureAction_AddWorldPartitionContent>(*ExistingAddWorldPartitionContent) : nullptr;
		if (!AddWorldPartitionContent)
		{
			// Find existing GameFeatureAction_AddWorldPartitionContent with no EDL asset
			UGameFeatureAction* const* ExistingAddWorldPartitionContentWithNoEDLAsset = Algo::FindByPredicate(GameFeatureData->GetActions(), [ExternalDataLayerAsset](UGameFeatureAction* Action) { return Action && Action->IsA<UGameFeatureAction_AddWorldPartitionContent>() && Cast<UGameFeatureAction_AddWorldPartitionContent>(Action)->GetExternalDataLayerAsset() == nullptr; });
			AddWorldPartitionContent = ExistingAddWorldPartitionContentWithNoEDLAsset ? Cast<UGameFeatureAction_AddWorldPartitionContent>(*ExistingAddWorldPartitionContentWithNoEDLAsset) : nullptr;
			if (!AddWorldPartitionContent)
			{
				// Create new GameFeatureAction_AddWorldPartitionContent for this EDL Asset
				AddWorldPartitionContent = NewObject<UGameFeatureAction_AddWorldPartitionContent>(GameFeatureData);
				GameFeatureData->GetMutableActionsInEditor().Add(AddWorldPartitionContent);
				PackagesToSave.Add(GameFeatureData->GetPackage());
				UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Added new Action of type 'GameFeatureAction_AddWorldPartitionContent' to GameFeatureData %s using External Data Layer Asset %s while converting Content Bundle %s."), *GameFeatureData->GetName(), *ExternalDataLayerAsset->GetPathName(), *ContentBundle->GetDisplayName());
			}

			// Set EDL Asset on Action if necessary
			if (AddWorldPartitionContent->ExternalDataLayerAsset != ExternalDataLayerAsset)
			{
				check(!AddWorldPartitionContent->ExternalDataLayerAsset);
				AddWorldPartitionContent->ExternalDataLayerAsset = ExternalDataLayerAsset;
				PackagesToSave.Add(GameFeatureData->GetPackage());
			}
		}
		
		// Backup the source ContentBundle Guid (to facilitate potential revert operation)
		AddWorldPartitionContent->ConvertedContentBundleGuid = ContentBundleDescriptor->GetGuid();
		PackagesToSave.Add(GameFeatureData->GetPackage());
		
		// Manually call OnExternalDataLayerAssetChanged to register the newly created GameFeatureAction_AddWorldPartitionContent
		AddWorldPartitionContent->OnExternalDataLayerAssetChanged(nullptr, ExternalDataLayerAsset);

		// First try to find existing ExternalDataLayerInstance
		UExternalDataLayerInstance* ExternalDataLayerInstance = ExternalDataLayerManager->GetExternalDataLayerInstance(ExternalDataLayerAsset);
		if (!ExternalDataLayerInstance)
		{
			// Create AWorldDataLayers and the ExternalDataLayerInstance for this EDL Asset
			FDataLayerCreationParameters CreationParams;
			CreationParams.DataLayerAsset = ExternalDataLayerAsset;
			CreationParams.WorldDataLayers = World->GetWorldDataLayers();
			ExternalDataLayerInstance = Cast<UExternalDataLayerInstance>(UDataLayerEditorSubsystem::Get()->CreateDataLayerInstance(CreationParams));
			UE_CLOG(ExternalDataLayerInstance, LogConvertContentBundleBuilder, Log, TEXT("Create External Data Layer Instance %s while converting Content Bundle %s."), *ExternalDataLayerAsset->GetPathName(), *ContentBundle->GetDisplayName());
			if (!ExternalDataLayerInstance)
			{
				UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to create External Data Layer Instance for External Data Layer Asset %s while converting Content Bundle %s"), *ExternalDataLayerAsset->GetName(), *ContentBundle->GetDisplayName());
				bIsSuccess = false;
				continue;
			}
			check(!ExternalDataLayerInstance->IsPackageExternal());
		}

		AWorldDataLayers* EDLWorldDataLayers = ExternalDataLayerInstance->GetDirectOuterWorldDataLayers();
		if (!EDLWorldDataLayers)
		{
			UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to find a valid WorldDataLayers actor for External Data Layer Asset %s while converting Content Bundle %s"), *ExternalDataLayerAsset->GetName(), *ContentBundle->GetDisplayName());
			bIsSuccess = false;
			continue;
		}

		// By default, newly create AWorldDataLayers will be marked as read-only which would block assignation of actors to the External Data Layer
		// Temporarily remove package flag
		bool bIsNewEDLWorldDataLayers = EDLWorldDataLayers->GetPackage()->HasAnyPackageFlags(PKG_NewlyCreated);
		if (bIsNewEDLWorldDataLayers)
		{
			EDLWorldDataLayers->GetPackage()->ClearPackageFlags(PKG_NewlyCreated);
			PackagesToSave.Add(EDLWorldDataLayers->GetPackage());
		}

		TArray<FGuid> ConvertedActorGuids;
		TArray<FWorldPartitionReference> ActorReferences;
		bool bContentBundleActorConversionSuccess = true;
		// Convert actors from Content Bundle to EDL
		for (UActorDescContainerInstance::TIterator<> It(ContentBundleContainerInstance); It; ++It)
		{
			FWorldPartitionReference ActorRef(ContentBundleContainerInstance, It->GetGuid());
			if (!ActorRef.IsValid())
			{
				UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to load actor %s(%s). Actor won't be converted."), *It->GetActorLabelOrName().ToString(), *It->GetActorPackage().ToString());
				bContentBundleActorConversionSuccess = false;
				break;
			}

			ActorReferences.Add(ActorRef);
			AActor* Actor = ActorRef.GetActor();
			UPackage* OldActorPackage = Actor->GetExternalPackage();
			ResetLoaders(OldActorPackage);

			FText FailureReason;
			if (!FExternalDataLayerHelper::MoveActorsToExternalDataLayer({ Actor }, ExternalDataLayerInstance, &FailureReason))
			{
				UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Can't create package for actor %s. %s"), *Actor->GetActorNameOrLabel(), *FailureReason.ToString());
				bContentBundleActorConversionSuccess = false;
				break;
			}

			check(Actor->GetExternalDataLayerAsset() == ExternalDataLayerAsset);
			check(!Actor->GetContentBundleGuid().IsValid());
			UPackage* NewPackage = Actor->GetPackage();
			PackagesToSave.Add(NewPackage);

			check(OldActorPackage->GetName() != NewPackage->GetName());
			PackagesToDelete.Add(OldActorPackage);
			
			UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Converted Actor %s(%s) to %s."), *Actor->GetName(), *NewPackage->GetName(), *ExternalDataLayerAsset->GetName());

			ConvertedActorGuids.Add(It->GetGuid());
		}

		// Remove actors from Content Bundle container (since we recycled the same Actor/guid)
		for (const FGuid& ActorGuid : ConvertedActorGuids)
		{
			ContentBundleContainerInstance->RemoveActor(ActorGuid);
		}
		
		if (bContentBundleActorConversionSuccess && bRemoveContentBundleAction)
		{
			// Remove Content Bundle GameFeatureData action
			GameFeatureData->GetMutableActionsInEditor().Remove(OldAddWPContentAction);
			PackagesToSave.Add(GameFeatureData->GetPackage());
		}

		// Restore package flag
		if (bIsNewEDLWorldDataLayers)
		{
			EDLWorldDataLayers->GetPackage()->SetPackageFlags(PKG_NewlyCreated);
		}
		
		// Log
		for (UPackage* PackageToSave : PackagesToSave)
		{
			UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Package to save: %s"), *PackageToSave->GetPathName());
		}

		for (UPackage* PackageToDelete : PackagesToDelete)
		{
			UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Package to delete: %s"), *PackageToDelete->GetPathName());
		}

		if (bContentBundleActorConversionSuccess && !bReportOnly)
		{
			if (!UWorldPartitionBuilder::SavePackages(PackagesToSave.Array(), PackageHelper))
			{
				UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to save packages. Conversion sanity is not guaranteed. Consult log for details."));
				bContentBundleActorConversionSuccess = false;
			}
			else if (!UWorldPartitionBuilder::DeletePackages(PackagesToDelete.Array(), PackageHelper))
			{
				UE_LOG(LogConvertContentBundleBuilder, Error, TEXT("Failed to delete packages. Conversion sanity is not guaranteed. Consult log for details."));
				bContentBundleActorConversionSuccess = false;
			}
		}

		if (bContentBundleActorConversionSuccess)
		{
			ConvertedContentBundles.Add(ContentBundle);
			FinalReport.Reserve(FinalReport.Num() + PackagesToSave.Num() + PackagesToDelete.Num() + 5);
			FinalReport.Add(TEXT("------------------------------------------------------------------------"));
			FinalReport.Add(FString::Printf(TEXT("Converted Content Bundle '%s' (%s) to EDL Asset '%s'"), *ContentBundle->GetDisplayName(), *ContentBundle->GetDescriptor()->GetGuid().ToString(), *ExternalDataLayerAsset->GetPathName()));
			FinalReport.Add(TEXT("------------------------------------------------------------------------"));
			FinalReport.Add(FString::Printf(TEXT("[+] Added %d packages: "), PackagesToSave.Num()));
			for (UPackage* PackageToSave : PackagesToSave)
			{
				FinalReport.Add(FString::Printf(TEXT(" |- %s"), *PackageToSave->GetPathName()));
			}
			if (PackagesToDelete.Num())
			{
				FinalReport.Add(FString::Printf(TEXT("[+] Deleted %d packages: "), PackagesToDelete.Num()));
				for (UPackage* PackageToDelete : PackagesToDelete)
				{
					FinalReport.Add(FString::Printf(TEXT(" |- %s"), *PackageToDelete->GetPathName()));
				}
			}
		}
		else
		{
			bIsSuccess = false;
		}
	}

	if (SkippedEmptyContentBundles.Num())
	{
		FinalReport.Reserve(FinalReport.Num() + SkippedEmptyContentBundles.Num() + 4);
		FinalReport.Add(TEXT("------------------------------------------------------------------------"));
		FinalReport.Add(FString::Printf(TEXT("%d Skipped Empty Content Bundles: "), SkippedEmptyContentBundles.Num()));
		FinalReport.Add(TEXT("------------------------------------------------------------------------"));
		FinalReport.Add(TEXT("[+] Content Bundles: "));
		for (const TSharedPtr<FContentBundleEditor>& CB : SkippedEmptyContentBundles)
		{
			FinalReport.Add(FString::Printf(TEXT(" |- %s (%s)"), *CB->GetDisplayName(), *CB->GetDescriptor()->GetGuid().ToString()));
		}
	}

	TArray<TSharedPtr<FContentBundleEditor>> FailedContentBundles;
	for (TSharedPtr<FContentBundleEditor>& ContentBundle : ContentBundlesToProcess)
	{
		if (!ConvertedContentBundles.Contains(ContentBundle) && !SkippedEmptyContentBundles.Contains(ContentBundle))
		{
			FailedContentBundles.Add(ContentBundle);
		}
	}

	if (FailedContentBundles.Num())
	{
		FinalReport.Reserve(FinalReport.Num() + FailedContentBundles.Num() + 4);
		FinalReport.Add(TEXT("------------------------------------------------------------------------"));
		FinalReport.Add(FString::Printf(TEXT("%d Failed Content Bundle Conversions: "), FailedContentBundles.Num()));
		FinalReport.Add(TEXT("------------------------------------------------------------------------"));
		FinalReport.Add(TEXT("[+] Content Bundles: "));
		for (const TSharedPtr<FContentBundleEditor>& CB : FailedContentBundles)
		{
			FinalReport.Add(FString::Printf(TEXT(" |- %s (%s)"), *CB->GetDisplayName(), *CB->GetDescriptor()->GetGuid().ToString()));
		}
	}

	if (FinalReport.Num())
	{
		UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("================================================================================================="));
		UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("Content Bundle Conversion : Found(%d) | Converted(%d) | Empty(%d) | Failed(%d)"), ContentBundlesToProcess.Num(), ConvertedContentBundles.Num(), SkippedEmptyContentBundles.Num(), FailedContentBundles.Num());
		UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("================================================================================================="));
		for (const FString& Str : FinalReport)
		{
			UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("%s"), *Str);
		}
		UE_LOG(LogConvertContentBundleBuilder, Log, TEXT("================================================================================================="));
	}

	return bIsSuccess;
}

UExternalDataLayerAsset* UGameFeatureActionConvertContentBundleWorldPartitionBuilder::GetOrCreateExternalDataLayerAsset(const UContentBundleDescriptor* InContentBundleDescriptor, UExternalDataLayerFactory* InExternalDataLayerFactory, TSet<UPackage*>& OutPackagesToSave) const
{
	const FString ContentBundleDescriptorPackage = InContentBundleDescriptor->GetPackage()->GetName();
	const FString AssetPath = FString(FPathViews::GetMountPointNameFromPath(ContentBundleDescriptorPackage, nullptr, false)) / DestinationFolder;
	const FString AssetName = UPackageTools::SanitizePackageName(InContentBundleDescriptor->GetDisplayName());
	const FString PackageName = AssetPath / AssetName;
	const FSoftObjectPath Path(FTopLevelAssetPath(FName(PackageName), FName(AssetName)).ToString());
	const FString ObjectPath = Path.ToString();
	if (UExternalDataLayerAsset* ExistingExternalDataLayerAsset = LoadObject<UExternalDataLayerAsset>(nullptr, *ObjectPath, nullptr, LOAD_Quiet | LOAD_NoWarn))
	{
		return ExistingExternalDataLayerAsset;
	}
	
	UObject* Asset = IAssetTools::Get().CreateAsset(AssetName, AssetPath, UExternalDataLayerAsset::StaticClass(), InExternalDataLayerFactory);
	UExternalDataLayerAsset* NewExternalDataLayerAsset = Asset ? CastChecked<UExternalDataLayerAsset>(Asset) : nullptr;
	UE_CLOG(!NewExternalDataLayerAsset, LogConvertContentBundleBuilder, Error, TEXT("Failed to create external data layer asset for %s."), *InContentBundleDescriptor->GetDisplayName());
	if (NewExternalDataLayerAsset)
	{
		OutPackagesToSave.Add(NewExternalDataLayerAsset->GetPackage());
	}
	return NewExternalDataLayerAsset;
}

GameFeatureActionConvertContentBundleWorldPartitionBuilder.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "WorldPartition/WorldPartitionBuilder.h"
#include "GameFeatureActionConvertContentBundleWorldPartitionBuilder.generated.h"

class UPackage;
class UExternalDataLayerFactory;
class UExternalDataLayerAsset;
class UContentBundleDescriptor;
class FContentBundleEditor;

UCLASS()
class UGameFeatureActionConvertContentBundleWorldPartitionBuilder : public UWorldPartitionBuilder
{
	GENERATED_UCLASS_BODY()
public:
	// UWorldPartitionBuilder interface begin
	virtual bool RequiresCommandletRendering() const override { return false; }
	virtual ELoadingMode GetLoadingMode() const override { return ELoadingMode::Custom; }
	virtual bool PreRun(UWorld* World, FPackageSourceControlHelper& PackageHelper) override;
	virtual bool RunInternal(UWorld* World, const FCellInfo& InCellInfo, FPackageSourceControlHelper& PackageHelper) override;
	// UWorldPartitionBuilder interface end

private:
	UExternalDataLayerAsset* GetOrCreateExternalDataLayerAsset(const UContentBundleDescriptor* InContentBundleDescriptor, UExternalDataLayerFactory* InExternalDataLayerFactory, TSet<UPackage*>& OutPackagesToSave) const;

	TSet<TSharedPtr<FContentBundleEditor>> SkippedEmptyContentBundles;
	TSet<TSharedPtr<FContentBundleEditor>> ConvertedContentBundles;
	TArray<FString> FinalReport;
	TArray<FString> ContentBundlesToConvert;
	FString DestinationFolder;
	bool bReportOnly;
	bool bRemoveContentBundleAction;
};

GameFeatureDataDetailsCustomization.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeatureDataDetailsCustomization.h"
#include "GameFeaturePluginOperationResult.h"
#include "UObject/Package.h"
#include "GameFeaturesSubsystem.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Images/SImage.h"
#include "SGameFeatureStateWidget.h"
#include "Widgets/Notifications/SErrorText.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"

#include "Interfaces/IPluginManager.h"
#include "Features/IPluginsEditorFeature.h"
#include "Features/EditorFeatures.h"
#include "Features/IModularFeatures.h"
#include "Misc/MessageDialog.h"
#include "Misc/Paths.h"

#include "GameFeatureData.h"
#include "GameFeatureTypes.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Text/STextBlock.h"

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////////
// FGameFeatureDataDetailsCustomization

TSharedRef<IDetailCustomization> FGameFeatureDataDetailsCustomization::MakeInstance()
{
	return MakeShareable(new FGameFeatureDataDetailsCustomization);
}

void FGameFeatureDataDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
	ErrorTextWidget = SNew(SErrorText)
		.ToolTipText(LOCTEXT("ErrorTooltip", "The error raised while attempting to change the state of this feature"));

	// Create a category so this is displayed early in the properties
	IDetailCategoryBuilder& TopCategory = DetailBuilder.EditCategory("Feature State", FText::GetEmpty(), ECategoryPriority::Important);

	PluginURL.Reset();
	ObjectsBeingCustomized.Empty();
	DetailBuilder.GetObjectsBeingCustomized(/*out*/ ObjectsBeingCustomized);

	if (ObjectsBeingCustomized.Num() == 1 && !ObjectsBeingCustomized[0]->GetPackage()->HasAnyPackageFlags(PKG_ForDiffing))
	{
		const UGameFeatureData* GameFeature = CastChecked<const UGameFeatureData>(ObjectsBeingCustomized[0]);

		TArray<FString> PathParts;
		GameFeature->GetOutermost()->GetName().ParseIntoArray(PathParts, TEXT("/"));

		UGameFeaturesSubsystem& Subsystem = UGameFeaturesSubsystem::Get();
		Subsystem.GetPluginURLByName(PathParts[0], /*out*/ PluginURL);
		PluginPtr = IPluginManager::Get().FindPlugin(PathParts[0]);

		const float Padding = 8.0f;

		if (PluginPtr.IsValid())
		{
			const FString ShortFilename = FPaths::GetCleanFilename(PluginPtr->GetDescriptorFileName());
			FDetailWidgetRow& EditPluginRow = TopCategory.AddCustomRow(LOCTEXT("InitialStateSearchText", "Initial State Edit Plugin"))
				.NameContent()
				[
					SNew(STextBlock)
					.Text(LOCTEXT("InitialState", "Initial State"))
					.ToolTipText(LOCTEXT("InitialStateTooltip", "The initial or default state of this game feature (determines the state that it will be in at game/editor startup)"))
					.Font(DetailBuilder.GetDetailFont())
				]

				.ValueContent()
				[
					SNew(SHorizontalBox)
					+SHorizontalBox::Slot()
					.AutoWidth()
					.Padding(0.0f, 0.0f, Padding, 0.0f)
					.VAlign(VAlign_Center)
					[
						SNew(STextBlock)
						.Text(this, &FGameFeatureDataDetailsCustomization::GetInitialStateText)
						.Font(DetailBuilder.GetDetailFont())
					]

					+SHorizontalBox::Slot()
					.AutoWidth()
					.VAlign(VAlign_Center)
					[
						SNew(SButton)
						.Text(LOCTEXT("EditPluginButton", "Edit Plugin"))
						.OnClicked_Lambda([this]()
							{
								IModularFeatures& ModularFeatures = IModularFeatures::Get();
								if (ModularFeatures.IsModularFeatureAvailable(EditorFeatures::PluginsEditor))
								{
									ModularFeatures.GetModularFeature<IPluginsEditorFeature>(EditorFeatures::PluginsEditor).OpenPluginEditor(PluginPtr.ToSharedRef(), nullptr, FSimpleDelegate());
								}
								else
								{
									FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("CannotEditPlugin_PluginBrowserDisabled", "Cannot open plugin editor because the PluginBrowser plugin is disabled)"));
								}
								return FReply::Handled();
							})
					]

				];
		}

		FDetailWidgetRow& ControlRow = TopCategory.AddCustomRow(LOCTEXT("ControlSearchText", "Plugin State Control"))
			.NameContent()
			[
				SNew(STextBlock)
				.Text(LOCTEXT("CurrentState", "Current State"))
				.ToolTipText(LOCTEXT("CurrentStateTooltip", "The current state of this game feature"))
				.Font(DetailBuilder.GetDetailFont())
			]

			.ValueContent()
			.MinDesiredWidth(400.0f)
			[
				SNew(SVerticalBox)
				+SVerticalBox::Slot()
				.AutoHeight()
				[
					SNew(SGameFeatureStateWidget)
					.CurrentState(this, &FGameFeatureDataDetailsCustomization::GetCurrentState)
					.OnStateChanged(this, &FGameFeatureDataDetailsCustomization::ChangeDesiredState)
				]
				+SVerticalBox::Slot()
				.HAlign(HAlign_Left)
				.Padding(0.0f, 4.0f, 0.0f, 0.0f)
				[
					SNew(SHorizontalBox)
					.Visibility(this, &FGameFeatureDataDetailsCustomization::GetVisbililty)
					+SHorizontalBox::Slot()
					.AutoWidth()
					.Padding(Padding)
					.VAlign(VAlign_Center)
					[
						SNew(SImage)
						.Image(FAppStyle::Get().GetBrush("Icons.Lock"))
					]
					+SHorizontalBox::Slot()
					.FillWidth(1.0f)
					.Padding(FMargin(0.f, Padding, Padding, Padding))
					.VAlign(VAlign_Center)
					[
						SNew(STextBlock)
						.WrapTextAt(300.0f)
						.Text(LOCTEXT("Active_PreventingEditing", "Deactivate the feature before editing the Game Feature Data"))
						.Font(DetailBuilder.GetDetailFont())
						.ColorAndOpacity(FAppStyle::Get().GetSlateColor(TEXT("Colors.AccentYellow")))
					]
				]
				+SVerticalBox::Slot()
				.HAlign(HAlign_Center)
				[
					ErrorTextWidget.ToSharedRef()
				]
			];

		FDetailWidgetRow& TagsRow = TopCategory.AddCustomRow(LOCTEXT("TagSearchText", "Gameplay Tag Config Path"))
			.NameContent()
			[
				SNew(STextBlock)
				.Text(LOCTEXT("TagConfigPath", "Gameplay Tag Config Path"))
				.ToolTipText(LOCTEXT("TagConfigPathTooltip", "Path to search for Gameplay Tag ini files. To create feature-specific tags use Add New Tag Source with this path and then Add New Gameplay Tag with that Tag Source."))
				.Font(DetailBuilder.GetDetailFont())
			]
			.ValueContent()
			.MinDesiredWidth(400.0f)
			[
				SNew(STextBlock)
				.Text(this, &FGameFeatureDataDetailsCustomization::GetTagConfigPathText)
				.Font(DetailBuilder.GetDetailFont())
		];

		if (FPlatformMisc::IsDebuggerPresent())
		{
			const FText Label = LOCTEXT("BreakStateLabel", "Break when State changes");
			const FText Tooltip = LOCTEXT("BreakStateTooltip", "When enabled, will trigger a breakpoint any time the state of this plugin changes");
			TopCategory.AddCustomRow(Label)
			.NameContent()
			[
				SNew(STextBlock)
				.Text(Label)
				.ToolTipText(Tooltip)
				.Font(DetailBuilder.GetDetailFont())
			]
			.ValueContent()
			[
				SNew(SCheckBox)
				.IsChecked( this, &FGameFeatureDataDetailsCustomization::GetDebugStateEnabled)
				.OnCheckStateChanged( this, &FGameFeatureDataDetailsCustomization::SetDebugStateEnabled)
				.ToolTipText(Tooltip)
			];
		}
//@TODO: This disables the mode switcher widget too (and it's a const cast hack...)
// 		if (IDetailsView* ConstHackDetailsView = const_cast<IDetailsView*>(DetailBuilder.GetDetailsView()))
// 		{
// 			ConstHackDetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateLambda([CapturedThis = this] { return CapturedThis->GetCurrentState() != EGameFeaturePluginState::Active; }));
// 		}
	}
}

void FGameFeatureDataDetailsCustomization::ChangeDesiredState(EGameFeaturePluginState DesiredState)
{
	EGameFeatureTargetState TargetState = EGameFeatureTargetState::Installed;
	switch (DesiredState)
	{
	case EGameFeaturePluginState::Installed:
		TargetState = EGameFeatureTargetState::Installed;
		break;
	case EGameFeaturePluginState::Registered:
		TargetState = EGameFeatureTargetState::Registered;
		break;
	case EGameFeaturePluginState::Loaded:
		TargetState = EGameFeatureTargetState::Loaded;
		break;
	case EGameFeaturePluginState::Active:
		TargetState = EGameFeatureTargetState::Active;
		break;
	}

	ErrorTextWidget->SetError(FText::GetEmpty());
	const TWeakPtr<FGameFeatureDataDetailsCustomization> WeakThisPtr = StaticCastSharedRef<FGameFeatureDataDetailsCustomization>(AsShared());

	UGameFeaturesSubsystem& Subsystem = UGameFeaturesSubsystem::Get();
	Subsystem.ChangeGameFeatureTargetState(PluginURL, TargetState, FGameFeaturePluginDeactivateComplete::CreateStatic(&FGameFeatureDataDetailsCustomization::OnOperationCompletedOrFailed, WeakThisPtr));
}


EGameFeaturePluginState FGameFeatureDataDetailsCustomization::GetCurrentState() const
{
	if (PluginURL.IsEmpty())
	{
		return EGameFeaturePluginState::Uninitialized;
	}
	return UGameFeaturesSubsystem::Get().GetPluginState(PluginURL);
}

EVisibility FGameFeatureDataDetailsCustomization::GetVisbililty() const
{
	return (GetCurrentState() == EGameFeaturePluginState::Active) ? EVisibility::Visible : EVisibility::Collapsed;
}

ECheckBoxState FGameFeatureDataDetailsCustomization::GetDebugStateEnabled() const
{
	if (PluginURL.IsEmpty())
	{
		return ECheckBoxState::Undetermined;
	}
	return UGameFeaturesSubsystem::Get().GetPluginDebugStateEnabled(PluginURL) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}

void FGameFeatureDataDetailsCustomization::SetDebugStateEnabled(ECheckBoxState InCheckState)
{
	if (!PluginURL.IsEmpty())
	{
		return UGameFeaturesSubsystem::Get().SetPluginDebugStateEnabled(PluginURL, InCheckState == ECheckBoxState::Checked);
	}
}

FText FGameFeatureDataDetailsCustomization::GetInitialStateText() const
{
	const EBuiltInAutoState AutoState = UGameFeaturesSubsystem::DetermineBuiltInInitialFeatureState(PluginPtr->GetDescriptor().CachedJson, FString());
	const EGameFeaturePluginState InitialState = UGameFeaturesSubsystem::ConvertInitialFeatureStateToTargetState(AutoState);
	return SGameFeatureStateWidget::GetDisplayNameOfState(InitialState);
}

FText FGameFeatureDataDetailsCustomization::GetTagConfigPathText() const
{
	if (PluginURL.IsEmpty())
	{
		return LOCTEXT("TagConfigPathInvalid", "Invalid Plugin");
	}

	FString PluginFile = UGameFeaturesSubsystem::Get().GetPluginFilenameFromPluginURL(PluginURL);
	FString PluginFolder = FPaths::GetPath(PluginFile);
	FString TagFolder = PluginFolder / TEXT("Config") / TEXT("Tags");
	if (FPaths::IsUnderDirectory(TagFolder, FPaths::ProjectDir()))
	{
		FPaths::MakePathRelativeTo(TagFolder, *FPaths::ProjectDir());
	}
	return FText::AsCultureInvariant(TagFolder);
}

void FGameFeatureDataDetailsCustomization::OnOperationCompletedOrFailed(const UE::GameFeatures::FResult& Result, const TWeakPtr<FGameFeatureDataDetailsCustomization> WeakThisPtr)
{
	if (Result.HasError())
	{
		TSharedPtr<FGameFeatureDataDetailsCustomization> StrongThis = WeakThisPtr.Pin();
		if (StrongThis.IsValid())
		{
			StrongThis->ErrorTextWidget->SetError(FText::AsCultureInvariant(Result.GetError()));
		}
	}
}

//////////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE

GameFeatureDataDetailsCustomization.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "IDetailCustomization.h"
#include "GameFeatureTypesFwd.h"
#include "UObject/WeakObjectPtrTemplates.h"

namespace UE::GameFeatures { struct FResult; }

class IDetailLayoutBuilder;
class SErrorText;
class IPlugin;
struct EVisibility;

enum class ECheckBoxState : uint8;

//////////////////////////////////////////////////////////////////////////
// FGameFeatureDataDetailsCustomization

class FGameFeatureDataDetailsCustomization : public IDetailCustomization
{
public:
	/** Makes a new instance of this detail layout class for a specific detail view requesting it */
	static TSharedRef<IDetailCustomization> MakeInstance();

	// IDetailCustomization interface
	virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override;
	// End of IDetailCustomization interface

protected:
	void ChangeDesiredState(EGameFeaturePluginState State);

	EGameFeaturePluginState GetCurrentState() const;
	EVisibility GetVisbililty() const;

	ECheckBoxState GetDebugStateEnabled() const;
	void SetDebugStateEnabled(ECheckBoxState InCheckState);
	
	FText GetInitialStateText() const;
	FText GetTagConfigPathText() const;

	static void OnOperationCompletedOrFailed(const UE::GameFeatures::FResult& Result, const TWeakPtr<FGameFeatureDataDetailsCustomization> WeakThisPtr);
protected:
	TArray<TWeakObjectPtr<UObject>> ObjectsBeingCustomized;
	FString PluginURL;
	TSharedPtr<IPlugin> PluginPtr;

	TSharedPtr<SErrorText> ErrorTextWidget;
};

GameFeaturePluginMetadataCustomization.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturePluginMetadataCustomization.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Dom/JsonValue.h"
#include "Interfaces/IPluginManager.h"
#include "SGameFeatureStateWidget.h"

#include "GameFeaturesSubsystem.h"
#include "GameFeatureTypes.h"
#include "Widgets/Text/STextBlock.h"

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////////
// FGameFeaturePluginMetadataCustomization

void FGameFeaturePluginMetadataCustomization::CustomizeDetails(FPluginEditingContext& InPluginContext, IDetailLayoutBuilder& DetailBuilder)
{
	Plugin = InPluginContext.PluginBeingEdited;
	
	const EBuiltInAutoState AutoState = UGameFeaturesSubsystem::DetermineBuiltInInitialFeatureState(Plugin->GetDescriptor().CachedJson, FString());
	InitialState = UGameFeaturesSubsystem::ConvertInitialFeatureStateToTargetState(AutoState);

	IDetailCategoryBuilder& TopCategory = DetailBuilder.EditCategory("Game Features", FText::GetEmpty(), ECategoryPriority::Important);

	FDetailWidgetRow& ControlRow = TopCategory.AddCustomRow(LOCTEXT("ControlSearchText", "Plugin State Control"))
		.NameContent()
		[
			SNew(STextBlock)
			.Text(LOCTEXT("InitialState", "Initial State"))
			.Font(DetailBuilder.GetDetailFont())
		]
		.ValueContent()
		[
			SNew(SGameFeatureStateWidget)
			.ToolTipText(LOCTEXT("DefaultStateSwitcherTooltip", "Change the default initial state of this game feature"))
			.CurrentState(this, &FGameFeaturePluginMetadataCustomization::GetDefaultState)
			.OnStateChanged(this, &FGameFeaturePluginMetadataCustomization::ChangeDefaultState)
		];
}

void FGameFeaturePluginMetadataCustomization::CommitEdits(FPluginDescriptor& Descriptor)
{
	FString StateStr;
	switch (InitialState)
	{
	case EGameFeaturePluginState::Installed:
		StateStr = TEXT("Installed");
		break;
	case EGameFeaturePluginState::Registered:
		StateStr = TEXT("Registered");
		break;
	case EGameFeaturePluginState::Loaded:
		StateStr = TEXT("Loaded");
		break;
	case EGameFeaturePluginState::Active:
		StateStr = TEXT("Active");
		break;
	}

	if (ensure(!StateStr.IsEmpty()))
	{
		Descriptor.AdditionalFieldsToWrite.FindOrAdd(TEXT("BuiltInInitialFeatureState")) = MakeShared<FJsonValueString>(StateStr);
	}
}

EGameFeaturePluginState FGameFeaturePluginMetadataCustomization::GetDefaultState() const
{
	return InitialState;
}

void FGameFeaturePluginMetadataCustomization::ChangeDefaultState(EGameFeaturePluginState DesiredState)
{
	InitialState = DesiredState;
}

//////////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE

GameFeaturePluginMetadataCustomization.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Features/IPluginsEditorFeature.h"
#include "GameFeatureTypesFwd.h"

class IDetailLayoutBuilder;
struct FPluginEditingContext;
class IPlugin;
struct FPluginDescriptor;

//////////////////////////////////////////////////////////////////////////
// FGameFeaturePluginMetadataCustomization

class FGameFeaturePluginMetadataCustomization : public FPluginEditorExtension
{
public:
	void CustomizeDetails(FPluginEditingContext& InPluginContext, IDetailLayoutBuilder& DetailBuilder);

	virtual void CommitEdits(FPluginDescriptor& Descriptor) override;
private:
	EGameFeaturePluginState GetDefaultState() const;

	void ChangeDefaultState(EGameFeaturePluginState DesiredState);

	TSharedPtr<IPlugin> Plugin;

	EGameFeaturePluginState InitialState;
};

GameFeaturePluginTemplate.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "GameFeaturePluginTemplate.h"

#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "Dom/JsonValue.h"
#include "Editor.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeaturesSubsystemSettings.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "Subsystems/AssetEditorSubsystem.h"

#define LOCTEXT_NAMESPACE "GameFeatures"

FGameFeaturePluginTemplateDescription::FGameFeaturePluginTemplateDescription(FText InName, FText InDescription, FString InOnDiskPath, FString InDefaultSubfolder, FString InDefaultPluginName, TSubclassOf<UGameFeatureData> GameFeatureDataClassOverride, FString GameFeatureDataNameOverride, EPluginEnabledByDefault InEnabledByDefault)
	: FPluginTemplateDescription(InName, InDescription, InOnDiskPath, /*bCanContainContent=*/ true, EHostType::Runtime)
{
	SortPriority = 10;
	bCanBePlacedInEngine = false;
	DefaultSubfolder = InDefaultSubfolder;
	DefaultPluginName = InDefaultPluginName;
	GameFeatureDataName = !GameFeatureDataNameOverride.IsEmpty() ? GameFeatureDataNameOverride : FString();
	GameFeatureDataClass = GameFeatureDataClassOverride != nullptr ? GameFeatureDataClassOverride : TSubclassOf<UGameFeatureData>(UGameFeatureData::StaticClass());
	PluginEnabledByDefault = InEnabledByDefault;
}

bool FGameFeaturePluginTemplateDescription::ValidatePathForPlugin(const FString& ProposedAbsolutePluginPath, FText& OutErrorMessage)
{
	if (!IsRootedInGameFeaturesRoot(ProposedAbsolutePluginPath))
	{
		OutErrorMessage = LOCTEXT("InvalidPathForGameFeaturePlugin", "Game features must be inside the Plugins/GameFeatures folder");
		return false;
	}

	OutErrorMessage = FText::GetEmpty();
	return true;
}

void FGameFeaturePluginTemplateDescription::UpdatePathWhenTemplateSelected(FString& InOutPath)
{
	if (!IsRootedInGameFeaturesRoot(InOutPath))
	{
		InOutPath = GetGameFeatureRoot();
	}
}

void FGameFeaturePluginTemplateDescription::UpdatePathWhenTemplateUnselected(FString& InOutPath)
{
	InOutPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForWrite(*FPaths::ProjectPluginsDir());
	FPaths::MakePlatformFilename(InOutPath);
}

void FGameFeaturePluginTemplateDescription::UpdatePluginNameTextWhenTemplateSelected(FText& OutPluginNameText)
{
	OutPluginNameText = FText::FromString(DefaultPluginName);
}

void FGameFeaturePluginTemplateDescription::UpdatePluginNameTextWhenTemplateUnselected(FText& OutPluginNameText)
{
	OutPluginNameText = FText::GetEmpty();
}

void FGameFeaturePluginTemplateDescription::CustomizeDescriptorBeforeCreation(FPluginDescriptor& Descriptor)
{
	Descriptor.bExplicitlyLoaded = true;
	Descriptor.AdditionalFieldsToWrite.FindOrAdd(TEXT("BuiltInInitialFeatureState")) = MakeShared<FJsonValueString>(TEXT("Active"));
	Descriptor.Category = TEXT("Game Features");

	// Game features should not be enabled by default if the game wants to strictly manage default settings in the target settings
	Descriptor.EnabledByDefault = PluginEnabledByDefault;

	if (Descriptor.Modules.Num() > 0)
	{
		Descriptor.Modules[0].Name = FName(*(Descriptor.Modules[0].Name.ToString() + TEXT("Runtime")));
	}
}

void FGameFeaturePluginTemplateDescription::OnPluginCreated(TSharedPtr<IPlugin> NewPlugin)
{
	// If the template includes an existing game feature data, do not create a new one.
	TArray<FAssetData> ObjectList;
	FARFilter AssetFilter;
	AssetFilter.ClassPaths.Add(UGameFeatureData::StaticClass()->GetClassPathName());
	AssetFilter.PackagePaths.Add(FName(NewPlugin->GetMountedAssetPath()));
	AssetFilter.bRecursiveClasses = true;
	AssetFilter.bRecursivePaths = true;

	IAssetRegistry::GetChecked().GetAssets(AssetFilter, ObjectList);

	UObject* GameFeatureDataAsset = nullptr;

	if (ObjectList.Num() <= 0)
	{
		// Create the game feature data asset
		FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
		FString const& AssetName = !GameFeatureDataName.IsEmpty() ? GameFeatureDataName : NewPlugin->GetName();
		GameFeatureDataAsset = AssetToolsModule.Get().CreateAsset(AssetName, NewPlugin->GetMountedAssetPath(), GameFeatureDataClass, /*Factory=*/ nullptr);
	}
	else
	{
		GameFeatureDataAsset = ObjectList[0].GetAsset();
	}


	// Activate the new game feature plugin
	auto AdditionalFilter = [](const FString&, const FGameFeaturePluginDetails&, FBuiltInGameFeaturePluginBehaviorOptions&) -> bool { return true; };
	UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugin(NewPlugin.ToSharedRef(), AdditionalFilter,
		FGameFeaturePluginLoadComplete::CreateLambda([GameFeatureDataAsset](const UE::GameFeatures::FResult&)
			{
				// Edit the new game feature data
				if (GameFeatureDataAsset != nullptr)
				{
					GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(GameFeatureDataAsset);
				}
			}));
}

FString FGameFeaturePluginTemplateDescription::GetGameFeatureRoot() const
{
	FString Result = IFileManager::Get().ConvertToAbsolutePathForExternalAppForWrite(*(FPaths::ProjectPluginsDir() / TEXT("GameFeatures/")));

	// Append the optional subfolder if specified.
	if (!DefaultSubfolder.IsEmpty())
	{
		Result /= DefaultSubfolder + TEXT("/");
	}

	FPaths::MakePlatformFilename(Result);
	return Result;
}

bool FGameFeaturePluginTemplateDescription::IsRootedInGameFeaturesRoot(const FString& InStr) const
{
	const FString ConvertedPath = FPaths::ConvertRelativePathToFull(FPaths::CreateStandardFilename(InStr / TEXT("test.uplugin")));
	return GetDefault<UGameFeaturesSubsystemSettings>()->IsValidGameFeaturePlugin(ConvertedPath);
}

#undef LOCTEXT_NAMESPACE

GameFeaturesEditorModule.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "Engine/AssetManager.h"
#include "Engine/AssetManagerSettings.h"
#include "Features/EditorFeatures.h"
#include "Features/IModularFeatures.h"
#include "Features/IPluginsEditorFeature.h"
#include "Framework/Notifications/NotificationManager.h"
#include "GameFeatureData.h"
#include "GameFeatureDataDetailsCustomization.h"
#include "GameFeaturePluginMetadataCustomization.h"
#include "GameFeaturePluginTemplate.h"
#include "GameFeaturesEditorSettings.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Interfaces/IPluginManager.h"
#include "Logging/MessageLog.h"
#include "Misc/App.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"
#include "SSettingsEditorCheckoutNotice.h"
#include "Widgets/Notifications/SNotificationList.h"

#define LOCTEXT_NAMESPACE "GameFeatures"

static FAutoConsoleCommand CCmdGFPPrintActionUsageStats(
	TEXT("GameFeaturePlugin.PrintActionUsage"),
	TEXT("List all uses of Game Feature Actions")
	TEXT("\nUsage: GameFeaturePlugin.PrintActionUsage [-Summary] [-Detailed [-Csv]]")
	TEXT("\n-Summary - Print the number of Game Feature Plugins and the number of uses found for each Game Feature Action")
	TEXT("\n-Details - Print the list of Game Feature Plugins and the list of plugins that use each Game Feature Action")
	TEXT("\n-Csv     - Prints the output in CSV format. Only affects -Detailed output."),
	FConsoleCommandWithArgsDelegate::CreateStatic([](const TArray<FString>& Args)
	{
		const UGameFeaturesSubsystem& GameFeatures = UGameFeaturesSubsystem::Get();

		// Gather disabled GFPs
		TArray<const FString*> DisabledPlugins;
		TArray<const FString*> DisallowedPlugins;
		{
			DisabledPlugins.Reserve(32);
			DisallowedPlugins.Reserve(32);

			IPluginManager& PluginManager = IPluginManager::Get();
			const UGameFeaturesSubsystemSettings& Settings = *GetDefault<UGameFeaturesSubsystemSettings>();

			TArray<TSharedRef<IPlugin>> Plugins = PluginManager.GetDiscoveredPlugins();
			for (const TSharedRef<IPlugin>& Plugin : Plugins)
			{
				const FString& DescriptorFileName = Plugin->GetDescriptorFileName();
				if (Settings.IsValidGameFeaturePlugin(DescriptorFileName))
				{
					if (!Plugin->IsEnabled())
					{
						DisabledPlugins.Add(&Plugin->GetName());
					}
					else
					{
						FString PluginURL;
						GameFeatures.GetBuiltInGameFeaturePluginURL(Plugin, PluginURL);
						if (!GameFeatures.IsPluginAllowed(PluginURL))
						{
							DisallowedPlugins.Add(&Plugin->GetName());
						}
					}
				}
			}
		}

		// Gather actions
		int32 RegisteredCount = 0;
		TMap<const UClass*, TArray<FString>> ActionUses;
		{
			ActionUses.Reserve(128);

			auto VisitGFP = [&RegisteredCount, &ActionUses](const UGameFeatureData* Data)
			{
				RegisteredCount++;

				FString PluginName;
				Data->GetPluginName(PluginName);

				const TArray<UGameFeatureAction*>& Actions = Data->GetActions();
				for (const UGameFeatureAction* Action : Actions)
				{
					const UClass* Class = Action->GetClass();
					TArray<FString>& Plugins = ActionUses.FindOrAdd(Class);
					Plugins.Add(PluginName);
				}
			};

			GameFeatures.ForEachRegisteredGameFeature<UGameFeatureData>(VisitGFP);

			ActionUses.ValueSort([](const TArray<FString>& A, const TArray<FString>& B) { return A.Num() > B.Num(); });
		}

		// Output
		{
			bool bDetails = Args.Contains(TEXT("-Details"));
			bool bSummary = Args.Contains(TEXT("-Summary")) || !bDetails;
			bool bCsv     = Args.Contains(TEXT("-Csv"));

			TStringBuilder<1024> Msg;
			Msg << TEXT("Game Feature Action usage\n");

			if (bSummary)
			{
				if (!DisabledPlugins.IsEmpty())
				{
					Msg << TEXT("Disabled Game Feature Plugins (Action usage for these will not be displayed): ") << DisabledPlugins.Num() << TEXT("\n");
				}

				if (!DisallowedPlugins.IsEmpty())
				{
					Msg << TEXT("Disallowed built-in Game Feature Plugins (Action usage for these will not be displayed): ") << DisallowedPlugins.Num() << TEXT("\n");
				}

				Msg << TEXT("Registered Game Feature Plugins: ") << RegisteredCount << TEXT("\n");
				if (!ActionUses.IsEmpty())
				{
					Msg << TEXT("Game Feature Action usage:\n{\n");
					for (TPair<const UClass*, TArray<FString>>& Pair : ActionUses)
					{
						Msg << TEXT("\t") << Pair.Key->GetFName() << TEXT(": ") << Pair.Value.Num() << TEXT("\n");
					}
					Msg << TEXT("}\n");
				}
				else
				{
					Msg << TEXT("No Game Feature Actions found\n");
				}
			}

			if (bDetails)
			{
				if (bCsv)
				{
					Msg << TEXT("Plugin Name,Action Class\n");

					if (!DisabledPlugins.IsEmpty())
					{
						for (const FString* PluginName : DisabledPlugins)
						{
							Msg << *PluginName << TEXT(",Disabled\n");
						}
					}

					if (!DisallowedPlugins.IsEmpty())
					{
						for (const FString* PluginName : DisallowedPlugins)
						{
							Msg << *PluginName << TEXT(",Disallowed\n");
						}
					}

					for (TPair<const UClass*, TArray<FString>>& Pair : ActionUses)
					{
						FName ActionName = Pair.Key->GetFName();
						for (const FString& PluginName : Pair.Value)
						{
							Msg << PluginName << TEXT(",") << ActionName << TEXT("\n");
						}
					}
				}
				else
				{
					if (!DisabledPlugins.IsEmpty())
					{
						Msg << TEXT("Disabled Game Feature Plugins (Action usage for these will not be displayed):\n{\n");
						for (const FString* PluginName : DisabledPlugins)
						{
							Msg << TEXT("\t") << *PluginName << TEXT("\n");
						}
						Msg << TEXT("}\n");
					}

					if (!DisallowedPlugins.IsEmpty())
					{
						Msg << TEXT("Disallowed built-in Game Feature Plugins (Action usage for these will not be displayed):\n{\n");
						for (const FString* PluginName : DisallowedPlugins)
						{
							Msg << TEXT("\t") << *PluginName << TEXT("\n");
						}
						Msg << TEXT("}\n");
					}

					Msg << TEXT("Game Feature Action usage:\n{\n");
					for (TPair<const UClass*, TArray<FString>>& Pair : ActionUses)
					{
						FName ActionName = Pair.Key->GetFName();

						Msg << TEXT("\t") << ActionName << TEXT(":\n\t{\n");
						for (const FString& PluginName : Pair.Value)
						{
							Msg << TEXT("\t\t") << PluginName << TEXT("\n");
						}
						Msg << TEXT("\t}\n");
					}
					Msg << TEXT("}\n");
				}
			}

			if (Msg.Len())
			{
				Msg.RemoveSuffix(1);
			}
			UE_LOG(LogGameFeatures, Display, TEXT("%s"), Msg.ToString());
		}
	})
);

class FGameFeaturesEditorModule : public FDefaultModuleImpl
{
	virtual void StartupModule() override
	{
		// Register the details customizations
		{
			FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
			PropertyModule.RegisterCustomClassLayout(UGameFeatureData::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FGameFeatureDataDetailsCustomization::MakeInstance));

			PropertyModule.NotifyCustomizationModuleChanged();
		}

		// Register to get a warning on startup if settings aren't configured correctly
		UAssetManager::CallOrRegister_OnAssetManagerCreated(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FGameFeaturesEditorModule::OnAssetManagerCreated));

		// Add templates to the new plugin wizard
		{
			GameFeaturesEditorSettingsWatcher = MakeShared<FGameFeaturesEditorSettingsWatcher>(this);
			GameFeaturesEditorSettingsWatcher->Init();

			CachePluginTemplates();

			IModularFeatures& ModularFeatures = IModularFeatures::Get();
			ModularFeatures.OnModularFeatureRegistered().AddRaw(this, &FGameFeaturesEditorModule::OnModularFeatureRegistered);
			ModularFeatures.OnModularFeatureUnregistered().AddRaw(this, &FGameFeaturesEditorModule::OnModularFeatureUnregistered);

			if (ModularFeatures.IsModularFeatureAvailable(EditorFeatures::PluginsEditor))
			{
				OnModularFeatureRegistered(EditorFeatures::PluginsEditor, &ModularFeatures.GetModularFeature<IPluginsEditorFeature>(EditorFeatures::PluginsEditor));
			}
		}
	}

	virtual void ShutdownModule() override
	{
		// Remove the customization
		if (UObjectInitialized() && FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
		{
			FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
			PropertyModule.UnregisterCustomClassLayout(UGameFeatureData::StaticClass()->GetFName());

			PropertyModule.NotifyCustomizationModuleChanged();
		}

		// Remove the plugin wizard override
		if (UObjectInitialized())
		{
			GameFeaturesEditorSettingsWatcher = nullptr;

			IModularFeatures& ModularFeatures = IModularFeatures::Get();
 			ModularFeatures.OnModularFeatureRegistered().RemoveAll(this);
 			ModularFeatures.OnModularFeatureUnregistered().RemoveAll(this);

			if (ModularFeatures.IsModularFeatureAvailable(EditorFeatures::PluginsEditor))
			{
				OnModularFeatureUnregistered(EditorFeatures::PluginsEditor, &ModularFeatures.GetModularFeature<IPluginsEditorFeature>(EditorFeatures::PluginsEditor));
			}
			UnregisterFunctionTemplates();
			PluginTemplates.Empty();
		}
	}

	void OnSettingsChanged(UObject* Settings, FPropertyChangedEvent& PropertyChangedEvent)
	{
		const FName PropertyName = PropertyChangedEvent.GetPropertyName();
		const FName MemberPropertyName = (PropertyChangedEvent.MemberProperty != nullptr) ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None;
		const FName PluginTemplatePropertyName = GET_MEMBER_NAME_CHECKED(UGameFeaturesEditorSettings, PluginTemplates);
		if (PropertyName == PluginTemplatePropertyName
			|| MemberPropertyName == PluginTemplatePropertyName)
		{
			ResetPluginTemplates();
		}
	}

	void CachePluginTemplates()
	{
		PluginTemplates.Reset();
		if (const UGameFeaturesEditorSettings* GameFeatureEditorSettings = GetDefault<UGameFeaturesEditorSettings>())
		{
			for (const FPluginTemplateData& PluginTemplate : GameFeatureEditorSettings->PluginTemplates)
			{
				PluginTemplates.Add(MakeShareable(new FGameFeaturePluginTemplateDescription(
					PluginTemplate.Label,
					PluginTemplate.Description,
					PluginTemplate.Path.Path,
					PluginTemplate.DefaultSubfolder,
					PluginTemplate.DefaultPluginName,
					PluginTemplate.DefaultGameFeatureDataClass,
					PluginTemplate.DefaultGameFeatureDataName,
					PluginTemplate.bIsEnabledByDefault ? EPluginEnabledByDefault::Enabled : EPluginEnabledByDefault::Disabled)));
			}
		}
	}

	void ResetPluginTemplates()
	{
		UnregisterFunctionTemplates();
		CachePluginTemplates();
		RegisterPluginTemplates();
	}

	void RegisterPluginTemplates()
	{
		if (IModularFeatures::Get().IsModularFeatureAvailable(EditorFeatures::PluginsEditor))
		{
			IPluginsEditorFeature& PluginEditor = IModularFeatures::Get().GetModularFeature<IPluginsEditorFeature>(EditorFeatures::PluginsEditor);
			for (const TSharedPtr<FGameFeaturePluginTemplateDescription, ESPMode::ThreadSafe>& TemplateDescription : PluginTemplates)
			{
				PluginEditor.RegisterPluginTemplate(TemplateDescription.ToSharedRef());
			}
			PluginEditorExtensionDelegate = PluginEditor.RegisterPluginEditorExtension(FOnPluginBeingEdited::CreateRaw(this, &FGameFeaturesEditorModule::CustomizePluginEditing));
		}
	}

	void UnregisterFunctionTemplates()
	{
		if (IModularFeatures::Get().IsModularFeatureAvailable(EditorFeatures::PluginsEditor))
		{
			IPluginsEditorFeature& PluginEditor = IModularFeatures::Get().GetModularFeature<IPluginsEditorFeature>(EditorFeatures::PluginsEditor);
			for (const TSharedPtr<FGameFeaturePluginTemplateDescription, ESPMode::ThreadSafe>& TemplateDescription : PluginTemplates)
			{
				PluginEditor.UnregisterPluginTemplate(TemplateDescription.ToSharedRef());
			}
			PluginEditor.UnregisterPluginEditorExtension(PluginEditorExtensionDelegate);
		}
		
	}

	void OnModularFeatureRegistered(const FName& Type, class IModularFeature* ModularFeature)
	{
		if (Type == EditorFeatures::PluginsEditor)
		{
			ResetPluginTemplates();
		}
	}

	void OnModularFeatureUnregistered(const FName& Type, class IModularFeature* ModularFeature)
	{
		if (Type == EditorFeatures::PluginsEditor)
		{
			UnregisterFunctionTemplates();
		}
	}

	void AddDefaultGameDataRule()
	{
		// Check out the ini or make it writable
		UAssetManagerSettings* Settings = GetMutableDefault<UAssetManagerSettings>();

		const FString& ConfigFileName = Settings->GetDefaultConfigFilename();

		bool bSuccess = false;

		FText NotificationOpText;
 		if (!SettingsHelpers::IsCheckedOut(ConfigFileName, true))
 		{
			FText ErrorMessage;
			bSuccess = SettingsHelpers::CheckOutOrAddFile(ConfigFileName, true, !IsRunningCommandlet(), &ErrorMessage);
			if (bSuccess)
			{
				NotificationOpText = LOCTEXT("CheckedOutAssetManagerIni", "Checked out {0}");
			}
			else
			{
				UE_LOG(LogGameFeatures, Error, TEXT("%s"), *ErrorMessage.ToString());
				bSuccess = SettingsHelpers::MakeWritable(ConfigFileName);

				if (bSuccess)
				{
					NotificationOpText = LOCTEXT("MadeWritableAssetManagerIni", "Made {0} writable (you may need to manually add to revision control)");
				}
				else
				{
					NotificationOpText = LOCTEXT("FailedToTouchAssetManagerIni", "Failed to check out {0} or make it writable, so no rule was added");
				}
			}
		}
		else
		{
			NotificationOpText = LOCTEXT("UpdatedAssetManagerIni", "Updated {0}");
			bSuccess = true;
		}

		// Add the rule to project settings
		if (bSuccess)
		{
			FPrimaryAssetTypeInfo NewTypeInfo(
				UGameFeatureData::StaticClass()->GetFName(),
				UGameFeatureData::StaticClass(),
				false,
				false);
			NewTypeInfo.Rules.CookRule = EPrimaryAssetCookRule::AlwaysCook;

			Settings->Modify(true);

			Settings->PrimaryAssetTypesToScan.Add(NewTypeInfo);

 			Settings->PostEditChange();
			Settings->TryUpdateDefaultConfigFile();

			UAssetManager::Get().ReinitializeFromConfig();
		}

		// Show a message that the file was checked out/updated and must be submitted
		FNotificationInfo Info(FText::Format(NotificationOpText, FText::FromString(FPaths::GetCleanFilename(ConfigFileName))));
		Info.ExpireDuration = 3.0f;
		FSlateNotificationManager::Get().AddNotification(Info);
	}

	void OnAssetManagerCreated()
	{
		// Make sure the game has the appropriate asset manager configuration or we won't be able to load game feature data assets
		FPrimaryAssetId DummyGameFeatureDataAssetId(UGameFeatureData::StaticClass()->GetFName(), NAME_None);
		FPrimaryAssetRules GameDataRules = UAssetManager::Get().GetPrimaryAssetRules(DummyGameFeatureDataAssetId);
		if (FApp::HasProjectName() && GameDataRules.IsDefault())
		{
			FMessageLog("LoadErrors").Error()
				->AddToken(FTextToken::Create(FText::Format(NSLOCTEXT("GameFeatures", "MissingRuleForGameFeatureData", "Asset Manager settings do not include an entry for assets of type {0}, which is required for game feature plugins to function."), FText::FromName(UGameFeatureData::StaticClass()->GetFName()))))
				->AddToken(FActionToken::Create(NSLOCTEXT("GameFeatures", "AddRuleForGameFeatureData", "Add entry to PrimaryAssetTypesToScan?"), FText(),
					FOnActionTokenExecuted::CreateRaw(this, &FGameFeaturesEditorModule::AddDefaultGameDataRule), true));
		}
	}

	TSharedPtr<FPluginEditorExtension> CustomizePluginEditing(FPluginEditingContext& InPluginContext, IDetailLayoutBuilder& DetailBuilder)
	{
		const bool bIsGameFeaturePlugin = InPluginContext.PluginBeingEdited->GetDescriptorFileName().Contains(TEXT("/GameFeatures/"));
		if (bIsGameFeaturePlugin)
		{
			TSharedPtr<FGameFeaturePluginMetadataCustomization> Result = MakeShareable(new FGameFeaturePluginMetadataCustomization);
			Result->CustomizeDetails(InPluginContext, DetailBuilder);
			return Result;
		}

		return nullptr;
	}
private:

	struct FGameFeaturesEditorSettingsWatcher : public TSharedFromThis<FGameFeaturesEditorSettingsWatcher>
	{
		FGameFeaturesEditorSettingsWatcher(FGameFeaturesEditorModule* InParentModule)
			: ParentModule(InParentModule)
		{

		}

		void Init()
		{
			GetMutableDefault<UGameFeaturesEditorSettings>()->OnSettingChanged().AddSP(this, &FGameFeaturesEditorSettingsWatcher::OnSettingsChanged);
		}

		void OnSettingsChanged(UObject* Settings, FPropertyChangedEvent& PropertyChangedEvent)
		{
			if (ParentModule != nullptr)
			{
				ParentModule->OnSettingsChanged(Settings, PropertyChangedEvent);
			}
		}
	private:

		FGameFeaturesEditorModule* ParentModule;
	};

	TSharedPtr<FGameFeaturesEditorSettingsWatcher> GameFeaturesEditorSettingsWatcher;

	// Array of Plugin templates populated from GameFeatureDeveloperSettings. Allows projects to
	//	specify reusable plugin templates for the plugin creation wizard.
	TArray<TSharedPtr<FGameFeaturePluginTemplateDescription>> PluginTemplates;
	FPluginEditorExtensionHandle PluginEditorExtensionDelegate;
};

IMPLEMENT_MODULE(FGameFeaturesEditorModule, GameFeaturesEditor)

#undef LOCTEXT_NAMESPACE

GameFeaturesEditorSettings.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Engine/DeveloperSettings.h"
#include "Engine/DeveloperSettings.h"
#include "Templates/SubclassOf.h"
#include "GameFeaturesEditorSettings.generated.h"

class UGameFeatureData;

/*
* Data for specifying a usable plugin template. 
*	-Plugin templates are a folder/file structure that are duplicated and renamed
*	 by the plugin creation wizard to easily create new plugins with a standard
*	 format.
* See PluginUtils.h for more information.
*/
USTRUCT()
struct FPluginTemplateData
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category = PluginTemplate, meta = (RelativePath))
	FDirectoryPath Path;

	UPROPERTY(EditAnywhere, Category = PluginTemplate)
	FText Label;

	UPROPERTY(EditAnywhere, Category = PluginTemplate)
	FText Description;

	/** Optional sub folder that new plugins will be created in. */
	UPROPERTY(EditAnywhere, Category = PluginTemplate)
	FString DefaultSubfolder;

	/** Optional plugin name to default the new plugin to. */
	UPROPERTY(EditAnywhere, Category = PluginTemplate)
	FString DefaultPluginName;

	/** The default class of game feature data to create for new game feature plugins (if not set, UGameFeatureData will be used) */
	UPROPERTY(config, EditAnywhere, Category = Plugins)
	TSubclassOf<UGameFeatureData> DefaultGameFeatureDataClass;

	/** The default name of the created game feature data assets. If empty, will use the plugin name. */
	UPROPERTY(config, EditAnywhere, Category = Plugins)
	FString DefaultGameFeatureDataName;

	/** If true, the created plugin will be enabled by default without needing to be added to the project file. */
	UPROPERTY(config, EditAnywhere, Category = Plugins)
	bool bIsEnabledByDefault = false;

};


UCLASS(MinimalAPI, config = Editor, defaultconfig)
class UGameFeaturesEditorSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	// Array of Plugin templates. Allows projects to specify reusable plugin templates for the plugin creation wizard.
	UPROPERTY(config, EditAnywhere, Category = Plugins)
	TArray<FPluginTemplateData> PluginTemplates;
};

IllegalPluginDependenciesValidator.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "IllegalPluginDependenciesValidator.h"

#include "DataValidationChangelist.h"
#include "GameFeaturesSubsystem.h"
#include "Interfaces/IPluginManager.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(IllegalPluginDependenciesValidator)

#define LOCTEXT_NAMESPACE "IllegalPluginDependenciesValidator"

namespace IllegalPluginDependencies
{
	bool IsGameFeaturePlugin(const TSharedPtr<IPlugin> InPlugin)
	{
		const FString PluginsFolderRoot = (InPlugin->GetLoadedFrom() == EPluginLoadedFrom::Project) ? FPaths::ProjectPluginsDir() : FPaths::EnginePluginsDir();
		FString PluginPathRelativeToDomain = InPlugin->GetBaseDir();
		if (FPaths::MakePathRelativeTo(PluginPathRelativeToDomain, *PluginsFolderRoot))
		{
			PluginPathRelativeToDomain = TEXT("/") + PluginPathRelativeToDomain + TEXT("/");
		}
		else
		{
			PluginPathRelativeToDomain = FString();
		}

		return (PluginPathRelativeToDomain.StartsWith(TEXT("/GameFeatures/")));
	}
}

bool UIllegalPluginDependenciesValidator::CanValidateAsset_Implementation(const FAssetData& AssetData, UObject* InAsset, FDataValidationContext& InContext) const
{
	return (InAsset->GetClass() == UDataValidationChangelist::StaticClass());
}

EDataValidationResult UIllegalPluginDependenciesValidator::ValidateLoadedAsset_Implementation(const FAssetData& AssetData, UObject* InAsset, FDataValidationContext& InContext)
{
	UDataValidationChangelist* DataValidationChangelist = CastChecked<UDataValidationChangelist>(InAsset);

	bool bChangelistContainsPluginFiles = false;
	for (const FString& ModifiedFile : DataValidationChangelist->ModifiedFiles)
	{
		if (ModifiedFile.EndsWith(TEXT(".uplugin")))
		{
			bChangelistContainsPluginFiles = true;
			break;
		}
	}

	// Early out if the changelist doesn't contain any uplugin files.
	if (!bChangelistContainsPluginFiles)
	{
		AssetPasses(InAsset);
		return GetValidationResult();
	}

	TSet<FString> GFPs;
	TArray<TSharedRef<IPlugin>> AllPlugins = IPluginManager::Get().GetDiscoveredPlugins();
	GFPs.Reserve(AllPlugins.Num());
	for (const TSharedRef<IPlugin>& Plugin : AllPlugins)
	{
		FGameFeaturePluginDetails PluginDetails;
		if (UGameFeaturesSubsystem::Get().GetBuiltInGameFeaturePluginDetails(Plugin, PluginDetails))
		{
			GFPs.Add(Plugin->GetName());
		}
	}

	for (const FString& ModifiedFile : DataValidationChangelist->ModifiedFiles)
	{
		if (ModifiedFile.EndsWith(TEXT(".uplugin")))
		{
			FString ModifiedPlugin = FPaths::GetBaseFilename(ModifiedFile);
			TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(ModifiedPlugin);
			if (Plugin.IsValid())
			{
				if (GFPs.Contains(ModifiedPlugin) || IllegalPluginDependencies::IsGameFeaturePlugin(Plugin))
				{
					continue;
				}

				// Plugin is not a GFP so it can not have any GFP dependencies
				const FPluginDescriptor& Descriptor = Plugin->GetDescriptor();
				for (const FPluginReferenceDescriptor& Dependency : Descriptor.Plugins)
				{
					TSharedPtr<IPlugin> DependencyPlugin = IPluginManager::Get().FindPlugin(Dependency.Name);
					if (DependencyPlugin.IsValid())
					{
						if (GFPs.Contains(Dependency.Name))
						{
							FText NewError = FText::Format(
								LOCTEXT("ValidationError.IllegalPluginDependency", "Plugin {0} depends on {1}. Non GameFeaturePlugins are not allowed to depend on GameFeaturePlugins. This can create an issue where objects will fail to load"),
								FText::FromString(ModifiedPlugin),
								FText::FromString(Dependency.Name)
							);
							AssetFails(InAsset, NewError);
						}
					}
				}
			}
		}
	}

	if (GetValidationResult() != EDataValidationResult::Invalid)
	{
		AssetPasses(InAsset);
	}

	return GetValidationResult();
}
#undef LOCTEXT_NAMESPACE

IllegalPluginDependenciesValidator.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "EditorValidatorBase.h"
#include "IllegalPluginDependenciesValidator.generated.h"

class FText;
class UObject;

/**
 * Ensures that non-GameFeaturePlugins do not depend on GameFeaturePlugins.
 * GameFeaturePlugins will load content later than non-GameFeaturePlugins which could cause linker load issues if they do not exist.
 */
UCLASS()
class UIllegalPluginDependenciesValidator : public UEditorValidatorBase
{
	GENERATED_BODY()

protected:
	virtual bool CanValidateAsset_Implementation(const FAssetData& InAssetData, UObject* InAsset, FDataValidationContext& InContext) const override;
	virtual EDataValidationResult ValidateLoadedAsset_Implementation(const FAssetData& InAssetData, UObject* InAsset, FDataValidationContext& InContext) override;
};

SGameFeatureStateWidget.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "SGameFeatureStateWidget.h"
#include "Widgets/Input/SSegmentedControl.h"
#include "GameFeatureTypes.h"

#define LOCTEXT_NAMESPACE "GameFeatures"

//////////////////////////////////////////////////////////////////////////
// FGameFeatureDataDetailsCustomization

void SGameFeatureStateWidget::Construct(const FArguments& InArgs)
{
	CurrentState = InArgs._CurrentState;

	ChildSlot
	[
		SNew(SHorizontalBox)
		+SHorizontalBox::Slot()
		.AutoWidth()
		.VAlign(VAlign_Center)
		[
			SNew(SSegmentedControl<EGameFeaturePluginState>)
			.Value(CurrentState)
			.OnValueChanged(InArgs._OnStateChanged)
			+SSegmentedControl<EGameFeaturePluginState>::Slot(EGameFeaturePluginState::Installed)
				.Text(GetDisplayNameOfState(EGameFeaturePluginState::Installed))
				.ToolTip(LOCTEXT("SwitchToInstalledTooltip", "Attempt to change the current state of this game feature to Installed.\n\nInstalled means that the plugin is in local storage (i.e., it is on the hard drive) but it has not been registered, loaded, or activated yet."))
			+ SSegmentedControl<EGameFeaturePluginState>::Slot(EGameFeaturePluginState::Registered)
				.Text(GetDisplayNameOfState(EGameFeaturePluginState::Registered))
				.ToolTip(LOCTEXT("SwitchToRegisteredTooltip", "Attempt to change the current state of this game feature to Registered.\n\nRegistered means that the assets in the plugin are known, but have not yet been loaded, except a few for discovery reasons (and it is not actively affecting gameplay yet)."))
			+ SSegmentedControl<EGameFeaturePluginState>::Slot(EGameFeaturePluginState::Loaded)
				.Text(GetDisplayNameOfState(EGameFeaturePluginState::Loaded))
				.ToolTip(LOCTEXT("SwitchToLoadedTooltip", "Attempt to change the current state of this game feature to Loaded.\n\nLoaded means that the plugin is loaded into memory and registered with some game systems, but not yet active and not affecting gameplay."))
			+ SSegmentedControl<EGameFeaturePluginState>::Slot(EGameFeaturePluginState::Active)
				.Text(GetDisplayNameOfState(EGameFeaturePluginState::Active))
				.ToolTip(LOCTEXT("SwitchToActiveTooltip", "Attempt to change the current state of this game feature to Active.\n\nActive means that the plugin is fully loaded and active. It is affecting the game."))
		]
		+SHorizontalBox::Slot()
		.Padding(8.0f, 0.0f, 0.0f, 0.0f)
		.VAlign(VAlign_Center)
		[
			SNew(STextBlock)
			.Text(this, &SGameFeatureStateWidget::GetStateStatusDisplay)
			.TextStyle(&FAppStyle::Get().GetWidgetStyle<FTextBlockStyle>("ButtonText"))
			.ToolTipText(this, &SGameFeatureStateWidget::GetStateStatusDisplayTooltip)
			.ColorAndOpacity(FAppStyle::Get().GetSlateColor(TEXT("Colors.AccentYellow")))
		]
	];
}

FText SGameFeatureStateWidget::GetDisplayNameOfState(EGameFeaturePluginState State)
{
#define GAME_FEATURE_PLUGIN_STATE_TEXT(inEnum, inText) case EGameFeaturePluginState::inEnum: return inText;
	switch (State)
	{
	GAME_FEATURE_PLUGIN_STATE_LIST(GAME_FEATURE_PLUGIN_STATE_TEXT)
	}
#undef GAME_FEATURE_PLUGIN_STATE_TEXT

	const FString FallbackString = UE::GameFeatures::ToString(State);
	ensureMsgf(false, TEXT("Unknown EGameFeaturePluginState entry %d %s"), (int)State, *FallbackString);
	return FText::AsCultureInvariant(FallbackString);
}

FText SGameFeatureStateWidget::GetTooltipOfState(EGameFeaturePluginState State)
{
	static_assert((int32)EGameFeaturePluginState::MAX == 37, "");

	switch (State)
	{
	case EGameFeaturePluginState::Uninitialized:
		return LOCTEXT("StateTooltip_Uninitialized", "Unset. Not yet been set up.");
	case EGameFeaturePluginState::Terminal:
		return LOCTEXT("StateTooltip_Terminal", "Final State before removal of the state machine");
	case EGameFeaturePluginState::UnknownStatus:
		return LOCTEXT("StateTooltip_UnknownStatus", "Initialized, but the only thing known is the URL to query status.");
	case EGameFeaturePluginState::Uninstalled:
		return LOCTEXT("StateTooltip_Uninstalled", "All installed data for this plugin has now been uninstalled from local storage (i.e the hard drive)");
	case EGameFeaturePluginState::Uninstalling:
		return LOCTEXT("StateTooltip_Uninstalling", "Transition state between StatusKnown -> Terminal for any plugin that can have data that needs to have local datat uninstalled.");
	case EGameFeaturePluginState::ErrorUninstalling:
		return LOCTEXT("StateTooltip_Error Uninstalling", "Error state for Uninstalling -> Terminal transition.");
	case EGameFeaturePluginState::CheckingStatus:
		return LOCTEXT("StateTooltip_CheckingStatus", "Transition state UnknownStatus -> StatusKnown. The status is in the process of being queried.");
	case EGameFeaturePluginState::ErrorCheckingStatus:
		return LOCTEXT("StateTooltip_ErrorCheckingStatus", "Error state for UnknownStatus -> StatusKnown transition.");
	case EGameFeaturePluginState::ErrorUnavailable:
		return LOCTEXT("StateTooltip_ErrorUnavailable", "Error state for UnknownStatus -> StatusKnown transition.");
	case EGameFeaturePluginState::StatusKnown:
		return LOCTEXT("StateTooltip_StatusKnown", "The plugin's information is known, but no action has taken place yet.");
	case EGameFeaturePluginState::Releasing:
		return LOCTEXT("StateTooltip_Releasing", "Transition State for Installed -> StatusKnown. Releases local data from any relevant caches.");
	case EGameFeaturePluginState::ErrorManagingData:
		return LOCTEXT("StateTooltip_ErrorManagingData", "Error state for Installed -> StatusKnown and StatusKnown -> Installed transitions.");
	case EGameFeaturePluginState::Downloading:
		return LOCTEXT("StateTooltip_Downloading", "Transition state StatusKnown -> Installed. In the process of adding to local storage.");
	case EGameFeaturePluginState::Installed:
		return LOCTEXT("StateTooltip_Installed", "The plugin is in local storage (i.e. it is on the hard drive)");
	case EGameFeaturePluginState::ErrorMounting:
		return LOCTEXT("StateTooltip_ErrorMounting", "Error state for Installed -> Registered and Registered -> Installed transitions.");
	case EGameFeaturePluginState::ErrorWaitingForDependencies:
		return LOCTEXT("StateTooltip_ErrorWaitingForDependencies", "Error state for Installed -> Registered and Registered -> Installed transitions.");
	case EGameFeaturePluginState::ErrorRegistering:
		return LOCTEXT("StateTooltip_ErrorRegistering", "Error state for Installed -> Registered and Registered -> Installed transitions.");
	case EGameFeaturePluginState::WaitingForDependencies:
		return LOCTEXT("StateTooltip_WaitingForDependencies", "Transition state Installed -> Registered. In the process of loading code/content for all dependencies into memory.");
	case EGameFeaturePluginState::AssetDependencyStreamOut:
		return LOCTEXT("StateTooltip_AssetDependencyStreamout", "Transition state Registered -> Installed. In the process of streaming out individual assets from dependencies.");
	case EGameFeaturePluginState::ErrorAssetDependencyStreaming:
		return LOCTEXT("StateTooltip_ErrorAssetDependencyStreaming", "Error state for Installed -> Registered and Registered -> Installed transitions.");
	case EGameFeaturePluginState::AssetDependencyStreaming:
		return LOCTEXT("StateTooltip_AssetDependencyStreaming", "Transition state Installed -> Registered. In the process of streaming individual assets from dependencies.");
	case EGameFeaturePluginState::Unmounting:
		return LOCTEXT("StateTooltip_Unmounting", "Transition state Registered -> Installed. The content file(s) (i.e. pak file) for the plugin is unmounting.");
	case EGameFeaturePluginState::Mounting:
		return LOCTEXT("StateTooltip_Mounting", "Transition state Installed -> Registered. The content file(s) (i.e. pak file) for the plugin is getting mounted.");
	case EGameFeaturePluginState::Unregistering:
		return LOCTEXT("StateTooltip_Unregistering", "Transition state Registered -> Installed. Cleaning up data gathered in Registering.");
	case EGameFeaturePluginState::Registering:
		return LOCTEXT("StateTooltip_Registering", "Transition state Installed -> Registered. Discovering assets in the plugin, but not loading them, except a few for discovery reasons.");
	case EGameFeaturePluginState::Registered:
		return LOCTEXT("StateTooltip_Registered", "The assets in the plugin are known, but have not yet been loaded, except a few for discovery reasons.");
	case EGameFeaturePluginState::ErrorLoading:
		return LOCTEXT("StateTooltip_ErrorLoading", "Error state for Loading -> Loaded transition.");
	case EGameFeaturePluginState::Unloading:
		return LOCTEXT("StateTooltip_Unloading", "Transition state Loaded -> Registered. In the process of removing code/content from memory.");
	case EGameFeaturePluginState::Loading:
		return LOCTEXT("StateTooltip_Loading", "Transition state Registered -> Loaded. In the process of loading code/content into memory.");
	case EGameFeaturePluginState::Loaded:
		return LOCTEXT("StateTooltip_Loaded", "The plugin is loaded into memory and registered with some game systems but not yet active.");
	case EGameFeaturePluginState::ErrorActivatingDependencies:
		return LOCTEXT("StateTooltip_ErrorActivateDependencies", "Error state for Loaded -> Active and Active -> Loaded transitions.");
	case EGameFeaturePluginState::ActivatingDependencies:
		return LOCTEXT("StateTooltip_ActivateDependencies", "Transition state Loaded -> Active. In the process of selectively activating dependencies.");
	case EGameFeaturePluginState::ErrorDeactivatingDependencies:
		return LOCTEXT("StateTooltip_ErrorDeactivatingDependencies", "Error state for Active -> Loaded transition.");
	case EGameFeaturePluginState::DeactivatingDependencies:
		return LOCTEXT("StateTooltip_DeactivateDependencies", "Transition state Active -> Loaded. In the process of selectively deactivating dependencies.");
	case EGameFeaturePluginState::Deactivating:
		return LOCTEXT("StateTooltip_Deactivating", "Transition state Active -> Loaded. Currently unregistering with game systems.");
	case EGameFeaturePluginState::Activating:
		return LOCTEXT("StateTooltip_Activating", "Transition state Loaded -> Active. Currently registering plugin code/content with game systems.");
	case EGameFeaturePluginState::Active:
		return LOCTEXT("StateTooltip_Active", "Plugin is fully loaded and active. It is affecting the game.");
	}

	return GetDisplayNameOfState(State);
}

FText SGameFeatureStateWidget::GetStateStatusDisplay() const
{
	// Display the current state/transition for anything but the four acceptable destination states (which are already covered by the switcher)
	const EGameFeaturePluginState State = CurrentState.Get();
	switch (State)
	{
		case EGameFeaturePluginState::Active:
		case EGameFeaturePluginState::Installed:
		case EGameFeaturePluginState::Loaded:
		case EGameFeaturePluginState::Registered:
			return FText::GetEmpty();
		default:
			return GetDisplayNameOfState(State);
	}
}

FText SGameFeatureStateWidget::GetStateStatusDisplayTooltip() const
{
	const EGameFeaturePluginState State = CurrentState.Get();

	return FText::Format(
		LOCTEXT("OtherStateToolTip", "The current state of this game feature plugin\n\n{0}"),
		GetTooltipOfState(State));
}

//////////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE

SGameFeatureStateWidget.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Widgets/SCompoundWidget.h"
#include "GameFeatureTypesFwd.h"

//////////////////////////////////////////////////////////////////////////
// FGameFeatureDataDetailsCustomization

/**
 * A delegate that is invoked when widgets want to notify a user that they have been clicked.
 * Intended for use by buttons and other button-like widgets.
 */
DECLARE_DELEGATE_OneParam(FOnWidgetChangesGameFeatureState, EGameFeaturePluginState)

class SGameFeatureStateWidget : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SGameFeatureStateWidget) {}
		SLATE_ATTRIBUTE(EGameFeaturePluginState, CurrentState)
		SLATE_EVENT(FOnWidgetChangesGameFeatureState, OnStateChanged)
	SLATE_END_ARGS()

public:
	SGameFeatureStateWidget() {}

	void Construct(const FArguments& InArgs);

	static FText GetDisplayNameOfState(EGameFeaturePluginState State);
	static FText GetTooltipOfState(EGameFeaturePluginState StateID);

private:
	FText GetStateStatusDisplay() const;
	FText GetStateStatusDisplayTooltip() const;

private:
	TAttribute<EGameFeaturePluginState> CurrentState;
};

GameFeaturePluginTemplate.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Features/IPluginsEditorFeature.h"
#include "GameFeatureData.h"
#include "PluginDescriptor.h"

#define UE_API GAMEFEATURESEDITOR_API

/**
 * Used to create custom templates for GameFeaturePlugins.
 */
struct FGameFeaturePluginTemplateDescription : public FPluginTemplateDescription
{
	UE_API FGameFeaturePluginTemplateDescription(FText InName, FText InDescription, FString InOnDiskPath, FString InDefaultSubfolder, FString InDefaultPluginName
		, TSubclassOf<UGameFeatureData> GameFeatureDataClassOverride, FString GameFeatureDataNameOverride, EPluginEnabledByDefault InEnabledByDefault);

	UE_API virtual bool ValidatePathForPlugin(const FString& ProposedAbsolutePluginPath, FText& OutErrorMessage) override;
	UE_API virtual void UpdatePathWhenTemplateSelected(FString& InOutPath) override;
	UE_API virtual void UpdatePathWhenTemplateUnselected(FString& InOutPath) override;

	UE_API virtual void UpdatePluginNameTextWhenTemplateSelected(FText& OutPluginNameText) override;
	UE_API virtual void UpdatePluginNameTextWhenTemplateUnselected(FText& OutPluginNameText) override;

	UE_API virtual void CustomizeDescriptorBeforeCreation(FPluginDescriptor& Descriptor) override;
	UE_API virtual void OnPluginCreated(TSharedPtr<IPlugin> NewPlugin) override;

	UE_API FString GetGameFeatureRoot() const;
	UE_API bool IsRootedInGameFeaturesRoot(const FString& InStr) const;

	FString DefaultSubfolder;
	FString DefaultPluginName;
	TSubclassOf<UGameFeatureData> GameFeatureDataClass;
	FString GameFeatureDataName;
	EPluginEnabledByDefault PluginEnabledByDefault = EPluginEnabledByDefault::Disabled;
};

#undef UE_API

PLUGIN_NAMERuntime.Build.cs

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class PLUGIN_NAMERuntime : ModuleRules
{
	public PLUGIN_NAMERuntime(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		
		PublicIncludePaths.AddRange(
			new string[] {
				// ... add public include paths required here ...
			}
			);
				
		
		PrivateIncludePaths.AddRange(
			new string[] {
				// ... add other private include paths required here ...
			}
			);
			
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
				// ... add other public dependencies that you statically link with here ...
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				// ... add private dependencies that you statically link with here ...	
			}
			);
		
		
		DynamicallyLoadedModuleNames.AddRange(
			new string[]
			{
				// ... add any modules that your module loads dynamically here ...
			}
			);
	}
}

PLUGIN_NAMERuntimeModule.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "PLUGIN_NAMERuntimeModule.h"

#define LOCTEXT_NAMESPACE "FPLUGIN_NAMERuntimeModule"

void FPLUGIN_NAMERuntimeModule::StartupModule()
{
	// This code will execute after your module is loaded into memory;
	// the exact timing is specified in the .uplugin file per-module
}

void FPLUGIN_NAMERuntimeModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.
	// For modules that support dynamic reloading, we call this function before unloading the module.
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FPLUGIN_NAMERuntimeModule, PLUGIN_NAMERuntime)

PLUGIN_NAMERuntimeModule.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FPLUGIN_NAMERuntimeModule : public IModuleInterface
{
public:
	//~IModuleInterface
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
	//~End of IModuleInterface
};

posted @ 2026-04-04 15:09  jeyar  阅读(4)  评论(0)    收藏  举报