From a5c2a1c786db34d5d8ac38ec636bceb633c23b00 Mon Sep 17 00:00:00 2001 From: Michael Delva Date: Mon, 31 Mar 2025 14:42:00 +0200 Subject: [PATCH 1/3] Made Load and Save async --- .../SaveGame/GBFSaveGameSubsystem.cpp | 69 +++++++++++++++---- .../SaveGame/GBFSaveGameSubsystem.h | 13 +++- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp index 26d55412..a37bf35c 100644 --- a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp +++ b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include void UGBFSaveGameSubsystem::NotifyPlayerAdded( ULocalPlayer * local_player ) { @@ -14,18 +16,18 @@ void UGBFSaveGameSubsystem::NotifyPlayerAdded( ULocalPlayer * local_player ) { PrimaryPlayer = local_player; - Load(); + Load( FGBFOnSaveGameLoaded() ); } } -void UGBFSaveGameSubsystem::Load() +bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) { const auto * world = GetWorld(); const auto * world_settings = Cast< AGBFWorldSettings >( world->GetWorldSettings() ); if ( world_settings->GetGameplayTags().HasTag( GBFTag_WorldSettings_NoSaveGame ) ) { - return; + return false; } auto * settings = GetDefault< UGameBaseFrameworkGameSettings >(); @@ -38,22 +40,65 @@ void UGBFSaveGameSubsystem::Load() } } - SaveGame = Cast< UGBFSaveGame >( UGBFSaveGame::LoadOrCreateSaveGameForLocalPlayer( settings->SaveGameClass, PrimaryPlayer.Get(), settings->SaveGameSlotName ) ); + auto callback = FOnLocalPlayerSaveGameLoadedNative::CreateLambda( [ & ]( ULocalPlayerSaveGame * save_game ) { + SaveGame = Cast< UGBFSaveGame >( save_game ); - for ( const auto & pending_savable : PendingSavables ) - { - SaveGame->RegisterSavable( pending_savable ); - } + for ( const auto & pending_savable : PendingSavables ) + { + SaveGame->RegisterSavable( pending_savable ); + } + + PendingSavables.Reset(); + + on_save_game_loaded.ExecuteIfBound( SaveGame ); + } ); - PendingSavables.Reset(); + return UGBFSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer( settings->SaveGameClass, PrimaryPlayer.Get(), settings->SaveGameSlotName, callback ); } -void UGBFSaveGameSubsystem::Save() +bool UGBFSaveGameSubsystem::Save( FGBFOnSaveGameSaved on_save_game_saved ) { - if ( SaveGame != nullptr ) + if ( SaveGame == nullptr ) + { + return false; + } + + if ( !ensure( PrimaryPlayer.Get() ) ) + { + return false; + } + + const auto request_user_index = SaveGame->GetPlatformUserIndex(); + const auto request_slot_name = SaveGame->GetSaveSlotName(); + if ( !ensure( request_slot_name.Len() > 0 ) ) { - SaveGame->AsyncSaveGameToSlotForLocalPlayer(); + return false; } + + SaveGame->HandlePreSave(); + + auto callback = FAsyncSaveGameToSlotDelegate::CreateLambda( [ & ]( const FString & /*slot_name*/, const int32 /*user_index*/, bool success ) { + on_save_game_saved.ExecuteIfBound( SaveGame, success ); + } ); + + return true; +} + +void UGBFSaveGameSubsystem::SaveNextTick( FGBFOnSaveGameSaved on_save_game_saved ) +{ + GetWorld()->GetTimerManager().SetTimerForNextTick( FTimerDelegate::CreateLambda( [ & ]() { + Save( on_save_game_saved ); + } ) ); +} + +void UGBFSaveGameSubsystem::SaveWithDelay( float delay, FGBFOnSaveGameSaved on_save_game_saved ) +{ + FTimerHandle handle; + GetWorld()->GetTimerManager().SetTimer( handle, FTimerDelegate::CreateLambda( [ & ]() { + Save( on_save_game_saved ); + } ), + delay, + false ); } void UGBFSaveGameSubsystem::Reset() diff --git a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h index a4982e5e..30ee1feb 100644 --- a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h +++ b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h @@ -7,6 +7,9 @@ #include "GBFSaveGameSubsystem.generated.h" +DECLARE_DYNAMIC_DELEGATE_OneParam( FGBFOnSaveGameLoaded, UGBFSaveGame *, SaveGame ); +DECLARE_DYNAMIC_DELEGATE_TwoParams( FGBFOnSaveGameSaved, UGBFSaveGame *, SaveGame, bool, Success ); + UCLASS() class GAMEBASEFRAMEWORK_API UGBFSaveGameSubsystem : public UGameInstanceSubsystem { @@ -16,10 +19,16 @@ class GAMEBASEFRAMEWORK_API UGBFSaveGameSubsystem : public UGameInstanceSubsyste void NotifyPlayerAdded( ULocalPlayer * local_player ); UFUNCTION( BlueprintCallable ) - void Load(); + bool Load( FGBFOnSaveGameLoaded on_save_game_loaded ); + + UFUNCTION( BlueprintCallable ) + bool Save( FGBFOnSaveGameSaved on_save_game_saved ); + + UFUNCTION( BlueprintCallable ) + void SaveNextTick( FGBFOnSaveGameSaved on_save_game_saved ); UFUNCTION( BlueprintCallable ) - void Save(); + void SaveWithDelay( float delay, FGBFOnSaveGameSaved on_save_game_saved ); UFUNCTION( BlueprintCallable ) void Reset(); From f027a7c9958c8965717c8f8bddfe45fcb2ec6759 Mon Sep 17 00:00:00 2001 From: Michael Delva Date: Mon, 31 Mar 2025 22:48:12 +0200 Subject: [PATCH 2/3] Added save and load frequency checks --- .../Private/GameBaseFrameworkGameSettings.cpp | 13 +--- .../SaveGame/GBFSaveGameSettings.cpp | 13 ++++ .../SaveGame/GBFSaveGameSubsystem.cpp | 63 ++++++++++++++++--- .../Public/GameBaseFrameworkGameSettings.h | 8 --- .../SaveGame/GBFSaveGameSettings.h | 35 +++++++++++ .../SaveGame/GBFSaveGameSubsystem.h | 7 +++ .../Public/Settings/GBFPerformanceSettings.h | 2 +- 7 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSettings.cpp create mode 100644 Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSettings.h diff --git a/Source/GameBaseFramework/Private/GameBaseFrameworkGameSettings.cpp b/Source/GameBaseFramework/Private/GameBaseFrameworkGameSettings.cpp index dc5b9ea6..b5608c8c 100644 --- a/Source/GameBaseFramework/Private/GameBaseFrameworkGameSettings.cpp +++ b/Source/GameBaseFramework/Private/GameBaseFrameworkGameSettings.cpp @@ -1,16 +1,5 @@ #include "GameBaseFrameworkGameSettings.h" -#include "GameFramework/SaveGame/GBFSaveGame.h" - -#include - UGameBaseFrameworkGameSettings::UGameBaseFrameworkGameSettings() { - SaveGameClass = UGBFSaveGame::StaticClass(); - SaveGameSlotName = TEXT( "SaveGame" ); -} - -FName UGameBaseFrameworkGameSettings::GetCategoryName() const -{ - return FApp::GetProjectName(); -} +} \ No newline at end of file diff --git a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSettings.cpp b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSettings.cpp new file mode 100644 index 00000000..1e9d7d72 --- /dev/null +++ b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSettings.cpp @@ -0,0 +1,13 @@ +#include "GameFramework/SaveGame/GBFSaveGameSettings.h" + +#include "GameFramework/SaveGame/GBFSaveGame.h" + +UGBFSaveGameSettings::UGBFSaveGameSettings() +{ + SaveGameClass = UGBFSaveGame::StaticClass(); + SaveGameSlotName = TEXT( "SaveGame" ); + MaxSaveFrequency = 10; + MaxSaveFrequencyDuration = 60.0f; + MaxLoadFrequency = 10; + MaxLoadFrequencyDuration = 60.0; +} \ No newline at end of file diff --git a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp index a37bf35c..aef5d285 100644 --- a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp +++ b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp @@ -1,15 +1,44 @@ #include "GameFramework/SaveGame/GBFSaveGameSubsystem.h" #include "GBFTags.h" -#include "GameBaseFrameworkGameSettings.h" #include "GameFramework/GBFWorldSettings.h" #include "GameFramework/SaveGame/GBFSaveGame.h" +#include "GameFramework/SaveGame/GBFSaveGameSettings.h" #include #include #include #include +DEFINE_LOG_CATEGORY_STATIC( LogGBFSaveGameSystem, Verbose, Verbose ) + +namespace +{ + bool IsFrequencyRespected( TDeque< float > & call_times, const float current_time, const float frequency_duration, const int max_frequency ) + { + while ( call_times.Num() > 0 && call_times[ 0 ] <= current_time - frequency_duration ) + { + call_times.PopFirst(); + } + + if ( call_times.Num() >= max_frequency ) + { + return false; + } + + call_times.PushLast( current_time ); + + return true; + } +} + +void UGBFSaveGameSubsystem::Initialize( FSubsystemCollectionBase & collection ) +{ + Super::Initialize( collection ); + + auto * settings = GetDefault< UGBFSaveGameSettings >(); +} + void UGBFSaveGameSubsystem::NotifyPlayerAdded( ULocalPlayer * local_player ) { if ( PrimaryPlayer == nullptr ) @@ -22,6 +51,15 @@ void UGBFSaveGameSubsystem::NotifyPlayerAdded( ULocalPlayer * local_player ) bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) { + const auto current_time = GetWorld()->GetTimeSeconds(); + auto * settings = GetDefault< UGBFSaveGameSettings >(); + + if ( !IsFrequencyRespected( LoadGameCallTimes, current_time, settings->MaxLoadFrequencyDuration, settings->MaxLoadFrequency ) ) + { + UE_LOG( LogGBFSaveGameSystem, Warning, TEXT( "Too much calls to Load. Max calls : %i in %f seconds" ), settings->MaxLoadFrequency, settings->MaxLoadFrequencyDuration ); + return false; + } + const auto * world = GetWorld(); const auto * world_settings = Cast< AGBFWorldSettings >( world->GetWorldSettings() ); @@ -30,8 +68,6 @@ bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) return false; } - auto * settings = GetDefault< UGameBaseFrameworkGameSettings >(); - if ( SaveGame != nullptr ) { for ( const auto & savable_data : SaveGame->SavablesData ) @@ -40,7 +76,7 @@ bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) } } - auto callback = FOnLocalPlayerSaveGameLoadedNative::CreateLambda( [ & ]( ULocalPlayerSaveGame * save_game ) { + auto callback = FOnLocalPlayerSaveGameLoadedNative::CreateLambda( [ &, delegate = MoveTemp( on_save_game_loaded ) ]( ULocalPlayerSaveGame * save_game ) { SaveGame = Cast< UGBFSaveGame >( save_game ); for ( const auto & pending_savable : PendingSavables ) @@ -50,7 +86,7 @@ bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) PendingSavables.Reset(); - on_save_game_loaded.ExecuteIfBound( SaveGame ); + delegate.ExecuteIfBound( SaveGame ); } ); return UGBFSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer( settings->SaveGameClass, PrimaryPlayer.Get(), settings->SaveGameSlotName, callback ); @@ -68,8 +104,17 @@ bool UGBFSaveGameSubsystem::Save( FGBFOnSaveGameSaved on_save_game_saved ) return false; } + const auto current_time = GetWorld()->GetTimeSeconds(); + auto * settings = GetDefault< UGBFSaveGameSettings >(); + + if ( !IsFrequencyRespected( SaveGameCallTimes, current_time, settings->MaxSaveFrequencyDuration, settings->MaxSaveFrequency ) ) + { + UE_LOG( LogGBFSaveGameSystem, Warning, TEXT( "Too much calls to Save. Max calls : %i in %f seconds" ), settings->MaxSaveFrequency, settings->MaxSaveFrequencyDuration ); + return false; + } + const auto request_user_index = SaveGame->GetPlatformUserIndex(); - const auto request_slot_name = SaveGame->GetSaveSlotName(); + const auto & request_slot_name = SaveGame->GetSaveSlotName(); if ( !ensure( request_slot_name.Len() > 0 ) ) { return false; @@ -77,10 +122,12 @@ bool UGBFSaveGameSubsystem::Save( FGBFOnSaveGameSaved on_save_game_saved ) SaveGame->HandlePreSave(); - auto callback = FAsyncSaveGameToSlotDelegate::CreateLambda( [ & ]( const FString & /*slot_name*/, const int32 /*user_index*/, bool success ) { - on_save_game_saved.ExecuteIfBound( SaveGame, success ); + auto callback = FAsyncSaveGameToSlotDelegate::CreateLambda( [ &, delegate = MoveTemp( on_save_game_saved ) ]( const FString & /*slot_name*/, const int32 /*user_index*/, bool success ) { + delegate.ExecuteIfBound( SaveGame, success ); } ); + UGameplayStatics::AsyncSaveGameToSlot( SaveGame, request_slot_name, request_user_index, callback ); + return true; } diff --git a/Source/GameBaseFramework/Public/GameBaseFrameworkGameSettings.h b/Source/GameBaseFramework/Public/GameBaseFrameworkGameSettings.h index b7e67b9c..8f5780e1 100644 --- a/Source/GameBaseFramework/Public/GameBaseFrameworkGameSettings.h +++ b/Source/GameBaseFramework/Public/GameBaseFrameworkGameSettings.h @@ -16,20 +16,12 @@ class UGameBaseFrameworkGameSettings final : public UDeveloperSettingsBackedByCV public: UGameBaseFrameworkGameSettings(); - FName GetCategoryName() const override; - UPROPERTY( BlueprintReadOnly, EditDefaultsOnly, config, Category = "UI" ) TSoftClassPtr< UCommonGameDialog > ConfirmationDialogClass; UPROPERTY( BlueprintReadOnly, EditDefaultsOnly, config, Category = "UI" ) TSoftClassPtr< UCommonGameDialog > ErrorDialogClass; - UPROPERTY( EditDefaultsOnly, config, Category = "SaveGame" ) - TSubclassOf< UGBFSaveGame > SaveGameClass; - - UPROPERTY( EditDefaultsOnly, config, Category = "SaveGame" ) - FString SaveGameSlotName; - UPROPERTY( BlueprintReadOnly, EditDefaultsOnly, config, Category = "Sounds" ) TSoftObjectPtr< USoundBase > BackHandlerSound; }; diff --git a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSettings.h b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSettings.h new file mode 100644 index 00000000..9765e8a3 --- /dev/null +++ b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSettings.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "GBFSaveGameSettings.generated.h" + +class UGBFSaveGame; + +UCLASS( config = Game, DefaultConfig, meta = ( DisplayName = "GameBaseFramework - SaveSystem" ) ) +class GAMEBASEFRAMEWORK_API UGBFSaveGameSettings final : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UGBFSaveGameSettings(); + + UPROPERTY( config, EditDefaultsOnly ) + TSubclassOf< UGBFSaveGame > SaveGameClass; + + UPROPERTY( config, EditDefaultsOnly ) + FString SaveGameSlotName; + + UPROPERTY( config, EditDefaultsOnly ) + int MaxSaveFrequency; + + UPROPERTY( config, EditDefaultsOnly, meta = ( ForceUnits = "s" ) ) + float MaxSaveFrequencyDuration; + + UPROPERTY( config, EditDefaultsOnly ) + int MaxLoadFrequency; + + UPROPERTY( config, EditDefaultsOnly, meta = ( ForceUnits = "s" ) ) + float MaxLoadFrequencyDuration; +}; diff --git a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h index 30ee1feb..7c611ab0 100644 --- a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h +++ b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h @@ -1,6 +1,8 @@ #pragma once #include "GBFSaveGame.h" +#include "Containers/Deque.h" +#include "Math/MovingAverage.h" #include #include @@ -16,6 +18,8 @@ class GAMEBASEFRAMEWORK_API UGBFSaveGameSubsystem : public UGameInstanceSubsyste GENERATED_BODY() public: + void Initialize( FSubsystemCollectionBase & collection ) override; + void NotifyPlayerAdded( ULocalPlayer * local_player ); UFUNCTION( BlueprintCallable ) @@ -49,6 +53,9 @@ class GAMEBASEFRAMEWORK_API UGBFSaveGameSubsystem : public UGameInstanceSubsyste TArray< TScriptInterface< IGBFSaveGameSystemSavableInterface > > PendingSavables; TWeakObjectPtr< ULocalPlayer > PrimaryPlayer; + + TDeque< float > LoadGameCallTimes; + TDeque< float > SaveGameCallTimes; }; template < typename _SAVE_GAME_CLASS_ > diff --git a/Source/GameBaseFramework/Public/Settings/GBFPerformanceSettings.h b/Source/GameBaseFramework/Public/Settings/GBFPerformanceSettings.h index 35e17411..557c30af 100644 --- a/Source/GameBaseFramework/Public/Settings/GBFPerformanceSettings.h +++ b/Source/GameBaseFramework/Public/Settings/GBFPerformanceSettings.h @@ -86,7 +86,7 @@ class UGBFPlatformSpecificRenderingSettings : public UPlatformSettings TArray< int32 > MobileFrameRateLimits; }; -UCLASS( config = Game, defaultconfig, meta = ( DisplayName = "Game Base Framework - Performance Settings" ) ) +UCLASS( config = Game, defaultconfig, meta = ( DisplayName = "GameBaseFramework - Performance Settings" ) ) class GAMEBASEFRAMEWORK_API UGBFPerformanceSettings : public UDeveloperSettingsBackedByCVars { GENERATED_BODY() From 5fa5b7f77417c5e74820cecf45073514fb05bfb8 Mon Sep 17 00:00:00 2001 From: Michael Delva Date: Tue, 1 Apr 2025 11:12:16 +0200 Subject: [PATCH 3/3] Added blueprint delegate when an event happens --- .../SaveGame/GBFSaveGameSubsystem.cpp | 5 +++++ .../SaveGame/GBFSaveGameSubsystem.h | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp index aef5d285..7459ebc4 100644 --- a/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp +++ b/Source/GameBaseFramework/Private/GameFramework/SaveGame/GBFSaveGameSubsystem.cpp @@ -87,8 +87,10 @@ bool UGBFSaveGameSubsystem::Load( FGBFOnSaveGameLoaded on_save_game_loaded ) PendingSavables.Reset(); delegate.ExecuteIfBound( SaveGame ); + OnOperationTriggeredDelegate.Broadcast( EGBFSaveGameSubsystemOperation::Load, EGBFSaveGameSubsystemOperationEvent::Ended ); } ); + OnOperationTriggeredDelegate.Broadcast( EGBFSaveGameSubsystemOperation::Load, EGBFSaveGameSubsystemOperationEvent::Started ); return UGBFSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer( settings->SaveGameClass, PrimaryPlayer.Get(), settings->SaveGameSlotName, callback ); } @@ -120,10 +122,13 @@ bool UGBFSaveGameSubsystem::Save( FGBFOnSaveGameSaved on_save_game_saved ) return false; } + OnOperationTriggeredDelegate.Broadcast( EGBFSaveGameSubsystemOperation::Save, EGBFSaveGameSubsystemOperationEvent::Started ); + SaveGame->HandlePreSave(); auto callback = FAsyncSaveGameToSlotDelegate::CreateLambda( [ &, delegate = MoveTemp( on_save_game_saved ) ]( const FString & /*slot_name*/, const int32 /*user_index*/, bool success ) { delegate.ExecuteIfBound( SaveGame, success ); + OnOperationTriggeredDelegate.Broadcast( EGBFSaveGameSubsystemOperation::Save, EGBFSaveGameSubsystemOperationEvent::Ended ); } ); UGameplayStatics::AsyncSaveGameToSlot( SaveGame, request_slot_name, request_user_index, callback ); diff --git a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h index 7c611ab0..2cb8a41f 100644 --- a/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h +++ b/Source/GameBaseFramework/Public/GameFramework/SaveGame/GBFSaveGameSubsystem.h @@ -1,14 +1,28 @@ #pragma once -#include "GBFSaveGame.h" #include "Containers/Deque.h" -#include "Math/MovingAverage.h" +#include "GBFSaveGame.h" #include #include #include "GBFSaveGameSubsystem.generated.h" +UENUM( BlueprintType ) +enum class EGBFSaveGameSubsystemOperation : uint8 +{ + Load, + Save +}; + +UENUM( BlueprintType ) +enum class EGBFSaveGameSubsystemOperationEvent : uint8 +{ + Started, + Ended +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FGBFOnOperationTriggeredDelegate, EGBFSaveGameSubsystemOperation, Operation, EGBFSaveGameSubsystemOperationEvent, Event ); DECLARE_DYNAMIC_DELEGATE_OneParam( FGBFOnSaveGameLoaded, UGBFSaveGame *, SaveGame ); DECLARE_DYNAMIC_DELEGATE_TwoParams( FGBFOnSaveGameSaved, UGBFSaveGame *, SaveGame, bool, Success ); @@ -52,6 +66,9 @@ class GAMEBASEFRAMEWORK_API UGBFSaveGameSubsystem : public UGameInstanceSubsyste UPROPERTY() TArray< TScriptInterface< IGBFSaveGameSystemSavableInterface > > PendingSavables; + UPROPERTY( BlueprintAssignable ) + FGBFOnOperationTriggeredDelegate OnOperationTriggeredDelegate; + TWeakObjectPtr< ULocalPlayer > PrimaryPlayer; TDeque< float > LoadGameCallTimes;