From 38ae567d20613a8618b2831ad6ce488e8d740875 Mon Sep 17 00:00:00 2001 From: LongLy Date: Tue, 7 Jan 2025 22:30:09 -0700 Subject: [PATCH] Added OLSAssetManager, OLSExperienceManager, and OLSAssetManagerStartupJob --- .../GameModes/OLSExperienceManager.cpp | 48 ++ .../OLSExperienceManagerComponent.cpp | 468 ++++++++++++++++++ Source/ols/Private/Player/OLSPlayerState.cpp | 60 ++- .../ols/Private/Systems/OLSAssetManager.cpp | 329 ++++++++++++ .../Systems/OLSAssetManagerStartupJob.cpp | 46 ++ .../OLSExperienceDefinitionPrimaryDataAsset.h | 2 +- .../DataAssets/OLSPawnPrimaryDataAsset.h | 2 +- .../Public/GameModes/OLSExperienceManager.h | 33 ++ .../GameModes/OLSExperienceManagerComponent.h | 85 +++- Source/ols/Public/Player/OLSPlayerState.h | 18 + Source/ols/Public/Systems/OLSAssetManager.h | 111 +++++ .../Systems/OLSAssetManagerStartupJob.h | 32 ++ Source/ols/ols.Build.cs | 2 +- 13 files changed, 1229 insertions(+), 7 deletions(-) create mode 100644 Source/ols/Private/GameModes/OLSExperienceManager.cpp create mode 100644 Source/ols/Private/Systems/OLSAssetManager.cpp create mode 100644 Source/ols/Private/Systems/OLSAssetManagerStartupJob.cpp create mode 100644 Source/ols/Public/GameModes/OLSExperienceManager.h create mode 100644 Source/ols/Public/Systems/OLSAssetManager.h create mode 100644 Source/ols/Public/Systems/OLSAssetManagerStartupJob.h diff --git a/Source/ols/Private/GameModes/OLSExperienceManager.cpp b/Source/ols/Private/GameModes/OLSExperienceManager.cpp new file mode 100644 index 0000000..393004c --- /dev/null +++ b/Source/ols/Private/GameModes/OLSExperienceManager.cpp @@ -0,0 +1,48 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "GameModes/OLSExperienceManager.h" + +#if WITH_EDITOR +void UOLSExperienceManager::OnPlayInEditorBegun() +{ + ensure(GameFeaturePluginRequestCountMap.IsEmpty()); + GameFeaturePluginRequestCountMap.Empty(); +} + +void UOLSExperienceManager::NotifyOfPluginActivation(const FString pluginURL) +{ + if (GIsEditor) + { + UOLSExperienceManager* experienceManagerSubsystem = GEngine->GetEngineSubsystem(); + check(experienceManagerSubsystem); + + // Track the number of requesters who activate this plugin. Multiple load/activation requests are always allowed because concurrent requests are handled. + int32& count = experienceManagerSubsystem->GameFeaturePluginRequestCountMap.FindOrAdd(pluginURL); + ++count; + } +} + +bool UOLSExperienceManager::RequestToDeactivatePlugin(const FString pluginURL) +{ + if (GIsEditor) + { + UOLSExperienceManager* experienceManagerSubsystem = GEngine->GetEngineSubsystem(); + check(experienceManagerSubsystem); + + // Only let the last requester to get this far deactivate the plugin + int32& count = experienceManagerSubsystem->GameFeaturePluginRequestCountMap.FindChecked(pluginURL); + --count; + + if (count == 0) + { + experienceManagerSubsystem->GameFeaturePluginRequestCountMap.Remove(pluginURL); + return true; + } + + return false; + } + + return true; +} +#endif \ No newline at end of file diff --git a/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp b/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp index c7063c6..3bf0103 100644 --- a/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp +++ b/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp @@ -3,3 +3,471 @@ #include "GameModes/OLSExperienceManagerComponent.h" +#include "GameFeatureAction.h" +#include "GameModes/OLSExperienceManager.h" +#include "GameFeaturesSubsystem.h" +#include "GameFeatureAction.h" +#include "GameFeaturesSubsystemSettings.h" +#include "TimerManager.h" +#include "DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h" +#include "Net/UnrealNetwork.h" +#include "Systems/OLSAssetManager.h" + +//@TODO: Async load the experience definition itself +//@TODO: Handle failures explicitly (go into a 'completed but failed' state rather than check()-ing) +//@TODO: Do the action phases at the appropriate times instead of all at once +//@TODO: Support deactivating an experience and do the unloading actions +//@TODO: Think about what deactivation/cleanup means for preloaded assets +//@TODO: Handle deactivating game features, right now we 'leak' them enabled +// (for a client moving from experience to experience we actually want to diff the requirements and only unload some, not unload everything for them to just be immediately reloaded) +//@TODO: Handle both built-in and URL-based plugins (search for colon?) + +namespace OLSConsoleVariables +{ + static float ExperienceLoadRandomDelayMin = 0.0f; + static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayMin( + TEXT("lyra.chaos.ExperienceDelayLoad.MinSecs"), + ExperienceLoadRandomDelayMin, + TEXT("This value (in seconds) will be added as a delay of load completion of the experience (along with the random value lyra.chaos.ExperienceDelayLoad.RandomSecs)"), + ECVF_Default); + + static float ExperienceLoadRandomDelayRange = 0.0f; + static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayRange( + TEXT("lyra.chaos.ExperienceDelayLoad.RandomSecs"), + ExperienceLoadRandomDelayRange, + TEXT("A random amount of time between 0 and this value (in seconds) will be added as a delay of load completion of the experience (along with the fixed value lyra.chaos.ExperienceDelayLoad.MinSecs)"), + ECVF_Default); + + float GetExperienceLoadDelayDuration() + { + return FMath::Max(0.0f, ExperienceLoadRandomDelayMin + FMath::FRand() * ExperienceLoadRandomDelayRange); + } +} + + +UOLSExperienceManagerComponent::UOLSExperienceManagerComponent(const FObjectInitializer& objectInitializer) : Super(objectInitializer) +{ + SetIsReplicatedByDefault(true); +} + +void UOLSExperienceManagerComponent::EndPlay(const EEndPlayReason::Type endPlayReason) +{ + Super::EndPlay(endPlayReason); + + // deactivate any features this experience loaded + //@TODO: This should be handled FILO as well + for (const FString& PluginURL : GameFeaturePluginURLs) + { + if (UOLSExperienceManager::RequestToDeactivatePlugin(PluginURL)) + { + UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL); + } + } + + //@TODO: Ensure proper handling of a partially-loaded state too + if (LoadState == EOLSExperienceLoadState::Loaded) + { + LoadState = EOLSExperienceLoadState::Deactivating; + + // Make sure we won't complete the transition prematurely if someone registers as a pauser but fires immediately + NumExpectedPausers = INDEX_NONE; + NumObservedPausers = 0; + + // Deactivate and unload the actions + FGameFeatureDeactivatingContext context(TEXT(""), [this](FStringView) { this->OnActionDeactivationCompleted(); }); + + const FWorldContext* existingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld()); + if (existingWorldContext) + { + context.SetRequiredWorldContextHandle(existingWorldContext->ContextHandle); + } + + auto DeactivateListOfActions = [&context](const TArray& ActionList) + { + for (UGameFeatureAction* Action : ActionList) + { + if (Action) + { + Action->OnGameFeatureDeactivating(context); + Action->OnGameFeatureUnregistering(); + } + } + }; + + DeactivateListOfActions(CurrentExperience->Actions); + // @Todo implement this. + // for (const TObjectPtr& ActionSet : CurrentExperience->ActionSets) + // { + // if (ActionSet != nullptr) + // { + // DeactivateListOfActions(ActionSet->Actions); + // } + // } + + NumExpectedPausers = context.GetNumPausers(); + + if (NumExpectedPausers > 0) + { + // @Todo replace this with our custom. + // UE_LOG(LogLyraExperience, Error, TEXT("Actions that have asynchronous deactivation aren't fully supported yet in Lyra experiences")); + } + + if (NumExpectedPausers == NumObservedPausers) + { + OnAllActionsDeactivated(); + } + } +} + +void UOLSExperienceManagerComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, CurrentExperience); +} + +bool UOLSExperienceManagerComponent::ShouldShowLoadingScreen(FString& outReason) const +{ + if (LoadState != EOLSExperienceLoadState::Loaded) + { + outReason = TEXT("Experience still loading"); + return true; + } + + return false; +} + +const UOLSExperienceDefinitionPrimaryDataAsset* UOLSExperienceManagerComponent::GetCurrentExperienceChecked() const +{ + check(LoadState == EOLSExperienceLoadState::Loaded); + check(CurrentExperience != nullptr); + return CurrentExperience; +} + +void UOLSExperienceManagerComponent::SetCurrentExperience(FPrimaryAssetId experienceId) +{ + UOLSAssetManager& assetManager = UOLSAssetManager::Get(); + FSoftObjectPath assetPath = assetManager.GetPrimaryAssetPath(experienceId); + TSubclassOf assetClass = Cast(assetPath.TryLoad()); + check(assetClass); + const UOLSExperienceDefinitionPrimaryDataAsset* experience = GetDefault(assetClass); + + check(experience != nullptr); + check(CurrentExperience == nullptr); + CurrentExperience = experience; + StartExperienceLoad(); +} + +bool UOLSExperienceManagerComponent::IsExperienceLoaded() const +{ + return (LoadState == EOLSExperienceLoadState::Loaded) && (CurrentExperience != nullptr); +} + +void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_HighPriority( + FOnOLSExperienceLoaded::FDelegate&& delegate) +{ + if (IsExperienceLoaded()) + { + delegate.Execute(CurrentExperience); + } + else + { + OnExperienceLoaded_HighPriority.Add(MoveTemp(delegate)); + } +} + +void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnOLSExperienceLoaded::FDelegate&& delegate) +{ + if (IsExperienceLoaded()) + { + delegate.Execute(CurrentExperience); + } + else + { + OnExperienceLoaded.Add(MoveTemp(delegate)); + } +} + +void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_LowPriority( + FOnOLSExperienceLoaded::FDelegate&& delegate) +{ + if (IsExperienceLoaded()) + { + delegate.Execute(CurrentExperience); + } + else + { + OnExperienceLoaded_LowPriority.Add(MoveTemp(delegate)); + } +} + +void UOLSExperienceManagerComponent::OnRep_CurrentExperience() +{ + StartExperienceLoad(); +} + +void UOLSExperienceManagerComponent::StartExperienceLoad() +{ + check(CurrentExperience != nullptr); + check(LoadState == EOLSExperienceLoadState::Unloaded); + + // @Todo replace this by custom log. + // UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"), + // *CurrentExperience->GetPrimaryAssetId().ToString(), + // *GetClientServerContextString(this)); + + LoadState = EOLSExperienceLoadState::Loading; + + UOLSAssetManager& assetManager = UOLSAssetManager::Get(); + + TSet bundleAssetList; + TSet rawAssetList; + + bundleAssetList.Add(CurrentExperience->GetPrimaryAssetId()); + // @Todo implement this + // for (const TObjectPtr& ActionSet : CurrentExperience->ActionSets) + // { + // if (ActionSet != nullptr) + // { + // BundleAssetList.Add(ActionSet->GetPrimaryAssetId()); + // } + // } + + // Load assets associated with the experience + + TArray bundlesToLoad; + bundlesToLoad.Add(FOLSBundles::Equipped); + + //@TODO: Centralize this client/server stuff into the OLSAssetManager + const ENetMode ownerNetMode = GetOwner()->GetNetMode(); + const bool shouldLoadClient = GIsEditor || (ownerNetMode != NM_DedicatedServer); + const bool shouldLoadServer = GIsEditor || (ownerNetMode != NM_Client); + if (shouldLoadClient) + { + bundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateClient); + } + if (shouldLoadServer) + { + bundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateServer); + } + + TSharedPtr bundleLoadHandle = nullptr; + if (bundleAssetList.Num() > 0) + { + bundleLoadHandle = assetManager.ChangeBundleStateForPrimaryAssets(bundleAssetList.Array(), bundlesToLoad, {}, false, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority); + } + + TSharedPtr rawLoadHandle = nullptr; + if (rawAssetList.Num() > 0) + { + rawLoadHandle = assetManager.LoadAssetList(rawAssetList.Array(), FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, TEXT("StartExperienceLoad()")); + } + + // If both async loads are running, combine them + TSharedPtr handle = nullptr; + if (bundleLoadHandle.IsValid() && rawLoadHandle.IsValid()) + { + handle = assetManager.GetStreamableManager().CreateCombinedHandle({ bundleLoadHandle, rawLoadHandle }); + } + else + { + handle = bundleLoadHandle.IsValid() ? bundleLoadHandle : rawLoadHandle; + } + + FStreamableDelegate onAssetsLoadedDelegate = FStreamableDelegate::CreateUObject(this, &ThisClass::OnExperienceLoadComplete); + if (!handle.IsValid() || handle->HasLoadCompleted()) + { + // Assets were already loaded, call the delegate now + FStreamableHandle::ExecuteDelegate(onAssetsLoadedDelegate); + } + else + { + handle->BindCompleteDelegate(onAssetsLoadedDelegate); + + handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([onAssetsLoadedDelegate]() + { + onAssetsLoadedDelegate.ExecuteIfBound(); + })); + } + + // This set of assets gets preloaded, but we don't block the start of the experience based on it + TSet preloadAssetList; + //@TODO: Determine assets to preload (but not blocking-ly) + if (preloadAssetList.Num() > 0) + { + assetManager.ChangeBundleStateForPrimaryAssets(preloadAssetList.Array(), bundlesToLoad, {}); + } +} + +void UOLSExperienceManagerComponent::OnExperienceLoadComplete() +{ + check(LoadState == EOLSExperienceLoadState::Loading); + check(CurrentExperience != nullptr); + + // @Todo replace this by our custom. + // UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: OnExperienceLoadComplete(CurrentExperience = %s, %s)"), + // *CurrentExperience->GetPrimaryAssetId().ToString(), + // *GetClientServerContextString(this)); + + // find the URLs for our GameFeaturePlugins - filtering out dupes and ones that don't have a valid mapping + GameFeaturePluginURLs.Reset(); + + auto collectGameFeaturePluginURLs = [This=this](const UPrimaryDataAsset* context, const TArray& featurePluginList) + { + for (const FString& pluginName : featurePluginList) + { + FString pluginURL; + if (UGameFeaturesSubsystem::Get().GetPluginURLByName(pluginName, /*out*/ pluginURL)) + { + This->GameFeaturePluginURLs.AddUnique(pluginURL); + } + else + { + ensureMsgf(false, TEXT("OnExperienceLoadComplete failed to find plugin URL from PluginName %s for experience %s - fix data, ignoring for this run"), *pluginName, *context->GetPrimaryAssetId().ToString()); + } + } + + // // Add in our extra plugin + // if (!CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent.IsEmpty()) + // { + // FString PluginURL; + // if (UGameFeaturesSubsystem::Get().GetPluginURLByName(CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent, PluginURL)) + // { + // GameFeaturePluginURLs.AddUnique(PluginURL); + // } + // } + }; + + collectGameFeaturePluginURLs(CurrentExperience, CurrentExperience->GameFeaturesToEnable); + // @Todo implement this. + // for (const TObjectPtr& ActionSet : CurrentExperience->ActionSets) + // { + // if (ActionSet != nullptr) + // { + // CollectGameFeaturePluginURLs(ActionSet, ActionSet->GameFeaturesToEnable); + // } + // } + + // Load and activate the features + NumGameFeaturePluginsLoading = GameFeaturePluginURLs.Num(); + if (NumGameFeaturePluginsLoading > 0) + { + LoadState = EOLSExperienceLoadState::LoadingGameFeatures; + for (const FString& pluginURL : GameFeaturePluginURLs) + { + UOLSExperienceManager::NotifyOfPluginActivation(pluginURL); + UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin( + pluginURL, + FGameFeaturePluginLoadComplete::CreateUObject(this, &ThisClass::OnGameFeaturePluginLoadComplete)); + } + } + else + { + OnExperienceFullLoadCompleted(); + } +} + +void UOLSExperienceManagerComponent::OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& result) +{ + // decrement the number of plugins that are loading + NumGameFeaturePluginsLoading--; + + if (NumGameFeaturePluginsLoading == 0) + { + OnExperienceFullLoadCompleted(); + } +} + +void UOLSExperienceManagerComponent::OnExperienceFullLoadCompleted() +{ + check(LoadState != EOLSExperienceLoadState::Loaded); + + // Insert a random delay for testing (if configured) + if (LoadState != EOLSExperienceLoadState::LoadingChaosTestingDelay) + { + const float delaySecs = OLSConsoleVariables::GetExperienceLoadDelayDuration(); + if (delaySecs > 0.0f) + { + FTimerHandle dummyHandle; + + LoadState = EOLSExperienceLoadState::LoadingChaosTestingDelay; + GetWorld()->GetTimerManager().SetTimer(dummyHandle, this, &ThisClass::OnExperienceFullLoadCompleted, delaySecs, /*bLooping=*/ false); + + return; + } + } + + LoadState = EOLSExperienceLoadState::ExecutingActions; + + // Execute the actions + FGameFeatureActivatingContext context; + + // Only apply to our specific world context if set + const FWorldContext* existingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld()); + if (existingWorldContext) + { + context.SetRequiredWorldContextHandle(existingWorldContext->ContextHandle); + } + + auto activateListOfActions = [&context](const TArray& actionList) + { + for (UGameFeatureAction* action : actionList) + { + if (action != nullptr) + { + //@TODO: The fact that these don't take a world are potentially problematic in client-server PIE + // The current behavior matches systems like gameplay tags where loading and registering apply to the entire process, + // but actually applying the results to actors is restricted to a specific world + action->OnGameFeatureRegistering(); + action->OnGameFeatureLoading(); + action->OnGameFeatureActivating(context); + } + } + }; + + activateListOfActions(CurrentExperience->Actions); + // @Todo implement this + // for (const TObjectPtr& ActionSet : CurrentExperience->ActionSets) + // { + // if (ActionSet != nullptr) + // { + // activateListOfActions(ActionSet->Actions); + // } + // } + + LoadState = EOLSExperienceLoadState::Loaded; + + OnExperienceLoaded_HighPriority.Broadcast(CurrentExperience); + OnExperienceLoaded_HighPriority.Clear(); + + OnExperienceLoaded.Broadcast(CurrentExperience); + OnExperienceLoaded.Clear(); + + OnExperienceLoaded_LowPriority.Broadcast(CurrentExperience); + OnExperienceLoaded_LowPriority.Clear(); + + // Apply any necessary scalability settings +#if !UE_SERVER + // @Todo implement this + // UOLSSettingsLocal::Get()->OnExperienceLoaded(); +#endif +} + +void UOLSExperienceManagerComponent::OnActionDeactivationCompleted() +{ + check(IsInGameThread()); + ++NumObservedPausers; + + if (NumObservedPausers == NumExpectedPausers) + { + OnAllActionsDeactivated(); + } +} + +void UOLSExperienceManagerComponent::OnAllActionsDeactivated() +{ + //@TODO: We actually only deactivated and didn't fully unload... + LoadState = EOLSExperienceLoadState::Unloaded; + CurrentExperience = nullptr; + //@TODO: GEngine->ForceGarbageCollection(true); +} diff --git a/Source/ols/Private/Player/OLSPlayerState.cpp b/Source/ols/Private/Player/OLSPlayerState.cpp index 05aad1c..88da7f0 100644 --- a/Source/ols/Private/Player/OLSPlayerState.cpp +++ b/Source/ols/Private/Player/OLSPlayerState.cpp @@ -4,13 +4,61 @@ #include "Player/OLSPlayerState.h" #include "AbilitySystem/OLSAbilitySystemComponent.h" +#include "Components/GameFrameworkComponentManager.h" +#include "DataAssets/OLSAbilitySetPrimaryDataAsset.h" +#include "Net/UnrealNetwork.h" #include "Player/OLSPlayerController.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSPlayerState) + +const FName AOLSPlayerState::NAME_OLSAbilityReady("OLSAbilitiesReady"); + AOLSPlayerState::AOLSPlayerState(const FObjectInitializer& objectInitializer) : Super(objectInitializer) { // Create attribute sets here. } +template +const T* AOLSPlayerState::GetPawnData() const +{ + return Cast(PawnData); +} + +void AOLSPlayerState::SetPawnData(const UOLSPawnPrimaryDataAsset* pawnData) +{ + check(pawnData); + + if (!HasAuthority()) + { + return; + } + + if (PawnData) + { + // @Todo: replace this by our custom log. + // UE_LOG(LogLyra, Error, TEXT("Trying to set PawnData [%s] on player state [%s] that already has valid PawnData [%s]."), *GetNameSafe(InPawnData), *GetNameSafe(this), *GetNameSafe(PawnData)); + return; + } + + MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, PawnData, this); + + for (const UOLSAbilitySetPrimaryDataAsset* abilityDataAsset : PawnData->AbilitySets) + { + if (abilityDataAsset) + { + abilityDataAsset->GiveToAbilitySystem(AbilitySystemComponent, nullptr); + } + } + + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, NAME_OLSAbilityReady); + + ForceNetUpdate(); +} + +void AOLSPlayerState::OnRep_PawnData() +{ +} + void AOLSPlayerState::PostInitializeComponents() { Super::PostInitializeComponents(); @@ -29,6 +77,16 @@ void AOLSPlayerState::PostInitializeComponents() } } +void AOLSPlayerState::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + FDoRepLifetimeParams SharedParams; + SharedParams.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, PawnData, SharedParams); +} + AOLSPlayerController* AOLSPlayerState::GetOLSPlayerController() const { return Cast(GetOwner()); @@ -37,4 +95,4 @@ AOLSPlayerController* AOLSPlayerState::GetOLSPlayerController() const UOLSAbilitySystemComponent* AOLSPlayerState::GetOLSAbilitySystemComponent() const { return AbilitySystemComponent; -} +} \ No newline at end of file diff --git a/Source/ols/Private/Systems/OLSAssetManager.cpp b/Source/ols/Private/Systems/OLSAssetManager.cpp new file mode 100644 index 0000000..7df5746 --- /dev/null +++ b/Source/ols/Private/Systems/OLSAssetManager.cpp @@ -0,0 +1,329 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "Systems/OLSAssetManager.h" +#include "Misc/App.h" +#include "Stats/StatsMisc.h" +#include "Misc/ScopedSlowTask.h" +#include "Engine/Engine.h" +#include "Systems/OLSAssetManagerStartupJob.h" +#include "DataAssets/OLSPawnPrimaryDataAsset.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSAssetManager) + +const FName FOLSBundles::Equipped("Equipped"); + +////////////////////////////////////////////////////////////////////// + +static FAutoConsoleCommand CVarDumpLoadedAssets( + TEXT("OLS.DumpLoadedAssets"), + TEXT("Shows all assets that were loaded via the asset manager and are currently in memory."), + FConsoleCommandDelegate::CreateStatic(UOLSAssetManager::DumpLoadedAssets) +); + +////////////////////////////////////////////////////////////////////// + +#define STARTUP_JOB_WEIGHTED(jobFunc, jobWeight) StartupJobs.Add(FOLSAssetManagerStartupJob(#jobFunc, [this](const FOLSAssetManagerStartupJob& startupJob, TSharedPtr& loadHandle){jobFunc;}, jobWeight)) +#define STARTUP_JOB(jobFunc) STARTUP_JOB_WEIGHTED(jobFunc, 1.f) + +////////////////////////////////////////////////////////////////////// + +UOLSAssetManager::UOLSAssetManager() +{ + DefaultPawnData = nullptr; +} + +UOLSAssetManager& UOLSAssetManager::Get() +{ + check(GEngine); + + if (UOLSAssetManager* Singleton = Cast(GEngine->AssetManager)) + { + return *Singleton; + } + + // @Todo replace this our custom log. + // UE_LOG(LogLyra, Fatal, TEXT("Invalid AssetManagerClassName in DefaultEngine.ini. It must be set to LyraAssetManager!")); + + // Fatal error above prevents this from being called. + return *NewObject(); +} + +template +AssetType* UOLSAssetManager::GetAsset(const TSoftObjectPtr& assetPointer, bool shouldKeepInMemory) +{ + AssetType* loadedAsset = nullptr; + + const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); + + if (assetPath.IsValid()) + { + loadedAsset = assetPointer.Get(); + if (!loadedAsset) + { + loadedAsset = Cast(SynchronousLoadAsset(assetPath)); + ensureAlwaysMsgf(loadedAsset, TEXT("Failed to load asset [%s]"), *assetPointer.ToString()); + } + + if (loadedAsset && shouldKeepInMemory) + { + // Added to loaded asset list. + Get().AddLoadedAsset(Cast(loadedAsset)); + } + } + + return loadedAsset; +} + +template +TSubclassOf UOLSAssetManager::GetSubclass(const TSoftClassPtr& assetPointer, bool shouldKeepInMemory) +{ + TSubclassOf loadedSubclass = nullptr; + + const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); + + if (assetPath.IsValid()) + { + loadedSubclass = assetPointer.Get(); + if (!loadedSubclass) + { + loadedSubclass = Cast(SynchronousLoadAsset(assetPath)); + ensureAlwaysMsgf(loadedSubclass, TEXT("Failed to load asset class [%s]"), *assetPointer.ToString()); + } + + if (loadedSubclass && shouldKeepInMemory) + { + // Added to loaded asset list. + Get().AddLoadedAsset(Cast(loadedSubclass)); + } + } + + return loadedSubclass; +} + +void UOLSAssetManager::DumpLoadedAssets() +{ +} + +const UOLSPawnPrimaryDataAsset* UOLSAssetManager::GetDefaultPawnData() const +{ + return GetAsset(DefaultPawnData); +} + +template +const GameDataClass& UOLSAssetManager::GetOrLoadTypedGameData(const TSoftObjectPtr& dataPath) +{ + if (const TObjectPtr* pResult = GameDataMap.Find(GameDataClass::StaticClass())) + { + return *CastChecked(*pResult); + } + + // Does a blocking load if needed + return *CastChecked( + LoadGameDataOfClass(GameDataClass::StaticClass(), dataPath, GameDataClass::StaticClass()->GetFName())); +} + + + +UObject* UOLSAssetManager::SynchronousLoadAsset(const FSoftObjectPath& assetPath) +{ + if (assetPath.IsValid()) + { + TUniquePtr logTimePtr; + + if (ShouldLogAssetLoads()) + { + logTimePtr = MakeUnique(*FString::Printf(TEXT("Synchronously loaded asset [%s]"), *assetPath.ToString()), nullptr, FScopeLogTime::ScopeLog_Seconds); + } + + if (UAssetManager::IsInitialized()) + { + return UAssetManager::GetStreamableManager().LoadSynchronous(assetPath, false); + } + + // Use LoadObject if asset manager isn't ready yet. + return assetPath.TryLoad(); + } + + return nullptr; +} + +bool UOLSAssetManager::ShouldLogAssetLoads() +{ + static bool shouldLogAssetLoads = FParse::Param(FCommandLine::Get(), TEXT("LogAssetLoads")); + return shouldLogAssetLoads; +} + +void UOLSAssetManager::AddLoadedAsset(const UObject* Asset) +{ + if (ensureAlways(Asset)) + { + FScopeLock LoadedAssetsLock(&LoadedAssetsCritical); + LoadedAssets.Add(Asset); + } +} + +void UOLSAssetManager::StartInitialLoading() +{ + SCOPED_BOOT_TIMING("UOLSAssetManager::StartInitialLoading"); + + // This does all of the scanning, need to do this now even if loads are deferred + Super::StartInitialLoading(); + + STARTUP_JOB(InitializeGameplayCueManager()); + + { + // Load base game data asset + // @Todo uncomment this after implementing GetGameData(). + // STARTUP_JOB_WEIGHTED(GetGameData(), 25.f); + } + + // Run all the queued up startup jobs + DoAllStartupJobs(); +} + +#if WITH_EDITOR +void UOLSAssetManager::PreBeginPIE(bool shouldStartSimulate) +{ + Super::PreBeginPIE(shouldStartSimulate); + + { + FScopedSlowTask slowTask(0, NSLOCTEXT("OLSEditor", "BeginLoadingPIEData", "Loading PIE Data")); + const bool shouldShowCancelButton = false; + const bool shouldAllowInPIE = true; + slowTask.MakeDialog(shouldShowCancelButton, shouldAllowInPIE); + + // @Todo unlock this comment. + // const ULyraGameData& LocalGameDataCommon = GetGameData(); + + // Intentionally after GetGameData to avoid counting GameData time in this timer + SCOPE_LOG_TIME_IN_SECONDS(TEXT("PreBeginPIE asset preloading complete"), nullptr); + + // You could add preloading of anything else needed for the experience we'll be using here + // (e.g., by grabbing the default experience from the world settings + the experience override in developer settings) + } +} +#endif + +UPrimaryDataAsset* UOLSAssetManager::LoadGameDataOfClass( + TSubclassOf dataClass, + const TSoftObjectPtr& dataClassPath, + FPrimaryAssetType primaryAssetType) +{ + UPrimaryDataAsset* asset = nullptr; + + DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Loading GameData Object"), STAT_GameData, STATGROUP_LoadTime); + if (!dataClassPath.IsNull()) + { +#if WITH_EDITOR + FScopedSlowTask slowTask(0, FText::Format(NSLOCTEXT("OLSEditor", "BeginLoadingGameDataTask", "Loading GameData {0}"), FText::FromName(dataClass->GetFName()))); + const bool shouldShowCancelButton = false; + const bool shouldAllowInPIE = true; + slowTask.MakeDialog(shouldShowCancelButton, shouldAllowInPIE); +#endif + // @Todo replace this log with our custom. + // UE_LOG(LogLyra, Log, TEXT("Loading GameData: %s ..."), *dataClassPath.ToString()); + SCOPE_LOG_TIME_IN_SECONDS(TEXT(" ... GameData loaded!"), nullptr); + + // This can be called recursively in the editor because it is called on demand from PostLoad so force a sync load for primary asset and async load the rest in that case + if (GIsEditor) + { + asset = dataClassPath.LoadSynchronous(); + LoadPrimaryAssetsWithType(primaryAssetType); + } + else + { + TSharedPtr handle = LoadPrimaryAssetsWithType(primaryAssetType); + if (handle.IsValid()) + { + handle->WaitUntilComplete(0.0f, false); + + // This should always work + asset = Cast(handle->GetLoadedAsset()); + } + } + } + + if (asset) + { + GameDataMap.Add(dataClass, asset); + } + else + { + // It is not acceptable to fail to load any GameData asset. It will result in soft failures that are hard to diagnose. + // @Todo replace this log with our custom. + // UE_LOG(LogLyra, Fatal, TEXT("Failed to load GameData asset at %s. Type %s. This is not recoverable and likely means you do not have the correct data to run %s."), *dataClassPath.ToString(), *primaryAssetType.ToString(), FApp::GetProjectName()); + } + + return asset; +} + +void UOLSAssetManager::DoAllStartupJobs() +{ + SCOPED_BOOT_TIMING("ULyraAssetManager::DoAllStartupJobs"); + const double AllStartupJobsStartTime = FPlatformTime::Seconds(); + + // if (IsRunningDedicatedServer()) + // { + // // No need for periodic progress updates, just run the jobs + // for (const FLyraAssetManagerStartupJob& StartupJob : StartupJobs) + // { + // StartupJob.DoJob(); + // } + // } + // else + // { + // if (StartupJobs.Num() > 0) + // { + // float TotalJobValue = 0.0f; + // for (const FLyraAssetManagerStartupJob& StartupJob : StartupJobs) + // { + // TotalJobValue += StartupJob.JobWeight; + // } + // + // float AccumulatedJobValue = 0.0f; + // for (FLyraAssetManagerStartupJob& StartupJob : StartupJobs) + // { + // const float JobValue = StartupJob.JobWeight; + // StartupJob.SubstepProgressDelegate.BindLambda([This = this, AccumulatedJobValue, JobValue, TotalJobValue](float NewProgress) + // { + // const float SubstepAdjustment = FMath::Clamp(NewProgress, 0.0f, 1.0f) * JobValue; + // const float OverallPercentWithSubstep = (AccumulatedJobValue + SubstepAdjustment) / TotalJobValue; + // + // This->UpdateInitialGameContentLoadPercent(OverallPercentWithSubstep); + // }); + // + // StartupJob.DoJob(); + // + // StartupJob.SubstepProgressDelegate.Unbind(); + // + // AccumulatedJobValue += JobValue; + // + // UpdateInitialGameContentLoadPercent(AccumulatedJobValue / TotalJobValue); + // } + // } + // else + // { + // UpdateInitialGameContentLoadPercent(1.0f); + // } + // } + // + // StartupJobs.Empty(); + + // @Todo replace this with our custom log. + // UE_LOG(LogLyra, Display, TEXT("All startup jobs took %.2f seconds to complete"), FPlatformTime::Seconds() - AllStartupJobsStartTime); +} + +void UOLSAssetManager::InitializeGameplayCueManager() +{ + SCOPED_BOOT_TIMING("UOLSAssetManager::InitializeGameplayCueManager"); + + // ULyraGameplayCueManager* GCM = ULyraGameplayCueManager::Get(); + // check(GCM); + // GCM->LoadAlwaysLoadedCues(); +} + +void UOLSAssetManager::UpdateInitialGameContentLoadPercent(float gameContentPercent) +{ + // Could route this to the early startup loading screen +} diff --git a/Source/ols/Private/Systems/OLSAssetManagerStartupJob.cpp b/Source/ols/Private/Systems/OLSAssetManagerStartupJob.cpp new file mode 100644 index 0000000..fc4f84f --- /dev/null +++ b/Source/ols/Private/Systems/OLSAssetManagerStartupJob.cpp @@ -0,0 +1,46 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "Systems/OLSAssetManagerStartupJob.h" + +TSharedPtr FOLSAssetManagerStartupJob::DoJob() const +{ + const double jobStartTime = FPlatformTime::Seconds(); + + TSharedPtr handle; + // @Todo replace this with our custom log. + // UE_LOG(LogLyra, Display, TEXT("Startup job \"%s\" starting"), *JobName); + JobFunc(*this, handle); + + if (handle.IsValid()) + { + handle->BindUpdateDelegate(FStreamableUpdateDelegate::CreateRaw(this, &FOLSAssetManagerStartupJob::UpdateSubstepProgressFromStreamable)); + handle->WaitUntilComplete(0.0f, false); + handle->BindUpdateDelegate(FStreamableUpdateDelegate()); + } + + // @Todo replace this with our custom log. + // UE_LOG(LogLyra, Display, TEXT("Startup job \"%s\" took %.2f seconds to complete"), *JobName, FPlatformTime::Seconds() - jobStartTime); + + return handle; +} + +void FOLSAssetManagerStartupJob::UpdateSubstepProgress(float newProgress) const +{ + SubstepProgressDelegate.ExecuteIfBound(newProgress); +} + +void FOLSAssetManagerStartupJob::UpdateSubstepProgressFromStreamable( + TSharedRef streamableHandle) const +{ + if (SubstepProgressDelegate.IsBound()) + { + // StreamableHandle::GetProgress traverses() a large graph and is quite expensive + double now = FPlatformTime::Seconds(); + if (LastUpdate - now > 1.0 / 60) + { + SubstepProgressDelegate.Execute(streamableHandle->GetProgress()); + LastUpdate = now; + } + } +} diff --git a/Source/ols/Public/DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h b/Source/ols/Public/DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h index 1c86adc..55ed3a3 100644 --- a/Source/ols/Public/DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h +++ b/Source/ols/Public/DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h @@ -35,7 +35,7 @@ public: #endif //~ End of UPrimaryDataAsset interface -protected: +public: // List of Game Feature Plugins this experience wants to have active UPROPERTY(EditDefaultsOnly, Category = "OLSExperienceDefinition") diff --git a/Source/ols/Public/DataAssets/OLSPawnPrimaryDataAsset.h b/Source/ols/Public/DataAssets/OLSPawnPrimaryDataAsset.h index 9fe5953..e4a95f8 100644 --- a/Source/ols/Public/DataAssets/OLSPawnPrimaryDataAsset.h +++ b/Source/ols/Public/DataAssets/OLSPawnPrimaryDataAsset.h @@ -26,7 +26,7 @@ public: virtual FString GetIdentifierString() const override; //~ End UOLSPrimaryDataAsset interface -protected: +public: // Class to instantiate for this pawn (should usually derive from AOLSPawn or AOLSCharacter). UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "OLS|Pawn") diff --git a/Source/ols/Public/GameModes/OLSExperienceManager.h b/Source/ols/Public/GameModes/OLSExperienceManager.h new file mode 100644 index 0000000..3fec6df --- /dev/null +++ b/Source/ols/Public/GameModes/OLSExperienceManager.h @@ -0,0 +1,33 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/EngineSubsystem.h" +#include "OLSExperienceManager.generated.h" + +/** + * Manager for experiences - primarily for arbitration between multiple PIE sessions + */ +UCLASS(MinimalAPI) +class UOLSExperienceManager : public UEngineSubsystem +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + OLS_API void OnPlayInEditorBegun(); + + static void NotifyOfPluginActivation(const FString pluginURL); + static bool RequestToDeactivatePlugin(const FString pluginURL); +#else + static void NotifyOfPluginActivation(const FString pluginURL) {} + static bool RequestToDeactivatePlugin(const FString pluginURL) { return true; } +#endif + +private: + + // The map of requests to active count for a given game feature plugin + // (to allow first in, last out activation management during PIE) + TMap GameFeaturePluginRequestCountMap; +}; diff --git a/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h b/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h index 4698581..44fba1d 100644 --- a/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h +++ b/Source/ols/Public/GameModes/OLSExperienceManagerComponent.h @@ -4,12 +4,12 @@ #include "CoreMinimal.h" #include "Components/GameStateComponent.h" -// #include "LoadingProcessInterface.h" +#include "LoadingProcessInterface.h" #include "OLSExperienceManagerComponent.generated.h" namespace UE::GameFeatures { struct FResult; } -// DECLARE_MULTICAST_DELEGATE_OneParam(FOnLyraExperienceLoaded, const ULyraExperienceDefinition* /*Experience*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnOLSExperienceLoaded, const class UOLSExperienceDefinitionPrimaryDataAsset* /*experience*/); enum class EOLSExperienceLoadState { @@ -23,8 +23,87 @@ enum class EOLSExperienceLoadState }; UCLASS() -class OLS_API UOLSExperienceManagerComponent : public UGameStateComponent /*, public ILoadingProcessInterface */ +class OLS_API UOLSExperienceManagerComponent : public UGameStateComponent, public ILoadingProcessInterface { GENERATED_BODY() + +public: + + UOLSExperienceManagerComponent(const FObjectInitializer& objectInitializer); + + //~ Begin UActorComponent interface + virtual void EndPlay(const EEndPlayReason::Type endPlayReason) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + //~ End UActorComponent interface + + //~ Begin ILoadingProcessInterface interface + virtual bool ShouldShowLoadingScreen(FString& outReason) const override; + //~ End ILoadingProcessInterface + +public: + + // This returns the current experience if it is fully loaded, asserting otherwise + // (i.e., if you called it too soon) + const UOLSExperienceDefinitionPrimaryDataAsset* GetCurrentExperienceChecked() const; + + // Tries to set the current experience, either a UI or gameplay one + void SetCurrentExperience(FPrimaryAssetId experienceId); + + // Returns true if the experience is fully loaded + bool IsExperienceLoaded() const; + + // Ensures the delegate is called once the experience has been loaded, + // before others are called. + // However, if the experience has already loaded, calls the delegate immediately. + void CallOrRegister_OnExperienceLoaded_HighPriority(FOnOLSExperienceLoaded::FDelegate&& delegate); + + // Ensures the delegate is called once the experience has been loaded + // If the experience has already loaded, calls the delegate immediately + void CallOrRegister_OnExperienceLoaded(FOnOLSExperienceLoaded::FDelegate&& delegate); + + // Ensures the delegate is called once the experience has been loaded + // If the experience has already loaded, calls the delegate immediately + void CallOrRegister_OnExperienceLoaded_LowPriority(FOnOLSExperienceLoaded::FDelegate&& delegate); + +private: + + UFUNCTION() + void OnRep_CurrentExperience(); + +private: + + void StartExperienceLoad(); + void OnExperienceLoadComplete(); + void OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& result); + void OnExperienceFullLoadCompleted(); + + void OnActionDeactivationCompleted(); + void OnAllActionsDeactivated(); +private: + + UPROPERTY(ReplicatedUsing = OnRep_CurrentExperience) + TObjectPtr CurrentExperience = nullptr; + +private: + + EOLSExperienceLoadState LoadState = EOLSExperienceLoadState::Unloaded; + + int32 NumGameFeaturePluginsLoading = 0; + TArray GameFeaturePluginURLs; + + int32 NumObservedPausers = 0; + int32 NumExpectedPausers = 0; + + /** + * Delegate called when the experience has finished loading just before others + * (e.g., subsystems that set up for regular gameplay) + */ + FOnOLSExperienceLoaded OnExperienceLoaded_HighPriority; + + /** Delegate called when the experience has finished loading */ + FOnOLSExperienceLoaded OnExperienceLoaded; + + /** Delegate called when the experience has finished loading */ + FOnOLSExperienceLoaded OnExperienceLoaded_LowPriority; }; diff --git a/Source/ols/Public/Player/OLSPlayerState.h b/Source/ols/Public/Player/OLSPlayerState.h index a01e5a2..b6d44f5 100644 --- a/Source/ols/Public/Player/OLSPlayerState.h +++ b/Source/ols/Public/Player/OLSPlayerState.h @@ -6,6 +6,7 @@ #include "ModularGameplayActors/OLSModularPlayerState.h" #include "OLSPlayerState.generated.h" + /** * AOLSPlayerState * @@ -20,9 +21,11 @@ public: AOLSPlayerState(const FObjectInitializer& objectInitializer); + static const FName NAME_OLSAbilityReady; //~ Begin AActor interface virtual void PostInitializeComponents() override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; //~ End AActor interface public: @@ -32,4 +35,19 @@ public: UFUNCTION(BlueprintCallable, Category = "OLS|PlayerState") class UOLSAbilitySystemComponent* GetOLSAbilitySystemComponent() const; + + template + const T* GetPawnData() const; + + void SetPawnData(const class UOLSPawnPrimaryDataAsset* pawnData); + +protected: + + UFUNCTION() + void OnRep_PawnData(); + +protected: + + UPROPERTY(ReplicatedUsing = OnRep_PawnData) + TObjectPtr PawnData = nullptr; }; diff --git a/Source/ols/Public/Systems/OLSAssetManager.h b/Source/ols/Public/Systems/OLSAssetManager.h new file mode 100644 index 0000000..a02a4cc --- /dev/null +++ b/Source/ols/Public/Systems/OLSAssetManager.h @@ -0,0 +1,111 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/AssetManager.h" +#include "OLSAssetManager.generated.h" + + +struct FOLSBundles +{ + static const FName Equipped; +}; + +/** + * UOLSAssetManager + * + * Game implementation of the asset manager that overrides functionality and stores game-specific types. + * It is expected that most games will want to override AssetManager as it provides a good place for game-specific loading logic. + * Thi + */ +UCLASS(Config = Game) +class OLS_API UOLSAssetManager : public UAssetManager +{ + GENERATED_BODY() + +public: + + UOLSAssetManager(); + + // Returns the AssetManager singleton object. + static UOLSAssetManager& Get(); + + // Returns the asset referenced by a TSoftObjectPtr. This will synchronously load the asset if it's not already loaded. + template + static AssetType* GetAsset(const TSoftObjectPtr& assetPointer, bool shouldKeepInMemory = true); + + // Returns the subclass referenced by a TSoftClassPtr. This will synchronously load the asset if it's not already loaded. + template + static TSubclassOf GetSubclass(const TSoftClassPtr& assetPointer, bool shouldKeepInMemory = true); + + // Logs all assets currently loaded and tracked by the asset manager. + static void DumpLoadedAssets(); + + // @Todo implement this function. + // const class UOLSPawnPrimaryDataAsset& GetGameData(); + const class UOLSPawnPrimaryDataAsset* GetDefaultPawnData() const; + +protected: + + template + const GameDataClass& GetOrLoadTypedGameData(const TSoftObjectPtr& dataPath); + + static UObject* SynchronousLoadAsset(const FSoftObjectPath& assetPath); + static bool ShouldLogAssetLoads(); + + // Thread safe way of adding a loaded asset to keep in memory. + void AddLoadedAsset(const UObject* Asset); + + //~UAssetManager interface + virtual void StartInitialLoading() override; +#if WITH_EDITOR + virtual void PreBeginPIE(bool shouldStartSimulate) override; +#endif + //~End of UAssetManager interface + + class UPrimaryDataAsset* LoadGameDataOfClass( + TSubclassOf dataClass, + const TSoftObjectPtr& dataClassPath, + FPrimaryAssetType primaryAssetType); + +private: + + // Flushes the StartupJobs array. Processes all startup work. + void DoAllStartupJobs(); + + // Sets up the ability system + void InitializeGameplayCueManager(); + + // Called periodically during loads, could be used to feed the status to a loading screen + void UpdateInitialGameContentLoadPercent(float gameContentPercent); + +protected: + + // @Todo implement this. + // Global game data asset to use. + // UPROPERTY(Config) + // TSoftObjectPtr LyraGameDataPath; + + // Loaded version of the game data + UPROPERTY(Transient) + TMap, TObjectPtr> GameDataMap; + + // Pawn data used when spawning player pawns if there isn't one set on the player state. + UPROPERTY(Config) + TSoftObjectPtr DefaultPawnData; + +private: + + // Assets loaded and tracked by the asset manager. + UPROPERTY() + TSet> LoadedAssets; + + // Used for a scope lock when modifying the list of load assets. + FCriticalSection LoadedAssetsCritical; + +private: + + // The list of tasks to execute on startup. Used to track startup progress. + TArray StartupJobs; +}; diff --git a/Source/ols/Public/Systems/OLSAssetManagerStartupJob.h b/Source/ols/Public/Systems/OLSAssetManagerStartupJob.h new file mode 100644 index 0000000..82f979a --- /dev/null +++ b/Source/ols/Public/Systems/OLSAssetManagerStartupJob.h @@ -0,0 +1,32 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "Engine/StreamableManager.h" + + +DECLARE_DELEGATE_OneParam(FOLSAssetManagerStartupJobSubstepProgress, float /*newProgress*/); + +/** Handles reporting progress from streamable handles */ +struct FOLSAssetManagerStartupJob +{ + FOLSAssetManagerStartupJobSubstepProgress SubstepProgressDelegate; + TFunction&)> JobFunc; + FString JobName; + float JobWeight; + mutable double LastUpdate = 0; + + /** Simple job that is all synchronous */ + FOLSAssetManagerStartupJob(const FString& jobName, const TFunction&)>& jobFunc, float jobWeight) + : JobFunc(jobFunc) + , JobName(jobName) + , JobWeight(jobWeight) + {} + + /** Perform actual loading, will return a handle if it created one */ + TSharedPtr DoJob() const; + + void UpdateSubstepProgress(float newProgress) const; + + void UpdateSubstepProgressFromStreamable(TSharedRef streamableHandle) const; +}; diff --git a/Source/ols/ols.Build.cs b/Source/ols/ols.Build.cs index 996c5e9..f819267 100644 --- a/Source/ols/ols.Build.cs +++ b/Source/ols/ols.Build.cs @@ -19,7 +19,7 @@ public class ols : ModuleRules "GameFeatures", "ModularGameplay", "EnhancedInput", - "OLSAnimation", "AIModule", + "OLSAnimation", "AIModule", "CommonLoadingScreen", }); PrivateDependencyModuleNames.AddRange(new[]