文件夹: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;
};
// 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
// 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;
};
// 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
// 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
};