diff --git a/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp new file mode 100644 index 00000000..5c124add --- /dev/null +++ b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp @@ -0,0 +1,148 @@ +#include "GameFeatures/GBFGameFeatureAction_AddLevelInstances.h" + +#include "Engine/LevelStreamingDynamic.h" +#include "Misc/DataValidation.h" + +#define LOCTEXT_NAMESPACE "AncientGameFeatures" + +void UGBFGameFeatureAction_AddLevelInstances::OnGameFeatureActivating( FGameFeatureActivatingContext & context ) +{ + FWorldDelegates::OnWorldCleanup.AddUObject( this, &UGBFGameFeatureAction_AddLevelInstances::OnWorldCleanup ); + + if ( !ensureAlways( AddedLevels.Num() == 0 ) ) + { + DestroyAddedLevels(); + } + + bIsActivated = true; + Super::OnGameFeatureActivating( context ); +} + +void UGBFGameFeatureAction_AddLevelInstances::OnGameFeatureDeactivating( FGameFeatureDeactivatingContext & context ) +{ + DestroyAddedLevels(); + bIsActivated = false; + + FWorldDelegates::OnWorldCleanup.RemoveAll( this ); + Super::OnGameFeatureDeactivating( context ); +} + +#if WITH_EDITOR +EDataValidationResult UGBFGameFeatureAction_AddLevelInstances::IsDataValid( FDataValidationContext & context ) const +{ + auto result = CombineDataValidationResults( Super::IsDataValid( context ), EDataValidationResult::Valid ); + + auto entry_index = 0; + for ( const auto & [ level, target_world, location, rotation ] : LevelInstanceList ) + { + if ( level.IsNull() ) + { + result = EDataValidationResult::Invalid; + context.AddError( FText::Format( LOCTEXT( "LevelEntryNull", "Null level reference at index {0} in LevelInstanceList" ), FText::AsNumber( entry_index ) ) ); + } + + ++entry_index; + } + + return result; +} +#endif + +void UGBFGameFeatureAction_AddLevelInstances::AddToWorld( const FWorldContext & world_context, const FGameFeatureStateChangeContext & change_context ) +{ + auto * world = world_context.World(); + + if ( const auto game_instance = world_context.OwningGameInstance; + ensureAlways( bIsActivated ) && ( game_instance != nullptr ) && ( world != nullptr ) && world->IsGameWorld() ) + { + AddedLevels.Reserve( AddedLevels.Num() + LevelInstanceList.Num() ); + + for ( const auto & entry : LevelInstanceList ) + { + if ( entry.Level.IsNull() ) + { + continue; + } + if ( !entry.TargetWorld.IsNull() ) + { + if ( const auto * target_world = entry.TargetWorld.Get(); + target_world != world ) + { + // This level is intended for a specific world (not this one) + continue; + } + } + + LoadDynamicLevelForEntry( entry, world ); + } + } + + GEngine->BlockTillLevelStreamingCompleted( world ); +} + +void UGBFGameFeatureAction_AddLevelInstances::OnWorldCleanup( UWorld * world, bool /*session_ended*/, bool /*cleanup_resources*/ ) +{ + const auto found_index = AddedLevels.IndexOfByPredicate( [ world ]( const ULevelStreamingDynamic * streaming_level ) { + return streaming_level && streaming_level->GetWorld() == world; + } ); + + if ( found_index != INDEX_NONE ) + { + CleanUpAddedLevel( AddedLevels[ found_index ] ); + AddedLevels.RemoveAtSwap( found_index ); + } +} + +ULevelStreamingDynamic * UGBFGameFeatureAction_AddLevelInstances::LoadDynamicLevelForEntry( const FGBFGameFeatureLevelInstanceEntry & entry, UWorld * target_world ) +{ + auto success = false; + auto * streaming_level_ref = ULevelStreamingDynamic::LoadLevelInstanceBySoftObjectPtr( target_world, entry.Level, entry.Location, entry.Rotation, success ); + + if ( !success ) + { + UE_LOG( LogGameFeatures, Error, TEXT( "[GameFeatureData %s]: Failed to load level instance `%s`." ), *GetPathNameSafe( this ), *entry.Level.ToString() ); + } + else if ( streaming_level_ref ) + { + AddedLevels.Add( streaming_level_ref ); + } + + return streaming_level_ref; +} + +void UGBFGameFeatureAction_AddLevelInstances::OnLevelLoaded() +{ + if ( ensureAlways( bIsActivated ) ) + { + // We don't have a way of knowing which instance this was triggered for, so we have to look through them all... + for ( auto * level : AddedLevels ) + { + if ( level && level->GetLevelStreamingState() == ELevelStreamingState::LoadedNotVisible ) + { + level->SetShouldBeVisible( true ); + } + } + } +} + +void UGBFGameFeatureAction_AddLevelInstances::DestroyAddedLevels() +{ + for ( auto * level : AddedLevels ) + { + CleanUpAddedLevel( level ); + } + AddedLevels.Empty(); +} + +void UGBFGameFeatureAction_AddLevelInstances::CleanUpAddedLevel( ULevelStreamingDynamic * level ) +{ + if ( level != nullptr ) + { + level->OnLevelLoaded.RemoveAll( this ); + level->SetIsRequestingUnloadAndRemoval( true ); + } +} + +////////////////////////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h b/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h new file mode 100644 index 00000000..6decaf2a --- /dev/null +++ b/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h @@ -0,0 +1,79 @@ +#pragma once + +#include "GBFGameFeatureAction_WorldActionBase.h" + +#include "GBFGameFeatureAction_AddLevelInstances.generated.h" + +class ULevelStreamingDynamic; + +// Description of a level to add to the world when this game feature is enabled +USTRUCT() +struct FGBFGameFeatureLevelInstanceEntry +{ + GENERATED_BODY() + + // The level instance to dynamically load at runtime. + UPROPERTY( EditAnywhere, Category = "Instance Info" ) + TSoftObjectPtr< UWorld > Level; + + // Specific world to load into. If left null, this level will be loaded for all worlds. + UPROPERTY( EditAnywhere, Category = "Instance Info" ) + TSoftObjectPtr< UWorld > TargetWorld; + + // The translational offset for this level instance. + UPROPERTY( EditAnywhere, Category = "Instance Info" ) + FVector Location = FVector( 0.f ); + + // The rotational tranform for this level instance. + UPROPERTY( EditAnywhere, Category = "Instance Info" ) + FRotator Rotation = FRotator( 0.f ); +}; + +////////////////////////////////////////////////////////////////////// +// UGameFeatureAction_AddLevelInstances + +/** + * Loads specified level instances at runtime. + */ +UCLASS( MinimalAPI, meta = ( DisplayName = "Add Level Instances" ) ) +class UGBFGameFeatureAction_AddLevelInstances final : public UGBFGameFeatureAction_WorldActionBase +{ + GENERATED_BODY() + +public: + //~ Begin UGameFeatureAction interface + void OnGameFeatureActivating( FGameFeatureActivatingContext & context ) override; + void OnGameFeatureDeactivating( FGameFeatureDeactivatingContext & context ) override; + //~ End UGameFeatureAction interface + + //~ Begin UObject interface +#if WITH_EDITOR + EDataValidationResult IsDataValid( FDataValidationContext & context ) const override; +#endif + //~ End UObject interface + +private: + //~ Begin UGameFeatureAction_WorldActionBase interface + void AddToWorld( const FWorldContext & world_context, const FGameFeatureStateChangeContext & change_context ) override; + //~ End UGameFeatureAction_WorldActionBase interface + + void OnWorldCleanup( UWorld * world, bool session_ended, bool cleanup_resources ); + + ULevelStreamingDynamic * LoadDynamicLevelForEntry( const FGBFGameFeatureLevelInstanceEntry & entry, UWorld * target_world ); + + UFUNCTION() // UFunction so we can bind to a dynamic delegate + void OnLevelLoaded(); + + void DestroyAddedLevels(); + void CleanUpAddedLevel( ULevelStreamingDynamic * level ); + + /** List of levels to dynamically load when this game feature is enabled */ + UPROPERTY( EditAnywhere, Category = "Level Instances", meta = ( TitleProperty = "Level", ShowOnlyInnerProperties ) ) + TArray< FGBFGameFeatureLevelInstanceEntry > LevelInstanceList; + + UPROPERTY( transient ) + TArray< ULevelStreamingDynamic * > AddedLevels; + + bool bIsActivated = false; + bool bLayerStateReentrantGuard = false; +};