diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c5958ef --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build + +# Controls when the action will run. +on: + push: + branches: + - '**' + pull_request: + workflow_call: + +jobs: + publish: + name: "${{ matrix.os }} ${{ matrix.config }} - UE ${{ matrix.version }}" + runs-on: [self-hosted, "UE-${{ matrix.version }}", "${{ matrix.os }}"] + env: + CI_PLUGIN: SaveExtension + strategy: + fail-fast: false + matrix: + config: [Release] + os: [Windows, Linux] + version: [ "5.3" ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Download Piperift Scripts + run: git clone https://github.com/PipeRift/CICDScripts Scripts + + - name: Cache Build + uses: actions/cache@v3 + with: + path: Build + key: ${{ matrix.os }}-${{ matrix.config }}-${{ matrix.version }} + + - name: Build Plugin + run: python Scripts/build.py plugin -n ${{ env.CI_PLUGIN }} -p . + diff --git a/.gitignore b/.gitignore index 832491c..f5f6af4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /Binaries -/Intermediate \ No newline at end of file +/Intermediate +/Build diff --git a/SaveExtension.uplugin b/SaveExtension.uplugin index 3a1ba45..4316657 100644 --- a/SaveExtension.uplugin +++ b/SaveExtension.uplugin @@ -10,7 +10,7 @@ "CreatedByURL": "https://piperift.com", "DocsURL": "https://piperift.com/SaveExtension/", "SupportURL": "info@piperift.com", - "EngineVersion": "4.26", + "EngineVersion": "5.3", "EnabledByDefault": true, "CanContainContent": false, "IsBetaVersion": false, @@ -21,13 +21,10 @@ "LoadingPhase": "PreDefault", "WhitelistPlatforms": [ "Win64", - "Win32", - "Linux", + "Mac", + "IOS", "Android", - "PS5", - "XboxOne", - "Switch", - "Mac" + "Linux" ] }, { @@ -36,13 +33,10 @@ "LoadingPhase": "PostEngineInit", "WhitelistPlatforms": [ "Win64", - "Win32", - "Linux", + "Mac", + "IOS", "Android", - "PS5", - "XboxOne", - "Switch", - "Mac" + "Linux" ] }, { @@ -51,7 +45,6 @@ "LoadingPhase": "PreDefault", "WhitelistPlatforms": [ "Win64", - "Win32", "Linux", "Mac" ] diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.cpp b/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.cpp deleted file mode 100644 index c6c5233..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "AssetTypeAction_SavePreset.h" - -#define LOCTEXT_NAMESPACE "AssetTypeActions" - - -////////////////////////////////////////////////////////////////////////// -// FAssetTypeAction_SavePreset - -FText FAssetTypeAction_SavePreset::GetName() const -{ - return LOCTEXT("FAssetTypeAction_SavePresetName", "Save Preset"); -} - -FColor FAssetTypeAction_SavePreset::GetTypeColor() const -{ - return FColor(63, 126, 255); -} - -////////////////////////////////////////////////////////////////////////// - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.h b/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.h deleted file mode 100644 index 09cf3a7..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SavePreset.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SaveExtensionEditor.h" - -#include "AssetTypeActions_Base.h" - -#include "SavePreset.h" - -#define LOCTEXT_NAMESPACE "SaveExtensionEditor" - - -class FAssetTypeAction_SavePreset : public FAssetTypeActions_Base -{ -public: - - virtual uint32 GetCategories() override { - return FSaveExtensionEditor::Get().AssetCategory; - } - - virtual FText GetName() const override; - virtual FColor GetTypeColor() const override; - - virtual UClass* GetSupportedClass() const override { return USavePreset::StaticClass(); } -}; - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.cpp b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.cpp new file mode 100644 index 0000000..3c29130 --- /dev/null +++ b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.cpp @@ -0,0 +1,23 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "AssetTypeAction_SaveSlot.h" + +#define LOCTEXT_NAMESPACE "AssetTypeActions" + + +////////////////////////////////////////////////////////////////////////// +// FAssetTypeAction_SaveSlot + +FText FAssetTypeAction_SaveSlot::GetName() const +{ + return LOCTEXT("FAssetTypeAction_SaveSlotName", "Save Slot"); +} + +FColor FAssetTypeAction_SaveSlot::GetTypeColor() const +{ + return FColor(63, 126, 255); +} + +////////////////////////////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.h b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.h new file mode 100644 index 0000000..eb8cb58 --- /dev/null +++ b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlot.h @@ -0,0 +1,28 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SaveExtensionEditor.h" + +#include +#include + + +class FAssetTypeAction_SaveSlot : public FAssetTypeActions_Base +{ +public: + virtual uint32 GetCategories() override + { + return FSaveExtensionEditor::Get().AssetCategory; + } + + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + + virtual UClass* GetSupportedClass() const override + { + return USaveSlot::StaticClass(); + } +}; + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.cpp b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.cpp new file mode 100644 index 0000000..d953edd --- /dev/null +++ b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.cpp @@ -0,0 +1,23 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "AssetTypeAction_SaveSlotData.h" + +#define LOCTEXT_NAMESPACE "AssetTypeActions" + + +////////////////////////////////////////////////////////////////////////// +// FAssetTypeAction_SaveSlotData + +FText FAssetTypeAction_SaveSlotData::GetName() const +{ + return LOCTEXT("FAssetTypeAction_SaveSlotDataName", "Save Slot Data"); +} + +FColor FAssetTypeAction_SaveSlotData::GetTypeColor() const +{ + return FColor(63, 126, 255); +} + +////////////////////////////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.h b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.h new file mode 100644 index 0000000..d8caa25 --- /dev/null +++ b/Source/Editor/Private/Asset/AssetTypeAction_SaveSlotData.h @@ -0,0 +1,28 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SaveExtensionEditor.h" + +#include +#include + + +class FAssetTypeAction_SaveSlotData : public FAssetTypeActions_Base +{ +public: + virtual uint32 GetCategories() override + { + return FSaveExtensionEditor::Get().AssetCategory; + } + + virtual FText GetName() const override; + virtual FColor GetTypeColor() const override; + + virtual UClass* GetSupportedClass() const override + { + return USaveSlotData::StaticClass(); + } +}; + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SlotData.cpp b/Source/Editor/Private/Asset/AssetTypeAction_SlotData.cpp deleted file mode 100644 index 77ecde3..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SlotData.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "AssetTypeAction_SlotData.h" - -#define LOCTEXT_NAMESPACE "AssetTypeActions" - - -////////////////////////////////////////////////////////////////////////// -// FAssetTypeAction_SavePreset - -FText FAssetTypeAction_SlotData::GetName() const -{ - return LOCTEXT("FAssetTypeAction_SlotDataName", "Save Data"); -} - -FColor FAssetTypeAction_SlotData::GetTypeColor() const -{ - return FColor(63, 126, 255); -} - -////////////////////////////////////////////////////////////////////////// - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SlotData.h b/Source/Editor/Private/Asset/AssetTypeAction_SlotData.h deleted file mode 100644 index a65877b..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SlotData.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SaveExtensionEditor.h" - -#include "AssetTypeActions_Base.h" - -#include "SlotData.h" - -#define LOCTEXT_NAMESPACE "SaveExtensionEditor" - - -class FAssetTypeAction_SlotData : public FAssetTypeActions_Base -{ -public: - - virtual uint32 GetCategories() override { - return FSaveExtensionEditor::Get().AssetCategory; - } - - virtual FText GetName() const override; - virtual FColor GetTypeColor() const override; - - virtual UClass* GetSupportedClass() const override { return USlotData::StaticClass(); } -}; - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.cpp b/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.cpp deleted file mode 100644 index 1da57f5..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "AssetTypeAction_SlotInfo.h" - -#define LOCTEXT_NAMESPACE "AssetTypeActions" - - -////////////////////////////////////////////////////////////////////////// -// FAssetTypeAction_SavePreset - -FText FAssetTypeAction_SlotInfo::GetName() const -{ - return LOCTEXT("FAssetTypeAction_SlotInfoName", "Save Info"); -} - -FColor FAssetTypeAction_SlotInfo::GetTypeColor() const -{ - return FColor(63, 126, 255); -} - -////////////////////////////////////////////////////////////////////////// - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.h b/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.h deleted file mode 100644 index f56b3d9..0000000 --- a/Source/Editor/Private/Asset/AssetTypeAction_SlotInfo.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SaveExtensionEditor.h" - -#include "AssetTypeActions_Base.h" - -#include "SlotInfo.h" - -#define LOCTEXT_NAMESPACE "SaveExtensionEditor" - - -class FAssetTypeAction_SlotInfo : public FAssetTypeActions_Base -{ -public: - - virtual uint32 GetCategories() override { - return FSaveExtensionEditor::Get().AssetCategory; - } - - virtual FText GetName() const override; - virtual FColor GetTypeColor() const override; - - virtual UClass* GetSupportedClass() const override { return USlotInfo::StaticClass(); } -}; - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Asset/SavePresetFactory.cpp b/Source/Editor/Private/Asset/SavePresetFactory.cpp deleted file mode 100644 index c33ef3c..0000000 --- a/Source/Editor/Private/Asset/SavePresetFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SavePresetFactory.h" -#include "Kismet2/KismetEditorUtilities.h" - - -USavePresetFactory::USavePresetFactory() : Super() { - bCreateNew = true; - bEditAfterNew = true; - SupportedClass = USavePreset::StaticClass(); -} - -UObject* USavePresetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { - check(Class->IsChildOf(USavePreset::StaticClass())); - - if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(Class)) - { - return nullptr; - } - return FKismetEditorUtilities::CreateBlueprint(Class, InParent, Name, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(),TEXT("AssetTypeActions")); -} diff --git a/Source/Editor/Private/Asset/SavePresetFactory.h b/Source/Editor/Private/Asset/SavePresetFactory.h deleted file mode 100644 index 55f5edf..0000000 --- a/Source/Editor/Private/Asset/SavePresetFactory.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SavePreset.h" - -#include "AssetTypeActions_Base.h" -#include "Factories/Factory.h" - -#include "SavePresetFactory.generated.h" - - -UCLASS() -class SAVEEXTENSIONEDITOR_API USavePresetFactory : public UFactory -{ - GENERATED_BODY() - -public: - - USavePresetFactory(); - - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; -}; diff --git a/Source/Editor/Private/Asset/SaveSlotDataFactory.cpp b/Source/Editor/Private/Asset/SaveSlotDataFactory.cpp new file mode 100644 index 0000000..04cea42 --- /dev/null +++ b/Source/Editor/Private/Asset/SaveSlotDataFactory.cpp @@ -0,0 +1,26 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Asset/SaveSlotDataFactory.h" + +#include + + +USaveSaveSlotDataFactory::USaveSaveSlotDataFactory() : Super() +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = USaveSlotData::StaticClass(); +} + +UObject* USaveSaveSlotDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, + EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(Class->IsChildOf(USaveSlotData::StaticClass())); + + if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(Class)) + { + return nullptr; + } + return FKismetEditorUtilities::CreateBlueprint(Class, InParent, Name, BPTYPE_Normal, + UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), TEXT("AssetTypeActions")); +} diff --git a/Source/Editor/Private/Asset/SaveSlotDataFactory.h b/Source/Editor/Private/Asset/SaveSlotDataFactory.h new file mode 100644 index 0000000..818f15a --- /dev/null +++ b/Source/Editor/Private/Asset/SaveSlotDataFactory.h @@ -0,0 +1,22 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "AssetTypeActions_Base.h" +#include "Factories/Factory.h" +#include "SaveSlotData.h" + +#include "SaveSlotDataFactory.generated.h" + + +UCLASS() +class SAVEEXTENSIONEDITOR_API USaveSaveSlotDataFactory : public UFactory +{ + GENERATED_BODY() + +public: + USaveSaveSlotDataFactory(); + + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, + UObject* Context, FFeedbackContext* Warn) override; +}; diff --git a/Source/Editor/Private/Asset/SaveSlotFactory.cpp b/Source/Editor/Private/Asset/SaveSlotFactory.cpp new file mode 100644 index 0000000..3ad9e40 --- /dev/null +++ b/Source/Editor/Private/Asset/SaveSlotFactory.cpp @@ -0,0 +1,26 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Asset/SaveSlotFactory.h" + +#include + + +USaveSlotFactory::USaveSlotFactory() : Super() +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = USaveSlot::StaticClass(); +} + +UObject* USaveSlotFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, + UObject* Context, FFeedbackContext* Warn) +{ + check(Class->IsChildOf(USaveSlot::StaticClass())); + + if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(Class)) + { + return nullptr; + } + return FKismetEditorUtilities::CreateBlueprint(Class, InParent, Name, BPTYPE_Normal, + UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), TEXT("AssetTypeActions")); +} diff --git a/Source/Editor/Private/Asset/SaveSlotFactory.h b/Source/Editor/Private/Asset/SaveSlotFactory.h new file mode 100644 index 0000000..dbf1df2 --- /dev/null +++ b/Source/Editor/Private/Asset/SaveSlotFactory.h @@ -0,0 +1,22 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "AssetTypeActions_Base.h" +#include "Factories/Factory.h" +#include "SaveSlot.h" + +#include "SaveSlotFactory.generated.h" + + +UCLASS() +class SAVEEXTENSIONEDITOR_API USaveSlotFactory : public UFactory +{ + GENERATED_BODY() + +public: + USaveSlotFactory(); + + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, + UObject* Context, FFeedbackContext* Warn) override; +}; diff --git a/Source/Editor/Private/Asset/SlotDataFactory.cpp b/Source/Editor/Private/Asset/SlotDataFactory.cpp deleted file mode 100644 index c37c8cf..0000000 --- a/Source/Editor/Private/Asset/SlotDataFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SlotDataFactory.h" -#include "Kismet2/KismetEditorUtilities.h" - - -USlotDataFactory::USlotDataFactory() : Super() { - bCreateNew = true; - bEditAfterNew = true; - SupportedClass = USlotData::StaticClass(); -} - -UObject* USlotDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { - check(Class->IsChildOf(USlotData::StaticClass())); - - if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(Class)) - { - return nullptr; - } - return FKismetEditorUtilities::CreateBlueprint(Class, InParent, Name, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), TEXT("AssetTypeActions")); -} diff --git a/Source/Editor/Private/Asset/SlotDataFactory.h b/Source/Editor/Private/Asset/SlotDataFactory.h deleted file mode 100644 index fcd340f..0000000 --- a/Source/Editor/Private/Asset/SlotDataFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SlotData.h" - -#include "AssetTypeActions_Base.h" -#include "Factories/Factory.h" - -#include "SlotDataFactory.generated.h" - - -UCLASS() -class SAVEEXTENSIONEDITOR_API USlotDataFactory : public UFactory { - GENERATED_BODY() - -public: - - USlotDataFactory(); - - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; -}; diff --git a/Source/Editor/Private/Asset/SlotInfoFactory.cpp b/Source/Editor/Private/Asset/SlotInfoFactory.cpp deleted file mode 100644 index 0bdfa8e..0000000 --- a/Source/Editor/Private/Asset/SlotInfoFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SlotInfoFactory.h" -#include "Kismet2/KismetEditorUtilities.h" - - -USlotInfoFactory::USlotInfoFactory() : Super() { - bCreateNew = true; - bEditAfterNew = true; - SupportedClass = USlotInfo::StaticClass(); -} - -UObject* USlotInfoFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { - check(Class->IsChildOf(USlotInfo::StaticClass())); - - if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(Class)) - { - return nullptr; - } - return FKismetEditorUtilities::CreateBlueprint(Class, InParent, Name, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), TEXT("AssetTypeActions")); -} diff --git a/Source/Editor/Private/Asset/SlotInfoFactory.h b/Source/Editor/Private/Asset/SlotInfoFactory.h deleted file mode 100644 index 60783ce..0000000 --- a/Source/Editor/Private/Asset/SlotInfoFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SlotInfo.h" - -#include "AssetTypeActions_Base.h" -#include "Factories/Factory.h" - -#include "SlotInfoFactory.generated.h" - - -UCLASS() -class SAVEEXTENSIONEDITOR_API USlotInfoFactory : public UFactory { - GENERATED_BODY() - -public: - - USlotInfoFactory(); - - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; -}; diff --git a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.cpp b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.cpp index 5b273ea..f848237 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.cpp +++ b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.cpp @@ -1,12 +1,14 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "ClassFilterHelpers.h" -#include + +#include "UnloadedBlueprintData.h" + +#include #include +#include #include -#include -#include "UnloadedBlueprintData.h" namespace ClassFilter @@ -19,8 +21,11 @@ namespace ClassFilter FClassHierarchy::FClassHierarchy() { // Register with the Asset Registry to be informed when it is done loading up files. - FAssetRegistryModule& AssetRegistryModule = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); - OnFilesLoadedRequestPopulateClassHierarchyDelegateHandle = AssetRegistryModule.Get().OnFilesLoaded().AddStatic(ClassFilter::Helpers::RequestPopulateClassHierarchy); + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); + OnFilesLoadedRequestPopulateClassHierarchyDelegateHandle = + AssetRegistryModule.Get().OnFilesLoaded().AddStatic( + ClassFilter::Helpers::RequestPopulateClassHierarchy); AssetRegistryModule.Get().OnAssetAdded().AddRaw(this, &FClassHierarchy::AddAsset); AssetRegistryModule.Get().OnAssetRemoved().AddRaw(this, &FClassHierarchy::RemoveAsset); @@ -28,8 +33,11 @@ namespace ClassFilter FCoreUObjectDelegates::ReloadCompleteDelegate.AddRaw(this, &FClassHierarchy::OnReloadComplete); // Register to have Populate called when a Blueprint is compiled. - OnBlueprintCompiledRequestPopulateClassHierarchyDelegateHandle = GEditor->OnBlueprintCompiled().AddStatic(ClassFilter::Helpers::RequestPopulateClassHierarchy); - OnClassPackageLoadedOrUnloadedRequestPopulateClassHierarchyDelegateHandle = GEditor->OnClassPackageLoadedOrUnloaded().AddStatic(ClassFilter::Helpers::RequestPopulateClassHierarchy); + OnBlueprintCompiledRequestPopulateClassHierarchyDelegateHandle = + GEditor->OnBlueprintCompiled().AddStatic(ClassFilter::Helpers::RequestPopulateClassHierarchy); + OnClassPackageLoadedOrUnloadedRequestPopulateClassHierarchyDelegateHandle = + GEditor->OnClassPackageLoadedOrUnloaded().AddStatic( + ClassFilter::Helpers::RequestPopulateClassHierarchy); FModuleManager::Get().OnModulesChanged().AddStatic(&OnModulesChanged); } @@ -39,8 +47,10 @@ namespace ClassFilter // Unregister with the Asset Registry to be informed when it is done loading up files. if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetRegistry"))) { - FAssetRegistryModule& AssetRegistryModule = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); - AssetRegistryModule.Get().OnFilesLoaded().Remove(OnFilesLoadedRequestPopulateClassHierarchyDelegateHandle); + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().OnFilesLoaded().Remove( + OnFilesLoadedRequestPopulateClassHierarchyDelegateHandle); AssetRegistryModule.Get().OnAssetAdded().RemoveAll(this); AssetRegistryModule.Get().OnAssetRemoved().RemoveAll(this); @@ -49,18 +59,23 @@ namespace ClassFilter if (GEditor) { // Unregister to have Populate called when a Blueprint is compiled. - GEditor->OnBlueprintCompiled().Remove(OnBlueprintCompiledRequestPopulateClassHierarchyDelegateHandle); - GEditor->OnClassPackageLoadedOrUnloaded().Remove(OnClassPackageLoadedOrUnloadedRequestPopulateClassHierarchyDelegateHandle); + GEditor->OnBlueprintCompiled().Remove( + OnBlueprintCompiledRequestPopulateClassHierarchyDelegateHandle); + GEditor->OnClassPackageLoadedOrUnloaded().Remove( + OnClassPackageLoadedOrUnloadedRequestPopulateClassHierarchyDelegateHandle); } } FModuleManager::Get().OnModulesChanged().RemoveAll(this); } - static FSEClassFilterNodePtr CreateNodeForClass(UClass* Class, const TMultiMap& BlueprintPackageToAssetDataMap) + static FSEClassFilterNodePtr CreateNodeForClass( + UClass* Class, const TMultiMap& BlueprintPackageToAssetDataMap) { - // Create the new node so it can be passed to AddChildren, fill it in with if it is placeable, abstract, and/or a brush. - TSharedPtr NewNode = MakeShared(Class->GetName(), Class->GetDisplayNameText().ToString()); + // Create the new node so it can be passed to AddChildren, fill it in with if it is placeable, + // abstract, and/or a brush. + TSharedPtr NewNode = + MakeShared(Class->GetName(), Class->GetDisplayNameText().ToString()); NewNode->Blueprint = ClassFilter::Helpers::GetBlueprint(Class); NewNode->Class = Class; NewNode->ClassPath = Class->GetClassPathName(); @@ -77,11 +92,13 @@ namespace ClassFilter ClassFilter::Helpers::RequestPopulateClassHierarchy(); } - void FClassHierarchy::AddChildren_NoFilter(FSEClassFilterNodePtr& InOutRootNode, const TMultiMap& BlueprintPackageToAssetDataMap) + void FClassHierarchy::AddChildren_NoFilter(FSEClassFilterNodePtr& InOutRootNode, + const TMultiMap& BlueprintPackageToAssetDataMap) { UClass* RootClass = UObject::StaticClass(); - ObjectClassRoot = MakeShared(RootClass->GetName(), RootClass->GetDisplayNameText().ToString()); + ObjectClassRoot = + MakeShared(RootClass->GetName(), RootClass->GetDisplayNameText().ToString()); ObjectClassRoot->Class = RootClass; TMap Nodes; @@ -115,7 +132,8 @@ namespace ClassFilter FSEClassFilterNodePtr& ParentEntry = Nodes.FindOrAdd(CurrentClass->GetSuperClass()); if (!ParentEntry.IsValid()) { - ParentEntry = CreateNodeForClass(CurrentClass->GetSuperClass(), BlueprintPackageToAssetDataMap); + ParentEntry = + CreateNodeForClass(CurrentClass->GetSuperClass(), BlueprintPackageToAssetDataMap); } FSEClassFilterNodePtr& MyEntry = Nodes.FindOrAdd(CurrentClass); @@ -136,7 +154,8 @@ namespace ClassFilter } } - FSEClassFilterNodePtr FClassHierarchy::FindParent(const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InParentClassPath, const UClass* InParentClass) + FSEClassFilterNodePtr FClassHierarchy::FindParent(const FSEClassFilterNodePtr& InRootNode, + FTopLevelAssetPath InParentClassPath, const UClass* InParentClass) { // Check if the current node is the parent class name that is being searched for. if (InRootNode->ClassPath == InParentClassPath) @@ -149,21 +168,21 @@ namespace ClassFilter // If a class does not have a generated class name, we look up the parent class and compare. const UClass* ParentClass = InParentClass; - if (const UClass * RootClass = InRootNode->Class.Get()) + if (const UClass* RootClass = InRootNode->Class.Get()) { if (ParentClass == RootClass) { return InRootNode; } } - } // Search the children recursively, one of them might have the parent. FSEClassFilterNodePtr ReturnNode; for (const auto& Child : InRootNode->GetChildrenList()) { - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. ReturnNode = FindParent(Child, InParentClassPath, InParentClass); if (ReturnNode.IsValid()) { @@ -173,7 +192,8 @@ namespace ClassFilter return {}; } - FSEClassFilterNodePtr FClassHierarchy::FindNodeByClassName(const FSEClassFilterNodePtr& InRootNode, const FString& InClassName) + FSEClassFilterNodePtr FClassHierarchy::FindNodeByClassName( + const FSEClassFilterNodePtr& InRootNode, const FString& InClassName) { FString NodeClassName = InRootNode->Class.IsValid() ? InRootNode->Class->GetPathName() : FString(); if (NodeClassName == InClassName) @@ -185,7 +205,8 @@ namespace ClassFilter FSEClassFilterNodePtr ReturnNode; for (const auto& Child : InRootNode->GetChildrenList()) { - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. ReturnNode = FindNodeByClassName(Child, InClassName); if (ReturnNode.IsValid()) { @@ -195,7 +216,8 @@ namespace ClassFilter return {}; } - FSEClassFilterNodePtr FClassHierarchy::FindNodeByClass(const FSEClassFilterNodePtr& InRootNode, const UClass* Class) + FSEClassFilterNodePtr FClassHierarchy::FindNodeByClass( + const FSEClassFilterNodePtr& InRootNode, const UClass* Class) { if (InRootNode->Class.IsValid() && InRootNode->Class == Class) { @@ -206,7 +228,8 @@ namespace ClassFilter FSEClassFilterNodePtr ReturnNode; for (const auto& Child : InRootNode->GetChildrenList()) { - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. ReturnNode = FindNodeByClass(Child, Class); if (ReturnNode.IsValid()) { @@ -216,7 +239,8 @@ namespace ClassFilter return {}; } - FSEClassFilterNodePtr FClassHierarchy::FindNodeByGeneratedClassPath(const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InGeneratedClassPath) + FSEClassFilterNodePtr FClassHierarchy::FindNodeByGeneratedClassPath( + const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InGeneratedClassPath) { if (InRootNode->ClassPath == InGeneratedClassPath) { @@ -227,7 +251,8 @@ namespace ClassFilter FSEClassFilterNodePtr ReturnNode; for (const auto& Child : InRootNode->GetChildrenList()) { - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. ReturnNode = FindNodeByGeneratedClassPath(Child, InGeneratedClassPath); if (ReturnNode.IsValid()) { @@ -237,7 +262,8 @@ namespace ClassFilter return {}; } - void FClassHierarchy::UpdateClassInNode(FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint) + void FClassHierarchy::UpdateClassInNode( + FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint) { FSEClassFilterNodePtr Node = FindNodeByGeneratedClassPath(ObjectClassRoot, InGeneratedClassPath); @@ -248,7 +274,8 @@ namespace ClassFilter } } - bool FClassHierarchy::FindAndRemoveNodeByClassPath(const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InClassPath) + bool FClassHierarchy::FindAndRemoveNodeByClassPath( + const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InClassPath) { bool bReturnValue = false; @@ -263,7 +290,8 @@ namespace ClassFilter return true; } - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. bReturnValue |= FindAndRemoveNodeByClassPath(Child, InClassPath); if (bReturnValue) { @@ -290,11 +318,13 @@ namespace ClassFilter void FClassHierarchy::AddAsset(const FAssetData& InAddedAssetData) { - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); if (!AssetRegistryModule.Get().IsLoadingAssets()) { TArray AncestorClassPaths; - AssetRegistryModule.Get().GetAncestorClassNames(InAddedAssetData.AssetClassPath, AncestorClassPaths); + AssetRegistryModule.Get().GetAncestorClassNames( + InAddedAssetData.AssetClassPath, AncestorClassPaths); if (AncestorClassPaths.Contains(UBlueprintCore::StaticClass()->GetClassPathName())) { @@ -304,7 +334,8 @@ namespace ClassFilter ClassObjectPath = FPackageName::ExportTextPathToObjectPath(ClassObjectPath); } - // Make sure that the node does not already exist. There is a bit of double adding going on at times and this prevents it. + // Make sure that the node does not already exist. There is a bit of double adding going on at + // times and this prevents it. if (!FindNodeByGeneratedClassPath(ObjectClassRoot, FTopLevelAssetPath{ClassObjectPath}) .IsValid()) { @@ -317,7 +348,8 @@ namespace ClassFilter // Resolve the parent's class name locally and use it to find the parent's class. FString ParentClassPath = NewNode->ParentClassPath.ToString(); UClass* ParentClass = FindObject(nullptr, *ParentClassPath); - FSEClassFilterNodePtr ParentNode = FindParent(ObjectClassRoot, NewNode->ParentClassPath, ParentClass); + FSEClassFilterNodePtr ParentNode = + FindParent(ObjectClassRoot, NewNode->ParentClassPath, ParentClass); if (ParentNode.IsValid()) { ParentNode->AddChild(NewNode); @@ -347,13 +379,14 @@ namespace ClassFilter void FClassHierarchy::SortChildren(FSEClassFilterNodePtr& InRootNode) { - TArray< FSEClassFilterNodePtr >& ChildList = InRootNode->GetChildrenList(); + TArray& ChildList = InRootNode->GetChildrenList(); for (auto& Child : InRootNode->GetChildrenList()) { // Setup the parent weak pointer, useful for going up the tree for unloaded blueprints. Child->ParentNode = InRootNode; - // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. + // Check the child, then check the return to see if it is valid. If it is valid, end the + // recursion. SortChildren(Child); } @@ -372,7 +405,8 @@ namespace ClassFilter } } - void FClassHierarchy::LoadUnloadedTagData(FSEClassFilterNodePtr& InOutClassFilterNode, const FAssetData& InAssetData) + void FClassHierarchy::LoadUnloadedTagData( + FSEClassFilterNodePtr& InOutClassFilterNode, const FAssetData& InAssetData) { const FString ClassName = InAssetData.AssetName.ToString(); FString ClassDisplayName = InAssetData.GetTagValueRef(FBlueprintTags::BlueprintDisplayName); @@ -394,20 +428,24 @@ namespace ClassFilter FString ParentClassPathString; if (InAssetData.GetTagValue(FBlueprintTags::ParentClassPath, ParentClassPathString)) { - InOutClassFilterNode->ParentClassPath = FPackageName::ExportTextPathToObjectPath(ParentClassPathString); + InOutClassFilterNode->ParentClassPath = + FPackageName::ExportTextPathToObjectPath(ParentClassPathString); } - InOutClassFilterNode->bIsBPNormalType = InAssetData.GetTagValueRef(FBlueprintTags::BlueprintType) == TEXT("BPType_Normal"); + InOutClassFilterNode->bIsBPNormalType = + InAssetData.GetTagValueRef(FBlueprintTags::BlueprintType) == TEXT("BPType_Normal"); // It is an unloaded blueprint, so we need to create the structure that will hold the data. - TSharedPtr UnloadedBlueprintData = MakeShared(InOutClassFilterNode); + TSharedPtr UnloadedBlueprintData = + MakeShared(InOutClassFilterNode); InOutClassFilterNode->UnloadedBlueprintData = UnloadedBlueprintData; // Get the class flags. const uint32 ClassFlags = InAssetData.GetTagValueRef(FBlueprintTags::ClassFlags); InOutClassFilterNode->UnloadedBlueprintData->SetClassFlags(ClassFlags); - const FString ImplementedInterfaces = InAssetData.GetTagValueRef(FBlueprintTags::ImplementedInterfaces); + const FString ImplementedInterfaces = + InAssetData.GetTagValueRef(FBlueprintTags::ImplementedInterfaces); if (!ImplementedInterfaces.IsEmpty()) { FString FullInterface; @@ -418,13 +456,16 @@ namespace ClassFilter { if (!CurrentString.StartsWith(TEXT("Graphs=("))) { - if (FullInterface.Split(TEXT("\""), &CurrentString, &InterfacePath, ESearchCase::CaseSensitive)) + if (FullInterface.Split( + TEXT("\""), &CurrentString, &InterfacePath, ESearchCase::CaseSensitive)) { // The interface paths in metadata end with "', so remove those InterfacePath.RemoveFromEnd(TEXT("\"'")); - FCoreRedirectObjectName ResolvedInterfaceName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class, FCoreRedirectObjectName(InterfacePath)); - UnloadedBlueprintData->AddImplementedInterface(ResolvedInterfaceName.ObjectName.ToString()); + FCoreRedirectObjectName ResolvedInterfaceName = FCoreRedirects::GetRedirectedName( + ECoreRedirectFlags::Type_Class, FCoreRedirectObjectName(InterfacePath)); + UnloadedBlueprintData->AddImplementedInterface( + ResolvedInterfaceName.ObjectName.ToString()); } } @@ -435,9 +476,10 @@ namespace ClassFilter void FClassHierarchy::PopulateClassHierarchy() { - TArray< FSEClassFilterNodePtr > RootLevelClasses; + TArray RootLevelClasses; - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); // Retrieve all blueprint classes TArray BlueprintList; @@ -447,7 +489,8 @@ namespace ClassFilter Filter.ClassPaths.Add(UAnimBlueprint::StaticClass()->GetClassPathName()); Filter.ClassPaths.Add(UBlueprintGeneratedClass::StaticClass()->GetClassPathName()); - // Include any Blueprint based objects as well, this includes things like Blutilities, UMG, and GameplayAbility objects + // Include any Blueprint based objects as well, this includes things like Blutilities, UMG, and + // GameplayAbility objects Filter.bRecursiveClasses = true; AssetRegistryModule.Get().GetAssets(Filter, BlueprintList); @@ -480,17 +523,18 @@ namespace ClassFilter for (int32 SearchNodeIdx = 0; SearchNodeIdx < RootLevelClasses.Num(); ++SearchNodeIdx) { - FSEClassFilterNodePtr ParentNode = FindParent(RootLevelClasses[SearchNodeIdx], RootLevelClasses[CurrentNodeIdx]->ParentClassPath, ParentClass); + FSEClassFilterNodePtr ParentNode = FindParent(RootLevelClasses[SearchNodeIdx], + RootLevelClasses[CurrentNodeIdx]->ParentClassPath, ParentClass); if (ParentNode.IsValid()) { - // AddUniqueChild makes sure that when a node was generated one by EditorClassHierarchy and one from LoadUnloadedTagData - the proper one is selected + // AddUniqueChild makes sure that when a node was generated one by + // EditorClassHierarchy and one from LoadUnloadedTagData - the proper one is selected ParentNode->AddUniqueChild(RootLevelClasses[CurrentNodeIdx]); RootLevelClasses.RemoveAtSwap(CurrentNodeIdx); --CurrentNodeIdx; break; } } - } } @@ -500,4 +544,4 @@ namespace ClassFilter // All viewers must refresh. ClassFilter::Helpers::RefreshAll(); } -} +} // namespace ClassFilter diff --git a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.h b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.h index 0468dcf..9aee42b 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.h +++ b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterHelpers.h @@ -1,26 +1,26 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "ClassFilter.h" #include "ClassFilterNode.h" + #include -#include -#include -#include -#include -#include #include -#include -#include #include -#include #include -#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include #include -#include "Misc/ClassFilter.h" +#include +#include +#include +#include + #define LOCTEXT_NAMESPACE "ClassFilterHelpers" @@ -47,7 +47,8 @@ namespace ClassFilter FClassHierarchy(); ~FClassHierarchy(); - /** Populates the class hierarchy tree, pulling all the loaded and unloaded classes into a master tree. */ + /** Populates the class hierarchy tree, pulling all the loaded and unloaded classes into a master + * tree. */ void PopulateClassHierarchy(); /** Recursive function to sort a tree. @@ -63,7 +64,7 @@ namespace ClassFilter // This node should always be valid. check(ObjectClassRoot.IsValid()) - return ObjectClassRoot; + return ObjectClassRoot; } /** Finds the parent of a node, recursively going deeper into the hierarchy. @@ -79,40 +80,47 @@ namespace ClassFilter /** Updates the Class of a node. Uses the generated class package name to find the node. * @param InGeneratedClassPath The path of the generated class to find the node for. * @param InNewClass The class to update the node with. - */ - void UpdateClassInNode(FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint); + */ + void UpdateClassInNode( + FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint); /** Finds the node, recursively going deeper into the hierarchy. Does so by comparing class names. - * @param InClassName The name of the generated class package to find the node for. - * - * @return The node. - */ - FSEClassFilterNodePtr FindNodeByClassName(const FSEClassFilterNodePtr& InRootNode, const FString& InClassName); + * @param InClassName The name of the generated class package to find the node for. + * + * @return The node. + */ + FSEClassFilterNodePtr FindNodeByClassName( + const FSEClassFilterNodePtr& InRootNode, const FString& InClassName); /** Finds the node, recursively going deeper into the hierarchy. Does so by comparing class names. - * @param InClass The pointer of the class to find the node for. - * - * @return The node. - */ + * @param InClass The pointer of the class to find the node for. + * + * @return The node. + */ FSEClassFilterNodePtr FindNodeByClass(const FSEClassFilterNodePtr& InRootNode, const UClass* Class); private: /** Recursive function to build a tree, will not filter. - * @param InOutRootNode The node that this function will add the children of to the tree. - * @param PackageNameToAssetDataMap The asset registry map of blueprint package names to blueprint data + * @param InOutRootNode The node that this function will add the children of + *to the tree. + * @param PackageNameToAssetDataMap The asset registry map of blueprint package names to + *blueprint data */ - void AddChildren_NoFilter(FSEClassFilterNodePtr& InOutRootNode, const TMultiMap& BlueprintPackageToAssetDataMap); + void AddChildren_NoFilter(FSEClassFilterNodePtr& InOutRootNode, + const TMultiMap& BlueprintPackageToAssetDataMap); /** Called when hot reload has finished */ void OnReloadComplete(EReloadCompleteReason Reason); - /** Finds the node, recursively going deeper into the hierarchy. Does so by comparing generated class package names. + /** Finds the node, recursively going deeper into the hierarchy. Does so by comparing generated class + *package names. * @param InGeneratedClassPath The path of the generated class to find the node for. * * @return The node. */ - FSEClassFilterNodePtr FindNodeByGeneratedClassPath(const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InGeneratedClassPath); + FSEClassFilterNodePtr FindNodeByGeneratedClassPath( + const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InGeneratedClassPath); /** * Loads the tag data for an unloaded blueprint asset. @@ -137,7 +145,8 @@ namespace ClassFilter * * @return Returns true if the asset was found and deleted successfully. */ - bool FindAndRemoveNodeByClassPath(const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InClassPath); + bool FindAndRemoveNodeByClassPath( + const FSEClassFilterNodePtr& InRootNode, FTopLevelAssetPath InClassPath); /** Callback registered to the Asset Registry to be notified when an asset is added. */ void AddAsset(const FAssetData& InAddedAssetData); @@ -149,10 +158,10 @@ namespace ClassFilter namespace Helpers { - DECLARE_MULTICAST_DELEGATE( FPopulateClassFilter ); + DECLARE_MULTICAST_DELEGATE(FPopulateClassFilter); /** The class hierarchy that manages the unfiltered class tree for the Class Viewer. */ - static TSharedPtr< FClassHierarchy > ClassHierarchy; + static TSharedPtr ClassHierarchy; /** Used to inform any registered Class Viewers to refresh. */ static FPopulateClassFilter PopulateClassFilterDelegate; @@ -161,8 +170,8 @@ namespace ClassFilter static bool bPopulateClassHierarchy; // Pre-declare these functions. - static bool CheckIfBlueprintBase( FSEClassFilterNodePtr InNode ); - static UBlueprint* GetBlueprint( UClass* InClass ); + static bool CheckIfBlueprintBase(FSEClassFilterNodePtr InNode); + static UBlueprint* GetBlueprint(UClass* InClass); static void UpdateClassInNode( FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint); @@ -177,10 +186,10 @@ namespace ClassFilter bool bIsClassDeprecated = InClass->HasAnyClassFlags(CLASS_Deprecated); InClass->ClassFlags &= ~CLASS_Deprecated; - bool bCanCreateBlueprintOfClass = FKismetEditorUtilities::CanCreateBlueprintOfClass( InClass ); + bool bCanCreateBlueprintOfClass = FKismetEditorUtilities::CanCreateBlueprintOfClass(InClass); // Reassign the deprecated flag if it was previously assigned - if(bIsClassDeprecated) + if (bIsClassDeprecated) { InClass->ClassFlags |= CLASS_Deprecated; } @@ -210,12 +219,12 @@ namespace ClassFilter /** Will create the instance of FClassHierarchy and populate the class hierarchy tree. */ static void ConstructClassHierarchy() { - if(!ClassHierarchy.IsValid()) + if (!ClassHierarchy.IsValid()) { ClassHierarchy = MakeShared(); // When created, populate the hierarchy. - GWarn->BeginSlowTask( LOCTEXT("RebuildingClassHierarchy", "Rebuilding Class Hierarchy"), true ); + GWarn->BeginSlowTask(LOCTEXT("RebuildingClassHierarchy", "Rebuilding Class Hierarchy"), true); ClassHierarchy->PopulateClassHierarchy(); GWarn->EndSlowTask(); } @@ -230,11 +239,11 @@ namespace ClassFilter /** Will populate the class hierarchy tree if previously requested. */ static void PopulateClassHierarchy() { - if(bPopulateClassHierarchy) + if (bPopulateClassHierarchy) { bPopulateClassHierarchy = false; - GWarn->BeginSlowTask( LOCTEXT("RebuildingClassHierarchy", "Rebuilding Class Hierarchy"), true ); + GWarn->BeginSlowTask(LOCTEXT("RebuildingClassHierarchy", "Rebuilding Class Hierarchy"), true); ClassHierarchy->PopulateClassHierarchy(); GWarn->EndSlowTask(); } @@ -252,12 +261,16 @@ namespace ClassFilter ClassFilter::Helpers::PopulateClassFilterDelegate.Broadcast(); } - /** Recursive function to build a tree, filtering out nodes based on the InitOptions and filter search terms. - * @param InInitOptions The class viewer's options, holds the AllowedClasses and DisallowedClasses. - * @param InOutRootNode The node that this function will add the children of to the tree. + /** Recursive function to build a tree, filtering out nodes based on the InitOptions and filter search + *terms. + * @param InInitOptions The class viewer's options, holds the AllowedClasses + *and DisallowedClasses. + * @param InOutRootNode The node that this function will add the children of + *to the tree. * @param InRootClassIndex The index of the root node. * @param bInOnlyBlueprintBases Filter option to remove non-blueprint base classes. - * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to class filter options. + * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to + *class filter options. * @param bInInternalClasses Filter option for showing internal classes. * @param InternalClasses The classes that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. @@ -265,20 +278,21 @@ namespace ClassFilter * @return Returns true if the child passed the filter. */ static bool AddChildren_Tree(const FSEClassFilter& Filter, FSEClassFilterNodePtr& InOutRootNode, - const FSEClassFilterNodePtr& InOriginalRootNode, - bool bInOnlyBlueprintBases, bool bInShowUnloadedBlueprints, bool bInInternalClasses, - const TArray& InternalClasses, const TArray& InternalPaths) + const FSEClassFilterNodePtr& InOriginalRootNode, bool bInOnlyBlueprintBases, + bool bInShowUnloadedBlueprints, bool bInInternalClasses, const TArray& InternalClasses, + const TArray& InternalPaths) { - bool bChildrenPassesFilter= false; + bool bChildrenPassesFilter = false; bool bReturnPassesFilter = false; - bool bPassesBlueprintBaseFilter = !bInOnlyBlueprintBases || CheckIfBlueprintBase(InOriginalRootNode); + bool bPassesBlueprintBaseFilter = + !bInOnlyBlueprintBases || CheckIfBlueprintBase(InOriginalRootNode); bool bIsUnloadedBlueprint = !InOriginalRootNode->Class.IsValid(); FString GeneratedClassPathString = InOriginalRootNode->ClassPath.ToString(); - // The INI files declare classes and folders that are considered internal only. Does this class match any of those patterns? - // INI path: /Script/ClassFilter.ClassFilterProjectSettings + // The INI files declare classes and folders that are considered internal only. Does this class + // match any of those patterns? INI path: /Script/ClassFilter.ClassFilterProjectSettings bool bPassesInternalFilter = true; if (!bInInternalClasses && InternalPaths.Num() > 0) { @@ -291,7 +305,8 @@ namespace ClassFilter } } } - if (!bInInternalClasses && InternalClasses.Num() > 0 && bPassesInternalFilter && InOriginalRootNode->Class.IsValid()) + if (!bInInternalClasses && InternalClasses.Num() > 0 && bPassesInternalFilter && + InOriginalRootNode->Class.IsValid()) { for (int i = 0; i < InternalClasses.Num(); i++) { @@ -303,27 +318,33 @@ namespace ClassFilter } } - // There are few options for filtering an unloaded blueprint, if it matches with this filter, it passes. - if(bIsUnloadedBlueprint) + // There are few options for filtering an unloaded blueprint, if it matches with this filter, it + // passes. + if (bIsUnloadedBlueprint) { - if(bInShowUnloadedBlueprints) + if (bInShowUnloadedBlueprints) { - bReturnPassesFilter = InOutRootNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && PassesFilter(*InOriginalRootNode->GetClassName()); + bReturnPassesFilter = InOutRootNode->bPassesFilter = + bPassesBlueprintBaseFilter && bPassesInternalFilter && + PassesFilter(*InOriginalRootNode->GetClassName()); } } else { - bReturnPassesFilter = InOutRootNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && PassesFilter(*InOriginalRootNode->GetClassName()); + bReturnPassesFilter = InOutRootNode->bPassesFilter = + bPassesBlueprintBaseFilter && bPassesInternalFilter && + PassesFilter(*InOriginalRootNode->GetClassName()); } - for(const auto& Child : InOriginalRootNode->GetChildrenList()) + for (const auto& Child : InOriginalRootNode->GetChildrenList()) { - FSEClassFilterNodePtr NewNode = MakeShared( *Child.Get() ); + FSEClassFilterNodePtr NewNode = MakeShared(*Child.Get()); NewNode->SetStateFromFilter(Filter); - bChildrenPassesFilter = AddChildren_Tree(Filter, NewNode, Child, bInOnlyBlueprintBases, bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); - if(bChildrenPassesFilter) + bChildrenPassesFilter = AddChildren_Tree(Filter, NewNode, Child, bInOnlyBlueprintBases, + bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); + if (bChildrenPassesFilter) { InOutRootNode->AddChild(NewNode); } @@ -333,26 +354,30 @@ namespace ClassFilter } /** Builds the class tree. - * @param InInitOptions The class viewer's options, holds the AllowedClasses and DisallowedClasses. + * @param InInitOptions The class viewer's options, holds the AllowedClasses + *and DisallowedClasses. * @param InOutRootNode The node to root the tree to. * @param bInOnlyBlueprintBases Filter option to remove non-blueprint base classes. - * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to class filter options. + * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to + *class filter options. * @param bInInternalClasses Filter option for showing internal classes. * @param InternalClasses The classes that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * @return A fully built tree. */ - static void GetClassTree(const FSEClassFilter& Filter, FSEClassFilterNodePtr& InOutRootNode, bool bInOnlyBlueprintBases, - bool bInShowUnloadedBlueprints, bool bInInternalClasses = true, - const TArray& InternalClasses = TArray(), const TArray& InternalPaths = TArray()) + static void GetClassTree(const FSEClassFilter& Filter, FSEClassFilterNodePtr& InOutRootNode, + bool bInOnlyBlueprintBases, bool bInShowUnloadedBlueprints, bool bInInternalClasses = true, + const TArray& InternalClasses = TArray(), + const TArray& InternalPaths = TArray()) { // Use BaseClass as root FSEClassFilterNodePtr RootNode; if (Filter.GetBaseClass()) { - RootNode = ClassHierarchy->FindNodeByClass(ClassHierarchy->GetObjectRootNode(), Filter.GetBaseClass()); + RootNode = ClassHierarchy->FindNodeByClass( + ClassHierarchy->GetObjectRootNode(), Filter.GetBaseClass()); } - else // Use UObject as root + else // Use UObject as root { RootNode = ClassHierarchy->GetObjectRootNode(); } @@ -360,34 +385,39 @@ namespace ClassFilter // Duplicate the node, it will have no children. InOutRootNode = MakeShared(*RootNode); - AddChildren_Tree(Filter, InOutRootNode, RootNode, bInOnlyBlueprintBases, bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); + AddChildren_Tree(Filter, InOutRootNode, RootNode, bInOnlyBlueprintBases, + bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); } - /** Recursive function to build the list, filtering out nodes based on the InitOptions and filter search terms. - * @param InInitOptions The class viewer's options, holds the AllowedClasses and DisallowedClasses. - * @param InOutRootNode The node that this function will add the children of to the tree. + /** Recursive function to build the list, filtering out nodes based on the InitOptions and filter + *search terms. + * @param InInitOptions The class viewer's options, holds the AllowedClasses + *and DisallowedClasses. + * @param InOutRootNode The node that this function will add the children of + *to the tree. * @param InRootClassIndex The index of the root node. * @param bInOnlyBlueprintBases Filter option to remove non-blueprint base classes. - * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to class filter options. + * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to + *class filter options. * @param bInInternalClasses Filter option for showing internal classes. * @param InternalClasses The classes that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * * @return Returns true if the child passed the filter. */ - static void AddChildren_List(TArray< FSEClassFilterNodePtr >& InOutNodeList, - const FSEClassFilterNodePtr& InOriginalRootNode, - bool bInOnlyBlueprintBases, bool bInShowUnloadedBlueprints, - bool bInInternalClasses, - const TArray& InternalClasses, const TArray& InternalPaths) + static void AddChildren_List(TArray& InOutNodeList, + const FSEClassFilterNodePtr& InOriginalRootNode, bool bInOnlyBlueprintBases, + bool bInShowUnloadedBlueprints, bool bInInternalClasses, const TArray& InternalClasses, + const TArray& InternalPaths) { - bool bPassesBlueprintBaseFilter = !bInOnlyBlueprintBases || CheckIfBlueprintBase(InOriginalRootNode); + bool bPassesBlueprintBaseFilter = + !bInOnlyBlueprintBases || CheckIfBlueprintBase(InOriginalRootNode); bool bIsUnloadedBlueprint = !InOriginalRootNode->Class.IsValid(); FString GeneratedClassPathString = InOriginalRootNode->ClassPath.ToString(); - // The INI files declare classes and folders that are considered internal only. Does this class match any of those patterns? - // INI path: /Script/ClassFilter.ClassFilterProjectSettings + // The INI files declare classes and folders that are considered internal only. Does this class + // match any of those patterns? INI path: /Script/ClassFilter.ClassFilterProjectSettings bool bPassesInternalFilter = true; if (!bInInternalClasses && InternalPaths.Num() > 0) { @@ -414,52 +444,58 @@ namespace ClassFilter FSEClassFilterNodePtr NewNode = MakeShared(*InOriginalRootNode.Get()); - // There are few options for filtering an unloaded blueprint, if it matches with this filter, it passes. - if(bIsUnloadedBlueprint) + // There are few options for filtering an unloaded blueprint, if it matches with this filter, it + // passes. + if (bIsUnloadedBlueprint) { - if(bInShowUnloadedBlueprints) + if (bInShowUnloadedBlueprints) { - NewNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && PassesFilter(*InOriginalRootNode->GetClassName()); + NewNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && + PassesFilter(*InOriginalRootNode->GetClassName()); } } else { - NewNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && PassesFilter(*InOriginalRootNode->GetClassName()); + NewNode->bPassesFilter = bPassesBlueprintBaseFilter && bPassesInternalFilter && + PassesFilter(*InOriginalRootNode->GetClassName()); } - if(NewNode->bPassesFilter) + if (NewNode->bPassesFilter) { InOutNodeList.Add(NewNode); } for (const auto& Child : InOriginalRootNode->GetChildrenList()) { - AddChildren_List(InOutNodeList, Child, bInOnlyBlueprintBases, - bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); + AddChildren_List(InOutNodeList, Child, bInOnlyBlueprintBases, bInShowUnloadedBlueprints, + bInInternalClasses, InternalClasses, InternalPaths); } } /** Builds the class list. - * @param InInitOptions The class viewer's options, holds the AllowedClasses and DisallowedClasses. + * @param InInitOptions The class viewer's options, holds the AllowedClasses + *and DisallowedClasses. * @param InOutNodeList The list to add all the nodes to. * @param bInOnlyBlueprintBases Filter option to remove non-blueprint base classes. - * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to class filter options. + * @param bInShowUnloadedBlueprints Filter option to not remove unloaded blueprints due to + *class filter options. * @param bInInternalClasses Filter option for showing internal classes. * @param InternalClasses The classes that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * * @return A fully built list. */ - static void GetClassList(TArray< FSEClassFilterNodePtr >& InOutNodeList, - bool bInOnlyBlueprintBases, bool bInShowUnloadedBlueprints, - bool bInInternalClasses = true, - const TArray& InternalClasses = TArray(), const TArray& InternalPaths = TArray()) + static void GetClassList(TArray& InOutNodeList, bool bInOnlyBlueprintBases, + bool bInShowUnloadedBlueprints, bool bInInternalClasses = true, + const TArray& InternalClasses = TArray(), + const TArray& InternalPaths = TArray()) { const FSEClassFilterNodePtr ObjectClassRoot = ClassHierarchy->GetObjectRootNode(); for (const auto& Child : ObjectClassRoot->GetChildrenList()) { - AddChildren_List(InOutNodeList, Child, bInOnlyBlueprintBases, bInShowUnloadedBlueprints, bInInternalClasses, InternalClasses, InternalPaths); + AddChildren_List(InOutNodeList, Child, bInOnlyBlueprintBases, bInShowUnloadedBlueprints, + bInInternalClasses, InternalClasses, InternalPaths); } } @@ -468,9 +504,9 @@ namespace ClassFilter * * @return The blueprint associated with the class index. */ - static UBlueprint* GetBlueprint( UClass* InClass ) + static UBlueprint* GetBlueprint(UClass* InClass) { - if( InClass->ClassGeneratedBy && InClass->ClassGeneratedBy->IsA(UBlueprint::StaticClass()) ) + if (InClass->ClassGeneratedBy && InClass->ClassGeneratedBy->IsA(UBlueprint::StaticClass())) { return Cast(InClass->ClassGeneratedBy); } @@ -485,11 +521,12 @@ namespace ClassFilter * * @return The blueprint associated with the class index. */ - static void GetClassInfo( TWeakObjectPtr InClass, bool& bInOutIsBlueprintBase, bool& bInOutHasBlueprint ) + static void GetClassInfo( + TWeakObjectPtr InClass, bool& bInOutIsBlueprintBase, bool& bInOutHasBlueprint) { if (UClass* Class = InClass.Get()) { - bInOutIsBlueprintBase = CanCreateBlueprintOfClass_IgnoreDeprecation( Class ); + bInOutIsBlueprintBase = CanCreateBlueprintOfClass_IgnoreDeprecation(Class); bInOutHasBlueprint = Class->ClassGeneratedBy != nullptr; } else @@ -504,17 +541,18 @@ namespace ClassFilter * * @return true if the class is a blueprint. */ - static bool CheckIfBlueprintBase( TSharedPtr< FSEClassFilterNode> InNode ) + static bool CheckIfBlueprintBase(TSharedPtr InNode) { // If there is no class, it may be an unloaded blueprint. - if(UClass* Class = InNode->Class.Get()) + if (UClass* Class = InNode->Class.Get()) { return CanCreateBlueprintOfClass_IgnoreDeprecation(Class); } - else if(InNode->bIsBPNormalType) + else if (InNode->bIsBPNormalType) { bool bAllowDerivedBlueprints = false; - GConfig->GetBool(TEXT("Kismet"), TEXT("AllowDerivedBlueprints"), /*out*/ bAllowDerivedBlueprints, GEngineIni); + GConfig->GetBool(TEXT("Kismet"), TEXT("AllowDerivedBlueprints"), + /*out*/ bAllowDerivedBlueprints, GEngineIni); return bAllowDerivedBlueprints; } @@ -525,7 +563,8 @@ namespace ClassFilter /** * Creates a blueprint from a class. * - * @param InOutClassNode Class node to pull what class to load and to update information in. + * @param InOutClassNode Class node to pull what class to load and to update information + * in. */ static void LoadClass(FSEClassFilterNodePtr InOutClassNode) { @@ -538,28 +577,31 @@ namespace ClassFilter InOutClassNode->Blueprint = Cast(Class->ClassGeneratedBy); InOutClassNode->Class = Class; - // Tell the original node to update so when a refresh happens it will still know about the newly loaded class. - ClassFilter::Helpers::UpdateClassInNode(InOutClassNode->ClassPath, InOutClassNode->Class.Get(), InOutClassNode->Blueprint.Get() ); + // Tell the original node to update so when a refresh happens it will still know about the + // newly loaded class. + ClassFilter::Helpers::UpdateClassInNode( + InOutClassNode->ClassPath, InOutClassNode->Class.Get(), InOutClassNode->Blueprint.Get()); } else { FMessageLog EditorErrors("EditorErrors"); FFormatNamedArguments Arguments; Arguments.Add(TEXT("ObjectName"), FText::FromString(InOutClassNode->ClassPath.ToString())); - EditorErrors.Error(FText::Format(LOCTEXT("PackageLoadFail", "Failed to load class {ObjectName}"), Arguments)); + EditorErrors.Error(FText::Format( + LOCTEXT("PackageLoadFail", "Failed to load class {ObjectName}"), Arguments)); } } /** Updates the Class of a node. Uses the generated class package name to find the node. - * @param InGeneratedClassPath The name of the generated class to find the node for. - * @param InNewClass The class to update the node with. - */ + * @param InGeneratedClassPath The name of the generated class to find the node for. + * @param InNewClass The class to update the node with. + */ static void UpdateClassInNode( FTopLevelAssetPath InGeneratedClassPath, UClass* InNewClass, UBlueprint* InNewBluePrint) { - ClassHierarchy->UpdateClassInNode(InGeneratedClassPath, InNewClass, InNewBluePrint ); + ClassHierarchy->UpdateClassInNode(InGeneratedClassPath, InNewClass, InNewBluePrint); } - } // namespace Helpers -} // namespace ClassFilter + } // namespace Helpers +} // namespace ClassFilter #undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.cpp b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.cpp index d0e0a10..3b9275b 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.cpp +++ b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.cpp @@ -1,10 +1,12 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "ClassFilterNode.h" + +#include "ClassFilter.h" + #include #include -#include "Misc/ClassFilter.h" FSEClassFilterNode::FSEClassFilterNode(const FString& InClassName, const FString& InClassDisplayName) @@ -18,7 +20,7 @@ FSEClassFilterNode::FSEClassFilterNode(const FString& InClassName, const FString Blueprint = nullptr; } -FSEClassFilterNode::FSEClassFilterNode( const FSEClassFilterNode& InCopyObject) +FSEClassFilterNode::FSEClassFilterNode(const FSEClassFilterNode& InCopyObject) { ClassName = InCopyObject.ClassName; ClassDisplayName = InCopyObject.ClassDisplayName; @@ -47,7 +49,7 @@ FSEClassFilterNode::FSEClassFilterNode( const FSEClassFilterNode& InCopyObject) void FSEClassFilterNode::AddChild(FSEClassFilterNodePtr& Child) { ChildrenList.Add(Child); - Child->ParentNode = TSharedRef{ AsShared() }; + Child->ParentNode = TSharedRef{AsShared()}; } void FSEClassFilterNode::AddUniqueChild(FSEClassFilterNodePtr& Child) @@ -65,7 +67,8 @@ void FSEClassFilterNode::AddUniqueChild(FSEClassFilterNodePtr& Child) if (bNewChildHasMoreInfo && !bOldChildHasMoreInfo) { // make sure, that new child has all needed children - for (int OldChildIndex = 0; OldChildIndex < CurrentChild->ChildrenList.Num(); ++OldChildIndex) + for (int OldChildIndex = 0; OldChildIndex < CurrentChild->ChildrenList.Num(); + ++OldChildIndex) { Child->AddUniqueChild(CurrentChild->ChildrenList[OldChildIndex]); } @@ -85,27 +88,28 @@ FString FSEClassFilterNode::GetClassName(EClassViewerNameTypeToDisplay NameType) { switch (NameType) { - case EClassViewerNameTypeToDisplay::ClassName: - return ClassName; + case EClassViewerNameTypeToDisplay::ClassName: + return ClassName; - case EClassViewerNameTypeToDisplay::DisplayName: - return ClassDisplayName; + case EClassViewerNameTypeToDisplay::DisplayName: + return ClassDisplayName; - case EClassViewerNameTypeToDisplay::Dynamic: - FString CombinedName; - FString SanitizedName = FName::NameToDisplayString(ClassName, false); - if (ClassDisplayName.IsEmpty() && !ClassDisplayName.Equals(SanitizedName) && !ClassDisplayName.Equals(ClassName)) - { - TArray Args; - Args.Add(ClassName); - Args.Add(ClassDisplayName); - CombinedName = FString::Format(TEXT("{0} ({1})"), Args); - } - else - { - CombinedName = ClassName; - } - return MoveTemp(CombinedName); + case EClassViewerNameTypeToDisplay::Dynamic: + FString CombinedName; + FString SanitizedName = FName::NameToDisplayString(ClassName, false); + if (ClassDisplayName.IsEmpty() && !ClassDisplayName.Equals(SanitizedName) && + !ClassDisplayName.Equals(ClassName)) + { + TArray Args; + Args.Add(ClassName); + Args.Add(ClassDisplayName); + CombinedName = FString::Format(TEXT("{0} ({1})"), Args); + } + else + { + CombinedName = ClassName; + } + return MoveTemp(CombinedName); } ensureMsgf(false, TEXT("FSEClassFilterNode::GetClassName called with invalid name type.")); @@ -139,7 +143,7 @@ void FSEClassFilterNode::SetOwnFilterState(EClassFilterState State) void FSEClassFilterNode::SetStateFromFilter(const FSEClassFilter& Filter) { - const TSoftClassPtr<> ClassAsset{ ClassPath.ToString() }; + const TSoftClassPtr<> ClassAsset{ClassPath.ToString()}; if (Filter.AllowedClasses.Contains(ClassAsset)) { diff --git a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.h b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.h index 039769d..5880141 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.h +++ b/Source/Editor/Private/Customizations/ClassFilter/ClassFilterNode.h @@ -1,10 +1,11 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include #include -#include +#include #include +#include + class IPropertyHandle; class IUnloadedBlueprintData; @@ -22,7 +23,6 @@ enum class EClassFilterState : uint8 class FSEClassFilterNode : public TSharedFromThis { public: - using FPtr = TSharedPtr; /** @@ -32,9 +32,9 @@ class FSEClassFilterNode : public TSharedFromThis * @param InClassDisplayName The display name of the class this node represents * @param bInIsPlaceable true if the class is a placeable class. */ - FSEClassFilterNode( const FString& InClassName, const FString& InClassDisplayName ); + FSEClassFilterNode(const FString& InClassName, const FString& InClassDisplayName); - FSEClassFilterNode( const FSEClassFilterNode& InCopyObject); + FSEClassFilterNode(const FSEClassFilterNode& InCopyObject); /** * Adds the specified child to the node. @@ -45,7 +45,8 @@ class FSEClassFilterNode : public TSharedFromThis void AddUniqueChild(FPtr& Child); /** - * Retrieves the class name this node is associated with. This is not the literal UClass name as it is missing the _C for blueprints + * Retrieves the class name this node is associated with. This is not the literal UClass name as it is + * missing the _C for blueprints * @param bUseDisplayName Whether to use the display name or class name */ const FString& GetClassName(bool bUseDisplayName = false) const @@ -54,7 +55,8 @@ class FSEClassFilterNode : public TSharedFromThis } /** - * Retrieves the class name this node is associated with. This is not the literal UClass name as it is missing the _C for blueprints + * Retrieves the class name this node is associated with. This is not the literal UClass name as it is + * missing the _C for blueprints * @param NameType Whether to use the display name or class name */ FString GetClassName(EClassViewerNameTypeToDisplay NameType) const; @@ -74,12 +76,15 @@ class FSEClassFilterNode : public TSharedFromThis /** Filter states */ void SetOwnFilterState(EClassFilterState State); void SetStateFromFilter(const struct FSEClassFilter& Filter); - EClassFilterState GetOwnFilterState() const { return FilterState; } + EClassFilterState GetOwnFilterState() const + { + return FilterState; + } EClassFilterState GetParentFilterState() const; private: - - /** The non-translated internal name for this class. This is not necessarily the UClass's name, as that may have _C for blueprints */ + /** The non-translated internal name for this class. This is not necessarily the UClass's name, as that + * may have _C for blueprints */ FString ClassName; /** The translated display name for this class */ @@ -91,7 +96,6 @@ class FSEClassFilterNode : public TSharedFromThis EClassFilterState FilterState = EClassFilterState::None; public: - TWeakPtr ParentNode; /** The class this node is associated with. */ @@ -112,7 +116,8 @@ class FSEClassFilterNode : public TSharedFromThis /** true if the class passed the filter. */ bool bPassesFilter; - /** true if the class is a "normal type", this is used to identify unloaded blueprints as blueprint bases. */ + /** true if the class is a "normal type", this is used to identify unloaded blueprints as blueprint bases. + */ bool bIsBPNormalType; /** Data for unloaded blueprints, only valid if the class is unloaded. */ diff --git a/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.cpp b/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.cpp index 68f0dd6..156932e 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.cpp +++ b/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.cpp @@ -1,40 +1,43 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SClassFilter.h" + +#include "AssetRegistry/AssetData.h" +#include "ClassFilterHelpers.h" +#include "Dialogs/Dialogs.h" +#include "Editor.h" +#include "EditorStyleSet.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Commands/UIAction.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/Notifications/NotificationManager.h" +#include "GameplayTagsModule.h" +#include "GameplayTagsSettings.h" +#include "Layout/WidgetPath.h" #include "Misc/ConfigCacheIni.h" +#include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SComboButton.h" -#include "Widgets/Images/SImage.h" -#include "EditorStyleSet.h" #include "Widgets/SWindow.h" -#include "Dialogs/Dialogs.h" -#include "GameplayTagsModule.h" + +#include +#include +#include #include #include -#include -#include #include #include -#include #include +#include #include -#include "Framework/Notifications/NotificationManager.h" -#include "GameplayTagsSettings.h" -#include "Layout/WidgetPath.h" -#include "Framework/Application/SlateApplication.h" -#include "AssetRegistry/AssetData.h" -#include "Editor.h" -#include "Framework/Commands/UIAction.h" -#include "Framework/MultiBox/MultiBoxBuilder.h" -#include "ClassFilterHelpers.h" -#include #define LOCTEXT_NAMESPACE "GameplayTagWidget" -void SClassFilter::Construct(const FArguments& InArgs, const TArray& EditableFilters) +void SClassFilter::Construct( + const FArguments& InArgs, const TArray& EditableFilters) { bNeedsRefresh = true; @@ -74,76 +77,53 @@ void SClassFilter::Construct(const FArguments& InArgs, const TArray) - .TreeItemsSource(&RootClasses) - .OnGenerateRow(this, &SClassFilter::OnGenerateRow) - .OnGetChildren(this, &SClassFilter::OnGetChildren) - .OnExpansionChanged( this, &SClassFilter::OnExpansionChanged) - .SelectionMode(ESelectionMode::Multi) - ] - ] - ] - ]; + [SNew(SBorder).BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [SNew(SVerticalBox) + + // Gameplay Tag Tree controls + + + SVerticalBox::Slot().AutoHeight().VAlign(VAlign_Top) + [SNew(SHorizontalBox) + + // Search + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(1.f) + .Padding(5, 1, 5, + 1)[SAssignNew(SearchBox, SSearchBox) + .HintText(LOCTEXT("ClassFilter_SearchBoxHint", "Search Classes")) + .OnTextChanged(this, &SClassFilter::OnSearchTextChanged)] + + // Expand All nodes + + SHorizontalBox::Slot() + .AutoWidth()[SNew(SButton) + .OnClicked(this, &SClassFilter::OnClickedExpandAll) + .Text(LOCTEXT("ClassFilter_ExpandAll", "Expand All"))] + + // Collapse All nodes + + SHorizontalBox::Slot() + .AutoWidth()[SNew(SButton) + .OnClicked(this, &SClassFilter::OnClickedCollapseAll) + .Text(LOCTEXT("ClassFilter_CollapseAll", "Collapse All"))] + + // Clear selections + + SHorizontalBox::Slot() + .AutoWidth()[SNew(SButton) + .OnClicked(this, &SClassFilter::OnClickedClearAll) + .Text(LOCTEXT("ClassFilter_ClearAll", "Clear All")) + .Visibility(this, + &SClassFilter::DetermineClearSelectionVisibility)]] + + // Classes tree + + SVerticalBox::Slot().MaxHeight(MaxHeight) + [SAssignNew(TreeContainerWidget, SBorder) + .Padding(FMargin( + 4.f))[SAssignNew(TreeWidget, STreeView) + .TreeItemsSource(&RootClasses) + .OnGenerateRow(this, &SClassFilter::OnGenerateRow) + .OnGetChildren(this, &SClassFilter::OnGetChildren) + .OnExpansionChanged(this, &SClassFilter::OnExpansionChanged) + .SelectionMode(ESelectionMode::Multi)]]]]; // Construct the class hierarchy. ClassFilter::Helpers::ConstructClassHierarchy(); @@ -155,7 +135,7 @@ void SClassFilter::Construct(const FArguments& InArgs, const TArrayGetDesiredSize(); @@ -168,7 +148,7 @@ FVector2D SClassFilter::ComputeDesiredSize(float LayoutScaleMultiplier) const return WidgetSize; } -void SClassFilter::OnSearchTextChanged( const FText& SearchText ) +void SClassFilter::OnSearchTextChanged(const FText& SearchText) { SearchString = SearchText.ToString(); @@ -230,53 +210,39 @@ bool SClassFilter::FilterChildrenCheck(const FSEClassFilterNodePtr& Class) return false; } -TSharedRef SClassFilter::OnGenerateRow(FSEClassFilterNodePtr Class, const TSharedRef& OwnerTable) +TSharedRef SClassFilter::OnGenerateRow( + FSEClassFilterNodePtr Class, const TSharedRef& OwnerTable) { return SNew(STableRow, OwnerTable) - .Style(FAppStyle::Get(), "GameplayTagTreeView") - [ - SNew(SBorder) - .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) - .BorderBackgroundColor(this, &SClassFilter::GetClassBackgroundColor, Class) - .Padding(0) - .Content() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Left) - [ - SNew(SButton) - .ButtonStyle(FAppStyle::Get(), "FlatButton") - .OnClicked(this, &SClassFilter::OnClassClicked, Class) - .ForegroundColor(this, &SClassFilter::GetClassIconColor, Class) - .ContentPadding(0) - .IsEnabled(this, &SClassFilter::CanSelectClasses) - [ - SNew(STextBlock) - .Font(FAppStyle::Get().GetFontStyle("FontAwesome.8")) - .Text(this, &SClassFilter::GetClassIconText, Class) - ] - ] - // Tag Selection (selection mode only) - +SHorizontalBox::Slot() - .FillWidth(1.0f) - .HAlign(HAlign_Left) - [ - SNew(STextBlock) - .Text(FText::FromString(Class->GetClassName(true))) - .ToolTipText(Class->GetClassTooltip()) - .IsEnabled(this, &SClassFilter::CanSelectClasses) - ] - ] - ]; + .Style(FAppStyle::Get(), "GameplayTagTreeView") + [SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderBackgroundColor(this, &SClassFilter::GetClassBackgroundColor, Class) + .Padding(0) + .Content()[SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Left) + [SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "FlatButton") + .OnClicked(this, &SClassFilter::OnClassClicked, Class) + .ForegroundColor(this, &SClassFilter::GetClassIconColor, Class) + .ContentPadding(0) + .IsEnabled(this, &SClassFilter::CanSelectClasses) + [SNew(STextBlock) + .Font(FAppStyle::Get().GetFontStyle("FontAwesome.8")) + .Text(this, &SClassFilter::GetClassIconText, Class)]] + // Tag Selection (selection mode only) + + SHorizontalBox::Slot().FillWidth(1.0f).HAlign( + HAlign_Left)[SNew(STextBlock) + .Text(FText::FromString(Class->GetClassName(true))) + .ToolTipText(Class->GetClassTooltip()) + .IsEnabled(this, &SClassFilter::CanSelectClasses)]]]; } void SClassFilter::OnGetChildren(FSEClassFilterNodePtr Class, TArray& OutChildren) { - for(auto& ChildClass : Class->GetChildrenList()) + for (auto& ChildClass : Class->GetChildrenList()) { - if(FilterChildrenCheck(ChildClass)) + if (FilterChildrenCheck(ChildClass)) { OutChildren.Add(ChildClass); } @@ -309,11 +275,11 @@ FText SClassFilter::GetClassIconText(FSEClassFilterNodePtr Class) const { switch (Class->GetOwnFilterState()) { - case EClassFilterState::Allowed: - return FText::FromString(FString(TEXT("\xf00c"))) /*fa-check*/; + case EClassFilterState::Allowed: + return FText::FromString(FString(TEXT("\xf00c"))) /*fa-check*/; - case EClassFilterState::Denied: - return FText::FromString(FString(TEXT("\xf00d"))) /*fa-times*/; + case EClassFilterState::Denied: + return FText::FromString(FString(TEXT("\xf00d"))) /*fa-times*/; } return FText::FromString(FString(TEXT("\xf096"))) /*fa-square-o*/; @@ -323,11 +289,11 @@ FSlateColor SClassFilter::GetClassIconColor(FSEClassFilterNodePtr Class) const { switch (Class->GetOwnFilterState()) { - case EClassFilterState::Allowed: - return { FLinearColor::Green }; + case EClassFilterState::Allowed: + return {FLinearColor::Green}; - case EClassFilterState::Denied: - return { FLinearColor::Red }; + case EClassFilterState::Denied: + return {FLinearColor::Red}; } return FSlateColor::UseForeground(); @@ -335,69 +301,71 @@ FSlateColor SClassFilter::GetClassIconColor(FSEClassFilterNodePtr Class) const void SClassFilter::MarkClass(FSEClassFilterNodePtr Class, EClassFilterState State) { - const TSoftClassPtr<> ClassAsset{ Class->ClassPath.ToString() }; + const TSoftClassPtr<> ClassAsset{Class->ClassPath.ToString()}; switch (State) { - case EClassFilterState::Allowed: - { - FScopedTransaction Transaction(LOCTEXT("ClassFilter_AllowClass", "Allow Class")); - if (Class) - { - Class->SetOwnFilterState(State); - - if(PropertyHandle) PropertyHandle->NotifyPreChange(); - for (const auto& Filter : Filters) + case EClassFilterState::Allowed: { + FScopedTransaction Transaction(LOCTEXT("ClassFilter_AllowClass", "Allow Class")); + if (Class) { - Filter.Filter->AllowedClasses.Add(ClassAsset); - Filter.Filter->IgnoredClasses.Remove(ClassAsset); + Class->SetOwnFilterState(State); + + if (PropertyHandle) + PropertyHandle->NotifyPreChange(); + for (const auto& Filter : Filters) + { + Filter.Filter->AllowedClasses.Add(ClassAsset); + Filter.Filter->IgnoredClasses.Remove(ClassAsset); + } + if (PropertyHandle) + PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); } - if (PropertyHandle) PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); + break; } - break; - } - case EClassFilterState::Denied: - { - FScopedTransaction Transaction(LOCTEXT("ClassFilter_DeniedClass", "Deny Class")); - if (Class) - { - Class->SetOwnFilterState(State); - - if (PropertyHandle) PropertyHandle->NotifyPreChange(); - for (const auto& Filter : Filters) + case EClassFilterState::Denied: { + FScopedTransaction Transaction(LOCTEXT("ClassFilter_DeniedClass", "Deny Class")); + if (Class) { - Filter.Filter->IgnoredClasses.Add(ClassAsset); - Filter.Filter->AllowedClasses.Remove(ClassAsset); + Class->SetOwnFilterState(State); + + if (PropertyHandle) + PropertyHandle->NotifyPreChange(); + for (const auto& Filter : Filters) + { + Filter.Filter->IgnoredClasses.Add(ClassAsset); + Filter.Filter->AllowedClasses.Remove(ClassAsset); + } + if (PropertyHandle) + PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); } - if (PropertyHandle) - PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); + break; } - break; - } - case EClassFilterState::None: - { - FScopedTransaction Transaction(LOCTEXT("ClassFilter_UnmarkClass", "Unmark Class")); - if (Class) - { - Class->SetOwnFilterState(State); - - if (PropertyHandle) PropertyHandle->NotifyPreChange(); - for (const auto& Filter : Filters) + case EClassFilterState::None: { + FScopedTransaction Transaction(LOCTEXT("ClassFilter_UnmarkClass", "Unmark Class")); + if (Class) { - Filter.Filter->IgnoredClasses.Remove(ClassAsset); - Filter.Filter->AllowedClasses.Remove(ClassAsset); + Class->SetOwnFilterState(State); + + if (PropertyHandle) + PropertyHandle->NotifyPreChange(); + for (const auto& Filter : Filters) + { + Filter.Filter->IgnoredClasses.Remove(ClassAsset); + Filter.Filter->AllowedClasses.Remove(ClassAsset); + } + if (PropertyHandle) + PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); } - if (PropertyHandle) - PropertyHandle->NotifyPostChange(EPropertyChangeType::Unspecified); + break; } - break; - }} + } OnFilterChanged.ExecuteIfBound(); } FReply SClassFilter::OnClickedClearAll() { - FScopedTransaction Transaction( LOCTEXT("GameplayTagWidget_RemoveAllTags", "Remove All Gameplay Tags") ); + FScopedTransaction Transaction(LOCTEXT("GameplayTagWidget_RemoveAllTags", "Remove All Gameplay Tags")); for (int32 ContainerIdx = 0; ContainerIdx < Filters.Num(); ++ContainerIdx) { @@ -426,7 +394,7 @@ FSlateColor SClassFilter::GetClassBackgroundColor(FSEClassFilterNodePtr Class) c { Color = FLinearColor::Red; } - else if(ParentState == EClassFilterState::Allowed) + else if (ParentState == EClassFilterState::Allowed) { Color = FLinearColor::Green; } @@ -436,10 +404,10 @@ FSlateColor SClassFilter::GetClassBackgroundColor(FSEClassFilterNodePtr Class) c } else { - return { FLinearColor::Transparent }; + return {FLinearColor::Transparent}; } Color.A = 0.3f; - return { Color }; + return {Color}; } FReply SClassFilter::OnClickedExpandAll() @@ -499,19 +467,20 @@ void SClassFilter::SetFilter(FSEClassFilter* OriginalFilter, FSEClassFilter* Edi if (PropertyHandle.IsValid() && bMultiSelect) { // Case for a tag container - //PropertyHandle->SetValueFromFormattedString(EditedFilter->ToString()); + // PropertyHandle->SetValueFromFormattedString(EditedFilter->ToString()); } else if (PropertyHandle.IsValid() && !bMultiSelect) { // Case for a single Tag - //FString FormattedString = TEXT("(TagName=\""); - //FormattedString += EditedFilter.First().GetTagName().ToString(); - //FormattedString += TEXT("\")"); - //PropertyHandle->SetValueFromFormattedString(FormattedString); + // FString FormattedString = TEXT("(TagName=\""); + // FormattedString += EditedFilter.First().GetTagName().ToString(); + // FormattedString += TEXT("\")"); + // PropertyHandle->SetValueFromFormattedString(FormattedString); } else { - // Not sure if we should get here, means the property handle hasn't been setup which could be right or wrong. + // Not sure if we should get here, means the property handle hasn't been setup which could be right or + // wrong. if (OwnerObj) { OwnerObj->PreEditChange(PropertyHandle.IsValid() ? PropertyHandle->GetProperty() : nullptr); @@ -536,7 +505,8 @@ void SClassFilter::Refresh() bNeedsRefresh = true; } -void SClassFilter::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +void SClassFilter::Tick( + const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Will populate the class hierarchy if needed ClassFilter::Helpers::PopulateClassHierarchy(); @@ -570,7 +540,8 @@ void SClassFilter::Populate() TArray InternalClasses; TArray InternalPaths; - // We aren't showing the internal classes, then we need to know what classes to consider Internal Only, so let's gather them up from the settings object. + // We aren't showing the internal classes, then we need to know what classes to consider Internal Only, so + // let's gather them up from the settings object. GetInternalOnlyPaths(InternalPaths); GetInternalOnlyClasses(InternalClassNames); @@ -579,8 +550,7 @@ void SClassFilter::Populate() { FString PackageClassName = InternalClassNames[i].ToString(); const FSEClassFilterNodePtr ClassNode = ClassFilter::Helpers::ClassHierarchy->FindNodeByClassName( - ClassFilter::Helpers::ClassHierarchy->GetObjectRootNode(), PackageClassName - ); + ClassFilter::Helpers::ClassHierarchy->GetObjectRootNode(), PackageClassName); if (ClassNode.IsValid()) { @@ -593,7 +563,8 @@ void SClassFilter::Populate() FSEClassFilterNodePtr RootNode; // Get the class tree, passing in certain filter options. - ClassFilter::Helpers::GetClassTree(*Filters[0].Filter, RootNode, false, true, false, InternalClasses, InternalPaths); + ClassFilter::Helpers::GetClassTree( + *Filters[0].Filter, RootNode, false, true, false, InternalClasses, InternalPaths); // Add all the children of the "Object" root. for (const auto& Child : RootNode->GetChildrenList()) @@ -620,10 +591,7 @@ EVisibility SClassFilter::DetermineClearSelectionVisibility() const return CanSelectClasses() ? EVisibility::Visible : EVisibility::Collapsed; } -void SClassFilter::OnExpansionChanged(FSEClassFilterNodePtr Class, bool bIsExpanded) -{ - -} +void SClassFilter::OnExpansionChanged(FSEClassFilterNodePtr Class, bool bIsExpanded) {} bool SClassFilter::CanSelectClasses() const { diff --git a/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.h b/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.h index bd2aca7..4235d97 100644 --- a/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.h +++ b/Source/Editor/Private/Customizations/ClassFilter/SClassFilter.h @@ -1,21 +1,22 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "ClassFilterNode.h" #include "CoreMinimal.h" -#include +#include "Input/Reply.h" +#include "Layout/Visibility.h" +#include "ClassFilter.h" #include "SlateFwd.h" #include "UObject/Object.h" -#include "Layout/Visibility.h" #include "Widgets/DeclarativeSyntaxSupport.h" -#include "Input/Reply.h" -#include "Widgets/SWidget.h" #include "Widgets/SCompoundWidget.h" -#include "Widgets/Views/STableViewBase.h" +#include "Widgets/SWidget.h" #include "Widgets/Views/STableRow.h" +#include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STreeView.h" -#include "Misc/ClassFilter.h" -#include "ClassFilterNode.h" +#include + class IPropertyHandle; @@ -23,7 +24,6 @@ class IPropertyHandle; class SClassFilter : public SCompoundWidget { public: - /** Called when the filter is changed */ DECLARE_DELEGATE(FOnFilterChanged) @@ -33,15 +33,15 @@ class SClassFilter : public SCompoundWidget , _PropertyHandle(nullptr) , _MaxHeight(260.0f) {} - SLATE_ARGUMENT(bool, ReadOnly) // Flag to set if the list is read only - SLATE_ARGUMENT(bool, MultiSelect) // If we can select multiple entries - SLATE_ARGUMENT(TSharedPtr, PropertyHandle) - SLATE_EVENT(FOnFilterChanged, OnFilterChanged) // Called when a tag status changes - SLATE_ARGUMENT(float, MaxHeight) // caps the height of the gameplay tag tree - SLATE_END_ARGS() - - /** Simple struct holding a tag container and its owner for generic re-use of the widget */ - struct FEditableClassFilterDatum + SLATE_ARGUMENT(bool, ReadOnly) // Flag to set if the list is read only + SLATE_ARGUMENT(bool, MultiSelect) // If we can select multiple entries + SLATE_ARGUMENT(TSharedPtr, PropertyHandle) + SLATE_EVENT(FOnFilterChanged, OnFilterChanged) // Called when a tag status changes + SLATE_ARGUMENT(float, MaxHeight) // caps the height of the gameplay tag tree + SLATE_END_ARGS() + + /** Simple struct holding a tag container and its owner for generic re-use of the widget */ + struct FEditableClassFilterDatum { /** Constructor */ FEditableClassFilterDatum(class UObject* InOwner, struct FSEClassFilter* InFilter) @@ -59,7 +59,8 @@ class SClassFilter : public SCompoundWidget /** Construct the actual widget */ void Construct(const FArguments& InArgs, const TArray& EditableFilters); - virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + virtual void Tick( + const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; /** Ensures that this widget will always account for the MaxHeight if it's specified */ virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; @@ -75,7 +76,6 @@ class SClassFilter : public SCompoundWidget private: - FString SearchString; bool bReadOnly; @@ -86,7 +86,8 @@ class SClassFilter : public SCompoundWidget /** The maximum height of the gameplay tag tree. If 0, the height is unbound. */ float MaxHeight; - /** True if the Class Filter needs to be repopulated at the next appropriate opportunity, occurs whenever classes are added, removed, renamed, etc. */ + /** True if the Class Filter needs to be repopulated at the next appropriate opportunity, occurs whenever + * classes are added, removed, renamed, etc. */ bool bNeedsRefresh; /* Array of tags to be displayed in the TreeView */ @@ -121,7 +122,8 @@ class SClassFilter : public SCompoundWidget * * @return Generated row widget for the item node */ - TSharedRef OnGenerateRow(FSEClassFilterNodePtr Class, const TSharedRef& OwnerTable); + TSharedRef OnGenerateRow( + FSEClassFilterNodePtr Class, const TSharedRef& OwnerTable); /** * Get children nodes of the specified node diff --git a/Source/Editor/Private/Customizations/SClassFilterGraphPin.cpp b/Source/Editor/Private/Customizations/SClassFilterGraphPin.cpp index 347bffe..930e59a 100644 --- a/Source/Editor/Private/Customizations/SClassFilterGraphPin.cpp +++ b/Source/Editor/Private/Customizations/SClassFilterGraphPin.cpp @@ -1,40 +1,34 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SClassFilterGraphPin.h" -#include "Widgets/Input/SComboButton.h" + #include "GameplayTagsModule.h" +#include "Widgets/Input/SComboButton.h" #include "Widgets/Layout/SScaleBox.h" + #define LOCTEXT_NAMESPACE "GameplayTagGraphPin" void SClassFilterGraphPin::Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj) { - SGraphPin::Construct( SGraphPin::FArguments(), InGraphPinObj ); + SGraphPin::Construct(SGraphPin::FArguments(), InGraphPinObj); } -TSharedRef SClassFilterGraphPin::GetDefaultValueWidget() +TSharedRef SClassFilterGraphPin::GetDefaultValueWidget() { ParseDefaultValueData(); - //Create widget - return SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Fill) - [ - SAssignNew( ComboButton, SComboButton ) - .OnGetMenuContent(this, &SClassFilterGraphPin::GetListContent) - .ButtonStyle(FAppStyle::Get(), "FlatButton") - .ForegroundColor(FSlateColor::UseForeground()) - .ContentPadding(FMargin(0.0f, 2.0f)) - .MenuPlacement(MenuPlacement_BelowAnchor) - .Visibility( this, &SGraphPin::GetDefaultValueVisibility ) - ] - +SHorizontalBox::Slot() - .AutoWidth() - [ - SelectedTags() - ]; + // Create widget + return SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign( + VAlign_Fill)[SAssignNew(ComboButton, SComboButton) + .OnGetMenuContent(this, &SClassFilterGraphPin::GetListContent) + .ButtonStyle(FAppStyle::Get(), "FlatButton") + .ForegroundColor(FSlateColor::UseForeground()) + .ContentPadding(FMargin(0.0f, 2.0f)) + .MenuPlacement(MenuPlacement_BelowAnchor) + .Visibility(this, &SGraphPin::GetDefaultValueVisibility)] + + SHorizontalBox::Slot().AutoWidth()[SelectedTags()]; } void SClassFilterGraphPin::ParseDefaultValueData() @@ -45,24 +39,19 @@ void SClassFilterGraphPin::ParseDefaultValueData() TSharedRef SClassFilterGraphPin::GetListContent() { EditableFilters.Empty(); - EditableFilters.Add( SClassFilter::FEditableClassFilterDatum( GraphPinObj->GetOwningNode(), &Filter ) ); - - return SNew( SVerticalBox ) - +SVerticalBox::Slot() - .AutoHeight() - .MaxHeight( 400 ) - [ - SNew( SClassFilter, EditableFilters ) - .OnFilterChanged(this, &SClassFilterGraphPin::RefreshPreviewList) - .Visibility( this, &SGraphPin::GetDefaultValueVisibility ) - ]; + EditableFilters.Add(SClassFilter::FEditableClassFilterDatum(GraphPinObj->GetOwningNode(), &Filter)); + + return SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().MaxHeight( + 400)[SNew(SClassFilter, EditableFilters) + .OnFilterChanged(this, &SClassFilterGraphPin::RefreshPreviewList) + .Visibility(this, &SGraphPin::GetDefaultValueVisibility)]; } TSharedRef SClassFilterGraphPin::SelectedTags() { RefreshPreviewList(); - SAssignNew( PreviewList, SListView> ) + SAssignNew(PreviewList, SListView>) .ListItemsSource(&PreviewClasses) .SelectionMode(ESelectionMode::None) .OnGenerateRow(this, &SClassFilterGraphPin::OnGeneratePreviewRow); @@ -70,7 +59,8 @@ TSharedRef SClassFilterGraphPin::SelectedTags() return PreviewList->AsShared(); } -TSharedRef SClassFilterGraphPin::OnGeneratePreviewRow(TSharedPtr Class, const TSharedRef& OwnerTable) +TSharedRef SClassFilterGraphPin::OnGeneratePreviewRow( + TSharedPtr Class, const TSharedRef& OwnerTable) { FLinearColor StateColor; FText StateText; @@ -85,26 +75,17 @@ TSharedRef SClassFilterGraphPin::OnGeneratePreviewRow(TSharedPtr>, OwnerTable) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - .Padding(0, 0, 2, 0) - [ - SNew(STextBlock) - .ColorAndOpacity(StateColor) - .Font(FAppStyle::Get().GetFontStyle("FontAwesome.8")) - .Text(StateText) - ] - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock).Text(FText::FromString(Class->ClassName)) - ] - ]; + return SNew(STableRow>, + OwnerTable)[SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(0, 0, 2, 0)[SNew(STextBlock) + .ColorAndOpacity(StateColor) + .Font(FAppStyle::Get().GetFontStyle("FontAwesome.8")) + .Text(StateText)] + + SHorizontalBox::Slot().AutoWidth().VAlign( + VAlign_Center)[SNew(STextBlock).Text(FText::FromString(Class->ClassName))]]; } void SClassFilterGraphPin::RefreshPreviewList() diff --git a/Source/Editor/Private/Customizations/SClassFilterGraphPin.h b/Source/Editor/Private/Customizations/SClassFilterGraphPin.h index 170532a..3d95914 100644 --- a/Source/Editor/Private/Customizations/SClassFilterGraphPin.h +++ b/Source/Editor/Private/Customizations/SClassFilterGraphPin.h @@ -1,14 +1,16 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "ClassFilter/SClassFilter.h" +#include "SEClassFilterCustomization.h" +#include "SGraphPin.h" + #include #include #include -#include #include -#include "ClassFilter/SClassFilter.h" -#include "SGraphPin.h" -#include "SEClassFilterCustomization.h" +#include + class SComboButton; @@ -22,11 +24,10 @@ class SClassFilterGraphPin : public SGraphPin void Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj); //~ Begin SGraphPin Interface - virtual TSharedRef GetDefaultValueWidget() override; + virtual TSharedRef GetDefaultValueWidget() override; //~ End SGraphPin Interface private: - /** Refreshes the list of tags displayed on the node. */ void RefreshPreviewList(); @@ -46,10 +47,10 @@ class SClassFilterGraphPin : public SGraphPin * Callback for populating rows of the SelectedTags List View. * @return widget that contains the name of a tag. */ - TSharedRef OnGeneratePreviewRow(TSharedPtr Class, const TSharedRef& OwnerTable); + TSharedRef OnGeneratePreviewRow( + TSharedPtr Class, const TSharedRef& OwnerTable); private: - // Combo Button for the drop down list. TSharedPtr ComboButton; diff --git a/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.cpp b/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.cpp deleted file mode 100644 index fdcafed..0000000 --- a/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Customizations/SEActorClassFilterCustomization.h" - -#include -#include - -#define LOCTEXT_NAMESPACE "FSEActorClassFilterCustomization" - - -TSharedPtr FSEActorClassFilterCustomization::GetFilterHandle(TSharedRef StructPropertyHandle) -{ - return StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FSEActorClassFilter, ClassFilter));; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.h b/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.h deleted file mode 100644 index 069cb9f..0000000 --- a/Source/Editor/Private/Customizations/SEActorClassFilterCustomization.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. -#pragma once - -#include "SEClassFilterCustomization.h" - - -class IPropertyHandle; - -class FSEActorClassFilterCustomization : public FSEClassFilterCustomization -{ -public: - - /** - * Creates a new instance. - * - * @return A new struct customization for Factions. - */ - static TSharedRef MakeInstance() - { - return MakeShared(); - } - -protected: - - virtual TSharedPtr GetFilterHandle(TSharedRef StructPropertyHandle) override; -}; - diff --git a/Source/Editor/Private/Customizations/SEClassFilterCustomization.cpp b/Source/Editor/Private/Customizations/SEClassFilterCustomization.cpp index 43a275b..b9d0980 100644 --- a/Source/Editor/Private/Customizations/SEClassFilterCustomization.cpp +++ b/Source/Editor/Private/Customizations/SEClassFilterCustomization.cpp @@ -1,15 +1,18 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "Customizations/SEClassFilterCustomization.h" #include -#include -#include -#include #include +#include +#include +#include #include +#include +#include #include + #define LOCTEXT_NAMESPACE "FSEClassFilterCustomization" @@ -18,13 +21,14 @@ FSEClassFilterCustomization::~FSEClassFilterCustomization() GEditor->UnregisterForUndo(this); } -void FSEClassFilterCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSEClassFilterCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, + FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { StructHandle = StructPropertyHandle; - FilterHandle = GetFilterHandle(StructPropertyHandle); BuildEditableFilterList(); + // clang-format off HeaderRow .NameContent() [ @@ -59,65 +63,54 @@ void FSEClassFilterCustomization::CustomizeHeader(TSharedRefRegisterForUndo(this); } -void FSEClassFilterCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +void FSEClassFilterCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, + class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) {} void FSEClassFilterCustomization::BuildEditableFilterList() { EditableFilters.Empty(); - if (FilterHandle.IsValid()) + if (StructHandle.IsValid()) { TArray RawStructData; - FilterHandle->AccessRawData(RawStructData); + StructHandle->AccessRawData(RawStructData); TArray Outers; - FilterHandle->GetOuterObjects(Outers); + StructHandle->GetOuterObjects(Outers); UObject* FirstOuter = Outers.Num() ? Outers[0] : nullptr; for (int32 ContainerIdx = 0; ContainerIdx < RawStructData.Num(); ++ContainerIdx) { - EditableFilters.Add(SClassFilter::FEditableClassFilterDatum(FirstOuter, (FSEClassFilter*)RawStructData[ContainerIdx])); + EditableFilters.Add(SClassFilter::FEditableClassFilterDatum( + FirstOuter, (FSEClassFilter*) RawStructData[ContainerIdx])); } } } TSharedRef FSEClassFilterCustomization::GetListContent() { - if (!FilterHandle.IsValid() || FilterHandle->GetProperty() == nullptr) + if (!StructHandle.IsValid() || StructHandle->GetProperty() == nullptr) { return SNullWidget::NullWidget; } - bool bReadOnly = FilterHandle->IsEditConst(); - - TSharedRef EditPopup = SNew(SClassFilter, EditableFilters) - .ReadOnly(bReadOnly) - .OnFilterChanged(this, &FSEClassFilterCustomization::RefreshClassList) - .PropertyHandle(FilterHandle); + bool bReadOnly = StructHandle->IsEditConst(); + // clang-format off + TSharedRef EditPopup = + SNew(SClassFilter, EditableFilters) + .ReadOnly(bReadOnly) + .OnFilterChanged(this, &FSEClassFilterCustomization::RefreshClassList) + .PropertyHandle(StructHandle); LastFilterPopup = EditPopup; - return SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() @@ -125,6 +118,7 @@ TSharedRef FSEClassFilterCustomization::GetListContent() [ EditPopup ]; + // clang-format on } void FSEClassFilterCustomization::OnPopupStateChanged(bool bIsOpened) @@ -139,22 +133,16 @@ void FSEClassFilterCustomization::OnPopupStateChanged(bool bIsOpened) } } -FReply FSEClassFilterCustomization::OnClearClicked() +void FSEClassFilterCustomization::OnClearClicked() { FScopedTransaction Transaction(LOCTEXT("ClassFilter_Filter", "Clear Filter")); + StructHandle->NotifyPreChange(); for (auto& Filter : EditableFilters) { - // Reset Filter - if (Filter.Owner.IsValid()) - { - Filter.Owner->Modify(); - } - *Filter.Filter = {}; } - + StructHandle->NotifyPostChange(EPropertyChangeType::ValueSet); RefreshClassList(); - return FReply::Handled(); } EVisibility FSEClassFilterCustomization::GetClassPreviewVisibility() const @@ -170,6 +158,7 @@ TSharedRef FSEClassFilterCustomization::GetClassPreview() { RefreshClassList(); + // clang-format off return SNew(SBox) .MinDesiredWidth(100.f) [ @@ -178,9 +167,11 @@ TSharedRef FSEClassFilterCustomization::GetClassPreview() .ListItemsSource(&PreviewClasses) .OnGenerateRow(this, &FSEClassFilterCustomization::OnGeneratePreviewRow) ]; + // clang-format on } -TSharedRef FSEClassFilterCustomization::OnGeneratePreviewRow(TSharedPtr Class, const TSharedRef& OwnerTable) +TSharedRef FSEClassFilterCustomization::OnGeneratePreviewRow( + TSharedPtr Class, const TSharedRef& OwnerTable) { FLinearColor StateColor; FText StateText; @@ -195,6 +186,7 @@ TSharedRef FSEClassFilterCustomization::OnGeneratePreviewRow(TSharedP StateText = FText::FromString(FString(TEXT("\xf00d"))) /*fa-times*/; } + // clang-format off return SNew(STableRow>, OwnerTable) [ SNew(SHorizontalBox) @@ -215,6 +207,7 @@ TSharedRef FSEClassFilterCustomization::OnGeneratePreviewRow(TSharedP SNew(STextBlock).Text(FText::FromString(Class->ClassName)) ] ]; + // clang-format on } void FSEClassFilterCustomization::PostUndo(bool bSuccess) diff --git a/Source/Editor/Private/Customizations/SEClassFilterCustomization.h b/Source/Editor/Private/Customizations/SEClassFilterCustomization.h index 0a8bfda..fb9f20d 100644 --- a/Source/Editor/Private/Customizations/SEClassFilterCustomization.h +++ b/Source/Editor/Private/Customizations/SEClassFilterCustomization.h @@ -1,33 +1,31 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include -#include #include "ClassFilter/SClassFilter.h" +#include +#include + class IPropertyHandle; -struct FSEClassFilterItem { +struct FSEClassFilterItem +{ FString ClassName; bool bAllowed; - FSEClassFilterItem(FString ClassName, bool bAllowed) - : ClassName{ClassName}, bAllowed{bAllowed} - {} + FSEClassFilterItem(FString ClassName, bool bAllowed) : ClassName{ClassName}, bAllowed{bAllowed} {} }; class FSEClassFilterCustomization : public IPropertyTypeCustomization, public FEditorUndoClient { protected: - /** Filters this customization edits */ TArray EditableFilters; TArray> PreviewClasses; TSharedPtr StructHandle; - TSharedPtr FilterHandle; TSharedPtr EditButton; @@ -36,12 +34,11 @@ class FSEClassFilterCustomization : public IPropertyTypeCustomization, public FE TWeakPtr LastFilterPopup; public: - /** - * Creates a new instance. - * - * @return A new struct customization for Factions. - */ + * Creates a new instance. + * + * @return A new struct customization for Factions. + */ static TSharedRef MakeInstance() { return MakeShared(); @@ -50,21 +47,19 @@ class FSEClassFilterCustomization : public IPropertyTypeCustomization, public FE ~FSEClassFilterCustomization(); protected: - /** IPropertyTypeCustomization interface */ - virtual void CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - virtual void CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeHeader(TSharedRef StructPropertyHandle, + class FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef StructPropertyHandle, + class IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; //~ Begin FEditorUndoClient Interface virtual void PostUndo(bool bSuccess) override; virtual void PostRedo(bool bSuccess) override; //~ End FEditorUndoClient Interface - virtual TSharedPtr GetFilterHandle(TSharedRef StructPropertyHandle) - { - return StructHandle; - } - /** Build List of Editable Containers */ void BuildEditableFilterList(); @@ -73,16 +68,16 @@ class FSEClassFilterCustomization : public IPropertyTypeCustomization, public FE void OnPopupStateChanged(bool bIsOpened); - FReply OnClearClicked(); + void OnClearClicked(); EVisibility GetClassPreviewVisibility() const; TSharedRef GetClassPreview(); - TSharedRef OnGeneratePreviewRow(TSharedPtr Class, const TSharedRef& OwnerTable); + TSharedRef OnGeneratePreviewRow( + TSharedPtr Class, const TSharedRef& OwnerTable); void RefreshClassList(); }; - diff --git a/Source/Editor/Private/Customizations/SEClassFilterGraphPanelPinFactory.h b/Source/Editor/Private/Customizations/SEClassFilterGraphPanelPinFactory.h index 91f3a1d..447c6fd 100644 --- a/Source/Editor/Private/Customizations/SEClassFilterGraphPanelPinFactory.h +++ b/Source/Editor/Private/Customizations/SEClassFilterGraphPanelPinFactory.h @@ -1,17 +1,18 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once #include "CoreMinimal.h" -#include "Widgets/DeclarativeSyntaxSupport.h" +#include "EdGraphSchema_K2.h" #include "EdGraphUtilities.h" #include "GameplayTagContainer.h" -#include "EdGraphSchema_K2.h" -#include "SGraphPin.h" +#include "ClassFilter.h" #include "SClassFilterGraphPin.h" -#include "Misc/ClassFilter.h" +#include "SGraphPin.h" +#include "Widgets/DeclarativeSyntaxSupport.h" + -class FSEClassFilterGraphPanelPinFactory: public FGraphPanelPinFactory +class FSEClassFilterGraphPanelPinFactory : public FGraphPanelPinFactory { virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override { diff --git a/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.cpp b/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.cpp deleted file mode 100644 index 58c4e5a..0000000 --- a/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Customizations/SEComponentClassFilterCustomization.h" -#include "PropertyHandle.h" - -#define LOCTEXT_NAMESPACE "FSEComponentClassFilterCustomization" - - -TSharedPtr FSEComponentClassFilterCustomization::GetFilterHandle(TSharedRef StructPropertyHandle) -{ - return StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FSEComponentClassFilter, ClassFilter));; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.h b/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.h deleted file mode 100644 index a3324fa..0000000 --- a/Source/Editor/Private/Customizations/SEComponentClassFilterCustomization.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. -#pragma once - -#include "SEClassFilterCustomization.h" - - -class IPropertyHandle; - -class FSEComponentClassFilterCustomization : public FSEClassFilterCustomization -{ -public: - - /** - * Creates a new instance. - * - * @return A new struct customization for Factions. - */ - static TSharedRef MakeInstance() - { - return MakeShared(); - } - -protected: - - virtual TSharedPtr GetFilterHandle(TSharedRef StructPropertyHandle) override; -}; - diff --git a/Source/Editor/Private/Customizations/SavePresetDetails.cpp b/Source/Editor/Private/Customizations/SavePresetDetails.cpp deleted file mode 100644 index 163306f..0000000 --- a/Source/Editor/Private/Customizations/SavePresetDetails.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SavePresetDetails.h" - -#include "DetailLayoutBuilder.h" -#include "DetailCategoryBuilder.h" -#include "DetailWidgetRow.h" -#include -#include - -#include "SavePreset.h" - -#define LOCTEXT_NAMESPACE "FSavePresetDetails" - - -/************************************************************************ - * FSavePresetDetails - */ - -TSharedRef FSavePresetDetails::MakeInstance() -{ - return MakeShareable(new FSavePresetDetails); -} - -void FSavePresetDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) -{ - TArray> Objects; - DetailBuilder.GetObjectsBeingCustomized(Objects); - - if (Objects.Num() && Objects[0] != nullptr) - { - Settings = CastChecked(Objects[0].Get()); - - DetailBuilder.EditCategory(TEXT("Gameplay")); - DetailBuilder.EditCategory(TEXT("Serialization")); - - IDetailCategoryBuilder& Category = DetailBuilder.EditCategory(TEXT("Asynchronous")); - { - Category.AddProperty(TEXT("MultithreadedSerialization")); - Category.AddProperty(TEXT("FrameSplittedSerialization")); - Category.AddCustomRow(LOCTEXT("AsyncWarning", "Asynchronous Warning")) - .Visibility({ this, &FSavePresetDetails::GetWarningVisibility }) - .ValueContent() - .MinDesiredWidth(300.f) - .MaxDesiredWidth(400.f) - [ - SNew(SBorder) - .Padding(2.f) - .BorderImage(FAppStyle::GetBrush("ErrorReporting.EmptyBox")) - .BorderBackgroundColor(this, &FSavePresetDetails::GetWarningColor) - [ - SNew(STextBlock) - .Text(LOCTEXT("AsyncWarningText", "WARNING: Frame-splitted loading or saving is not recommended while using Level Streaming or World Composition")) - .AutoWrapText(true) - ] - ]; - - Category.AddProperty(TEXT("MaxFrameMs")).EditCondition({ this, &FSavePresetDetails::CanEditAsynchronous }, nullptr); - } - - DetailBuilder.EditCategory(TEXT("Level Streaming")); - } -} - -FSlateColor FSavePresetDetails::GetWarningColor() const -{ - return FLinearColor{ FColor{ 234, 220, 25, 128 } }; -} - -EVisibility FSavePresetDetails::GetWarningVisibility() const -{ - if (Settings.IsValid()) - { - return Settings->GetFrameSplitSerialization() == ESaveASyncMode::OnlySync ? EVisibility::Collapsed : EVisibility::Visible; - } - return EVisibility::Collapsed; -} - -bool FSavePresetDetails::CanEditAsynchronous() const -{ - if (Settings.IsValid()) - { - return Settings->GetFrameSplitSerialization() != ESaveASyncMode::OnlySync; - } - return true; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Customizations/SaveSlotDetails.cpp b/Source/Editor/Private/Customizations/SaveSlotDetails.cpp new file mode 100644 index 0000000..857e616 --- /dev/null +++ b/Source/Editor/Private/Customizations/SaveSlotDetails.cpp @@ -0,0 +1,85 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "SaveSlotDetails.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" + +#include +#include +#include + + +#define LOCTEXT_NAMESPACE "FSaveSlotDetails" + + +/************************************************************************ + * FSaveSlotDetails + */ + +TSharedRef FSaveSlotDetails::MakeInstance() +{ + return MakeShareable(new FSaveSlotDetails); +} + +void FSaveSlotDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + TArray> Objects; + DetailBuilder.GetObjectsBeingCustomized(Objects); + + TSharedRef MultithreadedSerialization = DetailBuilder.GetProperty(FName("MultithreadedSerialization"), USaveSlot::StaticClass()); + TSharedRef FrameSplittedSerialization = DetailBuilder.GetProperty(FName("FrameSplittedSerialization"), USaveSlot::StaticClass()); + TSharedRef MaxFrameMs = DetailBuilder.GetProperty(FName("MaxFrameMs"), USaveSlot::StaticClass()); + + if (Objects.Num() && Objects[0] != nullptr) + { + Slot = CastChecked(Objects[0].Get()); + + // Sort categories + DetailBuilder.EditCategory(TEXT("Slot")); + DetailBuilder.EditCategory(TEXT("Automatic")); + DetailBuilder.EditCategory(TEXT("Serialization")); + DetailBuilder.EditCategory(TEXT("Files")); + DetailBuilder.EditCategory(TEXT("Async")); + + DetailBuilder.AddPropertyToCategory(MultithreadedSerialization); + DetailBuilder.AddPropertyToCategory(FrameSplittedSerialization); + DetailBuilder.AddCustomRowToCategory(FrameSplittedSerialization, LOCTEXT("AsyncWarning", "Asynchronous Warning")) + .Visibility({this, &FSaveSlotDetails::GetWarningVisibility}) + .ValueContent() + .MinDesiredWidth(300.f) + .MaxDesiredWidth(400.f) + [ + SNew(SBorder) + .Padding(2.f) + .BorderImage(FAppStyle::GetBrush("ErrorReporting.EmptyBox")) + .BorderBackgroundColor(this, &FSaveSlotDetails::GetWarningColor) + [ + SNew(STextBlock) + .Text(LOCTEXT("AsyncWarningText", + "WARNING: Frame-splitted loading or saving is not recommended " + "while using Level Streaming or World Composition")) + .AutoWrapText(true) + ] + ]; + DetailBuilder.AddPropertyToCategory(MaxFrameMs); + } +} + +FSlateColor FSaveSlotDetails::GetWarningColor() const +{ + return FLinearColor{FColor{234, 220, 25, 128}}; +} + +EVisibility FSaveSlotDetails::GetWarningVisibility() const +{ + if (Slot.IsValid()) + { + return Slot->GetFrameSplitSerialization() == ESEAsyncMode::SaveAndLoadSync ? EVisibility::Collapsed + : EVisibility::Visible; + } + return EVisibility::Collapsed; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Editor/Private/Customizations/SavePresetDetails.h b/Source/Editor/Private/Customizations/SaveSlotDetails.h similarity index 77% rename from Source/Editor/Private/Customizations/SavePresetDetails.h rename to Source/Editor/Private/Customizations/SaveSlotDetails.h index 7bcc334..4677b5e 100644 --- a/Source/Editor/Private/Customizations/SavePresetDetails.h +++ b/Source/Editor/Private/Customizations/SaveSlotDetails.h @@ -1,37 +1,34 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once #include "CoreMinimal.h" +#include "EditorUndoClient.h" #include "IDetailCustomization.h" -#include "Layout/Visibility.h" -#include "Widgets/Views/SListView.h" -#include "Widgets/Input/SEditableTextBox.h" #include "Input/Reply.h" +#include "Layout/Visibility.h" #include "PropertyHandle.h" -#include "EditorUndoClient.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Views/SListView.h" + class IDetailsView; class IDetailLayoutBuilder; -class USavePreset; +class USaveSlot; -class FSavePresetDetails : public IDetailCustomization +class FSaveSlotDetails : public IDetailCustomization { public: - /** Makes a new instance of this detail layout class for a specific detail view requesting it */ static TSharedRef MakeInstance(); private: - /** IDetailCustomization interface */ virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; FSlateColor GetWarningColor() const; EVisibility GetWarningVisibility() const; - bool CanEditAsynchronous() const; - - TWeakObjectPtr Settings; + TWeakObjectPtr Slot; }; diff --git a/Source/Editor/Private/SaveActorEditorTabSummoner.cpp b/Source/Editor/Private/SaveActorEditorTabSummoner.cpp index 1051dee..7a64743 100644 --- a/Source/Editor/Private/SaveActorEditorTabSummoner.cpp +++ b/Source/Editor/Private/SaveActorEditorTabSummoner.cpp @@ -2,47 +2,39 @@ #include "SaveActorEditorTabSummoner.h" -#include -#include -#include -#include -#include #include -#include -#include +#include #include #include +#include +#include +#include +#include #include +#include #define LOCTEXT_NAMESPACE "SaveActorEditorSummoner" -const TArray SSaveActorEditorWidget::TagList -{ - { TEXT("Save Tags"), TEXT("!SaveTags"), true, LOCTEXT("tags_tooltip", "Should save tags?") }, - { TEXT("Save Transform"), TEXT("!SaveTransform"), true, LOCTEXT("transform_tooltip", "Should save position, rotation and scale?") }, - { TEXT("Save Physics"), TEXT("!SavePhysics"), true, LOCTEXT("physics_tooltip", "Should save physics?") } -}; +const TArray SSaveActorEditorWidget::TagList{ + {TEXT("Save Tags"), TEXT("!SaveTags"), true, LOCTEXT("tags_tooltip", "Should save tags?")}, + {TEXT("Save Transform"), TEXT("!SaveTransform"), true, + LOCTEXT("transform_tooltip", "Should save position, rotation and scale?")}, + {TEXT("Save Physics"), TEXT("!SavePhysics"), true, LOCTEXT("physics_tooltip", "Should save physics?")}}; void SSaveActorEditorWidget::Construct(const FArguments&, TWeakPtr InBlueprintEditor) { bRefreshingVisuals = false; - OnBlueprintPreCompileHandle = GEditor->OnBlueprintPreCompile().AddSP(this, &SSaveActorEditorWidget::OnBlueprintPreCompile); - OnObjectPreSaveHandle = FCoreUObjectDelegates::OnObjectPreSave.AddSP(this, &SSaveActorEditorWidget::OnObjectPreSave); + OnBlueprintPreCompileHandle = + GEditor->OnBlueprintPreCompile().AddSP(this, &SSaveActorEditorWidget::OnBlueprintPreCompile); + OnObjectPreSaveHandle = + FCoreUObjectDelegates::OnObjectPreSave.AddSP(this, &SSaveActorEditorWidget::OnObjectPreSave); WeakBlueprintEditor = InBlueprintEditor; - ChildSlot - .Padding(10) - [ - SNew(SVerticalBox) - .Visibility(this, &SSaveActorEditorWidget::GetContentVisibility) - + SVerticalBox::Slot() - .AutoHeight().Padding(2) - [ - GenerateSettingsWidget().ToSharedRef() - ] - ]; + ChildSlot.Padding( + 10)[SNew(SVerticalBox).Visibility(this, &SSaveActorEditorWidget::GetContentVisibility) + + SVerticalBox::Slot().AutoHeight().Padding(2)[GenerateSettingsWidget().ToSharedRef()]]; RefreshVisuals(); } @@ -116,40 +108,27 @@ AActor* SSaveActorEditorWidget::GetDefaultActor() const TSharedPtr SSaveActorEditorWidget::GenerateSettingsWidget() { // TODO: This can be moved into FTagInfo - SettingItems.Add(SNew(SSaveActorSettingsItem).TagInfo(TagList[0]).OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); - SettingItems.Add(SNew(SSaveActorSettingsItem).TagInfo(TagList[1]).OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); - SettingItems.Add(SNew(SSaveActorSettingsItem).TagInfo(TagList[2]).OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); + SettingItems.Add(SNew(SSaveActorSettingsItem) + .TagInfo(TagList[0]) + .OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); + SettingItems.Add(SNew(SSaveActorSettingsItem) + .TagInfo(TagList[1]) + .OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); + SettingItems.Add(SNew(SSaveActorSettingsItem) + .TagInfo(TagList[2]) + .OnValueChanged(this, &SSaveActorEditorWidget::OnSettingChanged)); AActor* Actor = GetDefaultActor(); if (Actor) { return SNew(SBorder) - .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryMiddle")) - .Padding(0) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - [ - SettingItems[0] - ] - + SVerticalBox::Slot() - .AutoHeight() - [ - SettingItems[1] - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(6) - [ - SNew(SBorder) - .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryMiddle")) - .Padding(0) - [ - SettingItems[2] - ] - ] - ]; + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryMiddle")) + .Padding(0)[SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[SettingItems[0]] + + SVerticalBox::Slot().AutoHeight()[SettingItems[1]] + + SVerticalBox::Slot().AutoHeight().Padding( + 6)[SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryMiddle")) + .Padding(0)[SettingItems[2]]]]; } return SNullWidget::NullWidget; } @@ -175,7 +154,7 @@ void SSaveActorEditorWidget::RefreshVisuals() } } -const FTabId FSaveActorEditorSummoner::TabName {FName("SaveActorTab") }; +const FTabId FSaveActorEditorSummoner::TabName{FName("SaveActorTab")}; FSaveActorEditorSummoner::FSaveActorEditorSummoner(TSharedPtr BlueprintEditor) : FWorkflowTabFactory(TabName.TabType, BlueprintEditor) diff --git a/Source/Editor/Private/SaveExtensionEditor.cpp b/Source/Editor/Private/SaveExtensionEditor.cpp index 016708a..ff13acf 100644 --- a/Source/Editor/Private/SaveExtensionEditor.cpp +++ b/Source/Editor/Private/SaveExtensionEditor.cpp @@ -1,18 +1,14 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveExtensionEditor.h" -#include "Kismet2/KismetEditorUtilities.h" - -#include "Asset/AssetTypeAction_SlotInfo.h" -#include "Asset/AssetTypeAction_SlotData.h" -#include "Asset/AssetTypeAction_SavePreset.h" - -#include "Customizations/SavePresetDetails.h" +#include "Asset/AssetTypeAction_SaveSlot.h" +#include "Asset/AssetTypeAction_SaveSlotData.h" #include "Customizations/SEClassFilterCustomization.h" #include "Customizations/SEClassFilterGraphPanelPinFactory.h" -#include "Customizations/SEActorClassFilterCustomization.h" -#include "Customizations/SEComponentClassFilterCustomization.h" +#include "Customizations/SaveSlotDetails.h" +#include "Kismet2/KismetEditorUtilities.h" + #define LOCTEXT_NAMESPACE "SaveExtensionEditor" @@ -20,11 +16,11 @@ void FSaveExtensionEditor::StartupModule() { IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - AssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName("SaveExtension"), LOCTEXT("Category", "Save Extension")); + AssetCategory = AssetTools.RegisterAdvancedAssetCategory( + FName("SaveExtension"), LOCTEXT("Category", "Save Extension")); - AssetTools.RegisterAssetTypeActions(MakeShared()); - AssetTools.RegisterAssetTypeActions(MakeShared()); - AssetTools.RegisterAssetTypeActions(MakeShared()); + AssetTools.RegisterAssetTypeActions(MakeShared()); + AssetTools.RegisterAssetTypeActions(MakeShared()); RegisterPropertyTypeCustomizations(); @@ -45,35 +41,38 @@ void FSaveExtensionEditor::ShutdownModule() void FSaveExtensionEditor::RegisterPropertyTypeCustomizations() { - RegisterCustomClassLayout("SavePreset", FOnGetDetailCustomizationInstance::CreateStatic(&FSavePresetDetails::MakeInstance)); + RegisterCustomClassLayout( + "SaveSlot", FOnGetDetailCustomizationInstance::CreateStatic(&FSaveSlotDetails::MakeInstance)); - RegisterCustomPropertyTypeLayout("SEClassFilter", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSEClassFilterCustomization::MakeInstance)); - RegisterCustomPropertyTypeLayout("SEActorClassFilter", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSEActorClassFilterCustomization::MakeInstance)); - RegisterCustomPropertyTypeLayout("SEComponentClassFilter", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSEComponentClassFilterCustomization::MakeInstance)); - //RegisterCustomPropertyTypeLayout("SavePreset", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSavePresetCustomization::MakeInstance)); + RegisterCustomPropertyTypeLayout("SEClassFilter", + FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSEClassFilterCustomization::MakeInstance)); RegisterCustomPinFactory(); } -void FSaveExtensionEditor::RegisterCustomClassLayout(FName ClassName, FOnGetDetailCustomizationInstance DetailLayoutDelegate) +void FSaveExtensionEditor::RegisterCustomClassLayout( + FName ClassName, FOnGetDetailCustomizationInstance DetailLayoutDelegate) { check(ClassName != NAME_None); static FName PropertyEditor("PropertyEditor"); - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked(PropertyEditor); + FPropertyEditorModule& PropertyModule = + FModuleManager::GetModuleChecked(PropertyEditor); PropertyModule.RegisterCustomClassLayout(ClassName, DetailLayoutDelegate); } -void FSaveExtensionEditor::RegisterCustomPropertyTypeLayout(FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate) +void FSaveExtensionEditor::RegisterCustomPropertyTypeLayout( + FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate) { check(PropertyTypeName != NAME_None); static FName PropertyEditor("PropertyEditor"); - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked(PropertyEditor); + FPropertyEditorModule& PropertyModule = + FModuleManager::GetModuleChecked(PropertyEditor); PropertyModule.RegisterCustomPropertyTypeLayout(PropertyTypeName, PropertyTypeLayoutDelegate); } -template +template void FSaveExtensionEditor::RegisterCustomPinFactory() { TSharedPtr PinFactory = MakeShareable(new T()); diff --git a/Source/Editor/Private/SaveExtensionEditor.h b/Source/Editor/Private/SaveExtensionEditor.h index 50cdcd1..d74c899 100644 --- a/Source/Editor/Private/SaveExtensionEditor.h +++ b/Source/Editor/Private/SaveExtensionEditor.h @@ -1,45 +1,51 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include -#include -#include +#include "ISaveExtensionEditor.h" +#include "SaveActorEditorTabSummoner.h" + #include #include #include #include +#include +#include +#include -#include "SaveActorEditorTabSummoner.h" -#include "ISaveExtensionEditor.h" -/** Shared class type that ensures safe binding to RegisterBlueprintEditorTab through an SP binding without interfering with module ownership semantics */ -class FSaveActorEditorTabBinding - : public TSharedFromThis +/** Shared class type that ensures safe binding to RegisterBlueprintEditorTab through an SP binding without + * interfering with module ownership semantics */ +class FSaveActorEditorTabBinding : public TSharedFromThis { public: - FSaveActorEditorTabBinding() { - FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked("Kismet"); - BlueprintEditorTabSpawnerHandle = BlueprintEditorModule.OnRegisterTabsForEditor().AddRaw(this, &FSaveActorEditorTabBinding::RegisterBlueprintEditorTab); - BlueprintEditorLayoutExtensionHandle = BlueprintEditorModule.OnRegisterLayoutExtensions().AddRaw(this, &FSaveActorEditorTabBinding::RegisterBlueprintEditorLayout); + FBlueprintEditorModule& BlueprintEditorModule = + FModuleManager::LoadModuleChecked("Kismet"); + BlueprintEditorTabSpawnerHandle = BlueprintEditorModule.OnRegisterTabsForEditor().AddRaw( + this, &FSaveActorEditorTabBinding::RegisterBlueprintEditorTab); + BlueprintEditorLayoutExtensionHandle = BlueprintEditorModule.OnRegisterLayoutExtensions().AddRaw( + this, &FSaveActorEditorTabBinding::RegisterBlueprintEditorLayout); } void RegisterBlueprintEditorLayout(FLayoutExtender& Extender) { - Extender.ExtendLayout(FBlueprintEditorTabs::MyBlueprintID, ELayoutExtensionPosition::After, FTabManager::FTab(FSaveActorEditorSummoner::TabName, ETabState::OpenedTab)); + Extender.ExtendLayout(FBlueprintEditorTabs::MyBlueprintID, ELayoutExtensionPosition::After, + FTabManager::FTab(FSaveActorEditorSummoner::TabName, ETabState::OpenedTab)); } - void RegisterBlueprintEditorTab(FWorkflowAllowedTabSet& TabFactories, FName InModeName, TSharedPtr BlueprintEditor) + void RegisterBlueprintEditorTab( + FWorkflowAllowedTabSet& TabFactories, FName InModeName, TSharedPtr BlueprintEditor) { TabFactories.RegisterFactory(MakeShared(BlueprintEditor)); } ~FSaveActorEditorTabBinding() { - FBlueprintEditorModule* BlueprintEditorModule = FModuleManager::GetModulePtr("Kismet"); + FBlueprintEditorModule* BlueprintEditorModule = + FModuleManager::GetModulePtr("Kismet"); if (BlueprintEditorModule) { BlueprintEditorModule->OnRegisterTabsForEditor().Remove(BlueprintEditorTabSpawnerHandle); @@ -48,7 +54,6 @@ class FSaveActorEditorTabBinding } private: - /** Delegate binding handle for FBlueprintEditorModule::OnRegisterTabsForEditor */ FDelegateHandle BlueprintEditorTabSpawnerHandle, BlueprintEditorLayoutExtensionHandle; }; @@ -56,39 +61,40 @@ class FSaveActorEditorTabBinding class FSaveExtensionEditor : public ISaveExtensionEditor { public: - virtual void StartupModule() override; virtual void ShutdownModule() override; private: - void RegisterPropertyTypeCustomizations(); /** - * Registers a custom class - * - * @param ClassName The class name to register for property customization - * @param DetailLayoutDelegate The delegate to call to get the custom detail layout instance - */ + * Registers a custom class + * + * @param ClassName The class name to register for property customization + * @param DetailLayoutDelegate The delegate to call to get the custom detail layout instance + */ void RegisterCustomClassLayout(FName ClassName, FOnGetDetailCustomizationInstance DetailLayoutDelegate); /** - * Registers a custom struct - * - * @param StructName The name of the struct to register for property customization - * @param StructLayoutDelegate The delegate to call to get the custom detail layout instance - */ - void RegisterCustomPropertyTypeLayout(FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate); - - template + * Registers a custom struct + * + * @param StructName The name of the struct to register for property customization + * @param StructLayoutDelegate The delegate to call to get the custom detail layout instance + */ + void RegisterCustomPropertyTypeLayout( + FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate); + + template void RegisterCustomPinFactory(); - //Simplify Registering generated default events -#define RegisterDefaultEventChecked(Class, FuncName) \ - (FKismetEditorUtilities::RegisterAutoGeneratedDefaultEvent(this, Class::StaticClass(), GET_FUNCTION_NAME_CHECKED(Class, FuncName))) + // Simplify Registering generated default events +#define RegisterDefaultEventChecked(Class, FuncName) \ + (FKismetEditorUtilities::RegisterAutoGeneratedDefaultEvent( \ + this, Class::StaticClass(), GET_FUNCTION_NAME_CHECKED(Class, FuncName))) -#define RegisterDefaultEvent(Class, FuncName) \ - (FKismetEditorUtilities::RegisterAutoGeneratedDefaultEvent(this, Class::StaticClass(), FName(TEXT(#FuncName)))) +#define RegisterDefaultEvent(Class, FuncName) \ + (FKismetEditorUtilities::RegisterAutoGeneratedDefaultEvent( \ + this, Class::StaticClass(), FName(TEXT(#FuncName)))) TSharedPtr BlueprintEditorTabBinding; @@ -96,5 +102,3 @@ class FSaveExtensionEditor : public ISaveExtensionEditor /** All created pin factories. Cached here so that we can unregister them during shutdown. */ TArray> CreatedPinFactories; }; - - diff --git a/Source/Editor/Public/ISaveExtensionEditor.h b/Source/Editor/Public/ISaveExtensionEditor.h index d4b37eb..4c38e9b 100644 --- a/Source/Editor/Public/ISaveExtensionEditor.h +++ b/Source/Editor/Public/ISaveExtensionEditor.h @@ -1,23 +1,24 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "AssetTypeCategories.h" #include "Modules/ModuleManager.h" -#include "AssetTypeCategories.h" class ISaveExtensionEditor : public IModuleInterface { public: - EAssetTypeCategories::Type AssetCategory; - static inline ISaveExtensionEditor &Get() { + static inline ISaveExtensionEditor& Get() + { return FModuleManager::LoadModuleChecked("SaveExtensionEditor"); } - static inline bool IsAvailable() { + static inline bool IsAvailable() + { return FModuleManager::Get().IsModuleLoaded("SaveExtensionEditor"); } }; diff --git a/Source/Editor/SaveExtensionEditor.Build.cs b/Source/Editor/SaveExtensionEditor.Build.cs index 9cd7e34..f8354f8 100644 --- a/Source/Editor/SaveExtensionEditor.Build.cs +++ b/Source/Editor/SaveExtensionEditor.Build.cs @@ -5,12 +5,14 @@ namespace UnrealBuildTool.Rules { - public class SaveExtensionEditor : ModuleRules { - public SaveExtensionEditor(ReadOnlyTargetRules Target) : base(Target) { + public class SaveExtensionEditor : ModuleRules + { + public SaveExtensionEditor(ReadOnlyTargetRules Target) : base(Target) + { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bEnforceIWYU = true; + IWYUSupport = IWYUSupport.Full; - PublicDependencyModuleNames.AddRange( new string[] + PublicDependencyModuleNames.AddRange(new string[] { "Core", "Engine", @@ -19,7 +21,7 @@ public SaveExtensionEditor(ReadOnlyTargetRules Target) : base(Target) { "SaveExtension" }); - PrivateDependencyModuleNames.AddRange( new string[] + PrivateDependencyModuleNames.AddRange(new string[] { "AssetTools", "Projects", @@ -30,7 +32,8 @@ public SaveExtensionEditor(ReadOnlyTargetRules Target) : base(Target) { "EditorStyle", "ClassViewer", "BlueprintGraph", - "GraphEditor" + "GraphEditor", + "PropertyEditor" }); } } diff --git a/Source/SaveExtension/Private/Misc/ClassFilter.cpp b/Source/SaveExtension/Private/ClassFilter.cpp similarity index 78% rename from Source/SaveExtension/Private/Misc/ClassFilter.cpp rename to Source/SaveExtension/Private/ClassFilter.cpp index ce01946..16b3a52 100644 --- a/Source/SaveExtension/Private/Misc/ClassFilter.cpp +++ b/Source/SaveExtension/Private/ClassFilter.cpp @@ -1,15 +1,16 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. -#include "Misc/ClassFilter.h" +#include "ClassFilter.h" #include #include -FSEClassFilter::FSEClassFilter(UClass* BaseClass) - : BaseClass{ BaseClass } - , IgnoredClasses {} -{} +#if WITH_EDITORONLY_DATA +#include +#endif + +FSEClassFilter::FSEClassFilter(UClass* BaseClass) : BaseClass{BaseClass}, IgnoredClasses{} {} void FSEClassFilter::Merge(const FSEClassFilter& Other) { @@ -32,19 +33,19 @@ void FSEClassFilter::BakeAllowedClasses() const TRACE_CPUPROFILER_EVENT_SCOPE(FSEClassFilter::BakeAllowedClasses); BakedAllowedClasses.Empty(); - if(AllowedClasses.Num() <= 0) + if (AllowedClasses.Num() <= 0) { return; } TArray ChildrenOfAllowedClasses; { - TRACE_CPUPROFILER_EVENT_SCOPE(First Pass: Potential classes); - for(auto& AllowedClass : AllowedClasses) + TRACE_CPUPROFILER_EVENT_SCOPE(First Pass : Potential classes); + for (auto& AllowedClass : AllowedClasses) { UClass* const AllowedClassPtr = AllowedClass.Get(); - if(!AllowedClassPtr) + if (!AllowedClassPtr) { continue; } @@ -52,9 +53,15 @@ void FSEClassFilter::BakeAllowedClasses() const BakedAllowedClasses.Add(AllowedClassPtr); GetDerivedClasses(AllowedClassPtr, ChildrenOfAllowedClasses); } + +#if WITH_EDITORONLY_DATA + ChildrenOfAllowedClasses.RemoveAllSwap([](UClass* Class){ + return FKismetEditorUtilities::IsClassABlueprintSkeleton(Class); + }, false); +#endif } { - TRACE_CPUPROFILER_EVENT_SCOPE(Second Pass: Bake Classes); + TRACE_CPUPROFILER_EVENT_SCOPE(Second Pass : Bake Classes); for (UClass* Class : ChildrenOfAllowedClasses) { // Iterate parent classes of a class @@ -129,5 +136,5 @@ bool FSEClassFilter::operator==(const FSEClassFilter& Other) const { // Do all classes match? return AllowedClasses.Difference(Other.AllowedClasses).Num() <= 0 && - IgnoredClasses.Difference(Other.IgnoredClasses).Num() <= 0; + IgnoredClasses.Difference(Other.IgnoredClasses).Num() <= 0; } diff --git a/Source/SaveExtension/Private/LatentActions/DeleteSlotsAction.cpp b/Source/SaveExtension/Private/LatentActions/DeleteSlotsAction.cpp deleted file mode 100644 index 0b984f3..0000000 --- a/Source/SaveExtension/Private/LatentActions/DeleteSlotsAction.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "LatentActions/DeleteSlotsAction.h" - -#include "SaveManager.h" -#include "SlotInfo.h" - - -FDeleteSlotsAction::FDeleteSlotsAction(USaveManager* Manager, EDeleteSlotsResult& OutResult, const FLatentActionInfo& LatentInfo) - : Result(OutResult) - , bFinished(false) - , ExecutionFunction(LatentInfo.ExecutionFunction) - , OutputLink(LatentInfo.Linkage) - , CallbackTarget(LatentInfo.CallbackTarget) -{ - Manager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { - bFinished = true; - })); -} - -void FDeleteSlotsAction::UpdateOperation(FLatentResponse& Response) -{ - Response.FinishAndTriggerIf(bFinished, ExecutionFunction, OutputLink, CallbackTarget); -} diff --git a/Source/SaveExtension/Private/LatentActions/LoadGameAction.cpp b/Source/SaveExtension/Private/LatentActions/LoadGameAction.cpp deleted file mode 100644 index 4ecd87d..0000000 --- a/Source/SaveExtension/Private/LatentActions/LoadGameAction.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "LatentActions/LoadGameAction.h" -#include "SaveManager.h" -#include "Serialization/SlotDataTask_Loader.h" -#include "SlotInfo.h" - - -FLoadGameAction::FLoadGameAction(USaveManager* Manager, FName SlotName, ELoadGameResult& OutResult, const FLatentActionInfo& LatentInfo) - : Result(OutResult) - , ExecutionFunction(LatentInfo.ExecutionFunction) - , OutputLink(LatentInfo.Linkage) - , CallbackTarget(LatentInfo.CallbackTarget) -{ - const bool bStarted = Manager->LoadSlot(SlotName, FOnGameLoaded::CreateRaw(this, &FLoadGameAction::OnLoadFinished)); - if (!bStarted) - { - Result = ELoadGameResult::Failed; - } -} - -void FLoadGameAction::UpdateOperation(FLatentResponse& Response) -{ - Response.FinishAndTriggerIf(Result != ELoadGameResult::Loading, ExecutionFunction, OutputLink, CallbackTarget); -} - -void FLoadGameAction::OnLoadFinished(USlotInfo* SavedSlot) -{ - Result = SavedSlot ? ELoadGameResult::Continue : ELoadGameResult::Failed; -} diff --git a/Source/SaveExtension/Private/LatentActions/LoadInfosAction.cpp b/Source/SaveExtension/Private/LatentActions/LoadInfosAction.cpp deleted file mode 100644 index 645d5d6..0000000 --- a/Source/SaveExtension/Private/LatentActions/LoadInfosAction.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "LatentActions/LoadInfosAction.h" -#include "SaveManager.h" -#include "SlotInfo.h" - - -FLoadInfosAction::FLoadInfosAction(USaveManager* Manager, const bool bSortByRecent, TArray& InSlotInfos, ELoadInfoResult& OutResult, const FLatentActionInfo& LatentInfo) - : Result(OutResult) - , SlotInfos(InSlotInfos) - , bFinished(false) - , ExecutionFunction(LatentInfo.ExecutionFunction) - , OutputLink(LatentInfo.Linkage) - , CallbackTarget(LatentInfo.CallbackTarget) -{ - Manager->LoadAllSlotInfos(bSortByRecent, FOnSlotInfosLoaded::CreateLambda([this](const TArray& Results) { - SlotInfos = Results; - bFinished = true; - })); -} - -void FLoadInfosAction::UpdateOperation(FLatentResponse& Response) -{ - Response.FinishAndTriggerIf(bFinished, ExecutionFunction, OutputLink, CallbackTarget); - -} diff --git a/Source/SaveExtension/Private/LatentActions/SaveGameAction.cpp b/Source/SaveExtension/Private/LatentActions/SaveGameAction.cpp deleted file mode 100644 index 0220c60..0000000 --- a/Source/SaveExtension/Private/LatentActions/SaveGameAction.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "LatentActions/SaveGameAction.h" -#include "SaveManager.h" -#include "SlotInfo.h" - - -FSaveGameAction::FSaveGameAction(USaveManager* Manager, FName SlotName, bool bOverrideIfNeeded, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& OutResult, const FLatentActionInfo& LatentInfo) - : Result(OutResult) - , ExecutionFunction(LatentInfo.ExecutionFunction) - , OutputLink(LatentInfo.Linkage) - , CallbackTarget(LatentInfo.CallbackTarget) -{ - const bool bStarted = Manager->SaveSlot(SlotName, bOverrideIfNeeded, bScreenshot, Size, FOnGameSaved::CreateRaw(this, &FSaveGameAction::OnSaveFinished)); - - if (!bStarted) - { - Result = ESaveGameResult::Failed; - } -} - -void FSaveGameAction::UpdateOperation(FLatentResponse& Response) -{ - Response.FinishAndTriggerIf(Result != ESaveGameResult::Saving, ExecutionFunction, OutputLink, CallbackTarget); -} - -void FSaveGameAction::OnSaveFinished(USlotInfo* SavedSlot) -{ - Result = SavedSlot ? ESaveGameResult::Continue : ESaveGameResult::Failed; -} diff --git a/Source/SaveExtension/Private/LevelFilter.cpp b/Source/SaveExtension/Private/LevelFilter.cpp index c665d00..9ec288c 100644 --- a/Source/SaveExtension/Private/LevelFilter.cpp +++ b/Source/SaveExtension/Private/LevelFilter.cpp @@ -1,12 +1,27 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "LevelFilter.h" +#include "SaveSlot.h" + ///////////////////////////////////////////////////// // USaveDataTask -const FName FSELevelFilter::TagNoTransform { "!SaveTransform" }; -const FName FSELevelFilter::TagNoPhysics { "!SavePhysics" }; -const FName FSELevelFilter::TagNoTags { "!SaveTags" }; -const FName FSELevelFilter::TagTransform { "SaveTransform" }; + +void FSELevelFilter::BakeAllowedClasses() const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSELevelFilter::BakeAllowedClasses); + ActorFilter.BakeAllowedClasses(); + ComponentFilter.BakeAllowedClasses(); +} + +bool FSELevelFilter::Stores(const AActor* Actor) const +{ + return ActorFilter.IsAllowed(Actor->GetClass()); +} + +bool FSELevelFilter::Stores(const UActorComponent* Component) const +{ + return ComponentFilter.IsAllowed(Component->GetClass()); +} diff --git a/Source/SaveExtension/Private/LevelStreamingNotifier.cpp b/Source/SaveExtension/Private/LevelStreamingNotifier.cpp index bd8d261..9f14565 100644 --- a/Source/SaveExtension/Private/LevelStreamingNotifier.cpp +++ b/Source/SaveExtension/Private/LevelStreamingNotifier.cpp @@ -1,3 +1,3 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "LevelStreamingNotifier.h" diff --git a/Source/SaveExtension/Private/LifetimeComponent.cpp b/Source/SaveExtension/Private/LifetimeComponent.cpp index f4b0394..0652bfe 100644 --- a/Source/SaveExtension/Private/LifetimeComponent.cpp +++ b/Source/SaveExtension/Private/LifetimeComponent.cpp @@ -1,18 +1,17 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "LifetimeComponent.h" -#include "Serialization/MTTask.h" +#include "SaveExtension.h" -ULifetimeComponent::ULifetimeComponent() - : Super() -{} + +ULifetimeComponent::ULifetimeComponent() : Super() {} void ULifetimeComponent::BeginPlay() { Super::BeginPlay(); - if (USaveManager* Manager = GetManager()) + if (USaveManager* Manager = USaveManager::Get(this)) { Manager->SubscribeForEvents(this); @@ -25,13 +24,14 @@ void ULifetimeComponent::BeginPlay() } else { - UE_LOG(LogSaveExtension, Error, TEXT("LifetimeComponent couldnt find a SaveManager. It will do nothing.")) + UE_LOG(LogSaveExtension, Error, + TEXT("LifetimeComponent couldnt find a SaveManager. It will do nothing.")) } } void ULifetimeComponent::EndPlay(EEndPlayReason::Type Reason) { - if (USaveManager* Manager = GetManager()) + if (USaveManager* Manager = USaveManager::Get(this)) { // If manager is loading, it has probably manually destroyed // this actor and its not a natural destroy @@ -48,7 +48,7 @@ void ULifetimeComponent::EndPlay(EEndPlayReason::Type Reason) void ULifetimeComponent::OnSaveBegan(const FSELevelFilter& Filter) { - if (Filter.ShouldSave(GetOwner())) + if (Filter.Stores(GetOwner())) { Saved.Broadcast(); } @@ -56,7 +56,7 @@ void ULifetimeComponent::OnSaveBegan(const FSELevelFilter& Filter) void ULifetimeComponent::OnLoadFinished(const FSELevelFilter& Filter, bool bError) { - if (Filter.ShouldSave(GetOwner())) + if (Filter.Stores(GetOwner())) { Resume.Broadcast(); } diff --git a/Source/SaveExtension/Private/Misc/SlotHelpers.cpp b/Source/SaveExtension/Private/Misc/SlotHelpers.cpp deleted file mode 100644 index 64c7c6a..0000000 --- a/Source/SaveExtension/Private/Misc/SlotHelpers.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include -#include -#include - - -void FSlotHelpers::FindSlotFileNames(TArray& FoundSlots) -{ - FFindSlotVisitor Visitor{ FoundSlots }; - FPlatformFileManager::Get().GetPlatformFile().IterateDirectory(*FFileAdapter::GetSaveFolder(), Visitor); -} - -bool FSlotHelpers::FFindSlotVisitor::Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) -{ - if (bIsDirectory) - { - return true; - } - - const FString FullFilePath(FilenameOrDirectory); - - FString Folder; - FString Filename; - FString Extension; - FPaths::Split(FullFilePath, Folder, Filename, Extension); - if (Extension == TEXT("sav")) - { - FoundSlots.Add(Filename); - } - return true; -} - diff --git a/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp b/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp deleted file mode 100644 index 5e36387..0000000 --- a/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Multithreading/DeleteSlotsTask.h" - -#include - -#include "FileAdapter.h" -#include "SavePreset.h" -#include "SaveManager.h" -#include "Misc/SlotHelpers.h" -#include "HAL/FileManager.h" - - -FDeleteSlotsTask::FDeleteSlotsTask(const USaveManager* InManager, FName SlotName) - : Manager(InManager) -{ - check(Manager); - if(!SlotName.IsNone()) - { - SpecificSlotName = SlotName.ToString(); - } -} - -void FDeleteSlotsTask::DoWork() -{ - if (!SpecificSlotName.IsEmpty()) - { - // Delete a single slot by id - const FString ScreenshotPath = FFileAdapter::GetThumbnailPath(SpecificSlotName); - bool bIsDeleteSlotSuccess = FFileAdapter::DeleteFile(SpecificSlotName); - bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); - bSuccess = bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; - } - else - { - TArray FoundSlots; - FSlotHelpers::FindSlotFileNames(FoundSlots); - - for (const FString& File : FoundSlots) - { - FFileAdapter::DeleteFile(File); - } - bSuccess = true; - } -} diff --git a/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp b/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp deleted file mode 100644 index b8d947e..0000000 --- a/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Multithreading/LoadFileTask.h" - -#include "SavePreset.h" - - -///////////////////////////////////////////////////// -// FSaveFileTask diff --git a/Source/SaveExtension/Private/Multithreading/LoadSlotInfosTask.cpp b/Source/SaveExtension/Private/Multithreading/LoadSlotInfosTask.cpp deleted file mode 100644 index 79c95d1..0000000 --- a/Source/SaveExtension/Private/Multithreading/LoadSlotInfosTask.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Multithreading/LoadSlotInfosTask.h" - -#include - -#include "FileAdapter.h" -#include "SavePreset.h" -#include "SaveManager.h" -#include "Misc/SlotHelpers.h" - - -void FLoadSlotInfosTask::DoWork() -{ - if (!Manager) - { - return; - } - - TArray FileNames; - const bool bLoadingSingleInfo = !SlotName.IsNone(); - if(bLoadingSingleInfo) - { - FileNames.Add(SlotName.ToString()); - } - else - { - FSlotHelpers::FindSlotFileNames(FileNames); - } - - TArray LoadedFiles; - LoadedFiles.Reserve(FileNames.Num()); - for (const FString& FileName : FileNames) - { - // Load all files - FScopedFileReader Reader(FFileAdapter::GetSlotPath(FileName)); - if(Reader.IsValid()) - { - auto& File = LoadedFiles.AddDefaulted_GetRef(); - File.Read(Reader, true); - } - } - - // For cache friendlyness, we deserialize infos after loading all the files - LoadedSlots.Reserve(LoadedFiles.Num()); - for (const auto& File : LoadedFiles) - { - LoadedSlots.Add(File.CreateAndDeserializeInfo(Manager)); - } - - if (!bLoadingSingleInfo && bSortByRecent) - { - LoadedSlots.Sort([](const USlotInfo& A, const USlotInfo& B) { - return A.SaveDate > B.SaveDate; - }); - } -} - -void FLoadSlotInfosTask::AfterFinish() -{ - for(auto& Slot : LoadedSlots) - { - Slot->ClearInternalFlags(EInternalObjectFlags::Async); - } - Delegate.ExecuteIfBound(LoadedSlots); -} diff --git a/Source/SaveExtension/Private/Multithreading/SaveFileTask.cpp b/Source/SaveExtension/Private/Multithreading/SaveFileTask.cpp deleted file mode 100644 index d9a3695..0000000 --- a/Source/SaveExtension/Private/Multithreading/SaveFileTask.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Multithreading/SaveFileTask.h" - -#include "SavePreset.h" - - -///////////////////////////////////////////////////// -// FSaveFileTask diff --git a/Source/SaveExtension/Private/Multithreading/ScopedTaskManager.cpp b/Source/SaveExtension/Private/Multithreading/ScopedTaskManager.cpp deleted file mode 100644 index a1b5d43..0000000 --- a/Source/SaveExtension/Private/Multithreading/ScopedTaskManager.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Multithreading/ScopedTaskManager.h" diff --git a/Source/SaveExtension/Private/FileAdapter.cpp b/Source/SaveExtension/Private/SEFileHelpers.cpp similarity index 52% rename from Source/SaveExtension/Private/FileAdapter.cpp rename to Source/SaveExtension/Private/SEFileHelpers.cpp index e4290b9..72f26b8 100644 --- a/Source/SaveExtension/Private/FileAdapter.cpp +++ b/Source/SaveExtension/Private/SEFileHelpers.cpp @@ -1,22 +1,58 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. -#include "FileAdapter.h" +#include "SEFileHelpers.h" -#include -#include +#include "SaveExtension.h" +#include "SaveManager.h" +#include "SaveSlot.h" +#include "SaveSlotData.h" +#include "Serialization/SEArchive.h" + +#include +#include +#include +#include +#include #include #include -#include -#include -#include +#include +#include +#include + + +static const int SE_SAVEGAME_FILE_TYPE_TAG = 0x0001; // "sAvG" + +UE::Tasks::FPipe BackendPipe{TEXT("SaveExtensionPipe")}; + + +/** Used to find next available slot id */ +class FSEFindSlotVisitor : public IPlatformFile::FDirectoryVisitor +{ +public: + TArray& FoundSlots; -#include "SavePreset.h" -#include "SlotInfo.h" -#include "SlotData.h" -#include "Multithreading/SaveFileTask.h" + FSEFindSlotVisitor(TArray& FoundSlots) : FoundSlots(FoundSlots) {} + virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override + { + if (bIsDirectory) + { + return true; + } + const FString FullFilePath(FilenameOrDirectory); + + FString Folder; + FString Filename; + FString Extension; + FPaths::Split(FullFilePath, Folder, Filename, Extension); + if (Extension == TEXT("sav")) + { + FoundSlots.Add(Filename); + } + return true; + } +}; -static const int SE_SAVEGAME_FILE_TYPE_TAG = 0x0001; // "sAvG" struct FSaveGameFileVersion { @@ -83,7 +119,7 @@ void FSaveFile::Read(FScopedFileReader& Reader, bool bSkipData) Empty(); FArchive& Ar = Reader.GetArchive(); - { // Header information + { // Header information Ar << FileTypeTag; if (FileTypeTag != SE_SAVEGAME_FILE_TYPE_TAG) { @@ -104,22 +140,23 @@ void FSaveFile::Read(FScopedFileReader& Reader, bool bSkipData) if (SaveGameFileVersion >= FSaveGameFileVersion::AddedCustomVersions) { Ar << CustomVersionFormat; - CustomVersions.Serialize(Ar, static_cast(CustomVersionFormat)); + CustomVersions.Serialize( + Ar, static_cast(CustomVersionFormat)); Ar.SetCustomVersions(CustomVersions); } } - Ar << InfoClassName; - Ar << InfoBytes; + Ar << ClassName; + Ar << Bytes; Ar << DataClassName; - if(bSkipData || DataClassName.IsEmpty()) + if (bSkipData || DataClassName.IsEmpty()) { return; } Ar << bIsDataCompressed; - if(bIsDataCompressed) + if (bIsDataCompressed) { TArray CompressedDataBytes; Ar << CompressedDataBytes; @@ -149,26 +186,27 @@ void FSaveFile::Write(FScopedFileWriter& Writer, bool bCompressData) bIsDataCompressed = bCompressData; FArchive& Ar = Writer.GetArchive(); - { // Header information + { // Header information Ar << FileTypeTag; Ar << SaveGameFileVersion; Ar << PackageFileUEVersion; Ar << SavedEngineVersion; Ar << CustomVersionFormat; - CustomVersions.Serialize(Ar, static_cast(CustomVersionFormat)); + CustomVersions.Serialize( + Ar, static_cast(CustomVersionFormat)); } - Ar << InfoClassName; - Ar << InfoBytes; + Ar << ClassName; + Ar << Bytes; Ar << DataClassName; - if(!DataClassName.IsEmpty()) + if (!DataClassName.IsEmpty()) { Ar << bIsDataCompressed; - if(bIsDataCompressed) + if (bIsDataCompressed) { TArray CompressedDataBytes; - { // Compression + { // Compression TRACE_CPUPROFILER_EVENT_SCOPE(Compression); // Compress Object data FArchiveSaveCompressedProxy Compressor(CompressedDataBytes, NAME_Zlib); @@ -185,18 +223,18 @@ void FSaveFile::Write(FScopedFileWriter& Writer, bool bCompressData) Ar.Close(); } -void FSaveFile::SerializeInfo(USlotInfo* SlotInfo) +void FSaveFile::SerializeInfo(USaveSlot* Slot) { TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFile::SerializeInfo); - check(SlotInfo); - InfoBytes.Reset(); - InfoClassName = SlotInfo->GetClass()->GetPathName(); + check(Slot); + Bytes.Reset(); + ClassName = Slot->GetClass()->GetPathName(); - FMemoryWriter BytesWriter(InfoBytes); + FMemoryWriter BytesWriter(Bytes); FObjectAndNameAsStringProxyArchive Ar(BytesWriter, false); - SlotInfo->Serialize(Ar); + Slot->Serialize(Ar); } -void FSaveFile::SerializeData(USlotData* SlotData) +void FSaveFile::SerializeData(USaveSlotData* SlotData) { TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFile::SerializeData); check(SlotData); @@ -208,127 +246,152 @@ void FSaveFile::SerializeData(USlotData* SlotData) SlotData->Serialize(Ar); } -USlotInfo* FSaveFile::CreateAndDeserializeInfo(const UObject* Outer) const +bool FSEFileHelpers::SaveFileSync(USaveSlot* Slot, FStringView OverrideSlotName, const bool bUseCompression) { - TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFile::CreateAndDeserializeInfo); - UObject* Object = nullptr; - FFileAdapter::DeserializeObject(Object, InfoClassName, Outer, InfoBytes); - return Cast(Object); -} - -USlotData* FSaveFile::CreateAndDeserializeData(const UObject* Outer) const -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFile::CreateAndDeserializeData); - UObject* Object = nullptr; - FFileAdapter::DeserializeObject(Object, DataClassName, Outer, DataBytes); - return Cast(Object); -} + TRACE_CPUPROFILER_EVENT_SCOPE(FSEFileHelpers::SaveFileSync); -bool FFileAdapter::SaveFile(FStringView SlotName, USlotInfo* Info, USlotData* Data, const bool bUseCompression) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FFileAdapter::SaveFile); - - if (SlotName.IsEmpty()) - { - return false; - } - - if (!ensureMsgf(Info, TEXT("Info object must be valid")) || - !ensureMsgf(Data, TEXT("Data object must be valid"))) + if (!ensureMsgf(Slot, TEXT("Slot object must be valid")) || + !ensureMsgf(Slot->GetData(), TEXT("Slot Data object must be valid"))) { return false; } + FString SlotName = OverrideSlotName.IsEmpty() ? Slot->Name.ToString() : FString{OverrideSlotName}; FScopedFileWriter FileWriter(GetSlotPath(SlotName)); - if(FileWriter.IsValid()) + if (FileWriter.IsValid()) { FSaveFile File{}; - File.SerializeInfo(Info); - File.SerializeData(Data); + File.SerializeInfo(Slot); + File.SerializeData(Slot->GetData()); File.Write(FileWriter, bUseCompression); return !FileWriter.IsError(); } return false; } -bool FFileAdapter::LoadFile(FStringView SlotName, USlotInfo*& Info, USlotData*& Data, bool bLoadData, const UObject* Outer) +UE::Tasks::TTask FSEFileHelpers::SaveFile( + USaveSlot* Slot, FString OverrideSlotName, const bool bUseCompression) { - TRACE_CPUPROFILER_EVENT_SCOPE(FFileAdapter::LoadFile); + return BackendPipe.Launch(TEXT("SaveFile"), [Slot, OverrideSlotName, bUseCompression]() { + return SaveFileSync(Slot, OverrideSlotName, bUseCompression); + }); +} + + +USaveSlot* FSEFileHelpers::LoadFileSync( + FStringView SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEFileHelpers::LoadFileSync); + if (SlotName.IsEmpty() && SlotHint) + { + SlotName = SlotHint->Name.ToString(); + } FScopedFileReader Reader(GetSlotPath(SlotName)); - if(Reader.IsValid()) + if (Reader.IsValid()) { FSaveFile File{}; File.Read(Reader, !bLoadData); - Info = File.CreateAndDeserializeInfo(Outer); - Data = File.CreateAndDeserializeData(Outer); - return true; + USaveSlot* Slot; + { + TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeInfo) + Slot = Cast(DeserializeObject(SlotHint, File.ClassName, Manager, File.Bytes)); + } + if (bLoadData) + { + TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeData) + Slot->AssignData(Cast( + DeserializeObject(Slot->GetData(), File.DataClassName, Slot, File.DataBytes))); + } + return Slot; } - return false; + return nullptr; } -bool FFileAdapter::DeleteFile(FStringView SlotName) +UE::Tasks::TTask FSEFileHelpers::LoadFile( + FString SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager) +{ + return BackendPipe.Launch(TEXT("LoadFile"), [SlotName, SlotHint, bLoadData, Manager]() { + USaveSlot* Slot = LoadFileSync(SlotName, SlotHint, bLoadData, Manager); + // In case we create the slot from async loading thread + if (Slot) + { + Slot->ClearInternalFlags(EInternalObjectFlags::Async); + if (IsValid(Slot->GetData())) + { + Slot->GetData()->ClearInternalFlags(EInternalObjectFlags::Async); + } + } + return Slot; + }); +} + +bool FSEFileHelpers::DeleteFile(FStringView SlotName) { return IFileManager::Get().Delete(*GetSlotPath(SlotName), true, false, true); } -bool FFileAdapter::DoesFileExist(FStringView SlotName) +bool FSEFileHelpers::FileExists(FStringView SlotName) { return IFileManager::Get().FileSize(*GetSlotPath(SlotName)) >= 0; } -const FString& FFileAdapter::GetSaveFolder() +const FString& FSEFileHelpers::GetSaveFolder() { static const FString Folder = FString::Printf(TEXT("%sSaveGames/"), *FPaths::ProjectSavedDir()); return Folder; } -FString FFileAdapter::GetSlotPath(FStringView SlotName) +FString FSEFileHelpers::GetSlotPath(FStringView SlotName) { return GetSaveFolder() / FString::Printf(TEXT("%s.sav"), SlotName.GetData()); } -FString FFileAdapter::GetThumbnailPath(FStringView SlotName) +void FSEFileHelpers::FindAllFilesSync(TArray& FoundSlots) { - return GetSaveFolder() / FString::Printf(TEXT("%s.png"), SlotName.GetData()); + FSEFindSlotVisitor Visitor{FoundSlots}; + FPlatformFileManager::Get().GetPlatformFile().IterateDirectory(*FSEFileHelpers::GetSaveFolder(), Visitor); } -void FFileAdapter::DeserializeObject(UObject*& Object, FStringView ClassName, const UObject* Outer, const TArray& Bytes) +UObject* FSEFileHelpers::DeserializeObject( + UObject* Hint, FStringView ClassName, const UObject* Outer, const TArray& Bytes) { + UObject* Object = Hint; + if (ClassName.IsEmpty() || Bytes.Num() <= 0) { - return; + return Object; } - UClass* ObjectClass = FindObject(ANY_PACKAGE, ClassName.GetData()); + UClass* ObjectClass = FindObject(nullptr, ClassName.GetData()); if (!ObjectClass) { ObjectClass = LoadObject(nullptr, ClassName.GetData()); } if (!ObjectClass) { - return; + return Object; } - if(!Object) + // Can only reuse object if class matches + if (!Object || Object->GetClass() != ObjectClass) { - if(!Outer) + if (!Outer) { Outer = GetTransientPackage(); } Object = NewObject(const_cast(Outer), ObjectClass); } - // Can only reuse object if class matches - else if(Object->GetClass() != ObjectClass) - { - return; - } - if(Object) - { - FMemoryReader Reader{ Bytes }; - FObjectAndNameAsStringProxyArchive Ar(Reader, true); - Object->Serialize(Ar); - } + check(Object); + FMemoryReader Reader{Bytes}; + FSEArchive Ar(Reader, true); + Object->Serialize(Ar); + return Object; +} + +UE::Tasks::FPipe& FSEFileHelpers::GetPipe() +{ + return BackendPipe; } diff --git a/Source/SaveExtension/Private/SaveExtension.cpp b/Source/SaveExtension/Private/SaveExtension.cpp index 385ce26..b3a5ede 100644 --- a/Source/SaveExtension/Private/SaveExtension.cpp +++ b/Source/SaveExtension/Private/SaveExtension.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveExtension.h" @@ -6,3 +6,30 @@ DEFINE_LOG_CATEGORY(LogSaveExtension) IMPLEMENT_MODULE(FSaveExtension, SaveExtension); + +void FSaveExtension::Log(const USaveSlot* Slot, const FString& Message, FColor Color, bool bError, const float Duration) + { + if (Slot->bDebug) + { + if (bError) + { + Color = FColor::Red; + } + + const FString ComposedMessage{FString::Printf(TEXT("SE: %s"), *Message)}; + + if (bError) + { + UE_LOG(LogSaveExtension, Error, TEXT("%s"), *ComposedMessage); + } + else + { + UE_LOG(LogSaveExtension, Log, TEXT("%s"), *ComposedMessage); + } + + if (Slot->bDebugInScreen && GEngine) + { + GEngine->AddOnScreenDebugMessage(-1, Duration, Color, ComposedMessage); + } + } + } diff --git a/Source/SaveExtension/Private/SaveExtension.h b/Source/SaveExtension/Private/SaveExtension.h deleted file mode 100644 index faac276..0000000 --- a/Source/SaveExtension/Private/SaveExtension.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" - -#if WITH_EDITORONLY_DATA - #include "ISettingsModule.h" - #include "ISettingsSection.h" - #include "ISettingsContainer.h" -#endif - - -class FSaveExtension : public ISaveExtension -{ -public: - - virtual void StartupModule() override {} - virtual void ShutdownModule() override {} - - virtual bool SupportsDynamicReloading() override { return true; } -}; diff --git a/Source/SaveExtension/Private/SaveExtensionInterface.cpp b/Source/SaveExtension/Private/SaveExtensionInterface.cpp index a1c7425..c562b67 100644 --- a/Source/SaveExtension/Private/SaveExtensionInterface.cpp +++ b/Source/SaveExtension/Private/SaveExtensionInterface.cpp @@ -1,7 +1,7 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveExtensionInterface.h" -USaveExtensionInterface::USaveExtensionInterface(const FObjectInitializer &ObjectInitializer) +USaveExtensionInterface::USaveExtensionInterface(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {} diff --git a/Source/SaveExtension/Private/SaveExtensionLibrary.cpp b/Source/SaveExtension/Private/SaveExtensionLibrary.cpp index d07c6f2..b4cd797 100644 --- a/Source/SaveExtension/Private/SaveExtensionLibrary.cpp +++ b/Source/SaveExtension/Private/SaveExtensionLibrary.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveExtensionLibrary.h" @@ -6,13 +6,14 @@ #include "HighResScreenshot.h" #include "IImageWrapper.h" #include "IImageWrapperModule.h" -#include "Kismet/KismetSystemLibrary.h" #include "Kismet/GameplayStatics.h" - +#include "Kismet/KismetSystemLibrary.h" #include "SaveManager.h" -/*FSaveTimerHandle USaveExtensionLibrary::SetSaveTimerDelegate(FTimerDynamicDelegate Delegate, float Time, bool bLooping) + +/*FSaveTimerHandle USaveExtensionLibrary::SetSaveTimerDelegate(FTimerDynamicDelegate Delegate, float Time, +bool bLooping) { FTimerHandle Handle { UKismetSystemLibrary::K2_SetTimerDelegate(Delegate, Time, bLooping) }; return FSaveTimerHandle(Handle, Delegate, Time, bLooping); diff --git a/Source/SaveExtension/Private/SaveExtensionLibrary.h b/Source/SaveExtension/Private/SaveExtensionLibrary.h index 2ea550e..bc1eb9b 100644 --- a/Source/SaveExtension/Private/SaveExtensionLibrary.h +++ b/Source/SaveExtension/Private/SaveExtensionLibrary.h @@ -1,11 +1,11 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once #include "Runtime/Engine/Classes/Kismet/BlueprintFunctionLibrary.h" +#include "SaveSlotData.h" -#include "SlotData.h" -//#include "SaveTimerHandle.h" +// #include "SaveTimerHandle.h" #include "SaveExtensionLibrary.generated.h" @@ -16,7 +16,7 @@ class SAVEEXTENSION_API USaveExtensionLibrary : public UBlueprintFunctionLibrary GENERATED_BODY() public: - - //UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set Saveable Timer by Event"), Category = "Utilities|Time") - //static FSaveTimerHandle SetSaveTimerDelegate(FTimerDynamicDelegate Delegate, float Time, bool bLooping); + // UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set Saveable Timer by Event"), Category = + // "Utilities|Time") static FSaveTimerHandle SetSaveTimerDelegate(FTimerDynamicDelegate Delegate, float + // Time, bool bLooping); }; diff --git a/Source/SaveExtension/Private/SaveManager.cpp b/Source/SaveExtension/Private/SaveManager.cpp index a177840..97b73e0 100644 --- a/Source/SaveExtension/Private/SaveManager.cpp +++ b/Source/SaveExtension/Private/SaveManager.cpp @@ -1,29 +1,195 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveManager.h" -#include "FileAdapter.h" -#include "LatentActions/LoadInfosAction.h" -#include "Multithreading/DeleteSlotsTask.h" -#include "Multithreading/LoadSlotInfosTask.h" +#include "SEFileHelpers.h" +#include "SaveExtension.h" #include "SaveSettings.h" -#include "Serialization/SlotDataTask_LevelLoader.h" -#include "Serialization/SlotDataTask_LevelSaver.h" -#include "Serialization/SlotDataTask_Loader.h" -#include "Serialization/SlotDataTask_Saver.h" +#include "Serialization/SEDataTask_Load.h" +#include "Serialization/SEDataTask_LoadLevel.h" +#include "Serialization/SEDataTask_Save.h" +#include "Serialization/SEDataTask_SaveLevel.h" +#include #include +#include #include #include #include #include #include #include +#include #include #include +#include -USaveManager::USaveManager() : Super(), MTTasks{} {} +// From SaveGameSystem.cpp +void OnAsyncComplete(TFunction Callback) +{ + // NB. Using Ticker because AsyncTask may run during async package loading which may not be suitable for + // save data + FTSTicker::GetCoreTicker().AddTicker( + FTickerDelegate::CreateLambda([Callback = MoveTemp(Callback)](float) -> bool { + Callback(); + return false; + })); +} + +// BEGIN Async Actions + +class FSELoadSlotDataAction : public FPendingLatentAction +{ +public: + ESEContinueOrFail& Result; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FSELoadSlotDataAction(USaveManager* Manager, FName SlotName, ESEContinueOrFail& OutResult, + const FLatentActionInfo& LatentInfo) + : Result(OutResult) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) + { + const bool bStarted = Manager->LoadSlot( + SlotName, FOnGameLoaded::CreateRaw(this, &FSELoadSlotDataAction::OnLoadFinished)); + Result = bStarted ? ESEContinueOrFail::InProgress : ESEContinueOrFail::Failed; + } + void UpdateOperation(FLatentResponse& Response) override + { + Response.FinishAndTriggerIf( + Result != ESEContinueOrFail::InProgress, ExecutionFunction, OutputLink, CallbackTarget); + } + void OnLoadFinished(USaveSlot* SavedSlot) + { + Result = SavedSlot ? ESEContinueOrFail::Continue : ESEContinueOrFail::Failed; + } +#if WITH_EDITOR + // Returns a human readable description of the latent operation's current state + FString GetDescription() const override + { + return TEXT("Loading Game..."); + } +#endif +}; + + +class FDeleteAllSlotsAction : public FPendingLatentAction +{ +public: + ESEContinue& Result; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FDeleteAllSlotsAction(USaveManager* Manager, ESEContinue& OutResult, const FLatentActionInfo& LatentInfo) + : Result(OutResult) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) + { + Result = ESEContinue::InProgress; + Manager->DeleteAllSlots([this](int32 Count) { + Result = ESEContinue::Continue; + }); + } + void UpdateOperation(FLatentResponse& Response) override + { + Response.FinishAndTriggerIf( + Result != ESEContinue::InProgress, ExecutionFunction, OutputLink, CallbackTarget); + } +#if WITH_EDITOR + FString GetDescription() const override + { + return TEXT("Deleting all slots..."); + } +#endif +}; + +class FSEPreloadSlotsAction : public FPendingLatentAction +{ +public: + TArray& Slots; + ESEContinue& Result; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FSEPreloadSlotsAction(USaveManager* Manager, const bool bSortByRecent, TArray& OutSlots, + ESEContinue& OutResult, const FLatentActionInfo& LatentInfo) + : Slots(OutSlots) + , Result(OutResult) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) + { + Result = ESEContinue::InProgress; + Manager->PreloadAllSlots( + [this](const TArray& InSlots) { + Slots = InSlots; + Result = ESEContinue::Continue; + }, + bSortByRecent); + } + virtual void UpdateOperation(FLatentResponse& Response) override + { + Response.FinishAndTriggerIf( + Result != ESEContinue::InProgress, ExecutionFunction, OutputLink, CallbackTarget); + } +#if WITH_EDITOR + virtual FString GetDescription() const override + { + return TEXT("Loading all slots..."); + } +#endif +}; + + +class FSaveGameAction : public FPendingLatentAction +{ +public: + ESEContinueOrFail& Result; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FSaveGameAction(USaveManager* Manager, FName SlotName, bool bOverrideIfNeeded, bool bScreenshot, + const FScreenshotSize Size, ESEContinueOrFail& OutResult, const FLatentActionInfo& LatentInfo) + : Result(OutResult) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) + { + const bool bStarted = Manager->SaveSlot(SlotName, bOverrideIfNeeded, bScreenshot, Size, + FOnGameSaved::CreateRaw(this, &FSaveGameAction::OnSaveFinished)); + Result = bStarted ? ESEContinueOrFail::InProgress : ESEContinueOrFail::Failed; + } + + virtual void UpdateOperation(FLatentResponse& Response) override + { + Response.FinishAndTriggerIf( + Result != ESEContinueOrFail::InProgress, ExecutionFunction, OutputLink, CallbackTarget); + } + void OnSaveFinished(USaveSlot* SavedSlot) + { + Result = SavedSlot ? ESEContinueOrFail::Continue : ESEContinueOrFail::Failed; + } +#if WITH_EDITOR + // Returns a human readable description of the latent operation's current state + virtual FString GetDescription() const override + { + return TEXT("Saving Game..."); + } +#endif +}; + +// END Async Actions + + +USaveManager::USaveManager() : Super() {} void USaveManager::Initialize(FSubsystemCollectionBase& Collection) { @@ -34,15 +200,12 @@ void USaveManager::Initialize(FSubsystemCollectionBase& Collection) FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USaveManager::OnMapLoadStarted); FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &USaveManager::OnMapLoadFinished); - ActivePreset = GetDefault()->CreatePreset(this); - - // AutoLoad - if (GetPreset() && GetPreset()->bAutoLoad) + AssureActiveSlot(); + if (ActiveSlot && ActiveSlot->bLoadOnStart) { - ReloadCurrentSlot(); + ReloadActiveSlot(); } - TryInstantiateInfo(); UpdateLevelStreamings(); } @@ -50,42 +213,56 @@ void USaveManager::Deinitialize() { Super::Deinitialize(); - MTTasks.CancelAll(); + FSEFileHelpers::GetPipe().WaitUntilEmpty(); - if (GetPreset()->bSaveOnExit) - SaveCurrentSlot(); + if (GetActiveSlot()->bSaveOnClose) + SaveActiveSlot(); FCoreUObjectDelegates::PreLoadMap.RemoveAll(this); FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); FGameDelegates::Get().GetEndPlayMapDelegate().RemoveAll(this); } -bool USaveManager::SaveSlot( - FName SlotName, bool bOverrideIfNeeded, bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) +bool USaveManager::SaveSlot(FName SlotName, bool bOverrideIfNeeded, bool bScreenshot, + const FScreenshotSize Size, FOnGameSaved OnSaved) { if (!CanLoadOrSave()) return false; - const USavePreset* Preset = GetPreset(); if (SlotName.IsNone()) { - SELog(Preset, "Can't use an empty slot name to save.", true); + SELog(ActiveSlot, "Can't use an empty slot name to save.", true); return false; } // Saving - SELog(Preset, "Saving to Slot " + SlotName.ToString()); + SELog(ActiveSlot, "Saving to Slot " + SlotName.ToString()); UWorld* World = GetWorld(); check(World); // Launch task, always fail if it didn't finish or wasn't scheduled - auto* Task = CreateTask() - ->Setup(SlotName, bOverrideIfNeeded, bScreenshot, Size.Width, Size.Height) - ->Bind(OnSaved) - ->Start(); + auto& Task = CreateTask() + .Setup(SlotName, bOverrideIfNeeded, bScreenshot, Size.Width, Size.Height) + .Bind(OnSaved) + .Start(); + + return Task.IsSucceeded() || Task.IsScheduled(); +} + +bool USaveManager::SaveSlot(const USaveSlot* Slot, bool bOverrideIfNeeded, bool bScreenshot, + const FScreenshotSize Size, FOnGameSaved OnSaved) +{ + if (!Slot) + { + return false; + } + return SaveSlot(Slot->Name, bOverrideIfNeeded, bScreenshot, Size, OnSaved); +} - return Task->IsSucceeded() || Task->IsScheduled(); +bool USaveManager::SaveActiveSlot(bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) +{ + return SaveSlot(ActiveSlot, true, bScreenshot, Size, OnSaved); } bool USaveManager::LoadSlot(FName SlotName, FOnGameLoaded OnLoaded) @@ -95,164 +272,195 @@ bool USaveManager::LoadSlot(FName SlotName, FOnGameLoaded OnLoaded) return false; } - TryInstantiateInfo(); - - auto* Task = CreateTask() - ->Setup(SlotName) - ->Bind(OnLoaded) - ->Start(); + AssureActiveSlot(); - return Task->IsSucceeded() || Task->IsScheduled(); + auto& Task = CreateTask().Setup(SlotName).Bind(OnLoaded).Start(); + return Task.IsSucceeded() || Task.IsScheduled(); } -bool USaveManager::DeleteSlot(FName SlotName) +bool USaveManager::LoadSlot(const USaveSlot* Slot, FOnGameLoaded OnLoaded) { - if (SlotName.IsNone()) + if (!Slot) { return false; } + return LoadSlot(Slot->Name, OnLoaded); +} + +void USaveManager::PreloadAllSlots(FSEOnAllSlotsPreloaded Callback, bool bSortByRecent) +{ + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() { + TArray Slots; + PreloadAllSlotsSync(Slots, bSortByRecent); - bool bSuccess = false; - MTTasks.CreateTask(this, SlotName) - .OnFinished([&bSuccess](auto& Task) mutable { - bSuccess = Task->bSuccess; - }) - .StartSynchronousTask(); - MTTasks.Tick(); - return bSuccess; + if (Callback) + { + OnAsyncComplete([Slots = MoveTemp(Slots), Callback]() { + for (auto* Slot : Slots) + { + Slot->ClearInternalFlags(EInternalObjectFlags::Async); + } + Callback(Slots); + }); + } + }); } -void USaveManager::LoadAllSlotInfos(bool bSortByRecent, FOnSlotInfosLoaded Delegate) +void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRecent) { - MTTasks.CreateTask(this, bSortByRecent, MoveTemp(Delegate)) - .OnFinished([](auto& Task) { - Task->AfterFinish(); - }) - .StartBackgroundTask(); + TArray FileNames; + FSEFileHelpers::FindAllFilesSync(FileNames); + + TArray LoadedFiles; + LoadedFiles.Reserve(FileNames.Num()); + for (const FString& FileName : FileNames) + { + // Load all files + FScopedFileReader Reader(FSEFileHelpers::GetSlotPath(FileName)); + if (Reader.IsValid()) + { + LoadedFiles.AddDefaulted_GetRef().Read(Reader, true); + } + } + + Slots.Reserve(Slots.Num() + LoadedFiles.Num()); + for (const auto& File : LoadedFiles) + { + auto* Slot = + Cast(FSEFileHelpers::DeserializeObject(nullptr, File.ClassName, this, File.Bytes)); + if (Slot) + { + Slots.Add(Slot); + } + } + + if (bSortByRecent) + { + Slots.Sort([](const USaveSlot& A, const USaveSlot& B) { + return A.Stats.SaveDate > B.Stats.SaveDate; + }); + } } -void USaveManager::LoadAllSlotInfosSync(bool bSortByRecent, FOnSlotInfosLoaded Delegate) +bool USaveManager::DeleteSlotByNameSync(FName SlotName) { - MTTasks.CreateTask(this, bSortByRecent, MoveTemp(Delegate)) - .OnFinished([](auto& Task) { - Task->AfterFinish(); - }) - .StartSynchronousTask(); - MTTasks.Tick(); + const FString NameStr = SlotName.ToString(); + return FSEFileHelpers::DeleteFile(NameStr); } -void USaveManager::DeleteAllSlots(FOnSlotsDeleted Delegate) +void USaveManager::DeleteSlotByName(FName SlotName) { - MTTasks.CreateTask(this) - .OnFinished([Delegate](auto& Task) { - Delegate.ExecuteIfBound(); - }) - .StartBackgroundTask(); + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, SlotName]() { + DeleteSlotByNameSync(SlotName); + }); } -void USaveManager::BPSaveSlot(FName SlotName, bool bScreenshot, const FScreenshotSize Size, - ESaveGameResult& Result, struct FLatentActionInfo LatentInfo, bool bOverrideIfNeeded /*= true*/) +int32 USaveManager::DeleteAllSlotsSync() { - if (UWorld* World = GetWorld()) + TArray FoundSlots; + FSEFileHelpers::FindAllFilesSync(FoundSlots); + + int32 Count = 0; + for (const FString& SlotName : FoundSlots) { - Result = ESaveGameResult::Saving; + Count += FSEFileHelpers::DeleteFile(SlotName); + } + return Count; +} +void USaveManager::DeleteAllSlots(FSEOnAllSlotsDeleted Callback) +{ + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, Callback]() { + const int32 Count = DeleteAllSlotsSync(); + if (Callback) + { + OnAsyncComplete([Count, Callback]() { + Callback(Count); + }); + } + }); +} + +void USaveManager::BPSaveSlotByName(FName SlotName, bool bScreenshot, const FScreenshotSize Size, + ESEContinueOrFail& Result, struct FLatentActionInfo LatentInfo, bool bOverrideIfNeeded /*= true*/) +{ + if (UWorld* World = GetWorld()) + { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, - new FSaveGameAction(this, SlotName, bOverrideIfNeeded, bScreenshot, Size, Result, LatentInfo)); + new FSaveGameAction( + this, SlotName, bOverrideIfNeeded, bScreenshot, Size, Result, LatentInfo)); } return; } - Result = ESaveGameResult::Failed; + Result = ESEContinueOrFail::Failed; } -void USaveManager::BPLoadSlot( - FName SlotName, ELoadGameResult& Result, struct FLatentActionInfo LatentInfo) +void USaveManager::BPLoadSlotByName( + FName SlotName, ESEContinueOrFail& Result, struct FLatentActionInfo LatentInfo) { if (UWorld* World = GetWorld()) { - Result = ELoadGameResult::Loading; - FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); - if (LatentActionManager.FindExistingAction( + if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, - new FLoadGameAction(this, SlotName, Result, LatentInfo)); + new FSELoadSlotDataAction(this, SlotName, Result, LatentInfo)); } return; } - Result = ELoadGameResult::Failed; + Result = ESEContinueOrFail::Failed; } -void USaveManager::BPLoadAllSlotInfos(const bool bSortByRecent, TArray& SaveInfos, - ELoadInfoResult& Result, struct FLatentActionInfo LatentInfo) +void USaveManager::BPPreloadAllSlots(const bool bSortByRecent, TArray& SaveInfos, + ESEContinue& Result, struct FLatentActionInfo LatentInfo) { if (UWorld* World = GetWorld()) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); - if (LatentActionManager.FindExistingAction( + if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, - new FLoadInfosAction(this, bSortByRecent, SaveInfos, Result, LatentInfo)); + new FSEPreloadSlotsAction(this, bSortByRecent, SaveInfos, Result, LatentInfo)); } } } -void USaveManager::BPDeleteAllSlots(EDeleteSlotsResult& Result, struct FLatentActionInfo LatentInfo) +void USaveManager::BPDeleteAllSlots(ESEContinue& Result, struct FLatentActionInfo LatentInfo) { if (UWorld* World = GetWorld()) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); - if (LatentActionManager.FindExistingAction( + if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { - LatentActionManager.AddNewAction( - LatentInfo.CallbackTarget, LatentInfo.UUID, new FDeleteSlotsAction(this, Result, LatentInfo)); + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, + new FDeleteAllSlotsAction(this, Result, LatentInfo)); } } } -bool USaveManager::IsSlotSaved(FName SlotName) const +USaveSlot* USaveManager::PreloadSlot(FName SlotName) { - return FFileAdapter::DoesFileExist(SlotName.ToString()); + USaveSlot* Slot = nullptr; + const FString NameStr = SlotName.ToString(); + Slot = FSEFileHelpers::LoadFileSync(NameStr, nullptr, true, this); + return Slot; } -USavePreset* USaveManager::SetActivePreset(TSubclassOf PresetClass) -{ - // We can only change a preset if we have no tasks running - if (HasTasks() || !PresetClass.Get()) - { - return nullptr; - } - - // If We have a preset and its already of the same class, dont do anything - if (ActivePreset && ActivePreset->GetClass() == PresetClass) - { - return nullptr; - } - - ActivePreset = NewObject(this, PresetClass); - return ActivePreset; -} - -const USavePreset* USaveManager::GetPreset() const +bool USaveManager::IsSlotSaved(FName SlotName) const { - if (IsValid(ActivePreset)) - { - return ActivePreset; - } - return GetDefault(); + return FSEFileHelpers::FileExists(SlotName.ToString()); } bool USaveManager::CanLoadOrSave() { const AGameModeBase* GameMode = UGameplayStatics::GetGameMode(this); - if (GameMode && !GameMode->HasAuthority()) { return false; @@ -261,29 +469,32 @@ bool USaveManager::CanLoadOrSave() return IsValid(GetWorld()); } -void USaveManager::TryInstantiateInfo(bool bForced) +void USaveManager::SetActiveSlot(USaveSlot* NewSlot) { - if (IsInSlot() && !bForced) - return; - - const USavePreset* Preset = GetPreset(); - - UClass* InfoClass = Preset->SlotInfoClass.Get(); - if (!InfoClass) - InfoClass = USlotInfo::StaticClass(); + ActiveSlot = NewSlot; + // TODO: Ensure data is not null here +} - UClass* DataClass = Preset->SlotDataClass.Get(); - if (!DataClass) - DataClass = USlotData::StaticClass(); +void USaveManager::AssureActiveSlot(TSubclassOf ActiveSlotClass, bool bForced) +{ + if (HasActiveSlot() && !bForced) + return; - CurrentInfo = NewObject(GetTransientPackage(), InfoClass); - CurrentData = NewObject(GetTransientPackage(), DataClass); + if (!ActiveSlotClass) + { + ActiveSlotClass = GetDefault()->ActiveSlot.Get(); + if (!ActiveSlotClass) + { + ActiveSlotClass = USaveSlot::StaticClass(); + } + } + SetActiveSlot(NewObject(this, ActiveSlotClass)); } void USaveManager::UpdateLevelStreamings() { UWorld* World = GetWorld(); - if(!World) + if (!World) { return; } @@ -308,47 +519,20 @@ void USaveManager::SerializeStreamingLevel(ULevelStreaming* LevelStreaming) { if (!LevelStreaming->GetLoadedLevel()->bIsBeingRemoved) { - CreateTask()->Setup(LevelStreaming)->Start(); + CreateTask().Setup(LevelStreaming).Start(); } } void USaveManager::DeserializeStreamingLevel(ULevelStreaming* LevelStreaming) { - CreateTask()->Setup(LevelStreaming)->Start(); + CreateTask().Setup(LevelStreaming).Start(); } -USlotInfo* USaveManager::LoadInfo(FName SlotName) +void USaveManager::FinishTask(FSEDataTask* Task) { - if (SlotName.IsNone()) - { - SELog(GetPreset(), "Invalid Slot. Cant go under 0 or exceed MaxSlots", true); - return nullptr; - } - - auto& Task = MTTasks.CreateTask(this, SlotName) - .OnFinished([](auto& Task) - { - Task->AfterFinish(); - }); - Task.StartSynchronousTask(); - - check(Task.IsDone()); - - const auto& Infos = Task->GetLoadedSlots(); - return Infos.Num() > 0? Infos[0] : nullptr; -} - -USlotDataTask* USaveManager::CreateTask(TSubclassOf TaskType) -{ - USlotDataTask* Task = NewObject(this, TaskType.Get()); - Task->Prepare(CurrentData, *GetPreset()); - Tasks.Add(Task); - return Task; -} - -void USaveManager::FinishTask(USlotDataTask* Task) -{ - Tasks.Remove(Task); + Tasks.RemoveAll([Task](auto& TaskPtr) { + return TaskPtr.Get() == Task; + }); // Start next task if (Tasks.Num() > 0) @@ -357,36 +541,22 @@ void USaveManager::FinishTask(USlotDataTask* Task) } } -FName USaveManager::GetSlotNameFromId(const int32 SlotId) const -{ - if (const auto* Preset = GetPreset()) - { - FName Name; - Preset->BPGetSlotNameFromId(SlotId, Name); - return Name; - } - return FName{ FString::FromInt(SlotId) }; -} - bool USaveManager::IsLoading() const { - return HasTasks() && - (Tasks[0]->IsA() || Tasks[0]->IsA()); + return HasTasks() && Tasks[0]->Type == ESETaskType::Load; } void USaveManager::Tick(float DeltaTime) { if (Tasks.Num()) { - USlotDataTask* Task = Tasks[0]; + FSEDataTask* Task = Tasks[0].Get(); check(Task); if (Task->IsRunning()) { Task->Tick(DeltaTime); } } - - MTTasks.Tick(); } void USaveManager::SubscribeForEvents(const TScriptInterface& Interface) @@ -399,12 +569,13 @@ void USaveManager::UnsubscribeFromEvents(const TScriptInterfacetemplate Implements()); // C++ event @@ -416,12 +587,13 @@ void USaveManager::OnSaveBegan(const FSELevelFilter& Filter) }); } -void USaveManager::OnSaveFinished(const FSELevelFilter& Filter, const bool bError) +void USaveManager::OnSaveFinished(const bool bError) { TRACE_CPUPROFILER_EVENT_SCOPE(USaveManager::OnSaveFinished); - IterateSubscribedInterfaces([&Filter, bError](auto* Object) - { + // TODO: Needs reworking + FSELevelFilter Filter; + IterateSubscribedInterfaces([&Filter, bError](auto* Object) { check(Object->template Implements()); // C++ event @@ -434,16 +606,16 @@ void USaveManager::OnSaveFinished(const FSELevelFilter& Filter, const bool bErro if (!bError) { - OnGameSaved.Broadcast(CurrentInfo); + OnGameSaved.Broadcast(ActiveSlot); } } -void USaveManager::OnLoadBegan(const FSELevelFilter& Filter) +void USaveManager::OnLoadBegan() { TRACE_CPUPROFILER_EVENT_SCOPE(USaveManager::OnLoadBegan); - - IterateSubscribedInterfaces([&Filter](auto* Object) - { + + FSELevelFilter Filter; + IterateSubscribedInterfaces([&Filter](auto* Object) { check(Object->template Implements()); // C++ event @@ -455,12 +627,12 @@ void USaveManager::OnLoadBegan(const FSELevelFilter& Filter) }); } -void USaveManager::OnLoadFinished(const FSELevelFilter& Filter, const bool bError) +void USaveManager::OnLoadFinished(const bool bError) { TRACE_CPUPROFILER_EVENT_SCOPE(USaveManager::OnLoadFinished); - IterateSubscribedInterfaces([&Filter, bError](auto* Object) - { + FSELevelFilter Filter; + IterateSubscribedInterfaces([&Filter, bError](auto* Object) { check(Object->template Implements()); // C++ event @@ -473,20 +645,20 @@ void USaveManager::OnLoadFinished(const FSELevelFilter& Filter, const bool bErro if (!bError) { - OnGameLoaded.Broadcast(CurrentInfo); + OnGameLoaded.Broadcast(ActiveSlot); } } void USaveManager::OnMapLoadStarted(const FString& MapName) { - SELog(GetPreset(), "Loading Map '" + MapName + "'", FColor::Purple); + SELog(ActiveSlot, "Loading Map '" + MapName + "'", FColor::Purple); } void USaveManager::OnMapLoadFinished(UWorld* LoadedWorld) { - if(auto* ActiveLoader = Cast(Tasks.Num() ? Tasks[0] : nullptr)) + if (IsLoading()) { - ActiveLoader->OnMapLoaded(); + static_cast(Tasks[0].Get())->OnMapLoaded(); } UpdateLevelStreamings(); @@ -502,3 +674,64 @@ UWorld* USaveManager::GetWorld() const return GetGameInstance()->GetWorld(); } + +inline void USaveManager::BPSaveSlot(const USaveSlot* Slot, bool bScreenshot, const FScreenshotSize Size, + ESEContinueOrFail& Result, struct FLatentActionInfo LatentInfo, bool bOverrideIfNeeded) +{ + if (!Slot) + { + Result = ESEContinueOrFail::Failed; + return; + } + BPSaveSlotByName(Slot->Name, bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); +} + +void USaveManager::BPLoadSlot(const USaveSlot* Slot, ESEContinueOrFail& Result, FLatentActionInfo LatentInfo) +{ + if (!Slot) + { + Result = ESEContinueOrFail::Failed; + return; + } + BPLoadSlotByName(Slot->Name, Result, MoveTemp(LatentInfo)); +} + +void USaveManager::IterateSubscribedInterfaces(TFunction&& Callback) +{ + for (const TScriptInterface& Interface : SubscribedInterfaces) + { + if (UObject* const Object = Interface.GetObject()) + { + Callback(Object); + } + } +} + +USaveManager* USaveManager::Get(const UWorld* World) +{ + if (World) + { + return UGameInstance::GetSubsystem(World->GetGameInstance()); + } + return nullptr; +} +USaveManager* USaveManager::Get(const UObject* Context) +{ + return USaveManager::Get( + GEngine->GetWorldFromContextObject(Context, EGetWorldErrorMode::LogAndReturnNull)); +} + +bool USaveManager::IsTickable() const +{ + return !HasAnyFlags(RF_ClassDefaultObject) && IsValid(this); +} + +UWorld* USaveManager::GetTickableGameObjectWorld() const +{ + return bTickWithGameWorld ? GetWorld() : nullptr; +} + +TStatId USaveManager::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(USaveManager, STATGROUP_Tickables); +} diff --git a/Source/SaveExtension/Private/SavePreset.cpp b/Source/SaveExtension/Private/SavePreset.cpp deleted file mode 100644 index a8d9665..0000000 --- a/Source/SaveExtension/Private/SavePreset.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SavePreset.h" - -#include "LevelFilter.h" -#include "SlotData.h" -#include "SlotInfo.h" - - -USavePreset::USavePreset() - : Super() - , SlotInfoClass(USlotInfo::StaticClass()) - , SlotDataClass(USlotData::StaticClass()) -{} - -void USavePreset::BPGetSlotNameFromId_Implementation(int32 Id, FName& Name) const -{ - // Call C++ inheritance chain by default - return GetSlotNameFromId(Id, Name); -} - -void USavePreset::GetSlotNameFromId(int32 Id, FName& Name) const -{ - if (IsValidId(Id)) - { - Name = FName{ FString::FromInt(Id) }; - } -} - -FSELevelFilter USavePreset::ToFilter() const -{ - FSELevelFilter Filter{}; - Filter.FromPreset(*this); - return Filter; -} diff --git a/Source/SaveExtension/Private/SaveSlot.cpp b/Source/SaveExtension/Private/SaveSlot.cpp new file mode 100644 index 0000000..bfba652 --- /dev/null +++ b/Source/SaveExtension/Private/SaveSlot.cpp @@ -0,0 +1,177 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "SaveSlot.h" + +#include "SEFileHelpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +void USaveSlot::PostInitProperties() +{ + Super::PostInitProperties(); +} + +void USaveSlot::Serialize(FArchive& Ar) +{ + Super::Serialize(Ar); + + bool bHasThumbnail = IsValid(Thumbnail); + Ar << bHasThumbnail; + if (bHasThumbnail) + { + TArray64 ThumbnailData; + if (Ar.IsLoading()) + { + Ar << ThumbnailData; + Thumbnail = FImageUtils::ImportBufferAsTexture2D(ThumbnailData); + } + else + { + uint8* MipData = + static_cast(Thumbnail->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_ONLY)); + check(MipData != nullptr); + int64 MipDataSize = Thumbnail->GetPlatformData()->Mips[0].BulkData.GetBulkDataSize(); + + FImageView MipImage(MipData, Thumbnail->GetPlatformData()->SizeX, + Thumbnail->GetPlatformData()->SizeY, 1, ERawImageFormat::BGRA8, EGammaSpace::sRGB); + FImageUtils::CompressImage(ThumbnailData, TEXT("PNG"), MipImage); + Thumbnail->GetPlatformData()->Mips[0].BulkData.Unlock(); + Ar << ThumbnailData; + } + } +} + +void USaveSlot::CaptureThumbnail( + FSEOnThumbnailCaptured Callback, const int32 Width /*= 640*/, const int32 Height /*= 360*/) +{ + if (!GEngine || bCapturingThumbnail) + { + Callback.ExecuteIfBound(false); + return; + } + + auto* Viewport = GEngine->GameViewport ? GEngine->GameViewport->Viewport : nullptr; + if (!Viewport) + { + Callback.ExecuteIfBound(false); + return; + } + + FHighResScreenshotConfig& HighResScreenshotConfig = GetHighResScreenshotConfig(); + HighResScreenshotConfig.SetHDRCapture(false); + // Set Screenshot Resolution + GScreenshotResolutionX = Width; + GScreenshotResolutionY = Height; + GEngine->GameViewport->OnScreenshotCaptured().AddUObject(this, &USaveSlot::OnThumbnailCaptured); + bCapturingThumbnail = Viewport->TakeHighResScreenShot(); + if (!bCapturingThumbnail) + { + GEngine->GameViewport->OnScreenshotCaptured().RemoveAll(this); + Callback.ExecuteIfBound(false); + } + else + { + CapturedThumbnailDelegate = MoveTemp(Callback); + } +} + +USaveSlotData* USaveSlot::AssureData() +{ + if (!IsValid(Data)) + { + AssignData(NewObject(this, DataClass, NAME_None)); + } + return Data; +} + +bool USaveSlot::ShouldDeserializeAsync() const +{ + return MultithreadedSerialization == ESEAsyncMode::LoadAsync || + MultithreadedSerialization == ESEAsyncMode::SaveAndLoadAsync; +} +bool USaveSlot::ShouldSerializeAsync() const +{ + return MultithreadedSerialization == ESEAsyncMode::SaveAsync || + MultithreadedSerialization == ESEAsyncMode::SaveAndLoadAsync; +} + +ESEAsyncMode USaveSlot::GetFrameSplitSerialization() const +{ + return FrameSplittedSerialization; +} +float USaveSlot::GetMaxFrameMs() const +{ + return MaxFrameMs; +} + +bool USaveSlot::IsFrameSplitLoad() const +{ + return !ShouldDeserializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::LoadAsync || + FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); +} +bool USaveSlot::IsFrameSplitSave() const +{ + return !ShouldSerializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::SaveAsync || + FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); +} + +bool USaveSlot::ShouldLoadFileAsync() const +{ + return MultithreadedFiles == ESEAsyncMode::LoadAsync || + MultithreadedFiles == ESEAsyncMode::SaveAndLoadAsync; +} +bool USaveSlot::ShouldSaveFileAsync() const +{ + return MultithreadedFiles == ESEAsyncMode::SaveAsync || + MultithreadedFiles == ESEAsyncMode::SaveAndLoadAsync; +} + +bool USaveSlot::IsLoadingOrSaving() const +{ + return HasAnyInternalFlags(EInternalObjectFlags::Async); +} + +void USaveSlot::GetLevelFilter_Implementation(bool bIsLoading, FSELevelFilter& OutFilter) const +{ + OnGetLevelFilter(bIsLoading, OutFilter); +} + +void USaveSlot::OnGetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) const +{ + OutFilter.ActorFilter = ActorFilter; + OutFilter.ComponentFilter = ComponentFilter; +} + +void USaveSlot::OnThumbnailCaptured(int32 InSizeX, int32 InSizeY, const TArray& InImageData) +{ + if (GEngine->GameViewport) + { + GEngine->GameViewport->OnScreenshotCaptured().RemoveAll(this); + } + + Thumbnail = UTexture2D::CreateTransient(InSizeX, InSizeY, PF_B8G8R8A8); +#if WITH_EDITORONLY_DATA + Thumbnail->DeferCompression = true; +#endif + FColor* TextureData = + static_cast(Thumbnail->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE)); + for (int32 i = 0; i < InImageData.Num(); ++i, ++TextureData) + { + *TextureData = InImageData[i]; + } + Thumbnail->GetPlatformData()->Mips[0].BulkData.Unlock(); + Thumbnail->UpdateResource(); + + + bCapturingThumbnail = false; + CapturedThumbnailDelegate.ExecuteIfBound(true); + CapturedThumbnailDelegate = {}; +} diff --git a/Source/SaveExtension/Private/SaveSlotData.cpp b/Source/SaveExtension/Private/SaveSlotData.cpp new file mode 100644 index 0000000..8cf4397 --- /dev/null +++ b/Source/SaveExtension/Private/SaveSlotData.cpp @@ -0,0 +1,65 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "SaveSlotData.h" + +#include +#include + + +///////////////////////////////////////////////////// +// USaveSlotData + +void USaveSlotData::Serialize(FArchive& Ar) +{ + Super::Serialize(Ar); + Ar << TimeSeconds; + Ar << GameInstance; + RootLevel.Serialize(Ar); + Ar << SubLevels; +} + +void USaveSlotData::CleanRecords(bool bKeepSublevels) +{ + // Clean Up serialization data + GameInstance = {}; + + RootLevel.CleanRecords(); + if (!bKeepSublevels) + { + SubLevels.Empty(); + } +} + +FPlayerRecord& USaveSlotData::FindOrAddPlayerRecord(const FUniqueNetIdRepl& UniqueId) +{ + return Players[Players.AddUnique(FPlayerRecord(UniqueId))]; +} + +FPlayerRecord* USaveSlotData::FindPlayerRecord(const FUniqueNetIdRepl& UniqueId) +{ + const int32 Index = Players.IndexOfByPredicate([&UniqueId](const FPlayerRecord& Record) { + return Record.UniqueId == UniqueId; + }); + if (Index != INDEX_NONE) + { + return &Players[Index]; + } + return nullptr; +} + +bool USaveSlotData::FindPlayerRecord(const FUniqueNetIdRepl& UniqueId, FPlayerRecord& Record) +{ + if (FPlayerRecord* FoundRecord = FindPlayerRecord(UniqueId)) + { + Record = *FoundRecord; + return true; + } + return false; +} + +bool USaveSlotData::RemovePlayerRecord(const FUniqueNetIdRepl& UniqueId) +{ + return Players.RemoveAll([&UniqueId](const FPlayerRecord& Record) { + return Record.UniqueId == UniqueId; + }) > 0; +} diff --git a/Source/SaveExtension/Private/Serialization/LevelRecords.cpp b/Source/SaveExtension/Private/Serialization/LevelRecords.cpp index 99c7066..eb9ac3a 100644 --- a/Source/SaveExtension/Private/Serialization/LevelRecords.cpp +++ b/Source/SaveExtension/Private/Serialization/LevelRecords.cpp @@ -1,26 +1,20 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "Serialization/LevelRecords.h" -#include "SlotData.h" + +#include "SaveSlotData.h" ///////////////////////////////////////////////////// // LevelRecords -const FName FPersistentLevelRecord::PersistentName{ "Persistent" }; +const FName FPersistentLevelRecord::PersistentName{"Persistent"}; bool FLevelRecord::Serialize(FArchive& Ar) { Super::Serialize(Ar); - Ar << bOverrideGeneralFilter; - if (bOverrideGeneralFilter) - { - static UScriptStruct* const LevelFilterType{ FSELevelFilter::StaticStruct() }; - LevelFilterType->SerializeItem(Ar, &Filter, nullptr); - } - Ar << LevelScript; Ar << Actors; diff --git a/Source/SaveExtension/Private/Serialization/MTTask.cpp b/Source/SaveExtension/Private/Serialization/MTTask.cpp deleted file mode 100644 index cb4369d..0000000 --- a/Source/SaveExtension/Private/Serialization/MTTask.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/MTTask.h" diff --git a/Source/SaveExtension/Private/Serialization/MTTask_SerializeActors.cpp b/Source/SaveExtension/Private/Serialization/MTTask_SerializeActors.cpp deleted file mode 100644 index e2e8836..0000000 --- a/Source/SaveExtension/Private/Serialization/MTTask_SerializeActors.cpp +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/MTTask_SerializeActors.h" -#include -#include - -#include "SaveManager.h" -#include "SlotInfo.h" -#include "SlotData.h" -#include "SavePreset.h" -#include "Serialization/SEArchive.h" - - -///////////////////////////////////////////////////// -// FMTTask_SerializeActors -void FMTTask_SerializeActors::DoWork() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::DoWork); - if (bStoreGameInstance) - { - SerializeGameInstance(); - } - - for (int32 I = 0; I < Num; ++I) - { - const AActor* const Actor = (*LevelActors)[StartIndex + I]; - if (Actor && Filter.ShouldSave(Actor)) - { - FActorRecord& Record = ActorRecords.AddDefaulted_GetRef(); - SerializeActor(Actor, Record); - } - } -} - -void FMTTask_SerializeActors::SerializeGameInstance() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeGameInstance); - if (UGameInstance* GameInstance = World->GetGameInstance()) - { - FObjectRecord Record{ GameInstance }; - - //Serialize into Record Data - FMemoryWriter MemoryWriter(Record.Data, true); - FSEArchive Archive(MemoryWriter, false); - GameInstance->Serialize(Archive); - - SlotData->GameInstance = MoveTemp(Record); - } -} - -bool FMTTask_SerializeActors::SerializeActor(const AActor* Actor, FActorRecord& Record) const -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActor); - - //Clean the record - Record = { Actor }; - - Record.bHiddenInGame = Actor->IsHidden(); - Record.bIsProcedural = Filter.IsProcedural(Actor); - - if (Filter.StoresTags(Actor)) - { - Record.Tags = Actor->Tags; - } - else - { - // Only save save-tags - for (const auto& Tag : Actor->Tags) - { - if (Filter.IsSaveTag(Tag)) - { - Record.Tags.Add(Tag); - } - } - } - - if (Filter.StoresTransform(Actor)) - { - Record.Transform = Actor->GetTransform(); - - if (Filter.StoresPhysics(Actor)) - { - USceneComponent* const Root = Actor->GetRootComponent(); - if (Root && Root->Mobility == EComponentMobility::Movable) - { - if (auto* const Primitive = Cast(Root)) - { - Record.LinearVelocity = Primitive->GetPhysicsLinearVelocity(); - Record.AngularVelocity = Primitive->GetPhysicsAngularVelocityInRadians(); - } - else - { - Record.LinearVelocity = Root->GetComponentVelocity(); - } - } - } - } - - if (Filter.bStoreComponents) - { - SerializeActorComponents(Actor, Record, 1); - } - - TRACE_CPUPROFILER_EVENT_SCOPE(Serialize); - FMemoryWriter MemoryWriter(Record.Data, true); - FSEArchive Archive(MemoryWriter, false); - const_cast(Actor)->Serialize(Archive); - - return true; -} - -void FMTTask_SerializeActors::SerializeActorComponents(const AActor* Actor, FActorRecord& ActorRecord, int8 Indent /*= 0*/) const -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActorComponents); - - const TSet& Components = Actor->GetComponents(); - for (auto* Component : Components) - { - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActorComponents|Component); - if (Filter.ShouldSave(Component)) - { - FComponentRecord ComponentRecord; - ComponentRecord.Name = Component->GetFName(); - ComponentRecord.Class = Component->GetClass(); - - if (Filter.StoresTransform(Component)) - { - const USceneComponent* Scene = CastChecked(Component); - if (Scene->Mobility == EComponentMobility::Movable) - { - ComponentRecord.Transform = Scene->GetRelativeTransform(); - } - } - - if (Filter.StoresTags(Component)) - { - ComponentRecord.Tags = Component->ComponentTags; - } - - if (!Component->GetClass()->IsChildOf()) - { - FMemoryWriter MemoryWriter(ComponentRecord.Data, true); - FSEArchive Archive(MemoryWriter, false); - Component->Serialize(Archive); - } - ActorRecord.ComponentRecords.Add(ComponentRecord); - } - } -} diff --git a/Source/SaveExtension/Private/Serialization/Records.cpp b/Source/SaveExtension/Private/Serialization/Records.cpp index 1faf389..eda15fc 100644 --- a/Source/SaveExtension/Private/Serialization/Records.cpp +++ b/Source/SaveExtension/Private/Serialization/Records.cpp @@ -1,7 +1,16 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "Serialization/Records.h" -#include "SlotData.h" + +#include "ClassFilter.h" +#include "SaveExtension.h" +#include "SaveSlotData.h" +#include "Serialization/SEArchive.h" + +#include +#include +#include +#include ///////////////////////////////////////////////////// @@ -66,7 +75,7 @@ bool FActorRecord::Serialize(FArchive& Ar) // Reduce memory footprint to 1 bool if not moving bool bIsMoving = Ar.IsSaving() && (!LinearVelocity.IsNearlyZero() || !AngularVelocity.IsNearlyZero()); Ar << bIsMoving; - if(bIsMoving) + if (bIsMoving) { Ar << LinearVelocity; Ar << AngularVelocity; @@ -74,3 +83,252 @@ bool FActorRecord::Serialize(FArchive& Ar) Ar << ComponentRecords; return true; } + + +FSubsystemRecord::FSubsystemRecord(const USubsystem* Subsystem) : Super(Subsystem) {} + + +bool FPlayerRecord::operator==(const FPlayerRecord& Other) const +{ + return UniqueId == Other.UniqueId; +} + + +const FName SERecords::TagNoTransform{"!SaveTransform"}; +const FName SERecords::TagNoPhysics{"!SavePhysics"}; +const FName SERecords::TagNoTags{"!SaveTags"}; + + +void SERecords::SerializeActor( + const AActor* Actor, FActorRecord& Record, const FSEClassFilter& ComponentFilter) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActor); + + Record = FActorRecord{Actor}; + + Record.bHiddenInGame = Actor->IsHidden(); + Record.bIsProcedural = Actor->HasAnyFlags(RF_WasLoaded | RF_LoadCompleted); + + if (StoresTags(Actor)) + { + Record.Tags = Actor->Tags; + } + else // only save save-tags + { + for (const auto& Tag : Actor->Tags) + { + if (IsSaveTag(Tag)) + { + Record.Tags.Add(Tag); + } + } + } + + if (StoresTransform(Actor)) + { + Record.Transform = Actor->GetTransform(); + if (StoresPhysics(Actor)) + { + USceneComponent* const Root = Actor->GetRootComponent(); + if (Root && Root->Mobility == EComponentMobility::Movable) + { + if (auto* const Primitive = Cast(Root)) + { + Record.LinearVelocity = Primitive->GetPhysicsLinearVelocity(); + Record.AngularVelocity = Primitive->GetPhysicsAngularVelocityInRadians(); + } + else + { + Record.LinearVelocity = Root->GetComponentVelocity(); + } + } + } + } + + if (ComponentFilter.IsAnyAllowed()) + { + for (auto* Component : Actor->GetComponents()) + { + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActor | Component); + if (IsValid(Component) && ComponentFilter.IsAllowed(Component->GetClass())) + { + FComponentRecord& ComponentRecord = Record.ComponentRecords.Add_GetRef({Component}); + if (const auto* SceneComp = Cast(Component)) + { + if (SceneComp->Mobility == EComponentMobility::Movable) + { + ComponentRecord.Transform = SceneComp->GetRelativeTransform(); + } + } + + if (StoresTags(Component)) + { + ComponentRecord.Tags = Component->ComponentTags; + } + + if (Component->GetClass()->IsChildOf()) + { + continue; + } + + FMemoryWriter MemoryWriter(ComponentRecord.Data, true); + FSEArchive Archive(MemoryWriter, false); + Component->Serialize(Archive); + } + } + } + + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActor | Serialize); + FMemoryWriter MemoryWriter(Record.Data, true); + FSEArchive Archive(MemoryWriter, false); + const_cast(Actor)->Serialize(Archive); +} + +bool SERecords::DeserializeActor( + AActor* Actor, const FActorRecord& Record, const FSEClassFilter& ComponentFilter) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeActor); + + if (Actor->GetClass() != Record.Class) + { + UE_LOG(LogSaveExtension, Log, TEXT("Actor '{}' exists but class doesn't match"), Record.Name); + return false; + } + + Actor->Tags = Record.Tags; + + if (StoresTransform(Actor)) + { + Actor->SetActorTransform(Record.Transform); + if (StoresPhysics(Actor)) + { + USceneComponent* Root = Actor->GetRootComponent(); + if (auto* Primitive = Cast(Root)) + { + Primitive->SetPhysicsLinearVelocity(Record.LinearVelocity); + Primitive->SetPhysicsAngularVelocityInRadians(Record.AngularVelocity); + } + else + { + Root->ComponentVelocity = Record.LinearVelocity; + } + } + } + + Actor->SetActorHiddenInGame(Record.bHiddenInGame); + + TRACE_CPUPROFILER_EVENT_SCOPE(UFSEDataTask_Load::DeserializeActorComponents); + + if (ComponentFilter.IsAnyAllowed()) + { + for (auto* Component : Actor->GetComponents()) + { + TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeActor | Component); + if (!IsValid(Component) || !ComponentFilter.IsAllowed(Component->GetClass())) + { + continue; + } + + // Find the record + const FComponentRecord* ComponentRecord = Record.ComponentRecords.FindByKey(Component); + if (!ComponentRecord) + { + continue; // Record not found. + } + + if (USceneComponent* SceneComp = Cast(Component)) + { + if (SceneComp->Mobility == EComponentMobility::Movable) + { + SceneComp->SetRelativeTransform(ComponentRecord->Transform); + } + } + + Component->ComponentTags = ComponentRecord->Tags; + + if (!Component->GetClass()->IsChildOf()) + { + FMemoryReader MemoryReader(ComponentRecord->Data, true); + FSEArchive Archive(MemoryReader, false); + Component->Serialize(Archive); + } + } + } + + TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeActor | Deserialize); + FMemoryReader MemoryReader(Record.Data, true); + FSEArchive Archive(MemoryReader, false); + Actor->Serialize(Archive); + return true; +} + +void SERecords::SerializePlayer( + const APlayerState* PlayerState, FPlayerRecord& Record, const FSEClassFilter& ComponentFilter) +{ + check(PlayerState); + + APlayerController* PC = PlayerState->GetPlayerController(); + APawn* Pawn = PlayerState->GetPawn(); + + Record.UniqueId = PlayerState->GetUniqueId(); + SERecords::SerializeActor(PlayerState, Record.PlayerState, ComponentFilter); + if (Pawn) + { + SERecords::SerializeActor(Pawn, Record.Pawn, ComponentFilter); + } + if (PC) + { + SERecords::SerializeActor(PC, Record.Controller, ComponentFilter); + } +} + +void SERecords::DeserializePlayer( + APlayerState* PlayerState, const FPlayerRecord& Record, const FSEClassFilter& ComponentFilter) +{ + check(PlayerState); + check(PlayerState->GetUniqueId() == Record.UniqueId); + + APlayerController* PC = PlayerState->GetPlayerController(); + APawn* Pawn = PlayerState->GetPawn(); + + SERecords::DeserializeActor(PlayerState, Record.PlayerState, ComponentFilter); + if (Pawn) + { + SERecords::DeserializeActor(Pawn, Record.Pawn, ComponentFilter); + } + if (PC) + { + SERecords::DeserializeActor(PC, Record.Controller, ComponentFilter); + } +} + + +bool SERecords::IsSaveTag(const FName& Tag) +{ + return Tag == TagNoTransform || Tag == TagNoPhysics || Tag == TagNoTags; +} + +bool SERecords::StoresTransform(const AActor* Actor) +{ + return Actor->IsRootComponentMovable() && !Actor->ActorHasTag(TagNoTransform); +} + +bool SERecords::StoresPhysics(const AActor* Actor) +{ + return !Actor->ActorHasTag(TagNoPhysics); +} + +bool SERecords::StoresTags(const AActor* Actor) +{ + return !Actor->ActorHasTag(TagNoTags); +} + +bool SERecords::IsProcedural(const AActor* Actor) +{ + return Actor->HasAnyFlags(RF_WasLoaded | RF_LoadCompleted); +} + +bool SERecords::StoresTags(const UActorComponent* Component) +{ + return !Component->ComponentHasTag(TagNoTags); +} diff --git a/Source/SaveExtension/Private/Serialization/SEArchive.cpp b/Source/SaveExtension/Private/Serialization/SEArchive.cpp index 4255783..71eb8dc 100644 --- a/Source/SaveExtension/Private/Serialization/SEArchive.cpp +++ b/Source/SaveExtension/Private/Serialization/SEArchive.cpp @@ -1,9 +1,11 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "Serialization/SEArchive.h" + #include + ///////////////////////////////////////////////////// // FSEArchive @@ -45,7 +47,7 @@ FArchive& FSEArchive::operator<<(UObject*& Obj) if (Obj) { // Serialize the fully qualified object name - FString SavedString{ Obj->GetPathName() }; + FString SavedString{Obj->GetPathName()}; InnerArchive << SavedString; /*bool bIsLocallyOwned = IsObjectOwned(Obj); @@ -57,7 +59,7 @@ FArchive& FSEArchive::operator<<(UObject*& Obj) } else { - FString SavedString{ "" }; + FString SavedString{""}; InnerArchive << SavedString; /*bool bIsLocallyOwned = false; diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask.cpp new file mode 100644 index 0000000..0dd607e --- /dev/null +++ b/Source/SaveExtension/Private/Serialization/SEDataTask.cpp @@ -0,0 +1,64 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Serialization/SEDataTask.h" + +#include "SaveManager.h" +#include "SaveSlotData.h" + + +///////////////////////////////////////////////////// +// USaveDataTask + +FSEDataTask& FSEDataTask::Start() +{ + // If not running and first task is this + if (!bRunning && Manager->Tasks.Num() > 0 && Manager->Tasks[0].Get() == this) + { + bRunning = true; + OnStart(); + } + return *this; +} + +void FSEDataTask::Finish(bool bSuccess) +{ + if (bRunning) + { + OnFinish(bSuccess); + Manager->FinishTask(this); + bFinished = true; + bSucceeded = bSuccess; + } +} + +bool FSEDataTask::IsScheduled() const +{ + return Manager->Tasks.ContainsByPredicate([this](auto& Task) { + return Task.Get() == this; + }); +} + +FLevelRecord* FSEDataTask::FindLevelRecord(USaveSlotData& Data, const ULevelStreaming* Level) const +{ + if (Level) + { + return Data.SubLevels.FindByKey(Level); + } + return &Data.RootLevel; +} + +UWorld* FSEDataTask::GetWorld() const +{ + return Manager->GetWorld(); +} + +FString FSEDataTask::GetWorldName(const UWorld* World) +{ + check(World); + const FString MapName = World->GetOutermost()->GetName(); + if (World->IsPlayInEditor()) + { + return UWorld::RemovePIEPrefix(MapName); + } + return MapName; +} \ No newline at end of file diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp new file mode 100644 index 0000000..1b0e6fc --- /dev/null +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp @@ -0,0 +1,528 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Serialization/SEDataTask_Load.h" + +#include "SEFileHelpers.h" +#include "SaveExtension.h" +#include "SaveManager.h" +#include "SaveSlot.h" +#include "SaveSlotData.h" +#include "Serialization/Records.h" +#include "Serialization/SEArchive.h" + +#include +#include +#include +#include +#include + + +///////////////////////////////////////////////////// +// USaveDataTask_Loader + +FSEDataTask_Load::FSEDataTask_Load(USaveManager* Manager, USaveSlot* Slot) + : FSEDataTask(Manager, ESETaskType::Load) + , SlotData(Slot->GetData()) + , MaxFrameMs(Slot->GetMaxFrameMs()) +{} + +FSEDataTask_Load::~FSEDataTask_Load() +{ + if (!LoadFileTask.IsCompleted()) + { + LoadFileTask.Wait(); + } +} + +void FSEDataTask_Load::OnStart() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::OnStart); + + Slot = Manager->PreloadSlot(SlotName); + SELog(Slot, "Loading from Slot " + SlotName.ToString()); + if (!Slot) + { + SELog(Slot, "Slot Info not found! Can't load.", FColor::White, true, 1); + Finish(false); + return; + } + + // We load data while the map opens or GC runs + StartLoadingFile(); + + const UWorld* World = GetWorld(); + + // Cross-Level loading + // TODO: Handle empty Map as empty world + FName CurrentMapName{GetWorldName(World)}; + if (CurrentMapName != Slot->Map) + { + LoadState = ELoadDataTaskState::LoadingMap; + FString MapToOpen = Slot->Map.ToString(); + if (!GEngine->MakeSureMapNameIsValid(MapToOpen)) + { + UE_LOG(LogSaveExtension, Warning, + TEXT("Slot '%s' was saved in map '%s' but it did not exist while loading. Corrupted save " + "file?"), + *Slot->Name.ToString(), *MapToOpen); + Finish(false); + return; + } + + UGameplayStatics::OpenLevel(Manager, FName{MapToOpen}); + + SELog(Slot, + "Slot '" + SlotName.ToString() + "' is recorded on another Map. Loading before charging slot.", + FColor::White, false, 1); + return; + } + else if (CheckFileLoaded()) + { + StartDeserialization(); + } + else + { + LoadState = ELoadDataTaskState::WaitingForData; + } +} + +void FSEDataTask_Load::Tick(float DeltaTime) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::Tick); + switch (LoadState) + { + case ELoadDataTaskState::Deserializing: + if (CurrentLevel.IsValid()) + { + DeserializeASyncLoop(); + } + break; + + case ELoadDataTaskState::WaitingForData: + if (CheckFileLoaded()) + { + StartDeserialization(); + } + } +} + +void FSEDataTask_Load::OnFinish(bool bSuccess) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::OnFinish); + if (bSuccess) + { + SELog(Slot, "Finished Loading", FColor::Green); + } + + // Execute delegates + Delegate.ExecuteIfBound((bSuccess) ? Slot : nullptr); + + Manager->OnLoadFinished(!bSuccess); +} + +void FSEDataTask_Load::OnMapLoaded() +{ + if (LoadState != ELoadDataTaskState::LoadingMap) + { + return; + } + + const UWorld* World = GetWorld(); + if (!World) + { + UE_LOG(LogSaveExtension, Warning, TEXT("Failed loading map from saved slot.")); + Finish(false); + } + const FName NewMapName{GetWorldName(World)}; + if (NewMapName == Slot->Map) + { + if (CheckFileLoaded()) + { + StartDeserialization(); + } + else + { + LoadState = ELoadDataTaskState::WaitingForData; + } + } +} + +void FSEDataTask_Load::StartDeserialization() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::StartDeserialization); + check(Slot); + + LoadState = ELoadDataTaskState::Deserializing; + + if (!SlotData) + { + // Failed to load data + Finish(false); + return; + } + + Slot->Stats.LoadDate = FDateTime::Now(); + + // Apply current Info if succeeded + Manager->SetActiveSlot(Slot); + + Manager->OnLoadBegan(); + + BeforeDeserialize(); + + if (Slot->IsFrameSplitLoad()) + DeserializeASync(); + else + DeserializeSync(); +} + +void FSEDataTask_Load::StartLoadingFile() +{ + LoadFileTask = FSEFileHelpers::LoadFile(SlotName.ToString(), Slot, true, Manager); + if (!Slot->ShouldLoadFileAsync()) + { + LoadFileTask.Wait(); + CheckFileLoaded(); + } +} + +bool FSEDataTask_Load::CheckFileLoaded() +{ + if (LoadFileTask.IsCompleted()) + { + Slot = LoadFileTask.GetResult(); + SlotData = Slot->GetData(); + return true; + } + return false; +} + +void FSEDataTask_Load::BeforeDeserialize() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::BeforeDeserialize); + + SubsystemFilter = Slot->SubsystemFilter; + SubsystemFilter.BakeAllowedClasses(); + + UWorld* World = GetWorld(); + + // Set current game time to the saved value + World->TimeSeconds = SlotData->TimeSeconds; + + auto* GameInstance = GetWorld()->GetGameInstance(); + if (IsValid(GameInstance) && Slot->bStoreGameInstance) + { + if (GameInstance->GetClass() == SlotData->GameInstance.Class) + { + // Serialize from Record Data + FMemoryReader MemoryReader(SlotData->GameInstance.Data, true); + FSEArchive Archive(MemoryReader, false); + GameInstance->Serialize(Archive); + } + + for (const FSubsystemRecord& SubsystemRecord : SlotData->GameInstanceSubsystems) + { + if (SubsystemRecord.IsValid() && SubsystemFilter.IsAllowed(SubsystemRecord.Class)) + { + if (USubsystem* Subsystem = GameInstance->GetSubsystemBase(SubsystemRecord.Class)) + { + FMemoryReader SubsystemMemoryReader(SubsystemRecord.Data, true); + FSEArchive Ar(SubsystemMemoryReader, false); + Subsystem->Serialize(Ar); + } + } + } + } + + for (const FSubsystemRecord& SubsystemRecord : SlotData->WorldSubsystems) + { + if (SubsystemRecord.IsValid() && SubsystemFilter.IsAllowed(SubsystemRecord.Class)) + { + if (USubsystem* Subsystem = World->GetSubsystemBase(SubsystemRecord.Class)) + { + FMemoryReader SubsystemMemoryReader(SubsystemRecord.Data, true); + FSEArchive Ar(SubsystemMemoryReader, false); + Subsystem->Serialize(Ar); + } + } + } +} + +void FSEDataTask_Load::DeserializeSync() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::DeserializeSync); + + const UWorld* World = GetWorld(); + check(World); + + SELog(Slot, "World '" + World->GetName() + "'", FColor::Green, false, 1); + + PrepareAllLevels(); + + // Deserialize world + { + DeserializeLevelSync(World->GetCurrentLevel()); + + const TArray& Levels = World->GetStreamingLevels(); + for (const ULevelStreaming* Level : Levels) + { + if (Level->IsLevelLoaded()) + { + DeserializeLevelSync(Level->GetLoadedLevel(), Level); + } + } + } + + FinishedDeserializing(); +} + +void FSEDataTask_Load::DeserializeLevelSync(const ULevel* Level, const ULevelStreaming* StreamingLevel) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::DeserializeLevelSync); + + if (!IsValid(Level)) + { + return; + } + + const FName LevelName = + StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; + SELog(Slot, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); + + const FLevelRecord& LevelRecord = *FindLevelRecord(*SlotData, StreamingLevel); + for (const auto& RecordToActor : LevelRecord.RecordsToActors) + { + const FActorRecord* Record = RecordToActor.Key; + AActor* Actor = RecordToActor.Value.Get(); + check(Record && Actor); + SERecords::DeserializeActor(Actor, *Record, LevelRecord.Filter.ComponentFilter); + } +} + +void FSEDataTask_Load::DeserializeASync() +{ + // Deserialize world + { + SELog(Slot, "World '" + GetWorld()->GetName() + "'", FColor::Green, false, 1); + + PrepareAllLevels(); + DeserializeLevelASync(GetWorld()->GetCurrentLevel()); + } +} + +void FSEDataTask_Load::DeserializeLevelASync(ULevel* Level, ULevelStreaming* StreamingLevel) +{ + check(IsValid(Level)); + + const FName LevelName = + StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; + SELog(Slot, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); + + FLevelRecord* LevelRecord = FindLevelRecord(*SlotData, StreamingLevel); + if (!LevelRecord) + { + Finish(false); + return; + } + + const float StartMS = GetTimeMilliseconds(); + + CurrentLevel = Level; + CurrentSLevel = StreamingLevel; + CurrentActorIndex = 0; + + DeserializeASyncLoop(StartMS); +} + +void FSEDataTask_Load::DeserializeASyncLoop(float StartMS) +{ + if (StartMS <= 0) + { + StartMS = GetTimeMilliseconds(); + } + + FLevelRecord& LevelRecord = *FindLevelRecord(*SlotData, CurrentSLevel.Get()); + + // Continue Iterating actors every tick + for (; CurrentActorIndex < LevelRecord.RecordsToActors.Num(); ++CurrentActorIndex) + { + auto& RecordToActor = LevelRecord.RecordsToActors[CurrentActorIndex]; + + const FActorRecord* Record = RecordToActor.Key; + AActor* Actor = RecordToActor.Value.Get(); + check(Record); + if (!Actor) + { + continue; + } + SERecords::DeserializeActor(Actor, *Record, LevelRecord.Filter.ComponentFilter); + + const float CurrentMS = GetTimeMilliseconds(); + if (CurrentMS - StartMS >= MaxFrameMs) + { + // If x milliseconds passed, stop and continue on next frame + return; + } + } + + ULevelStreaming* CurrentLevelStreaming = CurrentSLevel.Get(); + FindNextAsyncLevel(CurrentLevelStreaming); + if (CurrentLevelStreaming) + { + // Iteration has ended. Deserialize next level + CurrentLevel = CurrentLevelStreaming->GetLoadedLevel(); + if (CurrentLevel.IsValid()) + { + DeserializeLevelASync(CurrentLevel.Get(), CurrentLevelStreaming); + return; + } + } + + // All levels deserialized + FinishedDeserializing(); +} + +void FSEDataTask_Load::PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::PrepareLevel); + + Slot->GetLevelFilter(true, LevelRecord.Filter); + LevelRecord.Filter.BakeAllowedClasses(); + + // Records not contained in Scene Actors => Actors to be Respawned + // Scene Actors not contained in loaded records => Actors to be Destroyed + // The rest => Just deserialize + + TArray ActorRecordsToSpawn; + ActorRecordsToSpawn.Reserve(LevelRecord.Actors.Num()); + for (FActorRecord& Record : LevelRecord.Actors) + { + ActorRecordsToSpawn.Add(&Record); + } + + TArray ActorsToDestroy{}; + { // Filter actors by whether they should be destroyed or spawned - O(M*Log(N)) + for (AActor* const Actor : Level->Actors) + { + if (UNLIKELY(!Actor)) + { + continue; + } + + const int32 Index = ActorRecordsToSpawn.IndexOfByPredicate([Actor](auto* Record) { + return *Record == Actor; + }); + if (Index != INDEX_NONE) // Actor found, therefore doesn't need to be spawned + { + if (LevelRecord.Filter.Stores(Actor)) + { + FActorRecord* Record = ActorRecordsToSpawn[Index]; + LevelRecord.RecordsToActors.Add({Record, Actor}); + } + ActorRecordsToSpawn.RemoveAtSwap(Index, 1, false); + } + else if (LevelRecord.Filter.Stores(Actor)) + { + ActorsToDestroy.Add(Actor); + } + // TODO: Consider unmatching class actors to be respawned + } + } + + // The serializable actors that were not found will be destroyed + for (AActor* Actor : ActorsToDestroy) + { + Actor->Destroy(); + } + + // Spawn Actors that don't exist but were saved + ActorRecordsToSpawn.Shrink(); + RespawnActors(ActorRecordsToSpawn, Level, LevelRecord); +} + +void FSEDataTask_Load::FinishedDeserializing() +{ + // Clean serialization data + SlotData->CleanRecords(true); + Slot->AssignData(SlotData); + Finish(true); +} + +void FSEDataTask_Load::PrepareAllLevels() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::PrepareAllLevels); + + const UWorld* World = GetWorld(); + check(World); + + // Prepare root level + PrepareLevel(World->GetCurrentLevel(), SlotData->RootLevel); + + // Prepare other loaded sub-levels + const TArray& Levels = World->GetStreamingLevels(); + for (const ULevelStreaming* Level : Levels) + { + if (Level->IsLevelLoaded()) + { + FLevelRecord* LevelRecord = FindLevelRecord(*SlotData, Level); + if (LevelRecord) + { + PrepareLevel(Level->GetLoadedLevel(), *LevelRecord); + } + } + } +} + +void FSEDataTask_Load::RespawnActors( + const TArray& Records, const ULevel* Level, FLevelRecord& LevelRecord) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::RespawnActors); + + FActorSpawnParameters SpawnInfo{}; + SpawnInfo.OverrideLevel = const_cast(Level); + SpawnInfo.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; + + UWorld* const World = GetWorld(); + + // Respawn all procedural actors + for (auto* Record : Records) + { + SpawnInfo.Name = Record->Name; + auto* NewActor = World->SpawnActor(Record->Class, &Record->Transform, SpawnInfo); + + // We update the name on the record in case it changed + Record->Name = NewActor->GetFName(); + LevelRecord.RecordsToActors.Add({Record, NewActor}); + } +} + +void FSEDataTask_Load::FindNextAsyncLevel(ULevelStreaming*& OutLevelStreaming) const +{ + OutLevelStreaming = nullptr; + + const UWorld* World = GetWorld(); + const TArray& Levels = World->GetStreamingLevels(); + if (CurrentLevel.IsValid() && Levels.Num() > 0) + { + if (!CurrentSLevel.IsValid()) + { + // Current is persistent, get first streaming level + OutLevelStreaming = Levels[0]; + return; + } + else + { + int32 CurrentIndex = Levels.IndexOfByKey(CurrentSLevel); + if (CurrentIndex != INDEX_NONE && Levels.Num() > CurrentIndex + 1) + { + OutLevelStreaming = Levels[CurrentIndex + 1]; + } + } + } + + // If this level is unloaded, find next + if (OutLevelStreaming && !OutLevelStreaming->IsLevelLoaded()) + { + FindNextAsyncLevel(OutLevelStreaming); + } +} diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_LoadLevel.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_LoadLevel.cpp new file mode 100644 index 0000000..5d0174a --- /dev/null +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_LoadLevel.cpp @@ -0,0 +1,74 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Serialization/SEDataTask_LoadLevel.h" + +#include "SaveSlot.h" + + +///////////////////////////////////////////////////// +// USaveDataTask_LevelLoader + +void FSEDataTask_LoadLevel::OnStart() +{ + if (!SlotData || !StreamingLevel || !StreamingLevel->IsLevelLoaded()) + { + Finish(false); + return; + } + + FLevelRecord* LevelRecord = FindLevelRecord(*SlotData, StreamingLevel); + if (!LevelRecord) + { + Finish(false); + return; + } + + PrepareLevel(StreamingLevel->GetLoadedLevel(), *LevelRecord); + + if (Slot->IsFrameSplitLoad()) + { + DeserializeLevelASync(StreamingLevel->GetLoadedLevel(), StreamingLevel); + } + else + { + DeserializeLevelSync(StreamingLevel->GetLoadedLevel(), StreamingLevel); + FinishedDeserializing(); + return; + } +} + +void FSEDataTask_LoadLevel::DeserializeASyncLoop(float StartMS /*= 0.0f*/) +{ + + if (StartMS <= 0) + { + StartMS = GetTimeMilliseconds(); + } + + FLevelRecord& LevelRecord = *FindLevelRecord(*SlotData, CurrentSLevel.Get()); + + // Continue Iterating actors every tick + for (; CurrentActorIndex < LevelRecord.RecordsToActors.Num(); ++CurrentActorIndex) + { + auto& RecordToActor = LevelRecord.RecordsToActors[CurrentActorIndex]; + + const FActorRecord* Record = RecordToActor.Key; + AActor* Actor = RecordToActor.Value.Get(); + check(Record); + if (!Actor) + { + continue; + } + SERecords::DeserializeActor(Actor, *Record, LevelRecord.Filter.ComponentFilter); + + const float CurrentMS = GetTimeMilliseconds(); + if (CurrentMS - StartMS >= MaxFrameMs) + { + // If x milliseconds passed, stop and continue on next frame + return; + } + } + + // All levels deserialized + FinishedDeserializing(); +} diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp new file mode 100644 index 0000000..bd0b104 --- /dev/null +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp @@ -0,0 +1,290 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Serialization/SEDataTask_Save.h" + +#include "SEFileHelpers.h" +#include "SaveExtension.h" +#include "SaveManager.h" +#include "SaveSlot.h" +#include "SaveSlotData.h" +#include "Serialization/Records.h" +#include "Serialization/SEArchive.h" + +#include +#include +#include +#include + + +///////////////////////////////////////////////////// +// FSEDataTask_Save + +FSEDataTask_Save::FSEDataTask_Save(USaveManager* Manager, USaveSlot* Slot) + : FSEDataTask(Manager, ESETaskType::Save) + , Slot(Slot) + , SlotData(Slot->AssureData()) +{} + +FSEDataTask_Save::~FSEDataTask_Save() +{ + if (!SaveFileTask.IsCompleted()) + { + SaveFileTask.Wait(); + } +} + +void FSEDataTask_Save::OnStart() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::OnStart); + Manager->AssureActiveSlot(); + + bool bSave = true; + const FString SlotNameStr = SlotName.ToString(); + // Overriding + { + const bool bFileExists = FSEFileHelpers::FileExists(SlotNameStr); + if (bOverride) + { + // Delete previous save + if (bFileExists) + { + FSEFileHelpers::DeleteFile(SlotNameStr); + } + } + else + { + // Only save if previous files don't exist + // We don't want to serialize since it won't be saved anyway + bSave = !bFileExists; + } + } + + if (!bSave) + { + Finish(false); + return; + } + + const UWorld* World = GetWorld(); + + Manager->OnSaveBegan(); + + check(SlotData->GetClass() == Slot->DataClass); + SlotData->CleanRecords(true); + + check(Slot && SlotData); + + const bool bSlotExisted = Slot->Name == SlotName; + Slot->Name = SlotName; + + if (bCaptureThumbnail) + { + bWaitingThumbnail = true; + Slot->CaptureThumbnail(FSEOnThumbnailCaptured::CreateLambda([this](bool bSuccess) { + bWaitingThumbnail = false; + }), + Width, Height); + } + + // Time stats + { + FSaveSlotStats& Stats = Slot->Stats; + Stats.SaveDate = FDateTime::Now(); + + // If this info has been loaded ever + const bool bWasLoaded = Stats.LoadDate.GetTicks() > 0; + if (bWasLoaded) + { + // Now - Loaded + const FTimespan SessionTime = Stats.SaveDate - Stats.LoadDate; + Stats.PlayedTime += SessionTime; + Stats.SlotPlayedTime = bSlotExisted ? (Stats.SlotPlayedTime + SessionTime) : SessionTime; + } + else + { + // Slot is new, played time is world seconds + Stats.PlayedTime = FTimespan::FromSeconds(World->TimeSeconds); + Stats.SlotPlayedTime = Stats.PlayedTime; + } + + // Save current game seconds + SlotData->TimeSeconds = World->TimeSeconds; + } + + // Save Level info + Slot->Map = FName{GetWorldName(World)}; + + SerializeWorld(); + + if (!bWaitingThumbnail) // Tick will check if thumbnail is not ready + { + SaveFile(); + } +} + +void FSEDataTask_Save::Tick(float DeltaTime) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::Tick); + FSEDataTask::Tick(DeltaTime); + + if (SaveFileTask.IsValid() && SaveFileTask.IsCompleted()) + { + Finish(SaveFileTask.GetResult()); + } + else if (!SaveFileTask.IsValid() && !bWaitingThumbnail) + { + SaveFile(); + } +} + +void FSEDataTask_Save::OnFinish(bool bSuccess) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::OnFinish); + if (bSuccess) + { + // Clean serialization data + SlotData->CleanRecords(true); + + SELog(Slot, "Finished Saving", FColor::Green); + } + + // Execute delegates + Delegate.ExecuteIfBound((Manager && bSuccess) ? Manager->GetActiveSlot() : nullptr); + + Manager->OnSaveFinished(!bSuccess); +} + +void FSEDataTask_Save::SerializeWorld() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SerializeWorld); + + // Must have Authority + const UWorld* World = GetWorld(); + if (!World->GetAuthGameMode()) + { + return; + } + + SELog(Slot, "World '" + World->GetName() + "'", FColor::Green, false, 1); + + SubsystemFilter = Slot->SubsystemFilter; + SubsystemFilter.BakeAllowedClasses(); + + const TArray& Levels = World->GetStreamingLevels(); + PrepareAllLevels(Levels); + + { // Serialization + UGameInstance* GameInstance = World->GetGameInstance(); + if (GameInstance && Slot->bStoreGameInstance) + { + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeGameInstance); + FObjectRecord Record{GameInstance}; + FMemoryWriter MemoryWriter(Record.Data, true); + FSEArchive Archive(MemoryWriter, false); + GameInstance->Serialize(Archive); + SlotData->GameInstance = MoveTemp(Record); + + SlotData->GameInstanceSubsystems.Reset(); + for (UGameInstanceSubsystem* Subsystem : + GameInstance->GetSubsystemArray()) + { + if (SubsystemFilter.IsAllowed(Subsystem->GetClass())) + { + auto& SubsystemRecord = SlotData->GameInstanceSubsystems.Add_GetRef({Subsystem}); + FMemoryWriter SubsystemMemoryWriter(SubsystemRecord.Data, true); + FSEArchive Ar(SubsystemMemoryWriter, false); + Subsystem->Serialize(Ar); + } + } + } + + SlotData->WorldSubsystems.Reset(); + for (UWorldSubsystem* Subsystem : World->GetSubsystemArray()) + { + if (SubsystemFilter.IsAllowed(Subsystem->GetClass())) + { + auto& SubsystemRecord = SlotData->WorldSubsystems.Add_GetRef({Subsystem}); + FMemoryWriter SubsystemMemoryWriter(SubsystemRecord.Data, true); + FSEArchive Ar(SubsystemMemoryWriter, false); + Subsystem->Serialize(Ar); + } + } + + SerializeLevel(World->GetCurrentLevel()); + for (const ULevelStreaming* Level : Levels) + { + if (Level->IsLevelLoaded()) + { + SerializeLevel(Level->GetLoadedLevel(), Level); + } + } + } +} + +void FSEDataTask_Save::PrepareAllLevels(const TArray& Levels) +{ + // Prepare root level + PrepareLevel(GetWorld()->GetCurrentLevel(), SlotData->RootLevel); + + // Create the sub-level records if non existent + for (const ULevelStreaming* Level : Levels) + { + if (Level->IsLevelLoaded()) + { + FLevelRecord& LevelRecord = SlotData->SubLevels.Add_GetRef({*Level}); + PrepareLevel(Level->GetLoadedLevel(), LevelRecord); + } + } +} + +void FSEDataTask_Save::PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord) +{ + Slot->GetLevelFilter(true, LevelRecord.Filter); + LevelRecord.Filter.BakeAllowedClasses(); +} + +void FSEDataTask_Save::SerializeLevel(const ULevel* Level, const ULevelStreaming* StreamingLevel) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SerializeLevel); + check(IsValid(Level)); + + const FName LevelName = + StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; + SELog(Slot, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); + + // Find level record. By default, main level + auto& LevelRecord = StreamingLevel ? *FindLevelRecord(*SlotData, StreamingLevel) : SlotData->RootLevel; + const FSELevelFilter& Filter = LevelRecord.Filter; + + LevelRecord.CleanRecords(); // Empty level record before serializing it + + TArray ActorsToSerialize; + for (AActor* Actor : Level->Actors) + { + if (Actor && Filter.Stores(Actor)) + { + ActorsToSerialize.Add(Actor); + } + } + LevelRecord.Actors.SetNum(ActorsToSerialize.Num()); + + ParallelFor( + ActorsToSerialize.Num(), + [&LevelRecord, &ActorsToSerialize, &Filter](int32 i) { + SERecords::SerializeActor(ActorsToSerialize[i], LevelRecord.Actors[i], Filter.ComponentFilter); + }, + Slot->ShouldSerializeAsync() ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); +} + +void FSEDataTask_Save::SaveFile() +{ + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SaveFile); + SaveFileTask = + FSEFileHelpers::SaveFile(Manager->GetActiveSlot(), SlotName.ToString(), Slot->bUseCompression); + + if (!Slot->ShouldSaveFileAsync()) + { + SaveFileTask.Wait(); + Finish(SaveFileTask.GetResult()); + } +} diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp new file mode 100644 index 0000000..32c59e4 --- /dev/null +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp @@ -0,0 +1,35 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "Serialization/SEDataTask_SaveLevel.h" + +#include "SaveExtension.h" + + +///////////////////////////////////////////////////// +// FSaveDataTask_LevelSaver + +void FSEDataTask_SaveLevel::OnStart() +{ + if (SlotData && StreamingLevel && StreamingLevel->IsLevelLoaded()) + { + FLevelRecord* LevelRecord = FindLevelRecord(*SlotData, StreamingLevel); + if (!LevelRecord) + { + Finish(false); + return; + } + + PrepareLevel(StreamingLevel->GetLoadedLevel(), *LevelRecord); + + SerializeLevel(StreamingLevel->GetLoadedLevel(), StreamingLevel); + + Finish(true); + return; + } + Finish(false); +} + +void FSEDataTask_SaveLevel::OnFinish(bool bSuccess) +{ + SELog(Slot, "Finished Serializing level", FColor::Green); +} diff --git a/Source/SaveExtension/Private/Serialization/SlotDataTask.cpp b/Source/SaveExtension/Private/Serialization/SlotDataTask.cpp deleted file mode 100644 index a5cd2de..0000000 --- a/Source/SaveExtension/Private/Serialization/SlotDataTask.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/SlotDataTask.h" - -#include "SaveManager.h" -#include "SavePreset.h" - - -///////////////////////////////////////////////////// -// USaveDataTask - -USlotDataTask* USlotDataTask::Start() -{ - const USaveManager* Manager = GetManager(); - - // If not running and first task is this - if (!bRunning && Manager->Tasks.Num() > 0 && Manager->Tasks[0] == this) - { - bRunning = true; - OnStart(); - } - return this; -} - -void USlotDataTask::Finish(bool bSuccess) -{ - if (bRunning) - { - OnFinish(bSuccess); - MarkAsGarbage(); - GetManager()->FinishTask(this); - bFinished = true; - bSucceeded = bSuccess; - } -} - -bool USlotDataTask::IsScheduled() const -{ - return GetManager()->Tasks.Contains(this); -} - -USaveManager* USlotDataTask::GetManager() const -{ - return Cast(GetOuter()); -} - -void USlotDataTask::BakeAllFilters() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask::BakeAllFilters); - SlotData->GeneralLevelFilter.BakeAllowedClasses(); - - if(SlotData->MainLevel.bOverrideGeneralFilter) - { - SlotData->MainLevel.Filter.BakeAllowedClasses(); - } - - for(const auto& Level : SlotData->SubLevels) - { - if(Level.bOverrideGeneralFilter) - { - Level.Filter.BakeAllowedClasses(); - } - } -} - -const FSELevelFilter& USlotDataTask::GetGeneralFilter() const -{ - check(SlotData); - return SlotData->GeneralLevelFilter; -} - -const FSELevelFilter& USlotDataTask::GetLevelFilter(const FLevelRecord& Level) const -{ - if(Level.bOverrideGeneralFilter) - { - return Level.Filter; - } - return GetGeneralFilter(); -} - -FLevelRecord* USlotDataTask::FindLevelRecord(const ULevelStreaming* Level) const -{ - if (!Level) - return &SlotData->MainLevel; - else // Find the Sub-Level - return SlotData->SubLevels.FindByKey(Level); -} - -UWorld* USlotDataTask::GetWorld() const -{ - return GetOuter()->GetWorld(); -} diff --git a/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelLoader.cpp b/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelLoader.cpp deleted file mode 100644 index 362e2de..0000000 --- a/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelLoader.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/SlotDataTask_LevelLoader.h" - - -///////////////////////////////////////////////////// -// USaveDataTask_LevelLoader - -void USlotDataTask_LevelLoader::OnStart() -{ - if (SlotData && StreamingLevel && StreamingLevel->IsLevelLoaded()) - { - FLevelRecord* LevelRecord = FindLevelRecord(StreamingLevel); - if (!LevelRecord) - { - Finish(false); - return; - } - - GetLevelFilter(*LevelRecord).BakeAllowedClasses(); - - if (Preset->IsFrameSplitLoad()) - { - DeserializeLevelASync(StreamingLevel->GetLoadedLevel(), StreamingLevel); - } - else - { - DeserializeLevelSync(StreamingLevel->GetLoadedLevel(), StreamingLevel); - FinishedDeserializing(); - } - return; - } - Finish(false); -} - -void USlotDataTask_LevelLoader::DeserializeASyncLoop(float StartMS /*= 0.0f*/) -{ - FLevelRecord& LevelRecord = *FindLevelRecord(CurrentSLevel.Get()); - - if (StartMS <= 0) - { - StartMS = GetTimeMilliseconds(); - } - - const auto& Filter = GetLevelFilter(LevelRecord); - - // Continue Iterating actors every tick - for (; CurrentActorIndex < CurrentLevelActors.Num(); ++CurrentActorIndex) - { - AActor* Actor{ CurrentLevelActors[CurrentActorIndex].Get() }; - if (Actor) - { - DeserializeLevel_Actor(Actor, LevelRecord, Filter); - - const float CurrentMS = GetTimeMilliseconds(); - // If x milliseconds passed, continue on next frame - if (CurrentMS - StartMS >= MaxFrameMs) - return; - } - } - - // All levels deserialized - FinishedDeserializing(); -} diff --git a/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelSaver.cpp b/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelSaver.cpp deleted file mode 100644 index 72eae1c..0000000 --- a/Source/SaveExtension/Private/Serialization/SlotDataTask_LevelSaver.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/SlotDataTask_LevelSaver.h" - - -///////////////////////////////////////////////////// -// FSaveDataTask_LevelSaver - -void USlotDataTask_LevelSaver::OnStart() -{ - if (SlotData && StreamingLevel && StreamingLevel->IsLevelLoaded()) - { - FLevelRecord* LevelRecord = FindLevelRecord(StreamingLevel); - if (!LevelRecord) - { - Finish(false); - return; - } - - GetLevelFilter(*LevelRecord).BakeAllowedClasses(); - - const int32 NumberOfThreads = FMath::Max(1, FPlatformMisc::NumberOfWorkerThreadsToSpawn()); - SerializeLevelSync(StreamingLevel->GetLoadedLevel(), NumberOfThreads, StreamingLevel); - - RunScheduledTasks(); - - Finish(true); - return; - } - Finish(false); -} diff --git a/Source/SaveExtension/Private/Serialization/SlotDataTask_Loader.cpp b/Source/SaveExtension/Private/Serialization/SlotDataTask_Loader.cpp deleted file mode 100644 index 720a229..0000000 --- a/Source/SaveExtension/Private/Serialization/SlotDataTask_Loader.cpp +++ /dev/null @@ -1,624 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/SlotDataTask_Loader.h" - -#include -#include -#include -#include -#include - -#include "Misc/SlotHelpers.h" -#include "SavePreset.h" -#include "SaveManager.h" -#include "Serialization/SEArchive.h" - - -///////////////////////////////////////////////////// -// Helpers - -namespace Loader -{ - static int32 RemoveSingleRecordPtrSwap(TArray& Records, AActor* Actor, bool bAllowShrinking = true) - { - if(!Actor) - { - return 0; - } - - const int32 I = Records.IndexOfByPredicate([Records, Actor](auto* Record) - { - return *Record == Actor; - }); - if (I != INDEX_NONE) - { - Records.RemoveAtSwap(I, 1, bAllowShrinking); - return 1; - } - return 0; - } -} - - -///////////////////////////////////////////////////// -// USaveDataTask_Loader - -void USlotDataTask_Loader::OnStart() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::OnStart); - USaveManager* Manager = GetManager(); - - SELog(Preset, "Loading from Slot " + SlotName.ToString()); - - NewSlotInfo = Manager->LoadInfo(SlotName); - if (!NewSlotInfo) - { - SELog(Preset, "Slot Info not found! Can't load.", FColor::White, true, 1); - Finish(false); - return; - } - - // We load data while the map opens or GC runs - StartLoadingData(); - - const UWorld* World = GetWorld(); - - // Cross-Level loading - // TODO: Handle empty Map as empty world - FName CurrentMapName { FSlotHelpers::GetWorldName(World) }; - if (CurrentMapName != NewSlotInfo->Map) - { - LoadState = ELoadDataTaskState::LoadingMap; - FString MapToOpen = NewSlotInfo->Map.ToString(); - if (!GEngine->MakeSureMapNameIsValid(MapToOpen)) - { - UE_LOG(LogSaveExtension, Warning, TEXT("Slot '%s' was saved in map '%s' but it did not exist while loading. Corrupted save file?"), *NewSlotInfo->FileName.ToString(), *MapToOpen); - Finish(false); - return; - } - - UGameplayStatics::OpenLevel(this, FName{ MapToOpen }); - - SELog(Preset, "Slot '" + SlotName.ToString() + "' is recorded on another Map. Loading before charging slot.", FColor::White, false, 1); - return; - } - else if (IsDataLoaded()) - { - StartDeserialization(); - } - else - { - LoadState = ELoadDataTaskState::WaitingForData; - } -} - -void USlotDataTask_Loader::Tick(float DeltaTime) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::Tick); - switch(LoadState) - { - case ELoadDataTaskState::Deserializing: - if (CurrentLevel.IsValid()) - { - DeserializeASyncLoop(); - } - break; - - case ELoadDataTaskState::WaitingForData: - if (IsDataLoaded()) - { - StartDeserialization(); - } - } -} - -void USlotDataTask_Loader::OnFinish(bool bSuccess) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::OnFinish); - if (bSuccess) - { - SELog(Preset, "Finished Loading", FColor::Green); - } - - // Execute delegates - Delegate.ExecuteIfBound((bSuccess) ? NewSlotInfo : nullptr); - - GetManager()->OnLoadFinished( - SlotData? GetGeneralFilter() : FSELevelFilter{}, - !bSuccess - ); -} - -void USlotDataTask_Loader::BeginDestroy() -{ - if (LoadDataTask) - { - LoadDataTask->EnsureCompletion(false); - delete LoadDataTask; - } - - Super::BeginDestroy(); -} - -void USlotDataTask_Loader::OnMapLoaded() -{ - if(LoadState != ELoadDataTaskState::LoadingMap) - { - return; - } - - const UWorld* World = GetWorld(); - if(!World) - { - UE_LOG(LogSaveExtension, Warning, TEXT("Failed loading map from saved slot.")); - Finish(false); - } - const FName NewMapName { FSlotHelpers::GetWorldName(World) }; - if (NewMapName == NewSlotInfo->Map) - { - if(IsDataLoaded()) - { - StartDeserialization(); - } - else - { - LoadState = ELoadDataTaskState::WaitingForData; - } - } -} - -void USlotDataTask_Loader::StartDeserialization() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::StartDeserialization); - check(NewSlotInfo); - - LoadState = ELoadDataTaskState::Deserializing; - - SlotData = GetLoadedData(); - if (!SlotData) - { - // Failed to load data - Finish(false); - return; - } - - NewSlotInfo->LoadDate = FDateTime::Now(); - - GetManager()->OnLoadBegan(GetGeneralFilter()); - //Apply current Info if succeeded - GetManager()->__SetCurrentInfo(NewSlotInfo); - - BakeAllFilters(); - - BeforeDeserialize(); - - if (Preset->IsFrameSplitLoad()) - DeserializeASync(); - else - DeserializeSync(); -} - -void USlotDataTask_Loader::StartLoadingData() -{ - LoadDataTask = new FAsyncTask(GetManager(), SlotName.ToString()); - - if (Preset->IsMTFilesLoad()) - LoadDataTask->StartBackgroundTask(); - else - LoadDataTask->StartSynchronousTask(); -} - -USlotData* USlotDataTask_Loader::GetLoadedData() const -{ - if (IsDataLoaded()) - { - return LoadDataTask->GetTask().GetData(); - } - return nullptr; -} - -void USlotDataTask_Loader::BeforeDeserialize() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::BeforeDeserialize); - UWorld* World = GetWorld(); - - // Set current game time to the saved value - World->TimeSeconds = SlotData->TimeSeconds; - - if (SlotData->bStoreGameInstance) - { - DeserializeGameInstance(); - } -} - -void USlotDataTask_Loader::DeserializeSync() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::DeserializeSync); - - const UWorld* World = GetWorld(); - check(World); - - SELog(Preset, "World '" + World->GetName() + "'", FColor::Green, false, 1); - - PrepareAllLevels(); - - // Deserialize world - { - DeserializeLevelSync(World->GetCurrentLevel()); - - const TArray& Levels = World->GetStreamingLevels(); - for (const ULevelStreaming* Level : Levels) - { - if (Level->IsLevelLoaded()) - { - DeserializeLevelSync(Level->GetLoadedLevel(), Level); - } - } - } - - FinishedDeserializing(); -} - -void USlotDataTask_Loader::DeserializeLevelSync(const ULevel* Level, const ULevelStreaming* StreamingLevel) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::DeserializeLevelSync); - - if (!IsValid(Level)) - return; - - const FName LevelName = StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; - SELog(Preset, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); - - if (FLevelRecord* LevelRecord = FindLevelRecord(StreamingLevel)) - { - const auto& Filter = GetLevelFilter(*LevelRecord); - - for (auto ActorItr = Level->Actors.CreateConstIterator(); ActorItr; ++ActorItr) - { - auto* Actor = *ActorItr; - if (IsValid(Actor) && Filter.ShouldSave(Actor)) - { - DeserializeLevel_Actor(Actor, *LevelRecord, Filter); - } - } - } -} - -void USlotDataTask_Loader::DeserializeASync() -{ - // Deserialize world - { - SELog(Preset, "World '" + GetWorld()->GetName() + "'", FColor::Green, false, 1); - - PrepareAllLevels(); - DeserializeLevelASync(GetWorld()->GetCurrentLevel()); - } -} - -void USlotDataTask_Loader::DeserializeLevelASync(ULevel* Level, ULevelStreaming* StreamingLevel) -{ - check(IsValid(Level)); - - const FName LevelName = StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; - SELog(Preset, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); - - FLevelRecord* LevelRecord = FindLevelRecord(StreamingLevel); - if (!LevelRecord) { - Finish(false); - return; - } - - const float StartMS = GetTimeMilliseconds(); - - CurrentLevel = Level; - CurrentSLevel = StreamingLevel; - CurrentActorIndex = 0; - - // Copy actors array. New actors won't be considered for deserialization - CurrentLevelActors.Empty(Level->Actors.Num()); - for (auto* Actor : Level->Actors) - { - if(IsValid(Actor)) - { - CurrentLevelActors.Add(Actor); - } - } - - DeserializeASyncLoop(StartMS); -} - -void USlotDataTask_Loader::DeserializeASyncLoop(float StartMS) -{ - FLevelRecord * LevelRecord = FindLevelRecord(CurrentSLevel.Get()); - if (!LevelRecord) - { - return; - } - - const auto& Filter = GetLevelFilter(*LevelRecord); - - if(StartMS <= 0) - { - StartMS = GetTimeMilliseconds(); - } - - // Continue Iterating actors every tick - for (; CurrentActorIndex < CurrentLevelActors.Num(); ++CurrentActorIndex) - { - AActor* const Actor{ CurrentLevelActors[CurrentActorIndex].Get() }; - if (IsValid(Actor) && Filter.ShouldSave(Actor)) - { - DeserializeLevel_Actor(Actor, *LevelRecord, Filter); - - const float CurrentMS = GetTimeMilliseconds(); - // If x milliseconds passed, stop and continue on next frame - if (CurrentMS - StartMS >= MaxFrameMs) - { - return; - } - } - } - - ULevelStreaming* CurrentLevelStreaming = CurrentSLevel.Get(); - FindNextAsyncLevel(CurrentLevelStreaming); - if (CurrentLevelStreaming) - { - // Iteration has ended. Deserialize next level - CurrentLevel = CurrentLevelStreaming->GetLoadedLevel(); - if (CurrentLevel.IsValid()) - { - DeserializeLevelASync(CurrentLevel.Get(), CurrentLevelStreaming); - return; - } - } - - // All levels deserialized - FinishedDeserializing(); -} - -void USlotDataTask_Loader::PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::PrepareLevel); - - const auto& Filter = GetLevelFilter(LevelRecord); - - - // Records not contained in Scene Actors => Actors to be Respawned - // Scene Actors not contained in loaded records => Actors to be Destroyed - // The rest => Just deserialize - - TArray ActorsToSpawn; - ActorsToSpawn.Reserve(LevelRecord.Actors.Num()); - for(FActorRecord& Record : LevelRecord.Actors) - { - ActorsToSpawn.Add(&Record); - } - - TArray ActorsToDestroy{}; - { - // O(M*Log(N)) - for (AActor* const Actor : Level->Actors) - { - // Remove records which actors do exist - const bool bFoundActorRecord = Loader::RemoveSingleRecordPtrSwap(ActorsToSpawn, Actor, false) > 0; - - if (Actor && Filter.ShouldSave(Actor)) - { - if (!bFoundActorRecord) // Don't destroy level actors - { - // If the actor wasn't found, mark it for destruction - Actor->Destroy(); - } - } - } - ActorsToSpawn.Shrink(); - } - - // Create Actors that doesn't exist now but were saved - RespawnActors(ActorsToSpawn, Level); -} - -void USlotDataTask_Loader::FinishedDeserializing() -{ - // Clean serialization data - SlotData->CleanRecords(true); - GetManager()->__SetCurrentData(SlotData); - - Finish(true); -} - -void USlotDataTask_Loader::PrepareAllLevels() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::PrepareAllLevels); - - const UWorld* World = GetWorld(); - check(World); - - // Prepare Main level - PrepareLevel(World->GetCurrentLevel(), SlotData->MainLevel); - - // Prepare other loaded sub-levels - const TArray& Levels = World->GetStreamingLevels(); - for (const ULevelStreaming* Level : Levels) - { - if (Level->IsLevelLoaded()) - { - FLevelRecord* LevelRecord = FindLevelRecord(Level); - if (LevelRecord) - { - PrepareLevel(Level->GetLoadedLevel(), *LevelRecord); - } - } - } -} - -void USlotDataTask_Loader::RespawnActors(const TArray& Records, const ULevel* Level) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::RespawnActors); - - FActorSpawnParameters SpawnInfo{}; - SpawnInfo.OverrideLevel = const_cast(Level); - SpawnInfo.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; - - UWorld* const World = GetWorld(); - - // Respawn all procedural actors - for (auto* Record : Records) - { - SpawnInfo.Name = Record->Name; - auto* NewActor = World->SpawnActor(Record->Class, &Record->Transform, SpawnInfo); - - // We update the name on the record in case it changed - Record->Name = NewActor->GetFName(); - } -} - -void USlotDataTask_Loader::DeserializeLevel_Actor(AActor* const Actor, const FLevelRecord& LevelRecord, const FSELevelFilter& Filter) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::DeserializeLevel_Actor); - - // Find the record - const FActorRecord* const Record = LevelRecord.Actors.FindByKey(Actor); - if (Record && Record->IsValid() && Record->Class == Actor->GetClass()) - { - DeserializeActor(Actor, *Record, Filter); - } -} - -void USlotDataTask_Loader::DeserializeGameInstance() -{ - bool bSuccess = true; - auto* GameInstance = GetWorld()->GetGameInstance(); - const FObjectRecord& Record = SlotData->GameInstance; - - if (!IsValid(GameInstance) || GameInstance->GetClass() != Record.Class) - bSuccess = false; - - if (bSuccess) - { - //Serialize from Record Data - FMemoryReader MemoryReader(Record.Data, true); - FSEArchive Archive(MemoryReader, false); - GameInstance->Serialize(Archive); - } - - SELog(Preset, "Game Instance '" + Record.Name.ToString() + "'", FColor::Green, !bSuccess, 1); -} - -bool USlotDataTask_Loader::DeserializeActor(AActor* Actor, const FActorRecord& Record, const FSELevelFilter& Filter) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Loader::DeserializeActor); - - // Always load saved tags - Actor->Tags = Record.Tags; - - const bool bSavesPhysics = FSELevelFilter::StoresPhysics(Actor); - if (FSELevelFilter::StoresTransform(Actor)) - { - Actor->SetActorTransform(Record.Transform); - - if (FSELevelFilter::StoresPhysics(Actor)) - { - USceneComponent* Root = Actor->GetRootComponent(); - if (auto* Primitive = Cast(Root)) - { - Primitive->SetPhysicsLinearVelocity(Record.LinearVelocity); - Primitive->SetPhysicsAngularVelocityInRadians(Record.AngularVelocity); - } - else - { - Root->ComponentVelocity = Record.LinearVelocity; - } - } - } - - Actor->SetActorHiddenInGame(Record.bHiddenInGame); - - DeserializeActorComponents(Actor, Record, Filter, 2); - - { - //Serialize from Record Data - FMemoryReader MemoryReader(Record.Data, true); - FSEArchive Archive(MemoryReader, false); - Actor->Serialize(Archive); - } - - return true; -} - -void USlotDataTask_Loader::DeserializeActorComponents(AActor* Actor, const FActorRecord& ActorRecord, const FSELevelFilter& Filter, int8 Indent) -{ - if (Filter.bStoreComponents) - { - TRACE_CPUPROFILER_EVENT_SCOPE(UUSlotDataTask_Loader::DeserializeActorComponents); - - const TSet& Components = Actor->GetComponents(); - for (auto* Component : Components) - { - if (!Filter.ShouldSave(Component)) - { - continue; - } - - // Find the record - const FComponentRecord* Record = ActorRecord.ComponentRecords.FindByKey(Component); - if (!Record) - { - SELog(Preset, "Component '" + Component->GetFName().ToString() + "' - Record not found", FColor::Red, false, Indent + 1); - continue; - } - - if (FSELevelFilter::StoresTransform(Component)) - { - USceneComponent* Scene = CastChecked(Component); - if (Scene->Mobility == EComponentMobility::Movable) - { - Scene->SetRelativeTransform(Record->Transform); - } - } - - if (FSELevelFilter::StoresTags(Component)) - { - Component->ComponentTags = Record->Tags; - } - - if (!Component->GetClass()->IsChildOf()) - { - FMemoryReader MemoryReader(Record->Data, true); - FSEArchive Archive(MemoryReader, false); - Component->Serialize(Archive); - } - } - } -} - -void USlotDataTask_Loader::FindNextAsyncLevel(ULevelStreaming*& OutLevelStreaming) const -{ - OutLevelStreaming = nullptr; - - const UWorld* World = GetWorld(); - const TArray& Levels = World->GetStreamingLevels(); - if (CurrentLevel.IsValid() && Levels.Num() > 0) - { - if (!CurrentSLevel.IsValid()) - { - //Current is persistent, get first streaming level - OutLevelStreaming = Levels[0]; - return; - } - else - { - int32 CurrentIndex = Levels.IndexOfByKey(CurrentSLevel); - if (CurrentIndex != INDEX_NONE && Levels.Num() > CurrentIndex + 1) - { - OutLevelStreaming = Levels[CurrentIndex + 1]; - } - } - } - - // If this level is unloaded, find next - if (OutLevelStreaming && !OutLevelStreaming->IsLevelLoaded()) - { - FindNextAsyncLevel(OutLevelStreaming); - } -} diff --git a/Source/SaveExtension/Private/Serialization/SlotDataTask_Saver.cpp b/Source/SaveExtension/Private/Serialization/SlotDataTask_Saver.cpp deleted file mode 100644 index c67418b..0000000 --- a/Source/SaveExtension/Private/Serialization/SlotDataTask_Saver.cpp +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "Serialization/SlotDataTask_Saver.h" - -#include -#include - -#include "Misc/SlotHelpers.h" -#include "SaveManager.h" -#include "SlotInfo.h" -#include "SlotData.h" -#include "SavePreset.h" -#include "FileAdapter.h" - - -///////////////////////////////////////////////////// -// USaveDataTask_Saver - -void USlotDataTask_Saver::OnStart() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::OnStart); - USaveManager* Manager = GetManager(); - Manager->TryInstantiateInfo(); - - bool bSave = true; - const FString SlotNameStr = SlotName.ToString(); - //Overriding - { - const bool bFileExists = FFileAdapter::DoesFileExist(SlotNameStr); - if (bOverride) - { - // Delete previous save - if (bFileExists) - { - FFileAdapter::DeleteFile(SlotNameStr); - } - } - else - { - //Only save if previous files don't exist - //We don't want to serialize since it won't be saved anyway - bSave = !bFileExists; - } - } - - if (bSave) - { - const UWorld* World = GetWorld(); - - GetManager()->OnSaveBegan(GetGeneralFilter()); - - SlotInfo = Manager->GetCurrentInfo(); - SlotData = Manager->GetCurrentData(); - SlotData->CleanRecords(true); - - check(SlotInfo && SlotData); - - const bool bSlotWasDifferent = SlotInfo->FileName != SlotName; - SlotInfo->FileName = SlotName; - - if (bSaveThumbnail) - { - SlotInfo->CaptureThumbnail(Width, Height); - } - - // Time stats - { - SlotInfo->SaveDate = FDateTime::Now(); - - // If this info has been loaded ever - const bool bWasLoaded = SlotInfo->LoadDate.GetTicks() > 0; - if (bWasLoaded) - { - // Now - Loaded - const FTimespan SessionTime = SlotInfo->SaveDate - SlotInfo->LoadDate; - - SlotInfo->PlayedTime += SessionTime; - - if (!bSlotWasDifferent) - SlotInfo->SlotPlayedTime += SessionTime; - else - SlotInfo->SlotPlayedTime = SessionTime; - } - else - { - // Slot is new, played time is world seconds - SlotInfo->PlayedTime = FTimespan::FromSeconds(World->TimeSeconds); - SlotInfo->SlotPlayedTime = SlotInfo->PlayedTime; - } - - // Save current game seconds - SlotData->TimeSeconds = World->TimeSeconds; - } - - //Save Level info in both files - SlotInfo->Map = FName{ FSlotHelpers::GetWorldName(World) }; - SlotData->Map = SlotData->Map; - - SlotData->bStoreGameInstance = Preset->bStoreGameInstance; - SlotData->GeneralLevelFilter = Preset->ToFilter(); - - SerializeWorld(); - SaveFile(); - return; - } - Finish(false); -} - -void USlotDataTask_Saver::Tick(float DeltaTime) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::Tick); - Super::Tick(DeltaTime); - - if (SaveTask && SaveTask->IsDone()) - { - if (bSaveThumbnail) - { - if (SlotInfo && SlotInfo->GetThumbnail()) - { - Finish(true); - } - } - else - { - Finish(true); - } - } -} - -void USlotDataTask_Saver::OnFinish(bool bSuccess) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::OnFinish); - if (bSuccess) - { - // Clean serialization data - SlotData->CleanRecords(true); - - SELog(Preset, "Finished Saving", FColor::Green); - } - - // Execute delegates - USaveManager* Manager = GetManager(); - check(Manager); - Delegate.ExecuteIfBound((Manager && bSuccess)? Manager->GetCurrentInfo() : nullptr); - Manager->OnSaveFinished( - SlotData? GetGeneralFilter() : FSELevelFilter{}, - !bSuccess - ); -} - -void USlotDataTask_Saver::BeginDestroy() -{ - if (SaveTask) - { - SaveTask->EnsureCompletion(false); - delete SaveTask; - } - - Super::BeginDestroy(); -} - -void USlotDataTask_Saver::SerializeWorld() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::SerializeWorld); - - // Must have Authority - if (!GetWorld()->GetAuthGameMode()) - { - return; - } - - const UWorld* World = GetWorld(); - SELog(Preset, "World '" + World->GetName() + "'", FColor::Green, false, 1); - - const TArray& Levels = World->GetStreamingLevels(); - PrepareAllLevels(Levels); - - // Threads available + 1 (Synchronous Thread) - const int32 NumberOfThreads = FMath::Max(1, FPlatformMisc::NumberOfWorkerThreadsToSpawn() + 1); - const int32 TasksPerLevel = FMath::Max(1, FMath::RoundToInt(float(NumberOfThreads) / (Levels.Num() + 1))); - Tasks.Reserve(NumberOfThreads); - - SerializeLevelSync(World->GetCurrentLevel(), TasksPerLevel); - for (const ULevelStreaming* Level : Levels) - { - if (Level->IsLevelLoaded()) - { - SerializeLevelSync(Level->GetLoadedLevel(), TasksPerLevel, Level); - } - } - - RunScheduledTasks(); -} - -void USlotDataTask_Saver::PrepareAllLevels(const TArray& Levels) -{ - BakeAllFilters(); - - // Create the sub-level records if non existent - for (const ULevelStreaming* Level : Levels) - { - if (Level->IsLevelLoaded()) - { - SlotData->SubLevels.AddUnique({ *Level }); - } - } -} - -void USlotDataTask_Saver::SerializeLevelSync(const ULevel* Level, int32 AssignedTasks, const ULevelStreaming* StreamingLevel) -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::SerializeLevelSync); - check(IsValid(Level)); - - if (!Preset->IsMTSerializationSave()) - { - AssignedTasks = 1; - } - - const FName LevelName = StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; - SELog(Preset, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); - - // Find level record. By default, main level - FLevelRecord* LevelRecord = &SlotData->MainLevel; - if (StreamingLevel) - { - LevelRecord = FindLevelRecord(StreamingLevel); - } - check(LevelRecord); - - // Empty level record before serializing it - LevelRecord->CleanRecords(); - - auto& Filter = GetLevelFilter(*LevelRecord); - - const int32 MinObjectsPerTask = 40; - const int32 ActorCount = Level->Actors.Num(); - const int32 NumBalancedPerTask = FMath::CeilToInt((float) ActorCount / AssignedTasks); - const int32 NumPerTask = FMath::Max(NumBalancedPerTask, MinObjectsPerTask); - - // Split all actors between multi-threaded tasks - int32 Index = 0; - while (Index < ActorCount) - { - const int32 NumRemaining = ActorCount - Index; - const int32 NumToSerialize = FMath::Min(NumRemaining, NumPerTask); - - // First task saves the GameInstance - bool bStoreGameInstance = Index <= 0 && SlotData->bStoreGameInstance; - // Add new Task - Tasks.Emplace(FMTTask_SerializeActors - { - GetWorld(), SlotData, &Level->Actors, Index, NumToSerialize, - bStoreGameInstance, LevelRecord, Filter - }); - - Index += NumToSerialize; - } -} - -void USlotDataTask_Saver::RunScheduledTasks() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::RunScheduledTasks); - // Start all serialization tasks - if (Tasks.Num() > 0) - { - for (int32 I = 1; I < Tasks.Num(); ++I) - { - if (Preset->IsMTSerializationSave()) - Tasks[I].StartBackgroundTask(); - else - Tasks[I].StartSynchronousTask(); - } - // First task stores - Tasks[0].StartSynchronousTask(); - } - // Wait until all tasks have finished - for (auto& AsyncTask : Tasks) - { - AsyncTask.EnsureCompletion(); - } - // All tasks finished, sync data - for (auto& AsyncTask : Tasks) - { - AsyncTask.GetTask().DumpData(); - } - Tasks.Empty(); -} - -void USlotDataTask_Saver::SaveFile() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(USlotDataTask_Saver::SaveFile); - USaveManager* Manager = GetManager(); - - SaveTask = new FAsyncTask( - Manager->GetCurrentInfo(), Manager->GetCurrentData(), - SlotName.ToString(), Preset->bUseCompression); - - if (Preset->IsMTFilesSave()) - { - SaveTask->StartBackgroundTask(); - } - else - { - SaveTask->StartSynchronousTask(); - - if (!bSaveThumbnail) - { - Finish(true); - } - } -} diff --git a/Source/SaveExtension/Private/SlotData.cpp b/Source/SaveExtension/Private/SlotData.cpp deleted file mode 100644 index 5454cef..0000000 --- a/Source/SaveExtension/Private/SlotData.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SlotData.h" -#include - -#include "SavePreset.h" - - -/** - * A macro for tidying up accessing of private members, through the above code - * - * @param InClass The class being accessed (not a string, just the class, i.e. FStackTracker) - * @param InObj Pointer to an instance of the specified class - * @param MemberName Name of the member being accessed (again, not a string) - * @return The value of the member - */ -#define GET_PRIVATE(InClass, InObj, MemberName) (*InObj).*GetPrivate(InClass##MemberName##Accessor()) - -///////////////////////////////////////////////////// -// USlotData - -void USlotData::Serialize(FArchive& Ar) -{ - Super::Serialize(Ar); - - Ar << bStoreGameInstance; - Ar << GameInstance; - - static UScriptStruct* const LevelFilterType{ FSELevelFilter::StaticStruct() }; - LevelFilterType->SerializeItem(Ar, &GeneralLevelFilter, nullptr); - MainLevel.Serialize(Ar); - Ar << SubLevels; -} - -void USlotData::CleanRecords(bool bKeepSublevels) -{ - //Clean Up serialization data - GameInstance = {}; - - MainLevel.CleanRecords(); - if (!bKeepSublevels) - { - SubLevels.Empty(); - } -} diff --git a/Source/SaveExtension/Private/SlotInfo.cpp b/Source/SaveExtension/Private/SlotInfo.cpp deleted file mode 100644 index a15794c..0000000 --- a/Source/SaveExtension/Private/SlotInfo.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#include "SlotInfo.h" - -#include -#include -#include -#include -#include -#include -#include - - -UTexture2D* USlotInfo::GetThumbnail() const -{ - if (ThumbnailPath.IsEmpty()) - { - return nullptr; - } - - if (CachedThumbnail) - { - return CachedThumbnail; - } - - // Load thumbnail as Texture2D - UTexture2D* Texture{ nullptr }; - TArray RawFileData; - if (GEngine && FFileHelper::LoadFileToArray(RawFileData, *ThumbnailPath)) - { - IImageWrapperModule & ImageWrapperModule = FModuleManager::LoadModuleChecked < IImageWrapperModule >(FName("ImageWrapper")); - TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); - if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num())) - { - TArray64 UncompressedBGRA; - if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA)) - { - Texture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8); - void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); - FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num()); - Texture->GetPlatformData()->Mips[0].BulkData.Unlock(); - Texture->UpdateResource(); - } - } - } - const_cast(this)->CachedThumbnail = Texture; - return Texture; -} - -bool USlotInfo::CaptureThumbnail(const int32 Width /*= 640*/, const int32 Height /*= 360*/) -{ - if (!GEngine || !GEngine->GameViewport || FileName.IsNone()) - { - return false; - } - - if (auto* Viewport = GEngine->GameViewport->Viewport) - { - _SetThumbnailPath(FFileAdapter::GetThumbnailPath(FileName.ToString())); - - // TODO: Removal of a thumbnail should be standarized in a function - IFileManager& FM = IFileManager::Get(); - if (ThumbnailPath.Len() > 0 && FM.FileExists(*ThumbnailPath)) - { - FM.Delete(*ThumbnailPath, false, true, true); - } - - FHighResScreenshotConfig& HighResScreenshotConfig = GetHighResScreenshotConfig(); - HighResScreenshotConfig.SetHDRCapture(false); - //Set Screenshot path - HighResScreenshotConfig.FilenameOverride = ThumbnailPath; - //Set Screenshot Resolution - GScreenshotResolutionX = Width; - GScreenshotResolutionY = Height; - Viewport->TakeHighResScreenShot(); - return true; - } - return false; -} - -void USlotInfo::_SetThumbnailPath(const FString& Path) -{ - if (ThumbnailPath != Path) - { - ThumbnailPath = Path; - CachedThumbnail = nullptr; - } -} - diff --git a/Source/SaveExtension/Public/ClassFilter.h b/Source/SaveExtension/Public/ClassFilter.h new file mode 100644 index 0000000..55b6c06 --- /dev/null +++ b/Source/SaveExtension/Public/ClassFilter.h @@ -0,0 +1,62 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "ClassFilter.generated.h" + + +USTRUCT(BlueprintType) +struct SAVEEXTENSION_API FSEClassFilter +{ + GENERATED_BODY() + +private: + // Used from editor side to limit displayed classes + UPROPERTY() + UClass* BaseClass; + +public: + /** This classes are allowed (and their children) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization") + TSet> AllowedClasses; + + /** This classes are ignored (and their children) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization") + TSet> IgnoredClasses; + +protected: + UPROPERTY(Transient) + mutable TSet BakedAllowedClasses; + + +public: + FSEClassFilter() : FSEClassFilter(UObject::StaticClass()) {} + FSEClassFilter(UClass* const BaseClass); + + // Merges another filter into this one. Other has priority. + void Merge(const FSEClassFilter& Other); + + /** Bakes a set of allowed classes based on the current settings */ + void BakeAllowedClasses() const; + + bool IsAllowed(UClass* Class) const + { + // Check is a single O(1) pointer hash comparison + return BakedAllowedClasses.Contains(Class); + } + + bool IsAnyAllowed() const + { + return BakedAllowedClasses.Num() > 0; + } + + UClass* GetBaseClass() const + { + return BaseClass; + } + + FString ToString(); + void FromString(FString String); + + bool operator==(const FSEClassFilter& Other) const; +}; diff --git a/Source/SaveExtension/Public/Delegates.h b/Source/SaveExtension/Public/Delegates.h deleted file mode 100644 index 5e1ddaa..0000000 --- a/Source/SaveExtension/Public/Delegates.h +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "SlotInfo.h" - - -/** Called when game has been saved - * @param SlotInfo the saved slot. Null if save failed - */ -DECLARE_DELEGATE_OneParam(FOnGameSaved, USlotInfo*); - -/** Called when game has been loaded - * @param SlotInfo the loaded slot. Null if load failed - */ -DECLARE_DELEGATE_OneParam(FOnGameLoaded, USlotInfo*); diff --git a/Source/SaveExtension/Public/FileAdapter.h b/Source/SaveExtension/Public/FileAdapter.h deleted file mode 100644 index 238cb5c..0000000 --- a/Source/SaveExtension/Public/FileAdapter.h +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ISaveExtension.h" - - -class USavePreset; -class USlotInfo; -class USlotData; -class FMemoryReader; -class FMemoryWriter; - - -struct FScopedFileWriter -{ -private: - FArchive* Writer = nullptr; - -public: - FScopedFileWriter(FStringView Filename, int32 Flags = 0); - ~FScopedFileWriter() - { - delete Writer; - } - - FArchive& GetArchive() { return *Writer; } - bool IsValid() const { return Writer != nullptr; } - bool IsError() const { return Writer && (Writer->IsError() || Writer->IsCriticalError()); } -}; - - -struct FScopedFileReader -{ -private: - FScopedLoadingState ScopedLoadingState; - FArchive* Reader = nullptr; - -public: - FScopedFileReader(FStringView Filename, int32 Flags = 0); - ~FScopedFileReader() - { - delete Reader; - } - - FArchive& GetArchive() { return *Reader; } - bool IsValid() const { return Reader != nullptr; } -}; - - -/** Based on GameplayStatics to add multi-threading */ -struct FSaveFile -{ - int32 FileTypeTag = 0; - int32 SaveGameFileVersion = 0; - FPackageFileVersion PackageFileUEVersion {}; - FEngineVersion SavedEngineVersion; - int32 CustomVersionFormat = int32(ECustomVersionSerializationFormat::Unknown); - FCustomVersionContainer CustomVersions; - - FString InfoClassName; - TArray InfoBytes; - - FString DataClassName; - bool bIsDataCompressed = false; - TArray DataBytes; - - - FSaveFile(); - - void Empty(); - bool IsEmpty() const; - - void Read(FScopedFileReader& Reader, bool bSkipData); - void Write(FScopedFileWriter& Writer, bool bCompressData); - - void SerializeInfo(USlotInfo* SlotInfo); - void SerializeData(USlotData* SlotData); - USlotInfo* CreateAndDeserializeInfo(const UObject* Outer) const; - USlotData* CreateAndDeserializeData(const UObject* Outer) const; -}; - - -/** Based on GameplayStatics to add multi-threading */ -class SAVEEXTENSION_API FFileAdapter -{ -public: - - static bool SaveFile(FStringView SlotName, USlotInfo* Info, USlotData* Data, const bool bUseCompression); - - // Not safe for Multi-threading - static bool LoadFile(FStringView SlotName, USlotInfo*& Info, USlotData*& Data, bool bLoadData, const UObject* Outer); - - static bool DeleteFile(FStringView SlotName); - static bool DoesFileExist(FStringView SlotName); - - static const FString& GetSaveFolder(); - static FString GetSlotPath(FStringView SlotName); - static FString GetThumbnailPath(FStringView SlotName); - - static void DeserializeObject(UObject*& Object, FStringView ClassName, const UObject* Outer, const TArray& Bytes); -}; diff --git a/Source/SaveExtension/Public/ISaveExtension.h b/Source/SaveExtension/Public/ISaveExtension.h deleted file mode 100644 index 2cfd2ae..0000000 --- a/Source/SaveExtension/Public/ISaveExtension.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "Modules/ModuleManager.h" -#include "Engine/Engine.h" - -#include "SavePreset.h" - - -DECLARE_LOG_CATEGORY_EXTERN(LogSaveExtension, All, All); - -class ISaveExtension : public IModuleInterface { -public: - static inline ISaveExtension& Get() { - return FModuleManager::LoadModuleChecked("SaveExtension"); - } - static inline bool IsAvailable() { - return FModuleManager::Get().IsModuleLoaded("SaveExtension"); - } - - static void Log(const USavePreset* Preset, const FString Message, bool bError) - { - Log(Preset, Message, FColor::White, bError, 2.f); - } - - static void Log(const USavePreset* Preset, const FString Message, FColor Color = FColor::White, bool bError = false, const float Duration = 2.f) - { - if (Preset->bDebug) - { - if (bError) { - Color = FColor::Red; - } - - const FString ComposedMessage { FString::Printf(TEXT("SE: %s"), *Message) }; - - if (bError) - { - UE_LOG(LogSaveExtension, Error, TEXT("%s"), *ComposedMessage); - } - else - { - UE_LOG(LogSaveExtension, Log, TEXT("%s"), *ComposedMessage); - } - - if (Preset->bDebugInScreen && GEngine) - { - GEngine->AddOnScreenDebugMessage(-1, Duration, Color, ComposedMessage); - } - } - } -}; - -//Only log in Editor -#if WITH_EDITORONLY_DATA -template -void SELog(Args&& ...args) { ISaveExtension::Log(Forward(args)...); } -#else -template -void SELog(Args&& ...args) {} // Optimized away by compiler -#endif diff --git a/Source/SaveExtension/Public/LatentActions/DeleteSlotsAction.h b/Source/SaveExtension/Public/LatentActions/DeleteSlotsAction.h deleted file mode 100644 index fea0a52..0000000 --- a/Source/SaveExtension/Public/LatentActions/DeleteSlotsAction.h +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include - - -class USaveManager; -class USlotInfo; - -/** - * Enum used to indicate quote execution results - */ -UENUM() -enum class EDeleteSlotsResult : uint8 -{ - Completed -}; - -/** FLoadInfosction */ -class FDeleteSlotsAction : public FPendingLatentAction -{ -public: - EDeleteSlotsResult& Result; - - bool bFinished; - - FName ExecutionFunction; - int32 OutputLink; - FWeakObjectPtr CallbackTarget; - - - /** - * @param SlotId will load that Saved Game if Id > 0, otherwise it will load all infos - */ - FDeleteSlotsAction(USaveManager* Manager, EDeleteSlotsResult& OutResult, const FLatentActionInfo& LatentInfo); - - virtual void UpdateOperation(FLatentResponse& Response) override; - -#if WITH_EDITOR - // Returns a human readable description of the latent operation's current state - virtual FString GetDescription() const override - { - return TEXT("Deleting all slots..."); - } -#endif -}; diff --git a/Source/SaveExtension/Public/LatentActions/LoadGameAction.h b/Source/SaveExtension/Public/LatentActions/LoadGameAction.h deleted file mode 100644 index 444d721..0000000 --- a/Source/SaveExtension/Public/LatentActions/LoadGameAction.h +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include - - -class USaveManager; -class USlotInfo; - -/** - * Enum used to indicate quote execution results - */ -UENUM() -enum class ELoadGameResult : uint8 -{ - Loading UMETA(Hidden), - Continue, - Failed -}; - -/** FLoadGameAction */ -class FLoadGameAction : public FPendingLatentAction -{ -public: - ELoadGameResult& Result; - - FName ExecutionFunction; - int32 OutputLink; - FWeakObjectPtr CallbackTarget; - - - FLoadGameAction(USaveManager* Manager, FName SlotName, ELoadGameResult& Result, const FLatentActionInfo& LatentInfo); - - virtual void UpdateOperation(FLatentResponse& Response) override; - - void OnLoadFinished(USlotInfo* SavedSlot); - -#if WITH_EDITOR - // Returns a human readable description of the latent operation's current state - virtual FString GetDescription() const override - { - return TEXT("Loading Game..."); - } -#endif -}; diff --git a/Source/SaveExtension/Public/LatentActions/LoadInfosAction.h b/Source/SaveExtension/Public/LatentActions/LoadInfosAction.h deleted file mode 100644 index 34df5e8..0000000 --- a/Source/SaveExtension/Public/LatentActions/LoadInfosAction.h +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include - -#include "Multithreading/LoadSlotInfosTask.h" - - -class USaveManager; -class USlotInfo; - -/** - * Enum used to indicate quote execution results - */ -UENUM() -enum class ELoadInfoResult : uint8 -{ - Completed -}; - -/** FLoadInfosction */ -class FLoadInfosAction : public FPendingLatentAction -{ - -public: - ELoadInfoResult& Result; - - TArray& SlotInfos; - bool bFinished; - - FName ExecutionFunction; - int32 OutputLink; - FWeakObjectPtr CallbackTarget; - - - /** - * @param SlotId will load that Saved Game if Id > 0, otherwise it will load all infos - */ - FLoadInfosAction(USaveManager* Manager, const bool bSortByRecent, TArray& SaveInfos, ELoadInfoResult& OutResult, const FLatentActionInfo& LatentInfo); - - virtual void UpdateOperation(FLatentResponse& Response) override; - -#if WITH_EDITOR - virtual FString GetDescription() const override - { - return TEXT("Loading all infos..."); - } -#endif -}; diff --git a/Source/SaveExtension/Public/LatentActions/SaveGameAction.h b/Source/SaveExtension/Public/LatentActions/SaveGameAction.h deleted file mode 100644 index 7c669c7..0000000 --- a/Source/SaveExtension/Public/LatentActions/SaveGameAction.h +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include - - -class USaveManager; -class USlotInfo; -struct FScreenshotSize; - -/** - * Enum used to indicate quote execution results - */ -UENUM() -enum class ESaveGameResult : uint8 -{ - Saving UMETA(Hidden), - Continue, - Failed -}; - -/** FSaveGameAction */ -class FSaveGameAction : public FPendingLatentAction -{ - -public: - ESaveGameResult& Result; - - FName ExecutionFunction; - int32 OutputLink; - FWeakObjectPtr CallbackTarget; - - - FSaveGameAction(USaveManager* Manager, FName SlotName, bool bOverrideIfNeeded, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& OutResult, const FLatentActionInfo& LatentInfo); - - virtual void UpdateOperation(FLatentResponse& Response) override; - - void OnSaveFinished(USlotInfo* SavedSlot); - -#if WITH_EDITOR - // Returns a human readable description of the latent operation's current state - virtual FString GetDescription() const override - { - return TEXT("Saving Game..."); - } -#endif -}; diff --git a/Source/SaveExtension/Public/LevelFilter.h b/Source/SaveExtension/Public/LevelFilter.h index 6bf7d72..647c297 100644 --- a/Source/SaveExtension/Public/LevelFilter.h +++ b/Source/SaveExtension/Public/LevelFilter.h @@ -1,11 +1,17 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include "SavePreset.h" +#include "ClassFilter.h" + +#include +#include + #include "LevelFilter.generated.h" + class USaveManager; +class USaveSlot; /** @@ -17,101 +23,18 @@ struct FSELevelFilter { GENERATED_BODY() - static const FName TagNoTransform; - static const FName TagNoPhysics; - static const FName TagNoTags; - static const FName TagTransform; - public: + UPROPERTY(SaveGame, BlueprintReadWrite, Category = LevelFilter) + FSEClassFilter ActorFilter{AActor::StaticClass()}; - UPROPERTY(SaveGame) - FSEActorClassFilter ActorFilter; - - UPROPERTY(SaveGame) - FSEActorClassFilter LoadActorFilter; - - UPROPERTY(SaveGame) - bool bStoreComponents = false; - - UPROPERTY(SaveGame) - FSEComponentClassFilter ComponentFilter; - - UPROPERTY(SaveGame) - FSEComponentClassFilter LoadComponentFilter; - - - FSELevelFilter() {} - - void FromPreset(const USavePreset& Preset) - { - ActorFilter = Preset.GetActorFilter(true); - LoadActorFilter = Preset.GetActorFilter(false); - bStoreComponents = Preset.bStoreComponents; - ComponentFilter = Preset.GetComponentFilter(true); - LoadComponentFilter = Preset.GetComponentFilter(false); - } - - void BakeAllowedClasses() const - { - TRACE_CPUPROFILER_EVENT_SCOPE(FSELevelFilter::BakeAllowedClasses); - ActorFilter.BakeAllowedClasses(); - ComponentFilter.BakeAllowedClasses(); - LoadActorFilter.BakeAllowedClasses(); - LoadComponentFilter.BakeAllowedClasses(); - } - - bool ShouldSave(const AActor* Actor) const - { - return ActorFilter.IsClassAllowed(Actor->GetClass()); - } - - bool ShouldLoad(const AActor* Actor) const - { - return LoadActorFilter.IsClassAllowed(Actor->GetClass()); - } - - bool ShouldSave(const UActorComponent* Component) const - { - return IsValid(Component) - && ComponentFilter.IsClassAllowed(Component->GetClass()); - } - - bool ShouldLoad(const UActorComponent* Component) const - { - return IsValid(Component) - && LoadComponentFilter.IsClassAllowed(Component->GetClass()); - } - - static bool StoresTransform(const UActorComponent* Component) - { - return Component->GetClass()->IsChildOf() - && HasTag(Component, TagTransform); - } - - static bool StoresTags(const UActorComponent* Component) - { - return !HasTag(Component, TagNoTags); - } + UPROPERTY(SaveGame, BlueprintReadWrite, Category = LevelFilter) + FSEClassFilter ComponentFilter{UActorComponent::StaticClass()}; - static bool IsSaveTag(const FName& Tag) - { - return Tag == TagNoTransform || Tag == TagNoPhysics || Tag == TagNoTags; - } - static FORCEINLINE bool StoresTransform(const AActor* Actor) { return Actor->IsRootComponentMovable() && !HasTag(Actor, TagNoTransform); } - static FORCEINLINE bool StoresPhysics(const AActor* Actor) { return !HasTag(Actor, TagNoPhysics); } - static FORCEINLINE bool StoresTags(const AActor* Actor) { return !HasTag(Actor, TagNoTags); } - static FORCEINLINE bool IsProcedural(const AActor* Actor) { return Actor->HasAnyFlags(RF_WasLoaded | RF_LoadCompleted); } + FSELevelFilter() = default; - static FORCEINLINE bool HasTag(const AActor* Actor, const FName Tag) - { - check(Actor); - return Actor->ActorHasTag(Tag); - } + void BakeAllowedClasses() const; - static FORCEINLINE bool HasTag(const UActorComponent * Component, const FName Tag) - { - check(Component); - return Component->ComponentHasTag(Tag); - } + bool Stores(const AActor* Actor) const; + bool Stores(const UActorComponent* Component) const; }; diff --git a/Source/SaveExtension/Public/LevelStreamingNotifier.h b/Source/SaveExtension/Public/LevelStreamingNotifier.h index f7ffcf5..aee208c 100644 --- a/Source/SaveExtension/Public/LevelStreamingNotifier.h +++ b/Source/SaveExtension/Public/LevelStreamingNotifier.h @@ -1,16 +1,18 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once #include #include + #include "LevelStreamingNotifier.generated.h" -DECLARE_DELEGATE_OneParam(FLevelNotifierLoaded, ULevelStreaming* ); -DECLARE_DELEGATE_OneParam(FLevelNotifierUnloaded, ULevelStreaming* ); -DECLARE_DELEGATE_OneParam(FLevelNotifierShown, ULevelStreaming* ); -DECLARE_DELEGATE_OneParam(FLevelNotifierHidden, ULevelStreaming* ); + +DECLARE_DELEGATE_OneParam(FLevelNotifierLoaded, ULevelStreaming*); +DECLARE_DELEGATE_OneParam(FLevelNotifierUnloaded, ULevelStreaming*); +DECLARE_DELEGATE_OneParam(FLevelNotifierShown, ULevelStreaming*); +DECLARE_DELEGATE_OneParam(FLevelNotifierHidden, ULevelStreaming*); /** ULevelStreamingNotifier is an adapter that expands UE4's native @@ -22,7 +24,6 @@ class SAVEEXTENSION_API ULevelStreamingNotifier : public UObject GENERATED_BODY() public: - void SetLevelStreaming(ULevelStreaming* InLevelStreaming) { UnBind(); @@ -38,13 +39,24 @@ class SAVEEXTENSION_API ULevelStreamingNotifier : public UObject } } - FLevelNotifierLoaded& OnLevelLoaded() { return LoadedDelegate; } - FLevelNotifierUnloaded& OnLevelUnloaded() { return UnloadedDelegate; } - FLevelNotifierShown& OnLevelShown() { return ShownDelegate; } - FLevelNotifierHidden& OnLevelHidden() { return HiddenDelegate; } + FLevelNotifierLoaded& OnLevelLoaded() + { + return LoadedDelegate; + } + FLevelNotifierUnloaded& OnLevelUnloaded() + { + return UnloadedDelegate; + } + FLevelNotifierShown& OnLevelShown() + { + return ShownDelegate; + } + FLevelNotifierHidden& OnLevelHidden() + { + return HiddenDelegate; + } private: - void UnBind() { if (LevelStreaming.IsValid()) @@ -68,29 +80,33 @@ class SAVEEXTENSION_API ULevelStreamingNotifier : public UObject // Associated Level Streaming TWeakObjectPtr LevelStreaming; - FLevelNotifierLoaded LoadedDelegate; + FLevelNotifierLoaded LoadedDelegate; FLevelNotifierUnloaded UnloadedDelegate; - FLevelNotifierShown ShownDelegate; - FLevelNotifierHidden HiddenDelegate; + FLevelNotifierShown ShownDelegate; + FLevelNotifierHidden HiddenDelegate; UFUNCTION() - void OnLoaded() { + void OnLoaded() + { LoadedDelegate.ExecuteIfBound(LevelStreaming.Get()); } UFUNCTION() - void OnUnloaded() { + void OnUnloaded() + { UnloadedDelegate.ExecuteIfBound(LevelStreaming.Get()); } UFUNCTION() - void OnShown() { + void OnShown() + { ShownDelegate.ExecuteIfBound(LevelStreaming.Get()); } UFUNCTION() - void OnHidden() { + void OnHidden() + { HiddenDelegate.ExecuteIfBound(LevelStreaming.Get()); } }; diff --git a/Source/SaveExtension/Public/LifetimeComponent.h b/Source/SaveExtension/Public/LifetimeComponent.h index 7fe7a00..a01260b 100644 --- a/Source/SaveExtension/Public/LifetimeComponent.h +++ b/Source/SaveExtension/Public/LifetimeComponent.h @@ -1,13 +1,13 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include -#include - #include "SaveExtensionInterface.h" #include "SaveManager.h" +#include +#include + #include "LifetimeComponent.generated.h" @@ -20,7 +20,6 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FLifetimeResumeSignature); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FLifetimeFinishSignature); - /** * Controls the complete saving and loading process */ @@ -33,7 +32,6 @@ class SAVEEXTENSION_API ULifetimeComponent : public UActorComponent, public ISav /* METHODS */ /************************************************************************/ public: - ULifetimeComponent(); virtual void BeginPlay() override; @@ -47,17 +45,10 @@ class SAVEEXTENSION_API ULifetimeComponent : public UActorComponent, public ISav virtual void OnLoadFinished(const FSELevelFilter& Filter, bool bError); - USaveManager* GetManager() const - { - return USaveManager::Get(GetWorld()); - } - - /***********************************************************************/ /* EVENTS */ /***********************************************************************/ protected: - // Called once when this actor gets created for the first time. Similar to BeginPlay UPROPERTY(BlueprintAssignable, Category = SaveExtension) FLifetimeStartSignature Start; @@ -76,9 +67,20 @@ class SAVEEXTENSION_API ULifetimeComponent : public UActorComponent, public ISav public: - - FLifetimeStartSignature& OnStart() { return Start; } - FLifetimeSavedSignature& OnSaved() { return Saved; } - FLifetimeResumeSignature& OnResume() { return Resume; } - FLifetimeFinishSignature& OnFinish() { return Finish; } + FLifetimeStartSignature& OnStart() + { + return Start; + } + FLifetimeSavedSignature& OnSaved() + { + return Saved; + } + FLifetimeResumeSignature& OnResume() + { + return Resume; + } + FLifetimeFinishSignature& OnFinish() + { + return Finish; + } }; diff --git a/Source/SaveExtension/Public/Misc/ClassFilter.h b/Source/SaveExtension/Public/Misc/ClassFilter.h deleted file mode 100644 index b31896b..0000000 --- a/Source/SaveExtension/Public/Misc/ClassFilter.h +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include -#include -#include "ClassFilter.generated.h" - - -USTRUCT(BlueprintType) -struct SAVEEXTENSION_API FSEClassFilter -{ - GENERATED_BODY() - -private: - - // Used from editor side to limit displayed classes - UPROPERTY() - UClass* BaseClass; - -public: - - /** This classes are allowed (and their children) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization") - TSet> AllowedClasses; - - /** This classes are ignored (and their children) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization") - TSet> IgnoredClasses; - -protected: - - UPROPERTY(Transient) - mutable TSet BakedAllowedClasses; - - -public: - - FSEClassFilter() : FSEClassFilter(UObject::StaticClass()) {} - FSEClassFilter(UClass* const BaseClass); - - // Merges another filter into this one. Other has priority. - void Merge(const FSEClassFilter& Other); - - /** Bakes a set of allowed classes based on the current settings */ - void BakeAllowedClasses() const; - - FORCEINLINE bool IsClassAllowed(UClass* const Class) const - { - // Check is a single O(1) pointer hash comparison - return BakedAllowedClasses.Contains(Class); - } - - FORCEINLINE UClass* GetBaseClass() const { return BaseClass; } - - FString ToString(); - void FromString(FString String); - - bool operator==(const FSEClassFilter& Other) const; -}; - - -USTRUCT(BlueprintType) -struct FSEActorClassFilter -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Filter) - FSEClassFilter ClassFilter; - - - FSEActorClassFilter() - : ClassFilter(AActor::StaticClass()) - {} - FSEActorClassFilter(TSubclassOf actorClass) : ClassFilter(actorClass) {} - - /** Bakes a set of allowed classes based on the current settings */ - void BakeAllowedClasses() const { ClassFilter.BakeAllowedClasses(); } - - FORCEINLINE bool IsClassAllowed(UClass* const Class) const - { - return ClassFilter.IsClassAllowed(Class); - } -}; - - -USTRUCT(BlueprintType) -struct FSEComponentClassFilter -{ - GENERATED_BODY() - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Filter) - FSEClassFilter ClassFilter; - - - FSEComponentClassFilter() - : ClassFilter(UActorComponent::StaticClass()) - {} - FSEComponentClassFilter(TSubclassOf compClass) : ClassFilter(compClass) {} - - /** Bakes a set of allowed classes based on the current settings */ - void BakeAllowedClasses() const { ClassFilter.BakeAllowedClasses(); } - - FORCEINLINE bool IsClassAllowed(UClass* const Class) const - { - return ClassFilter.IsClassAllowed(Class); - } -}; diff --git a/Source/SaveExtension/Public/Misc/SlotHelpers.h b/Source/SaveExtension/Public/Misc/SlotHelpers.h deleted file mode 100644 index 31440d8..0000000 --- a/Source/SaveExtension/Public/Misc/SlotHelpers.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include - -#include "FileAdapter.h" - - -struct FSlotHelpers -{ - static void FindSlotFileNames(TArray& FoundSlots); - - /** Used to find next available slot id */ - class FFindSlotVisitor : public IPlatformFile::FDirectoryVisitor - { - public: - TArray& FoundSlots; - - FFindSlotVisitor(TArray& FoundSlots) - : FoundSlots(FoundSlots) - {} - - virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override; - }; - - static FString GetWorldName(const UWorld* World) - { - check(World); - const FString MapName = World->GetOutermost()->GetName(); - if (World->IsPlayInEditor()) - { - return UWorld::RemovePIEPrefix(MapName); - } - return MapName; - } -}; diff --git a/Source/SaveExtension/Public/Misc/TypeTraits.h b/Source/SaveExtension/Public/Misc/TypeTraits.h deleted file mode 100644 index e6dc8f1..0000000 --- a/Source/SaveExtension/Public/Misc/TypeTraits.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "Templates/UnrealTypeTraits.h" - - -template -constexpr bool VariadicContainsType() { - return false; -}; - -template -constexpr bool VariadicContainsType() { - return TIsSame::Value || VariadicContainsType(); -}; - - -template -constexpr uint32 GetVariadicTypeIndex() { - return Index+1; -}; - -template -constexpr uint32 GetVariadicTypeIndex() { - if (TIsSame::Value) - return Index; - else - return GetVariadicTypeIndex(); -}; diff --git a/Source/SaveExtension/Public/Multithreading/Delegates.h b/Source/SaveExtension/Public/Multithreading/Delegates.h deleted file mode 100644 index 97576dd..0000000 --- a/Source/SaveExtension/Public/Multithreading/Delegates.h +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include - - -DECLARE_DELEGATE_OneParam(FOnSlotInfosLoaded, const TArray&); - -// @param Amount of slots removed -DECLARE_DELEGATE(FOnSlotsDeleted); diff --git a/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h b/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h deleted file mode 100644 index 10e20bd..0000000 --- a/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "FileAdapter.h" -#include "Multithreading/Delegates.h" -#include "SlotInfo.h" - -#include -#include - - -class USaveManager; - -/** - * FDeleteSlotsTask - * Async task to remove an specific or all slots - */ -class FDeleteSlotsTask : public FNonAbandonableTask { -protected: - - const USaveManager* const Manager = nullptr; - FString SpecificSlotName; - -public: - - bool bSuccess = false; - - /** All infos Constructor */ - explicit FDeleteSlotsTask(const USaveManager* InManager, FName SlotName = {}); - - void DoWork(); - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FDeleteSlotsTask, STATGROUP_ThreadPoolAsyncTasks); - } - -private: - - USlotInfo* LoadInfoFromFile(const FString Name) const; -}; diff --git a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h b/Source/SaveExtension/Public/Multithreading/LoadFileTask.h deleted file mode 100644 index f2f5ef6..0000000 --- a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include "FileAdapter.h" - - -///////////////////////////////////////////////////// -// FLoadFileTask -// Async task to load a File -class FLoadFileTask : public FNonAbandonableTask -{ -protected: - - TWeakObjectPtr Manager; - const FString SlotName; - - TWeakObjectPtr SlotInfo; - TWeakObjectPtr SlotData; - - -public: - - explicit FLoadFileTask(USaveManager* Manager, FStringView SlotName) - : Manager(Manager) - , SlotName(SlotName) - {} - ~FLoadFileTask() - { - if(SlotInfo.IsValid()) - { - SlotInfo->ClearInternalFlags(EInternalObjectFlags::Async); - } - if(SlotData.IsValid()) - { - SlotData->ClearInternalFlags(EInternalObjectFlags::Async); - } - } - - void DoWork() - { - FScopedFileReader FileReader(FFileAdapter::GetSlotPath(SlotName)); - if(FileReader.IsValid()) - { - FSaveFile File; - File.Read(FileReader, false); - SlotInfo = File.CreateAndDeserializeInfo(Manager.Get()); - SlotData = File.CreateAndDeserializeData(Manager.Get()); - } - } - - USlotInfo* GetInfo() - { - return SlotInfo.Get(); - } - - USlotData* GetData() - { - return SlotData.Get(); - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FLoadFileTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/Multithreading/LoadSlotInfosTask.h b/Source/SaveExtension/Public/Multithreading/LoadSlotInfosTask.h deleted file mode 100644 index 9082a13..0000000 --- a/Source/SaveExtension/Public/Multithreading/LoadSlotInfosTask.h +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include "FileAdapter.h" -#include "Multithreading/Delegates.h" - -#include "SlotInfo.h" - -class USaveManager; - - -/** - * FLoadSlotInfosTask - * Async task to load one or many slot infos - */ -class FLoadSlotInfosTask : public FNonAbandonableTask -{ -protected: - - const USaveManager* Manager; - - const bool bSortByRecent = false; - // If not empty, only this specific slot will be loaded - const FName SlotName; - - TArray LoadedSlots; - - FOnSlotInfosLoaded Delegate; - - -public: - - /** All infos Constructor */ - explicit FLoadSlotInfosTask(const USaveManager* Manager, bool bInSortByRecent, const FOnSlotInfosLoaded& Delegate) - : Manager(Manager) - , bSortByRecent(bInSortByRecent) - , Delegate(Delegate) - {} - - /** One info Constructor */ - explicit FLoadSlotInfosTask(USaveManager* Manager, FName SlotName) - : Manager(Manager) - , SlotName(SlotName) - {} - - void DoWork(); - - /** Called after the task has finished */ - void AfterFinish(); - - const TArray& GetLoadedSlots() const - { - return LoadedSlots; - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FLoadAllSlotInfosTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/Multithreading/SaveFileTask.h b/Source/SaveExtension/Public/Multithreading/SaveFileTask.h deleted file mode 100644 index 2162649..0000000 --- a/Source/SaveExtension/Public/Multithreading/SaveFileTask.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include "FileAdapter.h" - - -///////////////////////////////////////////////////// -// FSaveFileTask -// Async task to save a File -class FSaveFileTask : public FNonAbandonableTask { -protected: - - USlotInfo* Info; - USlotData* Data; - const FString SlotName; - const bool bUseCompression; - -public: - - FSaveFileTask(USlotInfo* Info, USlotData* Data, const FString& InSlotName, const bool bInUseCompression) : - Info(Info), - Data(Data), - SlotName(InSlotName), - bUseCompression(bInUseCompression) - {} - - void DoWork() - { - FFileAdapter::SaveFile(SlotName, Info, Data, bUseCompression); - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FSaveFileTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h b/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h deleted file mode 100644 index 0c11117..0000000 --- a/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "Misc/TypeTraits.h" - -#include - -class ITaskHolder -{ -public: - virtual bool Tick() = 0; - virtual void Cancel(bool bFinishSynchronously) = 0; - virtual ~ITaskHolder() - { - } -}; - -template -class FTaskHolder : public FAsyncTask, public ITaskHolder -{ - using Super = FAsyncTask; - -public: - DECLARE_EVENT_OneParam(FTaskHolder, FFinishedEvent, FTaskHolder&); - - bool bNotified = false; - FFinishedEvent _OnFinished; - - FTaskHolder() : ITaskHolder(), Super() - { - } - virtual ~FTaskHolder() - { - } - - template - FTaskHolder(ArgTypes&&... CtrArgs) : Super(Forward(CtrArgs)...), ITaskHolder() - { - } - - auto& OnFinished(TFunction&)> Delegate) - { - _OnFinished.AddLambda(Delegate); - return *this; - } - - virtual bool Tick() override - { - if (Super::IsDone()) - { - TryNotifyFinish(); - return true; - } - return false; - } - - virtual void Cancel(bool bFinishSynchronously) override - { - if (!Super::IsIdle()) - { - Super::EnsureCompletion(bFinishSynchronously); - TryNotifyFinish(); - } - else if (Super::IsDone()) - { - TryNotifyFinish(); - } - } - - TaskType* operator->() - { - return &Super::GetTask(); - } - -private: - - void TryNotifyFinish() - { - if (!bNotified) - { - _OnFinished.Broadcast(*this); - bNotified = true; - } - } -}; - -/** Manages the lifetime of many multi-threaded tasks */ -class FScopedTaskList -{ - TArray> Tasks; - -public: - FScopedTaskList() - { - } - - template - inline FTaskHolder& CreateTask(ArgTypes&&... CtrArgs) - { - auto NewTask = MakeUnique>(Forward(CtrArgs)...); - auto* TaskPtr = NewTask.Get(); - Tasks.Add(MoveTemp(NewTask)); - return *TaskPtr; - } - - void Tick() - { - // Tick all running tasks and remove the ones that finished - Tasks.RemoveAllSwap([](auto& Task) { return Task->Tick(); }); - } - - void CancelAll() - { - for (auto& Task : Tasks) - { - Task->Cancel(true); - } - Tasks.Empty(); - } -}; diff --git a/Source/SaveExtension/Public/SEFileHelpers.h b/Source/SaveExtension/Public/SEFileHelpers.h new file mode 100644 index 0000000..cb210d7 --- /dev/null +++ b/Source/SaveExtension/Public/SEFileHelpers.h @@ -0,0 +1,126 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include +#include + + +class USaveManager; +class USaveSlot; +class USaveSlotData; +class FMemoryReader; +class FMemoryWriter; + + +struct FScopedFileWriter +{ +private: + FArchive* Writer = nullptr; + +public: + FScopedFileWriter(FStringView Filename, int32 Flags = 0); + ~FScopedFileWriter() + { + delete Writer; + } + + FArchive& GetArchive() + { + return *Writer; + } + bool IsValid() const + { + return Writer != nullptr; + } + bool IsError() const + { + return Writer && (Writer->IsError() || Writer->IsCriticalError()); + } +}; + + +struct FScopedFileReader +{ +private: + FScopedLoadingState ScopedLoadingState; + FArchive* Reader = nullptr; + +public: + FScopedFileReader(FStringView Filename, int32 Flags = 0); + ~FScopedFileReader() + { + delete Reader; + } + + FArchive& GetArchive() + { + return *Reader; + } + bool IsValid() const + { + return Reader != nullptr; + } +}; + + +/** Based on GameplayStatics to add multi-threading */ +struct FSaveFile +{ + int32 FileTypeTag = 0; + int32 SaveGameFileVersion = 0; + FPackageFileVersion PackageFileUEVersion{}; + FEngineVersion SavedEngineVersion; + int32 CustomVersionFormat = int32(ECustomVersionSerializationFormat::Unknown); + FCustomVersionContainer CustomVersions; + + FString ClassName; + TArray Bytes; + + FString DataClassName; + bool bIsDataCompressed = false; + TArray DataBytes; + + + FSaveFile(); + + void Empty(); + bool IsEmpty() const; + + void Read(FScopedFileReader& Reader, bool bSkipData); + void Write(FScopedFileWriter& Writer, bool bCompressData); + + void SerializeInfo(USaveSlot* Slot); + void SerializeData(USaveSlotData* SlotData); +}; + + +/** Based on GameplayStatics to add multi-threading */ +class SAVEEXTENSION_API FSEFileHelpers +{ +public: + static bool SaveFileSync(USaveSlot* Slot, FStringView OverrideSlotName = {}, const bool bUseCompression = true); + static UE::Tasks::TTask SaveFile(USaveSlot* Slot, FString OverrideSlotName = {}, const bool bUseCompression = true); + + static USaveSlot* LoadFileSync(FStringView SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager); + static UE::Tasks::TTask LoadFile(FString SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager); + + static bool DeleteFile(FStringView SlotName); + static bool FileExists(FStringView SlotName); + + static const FString& GetSaveFolder(); + static FString GetSlotPath(FStringView SlotName); + + static UObject* DeserializeObject( + UObject* Hint, FStringView ClassName, const UObject* Outer, const TArray& Bytes); + + + static void FindAllFilesSync(TArray& FoundSlots); + + // @return the pipe used for save file operations + static class UE::Tasks::FPipe& GetPipe(); +}; diff --git a/Source/SaveExtension/Public/SaveExtension.h b/Source/SaveExtension/Public/SaveExtension.h new file mode 100644 index 0000000..9675ff1 --- /dev/null +++ b/Source/SaveExtension/Public/SaveExtension.h @@ -0,0 +1,52 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "Engine/Engine.h" +#include "Modules/ModuleManager.h" +#include "SaveSlot.h" + + +DECLARE_LOG_CATEGORY_EXTERN(LogSaveExtension, All, All); + + +class FSaveExtension : public IModuleInterface +{ +public: + void StartupModule() override {} + void ShutdownModule() override {} + bool SupportsDynamicReloading() override + { + return true; + } + + static inline FSaveExtension& Get() + { + return FModuleManager::LoadModuleChecked("SaveExtension"); + } + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("SaveExtension"); + } + + static void Log(const USaveSlot* Slot, const FString& Message, bool bError) + { + Log(Slot, Message, FColor::White, bError, 2.f); + } + + static void Log(const USaveSlot* Slot, const FString& Message, FColor Color = FColor::White, + bool bError = false, const float Duration = 2.f); +}; + +// Only log in Editor +#if WITH_EDITORONLY_DATA +template +void SELog(Args&&... args) +{ + FSaveExtension::Log(Forward(args)...); +} +#else +template +void SELog(Args&&... args) +{} // Optimized away by compiler +#endif diff --git a/Source/SaveExtension/Public/SaveExtensionInterface.h b/Source/SaveExtension/Public/SaveExtensionInterface.h index 32cec10..2554d8f 100644 --- a/Source/SaveExtension/Public/SaveExtensionInterface.h +++ b/Source/SaveExtension/Public/SaveExtensionInterface.h @@ -1,13 +1,15 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "LevelFilter.h" + #include -#include "LevelFilter.h" #include "SaveExtensionInterface.generated.h" + UINTERFACE(Category = SaveExtension, BlueprintType) class SAVEEXTENSION_API USaveExtensionInterface : public UInterface { @@ -16,11 +18,9 @@ class SAVEEXTENSION_API USaveExtensionInterface : public UInterface class SAVEEXTENSION_API ISaveExtensionInterface { - GENERATED_BODY() public: - /** BP API **/ // Event called when Save process starts diff --git a/Source/SaveExtension/Public/SaveManager.h b/Source/SaveExtension/Public/SaveManager.h index 5af97e8..e9929d8 100644 --- a/Source/SaveExtension/Public/SaveManager.h +++ b/Source/SaveExtension/Public/SaveManager.h @@ -1,36 +1,46 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include "Delegates.h" -#include "LatentActions/DeleteSlotsAction.h" -#include "LatentActions/LoadGameAction.h" -#include "LatentActions/SaveGameAction.h" #include "LevelStreamingNotifier.h" -#include "Multithreading/ScopedTaskManager.h" -#include "Multithreading/Delegates.h" #include "SaveExtensionInterface.h" -#include "SavePreset.h" -#include "Serialization/SlotDataTask.h" -#include "SlotData.h" -#include "SlotInfo.h" - -#include -#include -#include -#include -#include +#include "SaveSlot.h" +#include "SaveSlotData.h" +#include "Serialization/SEDataTask.h" +#include "Serialization/SEDataTask_Load.h" +#include "Serialization/SEDataTask_Save.h" + #include #include #include "SaveManager.generated.h" -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameSavedMC, USlotInfo*, SlotInfo); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameLoadedMC, USlotInfo*, SlotInfo); +struct FLatentActionInfo; +class UGameInstance; -struct FLatentActionInfo; +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameSavedMC, USaveSlot*, Slot); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameLoadedMC, USaveSlot*, Slot); +using FSEOnAllSlotsPreloaded = TFunction& Slots)>; +using FSEOnAllSlotsDeleted = TFunction; + + +UENUM() +enum class ESEContinue : uint8 +{ + InProgress UMETA(Hidden), + Continue +}; + +UENUM() +enum class ESEContinueOrFail : uint8 +{ + InProgress UMETA(Hidden), + Continue, + Failed +}; + USTRUCT(BlueprintType) struct FScreenshotSize @@ -56,44 +66,32 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi { GENERATED_BODY() - friend USlotDataTask; + friend FSEDataTask; /************************************************************************/ /* PROPERTIES */ /************************************************************************/ public: - // Loaded from settings. Can be changed at runtime - UPROPERTY(Transient, BlueprintReadWrite, Category=SaveManager) + UPROPERTY(Transient, BlueprintReadWrite, Category = SaveManager) bool bTickWithGameWorld = false; private: - UPROPERTY(Transient) - USavePreset* ActivePreset; - - /** Currently loaded SaveInfo. SaveInfo stores basic information about a saved game. Played time, levels, - * progress, etc. */ + // Active SaveSlot used for current saves (load on start, periodic save, etc) UPROPERTY() - USlotInfo* CurrentInfo; - - /** Currently loaded SaveData. SaveData stores all serialized info about the world. */ - UPROPERTY() - USlotData* CurrentData; + TObjectPtr ActiveSlot; /** The game instance to which this save manager is owned. */ TWeakObjectPtr OwningGameInstance; - FScopedTaskList MTTasks; - UPROPERTY(Transient) TArray LevelStreamingNotifiers; UPROPERTY(Transient) TArray> SubscribedInterfaces; - UPROPERTY(Transient) - TArray Tasks; + TArray> Tasks; /************************************************************************/ @@ -120,48 +118,50 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi bool SaveSlot(FName SlotName, bool bOverrideIfNeeded = true, bool bScreenshot = false, const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); - /** Save the Game info an SlotInfo */ - bool SaveSlot(const USlotInfo* SlotInfo, bool bOverrideIfNeeded = true, bool bScreenshot = false, - const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); - - /** Save the Game into an specified slot id */ - bool SaveSlot(int32 SlotId, bool bOverrideIfNeeded = true, bool bScreenshot = false, + /** Save the Game info an Slot */ + bool SaveSlot(const USaveSlot* Slot, bool bOverrideIfNeeded = true, bool bScreenshot = false, const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); /** Save the currently loaded Slot */ - bool SaveCurrentSlot(bool bScreenshot = false, const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); + bool SaveActiveSlot(bool bScreenshot = false, const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); /** Load game from a file name */ bool LoadSlot(FName SlotName, FOnGameLoaded OnLoaded = {}); - /** Load game from a slot Id */ - bool LoadSlot(int32 SlotId, FOnGameLoaded OnLoaded = {}); - - /** Load game from a SlotInfo */ - bool LoadSlot(const USlotInfo* SlotInfo, FOnGameLoaded OnLoaded = {}); + /** Load game from a Slot */ + bool LoadSlot(const USaveSlot* Slot, FOnGameLoaded OnLoaded = {}); /** Reload the currently loaded slot if any */ - bool ReloadCurrentSlot(FOnGameLoaded OnLoaded = {}) + bool ReloadActiveSlot(FOnGameLoaded OnLoaded = {}) { - return LoadSlot(CurrentInfo, MoveTemp(OnLoaded)); + return LoadSlot(ActiveSlot, MoveTemp(OnLoaded)); } /** - * Find all saved games and return their SlotInfos + * Find all saved slots and preload them, without loading their data + * @param Slots preloaded from on disk + * @param bSortByRecent Should slots be ordered by save date? + */ + void PreloadAllSlots(FSEOnAllSlotsPreloaded Callback, bool bSortByRecent = false); + /** + * Find all saved slots and preload them asynchronously, without loading their data + * Performance: Interacts with disk, can be slow + * @param Slots preloaded from on disk * @param bSortByRecent Should slots be ordered by save date? - * @param SaveInfos All saved games found on disk */ - void LoadAllSlotInfos(bool bSortByRecent, FOnSlotInfosLoaded Delegate); - void LoadAllSlotInfosSync(bool bSortByRecent, FOnSlotInfosLoaded Delegate); + void PreloadAllSlotsSync(TArray& Slots, bool bSortByRecent = false); /** Delete a saved game on an specified slot name * Performance: Interacts with disk, can be slow */ - bool DeleteSlot(FName SlotName); - - /** Delete all saved slots from disk, loaded or not */ - void DeleteAllSlots(FOnSlotsDeleted Delegate); + bool DeleteSlotByNameSync(FName SlotName); + /** Deletes all saved slots in disk. Does not affect slots in memory. + * Performance: Interacts with disk, can be slow + */ + int32 DeleteAllSlotsSync(); + /** Deletes all saved slots in disk. Does not affect slots in memory. */ + void DeleteAllSlots(FSEOnAllSlotsDeleted Delegate); /** BLUEPRINT ONLY API */ @@ -170,138 +170,97 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi // with the normal C++ API /** Save the Game into an specified Slot */ - UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, meta = (AdvancedDisplay = "bScreenshot, Size", - DisplayName = "Save Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveSlot(FName SlotName, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true); - - /** Save the Game into an specified Slot */ - UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, meta = (AdvancedDisplay = "bScreenshot, Size", - DisplayName = "Save Slot by Id", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveSlotById(int32 SlotId, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true); + UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, + meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Slot by Name", Latent, + LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) + void BPSaveSlotByName(FName SlotName, bool bScreenshot, + UPARAM(meta = (EditCondition = bScreenshot)) const FScreenshotSize Size, ESEContinueOrFail& Result, + FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true); /** Save the Game to a Slot */ - UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, meta = (AdvancedDisplay = "bScreenshot, Size", - DisplayName = "Save Slot by Info", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveSlotByInfo(const USlotInfo* SlotInfo, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true); + UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, + meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Slot", Latent, + LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) + void BPSaveSlot(const USaveSlot* Slot, bool bScreenshot, + UPARAM(meta = (EditCondition = bScreenshot)) const FScreenshotSize Size, ESEContinueOrFail& Result, + FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true); /** Save the currently loaded Slot */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Saving", - meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Current Slot", Latent, + meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Active Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveCurrentSlot(bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo) + void BPSaveActiveSlot(bool bScreenshot, + UPARAM(meta = (EditCondition = bScreenshot)) const FScreenshotSize Size, ESEContinueOrFail& Result, + FLatentActionInfo LatentInfo) { - BPSaveSlotByInfo(CurrentInfo, bScreenshot, Size, Result, MoveTemp(LatentInfo), true); + BPSaveSlot(ActiveSlot, bScreenshot, Size, Result, MoveTemp(LatentInfo), true); } /** Load game from a slot name */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", - meta = (DisplayName = "Load Slot", Latent, LatentInfo = "LatentInfo", + meta = (DisplayName = "Load Slot by Name", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPLoadSlot(FName SlotName, ELoadGameResult& Result, FLatentActionInfo LatentInfo); + void BPLoadSlotByName(FName SlotName, ESEContinueOrFail& Result, FLatentActionInfo LatentInfo); - /** Load game from a slot Id */ + /** Load game from a Slot */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", - meta = (DisplayName = "Load Slot by Id", Latent, LatentInfo = "LatentInfo", - ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPLoadSlotById(int32 SlotId, ELoadGameResult& Result, FLatentActionInfo LatentInfo); - - /** Load game from a SlotInfo */ - UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", - meta = (DisplayName = "Load Slot by Info", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", + meta = (DisplayName = "Load Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPLoadSlotByInfo(const USlotInfo* SlotInfo, ELoadGameResult& Result, FLatentActionInfo LatentInfo); + void BPLoadSlot(const USaveSlot* Slot, ESEContinueOrFail& Result, FLatentActionInfo LatentInfo); /** Reload the currently loaded slot if any */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", - meta = (DisplayName = "Reload Current Slot", Latent, LatentInfo = "LatentInfo", + meta = (DisplayName = "Reload Active Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPReloadCurrentSlot(ELoadGameResult& Result, FLatentActionInfo LatentInfo) + void BPReloadActiveSlot(ESEContinueOrFail& Result, FLatentActionInfo LatentInfo) { - BPLoadSlotByInfo(CurrentInfo, Result, MoveTemp(LatentInfo)); + BPLoadSlot(ActiveSlot, Result, MoveTemp(LatentInfo)); } /** - * Find all saved games and return their SlotInfos + * Find all saved slots and preload them, without loading their data + * @param Slots preloaded from on disk * @param bSortByRecent Should slots be ordered by save date? - * @param SaveInfos All saved games found on disk */ UFUNCTION(BlueprintCallable, Category = "SaveExtension", meta = (Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", - DisplayName = "Load All Slot Infos")) - void BPLoadAllSlotInfos(const bool bSortByRecent, TArray& SaveInfos, ELoadInfoResult& Result, + DisplayName = "Preload All Slots")) + void BPPreloadAllSlots(const bool bSortByRecent, TArray& Slots, ESEContinue& Result, struct FLatentActionInfo LatentInfo); - /** Delete a saved game on an specified slot Id - * Performance: Interacts with disk, can be slow - */ - UFUNCTION(BlueprintCallable, Category = "SaveExtension") - FORCEINLINE bool DeleteSlotById(int32 SlotId) - { - if (!IsValidSlot(SlotId)) - { - return false; - } - return DeleteSlot(GetSlotNameFromId(SlotId)); - } - /** Delete all saved slots from disk, loaded or not */ UFUNCTION(BlueprintCallable, Category = "SaveExtension", meta = (Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", DisplayName = "Delete All Slots")) - void BPDeleteAllSlots(EDeleteSlotsResult& Result, FLatentActionInfo LatentInfo); - - UFUNCTION(BlueprintPure, Category = "SaveExtension") - USavePreset* BPGetPreset() const - { - return ActivePreset; - } + void BPDeleteAllSlots(ESEContinue& Result, FLatentActionInfo LatentInfo); /** BLUEPRINTS & C++ API */ public: - - /** Delete a saved game on an specified slot - * Performance: Interacts with disk, can be slow - */ + /** Delete a saved game on an specified slot name */ UFUNCTION(BlueprintCallable, Category = "SaveExtension") - bool DeleteSlot(USlotInfo* Slot) - { - return Slot? DeleteSlot(Slot->FileName) : false; - } + void DeleteSlotByName(FName SlotName); - /** Get the currently loaded SlotInfo. If game was never loaded returns a new SlotInfo */ - UFUNCTION(BlueprintPure, Category = "SaveExtension") - FORCEINLINE USlotInfo* GetCurrentInfo() + /** Delete a saved game on an specified slot */ + UFUNCTION(BlueprintCallable, Category = "SaveExtension") + void DeleteSlot(USaveSlot* Slot) { - TryInstantiateInfo(); - return CurrentInfo; + if (Slot) + { + DeleteSlotByName(Slot->Name); + } } - /** Get the currently loaded SlotData. If game was never loaded returns an empty SlotData */ + /** Get the currently loaded Slot. If game was never loaded returns a new Slot */ UFUNCTION(BlueprintPure, Category = "SaveExtension") - FORCEINLINE USlotData* GetCurrentData() + FORCEINLINE USaveSlot* GetActiveSlot() { - TryInstantiateInfo(); - return CurrentData; + AssureActiveSlot(); + return ActiveSlot; } - /** - * Load and return an SlotInfo by Id if it exists - * Performance: Interacts with disk, could be slow if called frequently - * @param SlotId Id of the SlotInfo to be loaded - * @return the SlotInfo associated with an Id - */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Slots") - FORCEINLINE USlotInfo* GetSlotInfoById(int32 SlotId) - { - return LoadInfo(SlotId); - } - - UFUNCTION(BlueprintCallable, Category = "SaveExtension|Slots") - FORCEINLINE USlotInfo* GetSlotInfo(FName SlotName) - { - return LoadInfo(SlotName); - } + USaveSlot* PreloadSlot(FName SlotName); /** Check if an slot exists on disk * @return true if the slot exists @@ -309,55 +268,21 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi UFUNCTION(BlueprintPure, Category = "SaveExtension|Slots") bool IsSlotSaved(FName SlotName) const; - /** Check if an slot exists on disk - * @return true if the slot exists - */ - UFUNCTION(BlueprintPure, Category = "SaveExtension|Slots") - bool IsSlotSavedById(int32 SlotId) const - { - return IsValidSlot(SlotId)? IsSlotSaved(GetSlotNameFromId(SlotId)) : false; - } /** Check if currently playing in a saved slot * @return true if currently playing in a saved slot */ UFUNCTION(BlueprintPure, Category = "SaveExtension|Slots") - FORCEINLINE bool IsInSlot() const + FORCEINLINE bool HasActiveSlot() const { - return CurrentInfo && CurrentData; + return ActiveSlot != nullptr; } - /** - * Set the preset to be used for saving and loading - * @return true if the preset was set successfully - */ - UFUNCTION(BlueprintCallable, Category = "SaveExtension") - USavePreset* SetActivePreset(TSubclassOf PresetClass); - - const USavePreset* GetPreset() const; + // Assigns a new active slot. If this slot is preloaded, empty data is assigned to it. + // This does not load the game! + void SetActiveSlot(USaveSlot* NewSlot); - void TryInstantiateInfo(bool bForced = false); - - UFUNCTION(BlueprintPure, Category = "SaveExtension") - FName GetSlotNameFromId(const int32 SlotId) const; - - bool IsValidSlot(const int32 Slot) const; - - void __SetCurrentInfo(USlotInfo* NewInfo) - { - CurrentInfo = NewInfo; - } - - void __SetCurrentData(USlotData* NewData) - { - CurrentData = NewData; - } - - USlotInfo* LoadInfo(FName SlotName); - USlotInfo* LoadInfo(uint32 SlotId) - { - return IsValidSlot(SlotId)? LoadInfo(GetSlotNameFromId(SlotId)) : nullptr; - } + void AssureActiveSlot(TSubclassOf ActiveSlotClass = {}, bool bForced = false); protected: bool CanLoadOrSave(); @@ -374,15 +299,13 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi void OnLevelLoaded(ULevelStreaming* StreamingLevel) {} - USlotDataTask* CreateTask(TSubclassOf TaskType); - - template - TaskType* CreateTask() + template + TaskType& CreateTask() { - return Cast(CreateTask(TaskType::StaticClass())); + return static_cast(*Tasks.Add_GetRef(MakeUnique(this, ActiveSlot))); } - void FinishTask(USlotDataTask* Task); + void FinishTask(FSEDataTask* Task); public: bool HasTasks() const @@ -431,10 +354,10 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi UFUNCTION(Category = SaveExtension, BlueprintCallable) void UnsubscribeFromEvents(const TScriptInterface& Interface); - void OnSaveBegan(const FSELevelFilter& Filter); - void OnSaveFinished(const FSELevelFilter& Filter, const bool bError); - void OnLoadBegan(const FSELevelFilter& Filter); - void OnLoadFinished(const FSELevelFilter& Filter, const bool bError); + void OnSaveBegan(); + void OnSaveFinished(const bool bError); + void OnLoadBegan(); + void OnLoadFinished(const bool bError); private: void OnMapLoadStarted(const FString& MapName); @@ -448,150 +371,6 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /***********************************************************************/ public: /** Get the global save manager */ + static USaveManager* Get(const UWorld* World); static USaveManager* Get(const UObject* ContextObject); - - - /***********************************************************************/ - /* DEPRECATED */ - /***********************************************************************/ - - UFUNCTION(Category = "SaveExtension|Saving", BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage="Use 'Save Slot by Id' instead.", - AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Slot to Id", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveSlotToId(int32 SlotId, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo, bool bOverrideIfNeeded = true) - { - BPSaveSlotById(SlotId, bScreenshot, Size, Result, LatentInfo, bOverrideIfNeeded); - } - - UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", meta = (DeprecatedFunction, DeprecationMessage="Use 'Load Slot by Id' instead.", - DisplayName = "Load Slot from Id", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPLoadSlotFromId(int32 SlotId, ELoadGameResult& Result, FLatentActionInfo LatentInfo) - { - BPLoadSlotById(SlotId, Result, LatentInfo); - } }; - - -inline bool USaveManager::SaveSlot(int32 SlotId, bool bOverrideIfNeeded, bool bScreenshot, - const FScreenshotSize Size, FOnGameSaved OnSaved) -{ - if (!IsValidSlot(SlotId)) - { - SELog(GetPreset(), "Invalid Slot. Cant go under 0 or exceed MaxSlots.", true); - return false; - } - return SaveSlot(GetSlotNameFromId(SlotId), bOverrideIfNeeded, bScreenshot, Size, OnSaved); -} - -inline bool USaveManager::SaveSlot(const USlotInfo* SlotInfo, bool bOverrideIfNeeded, bool bScreenshot, - const FScreenshotSize Size, FOnGameSaved OnSaved) -{ - if (!SlotInfo) - { - return false; - } - return SaveSlot(SlotInfo->FileName, bOverrideIfNeeded, bScreenshot, Size, OnSaved); -} - -inline void USaveManager::BPSaveSlotById(int32 SlotId, bool bScreenshot, const FScreenshotSize Size, ESaveGameResult& Result, FLatentActionInfo LatentInfo, bool bOverrideIfNeeded) -{ - if (!IsValidSlot(SlotId)) - { - SELog(GetPreset(), "Invalid Slot. Cant go under 0 or exceed MaxSlots.", true); - Result = ESaveGameResult::Failed; - return; - } - BPSaveSlot(GetSlotNameFromId(SlotId), bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); -} - -inline void USaveManager::BPSaveSlotByInfo(const USlotInfo* SlotInfo, bool bScreenshot, const FScreenshotSize Size, - ESaveGameResult& Result, struct FLatentActionInfo LatentInfo, bool bOverrideIfNeeded) -{ - if (!SlotInfo) - { - Result = ESaveGameResult::Failed; - return; - } - BPSaveSlot(SlotInfo->FileName, bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); -} - -/** Save the currently loaded Slot */ -inline bool USaveManager::SaveCurrentSlot(bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) -{ - return SaveSlot(CurrentInfo, true, bScreenshot, Size, OnSaved); -} - -inline bool USaveManager::LoadSlot(int32 SlotId, FOnGameLoaded OnLoaded) -{ - if (!IsValidSlot(SlotId)) - { - SELog(GetPreset(), "Invalid Slot. Can't go under 0 or exceed MaxSlots.", true); - return false; - } - return LoadSlot(GetSlotNameFromId(SlotId), OnLoaded); -} - -inline bool USaveManager::LoadSlot(const USlotInfo* SlotInfo, FOnGameLoaded OnLoaded) -{ - if (!SlotInfo) - { - return false; - } - return LoadSlot(SlotInfo->FileName, OnLoaded); -} - -inline void USaveManager::BPLoadSlotById( - int32 SlotId, ELoadGameResult& Result, struct FLatentActionInfo LatentInfo) -{ - BPLoadSlot(GetSlotNameFromId(SlotId), Result, MoveTemp(LatentInfo)); -} - -inline void USaveManager::BPLoadSlotByInfo(const USlotInfo* SlotInfo, ELoadGameResult& Result, FLatentActionInfo LatentInfo) -{ - if (!SlotInfo) - { - Result = ELoadGameResult::Failed; - return; - } - BPLoadSlot(SlotInfo->FileName, Result, MoveTemp(LatentInfo)); -} - -inline bool USaveManager::IsValidSlot(const int32 Slot) const -{ - return GetPreset()->IsValidId(Slot); -} - -inline void USaveManager::IterateSubscribedInterfaces(TFunction&& Callback) -{ - for (const TScriptInterface& Interface : SubscribedInterfaces) - { - if (UObject* const Object = Interface.GetObject()) - { - Callback(Object); - } - } -} - -inline USaveManager* USaveManager::Get(const UObject* Context) -{ - UWorld* World = GEngine->GetWorldFromContextObject(Context, EGetWorldErrorMode::LogAndReturnNull); - if (World) - { - return UGameInstance::GetSubsystem(World->GetGameInstance()); - } - return nullptr; -} - -inline bool USaveManager::IsTickable() const -{ - return !HasAnyFlags(RF_ClassDefaultObject) && IsValid(this); -} - -inline UWorld* USaveManager::GetTickableGameObjectWorld() const -{ - return bTickWithGameWorld? GetWorld() : nullptr; -} - -inline TStatId USaveManager::GetStatId() const -{ - RETURN_QUICK_DECLARE_CYCLE_STAT(USaveManager, STATGROUP_Tickables); -} diff --git a/Source/SaveExtension/Public/SavePreset.h b/Source/SaveExtension/Public/SavePreset.h deleted file mode 100644 index 16713bb..0000000 --- a/Source/SaveExtension/Public/SavePreset.h +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Engine/DataAsset.h" -#include "UObject/NoExportTypes.h" - -#include "Misc/ClassFilter.h" -#include "SavePreset.generated.h" - - -/** -* Specifies the behavior while saving or loading -*/ -UENUM() -enum class ESaveASyncMode : uint8 { - OnlySync, - LoadAsync, - SaveAsync, - SaveAndLoadAsync -}; - -class USlotInfo; -class USlotData; - - -/** - * What to save, how to save it, when, every x minutes, what info file, what data file, save by level streaming? - */ -UCLASS(ClassGroup = SaveExtension, Blueprintable) -class SAVEEXTENSION_API USavePreset : public UObject -{ - GENERATED_BODY() - -public: - - /** - * Specify the SaveInfo object to be used with this preset - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) - TSubclassOf SlotInfoClass; - - /** - * Specify the SaveData object to be used with this preset - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) - TSubclassOf SlotDataClass; - - /** Maximum amount of saved slots at the same time */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Gameplay, meta = (ClampMin = "0")) - int32 MaxSlots = 0; - - /** If checked, will attempt to Save Game to first Slot found, timed event. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) - bool bAutoSave = true; - - /** Interval in seconds for auto saving */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay, meta = (EditCondition = "bAutoSave", UIMin = "15", UIMax = "3600")) - int32 AutoSaveInterval = 120.f; - - /** If checked, will attempt to Save Game to first Slot found, timed event. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay, meta = (EditCondition = "bAutoSave")) - bool bSaveOnExit = false; - - /** If checked, will attempt to Load Game from last Slot found, when game starts */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) - bool bAutoLoad = true; - - /** - * If checked, will print messages to Log, and Viewport if DebugInScreen is enabled. - * Ignored in package or Shipping mode. - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay, AdvancedDisplay) - bool bDebug = false; - - /** - * If checked and Debug is enabled, will print messages to Viewport. - * Ignored in package or Shipping mode. - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay, AdvancedDisplay, meta = (EditCondition = "bDebug")) - bool bDebugInScreen = true; - - - /** If true save files will be compressed - * Performance: Can add from 10ms to 20ms to loading and saving (estimate) but reduce file sizes making them up to 30x smaller - */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Serialization) - bool bUseCompression = true; - - /** If true will store the game instance */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Serialization) - bool bStoreGameInstance = true; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Actors") - FSEActorClassFilter ActorFilter; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Actors", meta = (PinHiddenByDefault, InlineEditConditionToggle)) - bool bUseLoadActorFilter = false; - - /** If enabled, this filter will be used while loading instead of "ActorFilter" */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Actors", meta = (EditCondition="bUseLoadActorFilter")) - FSEActorClassFilter LoadActorFilter; - - /** If true will store ActorComponents depending on the filters */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Components") - bool bStoreComponents = true; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Components") - FSEComponentClassFilter ComponentFilter; - - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Components", meta = (PinHiddenByDefault, InlineEditConditionToggle)) - bool bUseLoadComponentFilter = false; - - /** If enabled, this filter will be used while loading instead of "ComponentFilter" */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Serialization|Components", meta = (EditCondition = "bUseLoadComponentFilter")) - FSEComponentClassFilter LoadComponentFilter; - -public: - - /** Serialization will be multi-threaded between all available cores. */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Asynchronous") - ESaveASyncMode MultithreadedSerialization = ESaveASyncMode::SaveAsync; - - /** Split serialization between multiple frames. Ignored if MultithreadedSerialization is used - * Currently only implemented on Loading - */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Asynchronous") - ESaveASyncMode FrameSplittedSerialization = ESaveASyncMode::OnlySync; - - - /** Max milliseconds to use every frame in an asynchronous operation. - * If running at 60Fps (16.6ms), loading or saving asynchronously will add MaxFrameMS: - * 16.6ms + 5MS = 21.6ms -> 46Fps - * This means gameplay will not be stopped nor have frame drops while saving or loading. Works best for non multi-threaded platforms - */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Asynchronous", meta = (UIMin="3", UIMax="10")) - float MaxFrameMs = 5.f; - - /** Files will be loaded or saved on a secondary thread while game continues */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Asynchronous") - ESaveASyncMode MultithreadedFiles = ESaveASyncMode::SaveAndLoadAsync; - - -protected: - - /** If true, will Save and Load levels when they are shown or hidden. - * This includes level streaming and world composition. - */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Level Streaming") - bool bSaveAndLoadSublevels = true; - - -public: - - USavePreset(); - - UFUNCTION(BlueprintNativeEvent, Category = "Slots", meta = (DisplayName="Get Slot Name from Id")) - void BPGetSlotNameFromId(int32 Id, FName& Name) const; - -protected: - - virtual void GetSlotNameFromId(int32 Id, FName& Name) const; - - - /** HELPERS */ -public: - - int32 GetMaxIds() const; - - bool IsValidId(int32 Id) const; - - UFUNCTION(BlueprintPure, Category = SavePreset) - FSEActorClassFilter& GetActorFilter(bool bIsLoading) - { - return (bIsLoading && bUseLoadActorFilter)? LoadActorFilter : ActorFilter; - } - - const FSEActorClassFilter& GetActorFilter(bool bIsLoading) const - { - return (bIsLoading && bUseLoadActorFilter)? LoadActorFilter : ActorFilter; - } - - UFUNCTION(BlueprintPure, Category = SavePreset) - FSEComponentClassFilter& GetComponentFilter(bool bIsLoading) - { - return (bIsLoading && bUseLoadComponentFilter) ? LoadComponentFilter : ComponentFilter; - } - - const FSEComponentClassFilter& GetComponentFilter(bool bIsLoading) const - { - return (bIsLoading && bUseLoadActorFilter) ? LoadComponentFilter : ComponentFilter; - } - - bool IsMTSerializationLoad() const - { - return MultithreadedSerialization == ESaveASyncMode::LoadAsync || MultithreadedSerialization == ESaveASyncMode::SaveAndLoadAsync; - } - bool IsMTSerializationSave() const - { - return MultithreadedSerialization == ESaveASyncMode::SaveAsync || MultithreadedSerialization == ESaveASyncMode::SaveAndLoadAsync; - } - - ESaveASyncMode GetFrameSplitSerialization() const { return FrameSplittedSerialization; } - float GetMaxFrameMs() const { return MaxFrameMs; } - - bool IsFrameSplitLoad() const - { - return !IsMTSerializationLoad() && (FrameSplittedSerialization == ESaveASyncMode::LoadAsync || FrameSplittedSerialization == ESaveASyncMode::SaveAndLoadAsync); - } - bool IsFrameSplitSave() const - { - return !IsMTSerializationSave() && (FrameSplittedSerialization == ESaveASyncMode::SaveAsync || FrameSplittedSerialization == ESaveASyncMode::SaveAndLoadAsync); - } - - bool IsMTFilesLoad() const - { - return MultithreadedFiles == ESaveASyncMode::LoadAsync || MultithreadedFiles == ESaveASyncMode::SaveAndLoadAsync; - } - bool IsMTFilesSave() const - { - return MultithreadedFiles == ESaveASyncMode::SaveAsync || MultithreadedFiles == ESaveASyncMode::SaveAndLoadAsync; - } - - struct FSELevelFilter ToFilter() const; -}; - - -inline int32 USavePreset::GetMaxIds() const -{ - return MaxSlots <= 0 ? 16384 : MaxSlots; -} - -inline bool USavePreset::IsValidId(int32 Id) const -{ - const int32 MaxIds = GetMaxIds(); - return Id >= 0 && (MaxIds <= 0 || Id < MaxIds); -} diff --git a/Source/SaveExtension/Public/SaveSettings.h b/Source/SaveExtension/Public/SaveSettings.h index 33ad07d..00241ad 100644 --- a/Source/SaveExtension/Public/SaveSettings.h +++ b/Source/SaveExtension/Public/SaveSettings.h @@ -1,41 +1,28 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include "SavePreset.h" +#include "SaveSlot.h" -#include #include #include "SaveSettings.generated.h" -UCLASS(ClassGroup = SaveExtension, defaultconfig, config = Game, meta=(DisplayName="Save Extension")) +UCLASS(ClassGroup = SaveExtension, defaultconfig, config = Game, meta = (DisplayName = "Save Extension")) class SAVEEXTENSION_API USaveSettings : public UDeveloperSettings { - GENERATED_BODY() - -protected: - - UPROPERTY(EditAnywhere, Category = "Save Extension", Config, meta = (DisplayName = "Preset")) - TSubclassOf Preset; + GENERATED_BODY() public: - - // If true SaveManager will tick with the world. If game is paused, saving process may be interrupted. - UPROPERTY(EditAnywhere, Category = "Save Extension", Config) - bool bTickWithGameWorld = false; - - - USavePreset* CreatePreset(UObject* Outer) const; + // Active Slot classes are initialized as active slots. + // Active saves represent an always valid save game in memory that can be filled periodically or + // saved. + UPROPERTY(EditAnywhere, Category = "Save Extension", Config) + TSubclassOf ActiveSlot; + + // If true SaveManager will tick with the world, otherwise with the engine. If true, the saving process + // may be interrupted if the game is paused. + UPROPERTY(EditAnywhere, Category = "Save Extension", Config) + bool bTickWithGameWorld = false; }; - - -inline USavePreset* USaveSettings::CreatePreset(UObject* Outer) const -{ - if(UClass* PresetClass = Preset.Get()) - { - return NewObject(Outer, PresetClass); - } - return NewObject(Outer); -} diff --git a/Source/SaveExtension/Public/SaveSlot.h b/Source/SaveExtension/Public/SaveSlot.h new file mode 100644 index 0000000..765aaf2 --- /dev/null +++ b/Source/SaveExtension/Public/SaveSlot.h @@ -0,0 +1,243 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SaveSlotData.h" + +#include +#include +#include +#include +#include + +#include "SaveSlot.generated.h" + + +struct FSELevelFilter; +class UTexture2D; + + +DECLARE_DELEGATE_OneParam(FSEOnThumbnailCaptured, bool); + +/** + * Specifies the behavior while saving or loading + */ +UENUM() +enum class ESEAsyncMode : uint8 +{ + SaveAndLoadSync = 0, + LoadAsync = 1, + SaveAsync = 2, + SaveAndLoadAsync = LoadAsync | SaveAsync +}; + + +USTRUCT(Blueprintable) +struct FSaveSlotStats +{ + GENERATED_BODY() + + /** Played time since this saved game was started. Not related to slots, slots can change */ + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) + FTimespan PlayedTime = FTimespan::Zero(); + + /** Played time since this saved game was created */ + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) + FTimespan SlotPlayedTime = FTimespan::Zero(); + + /** Last date at which this slot was saved. */ + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) + FDateTime SaveDate = FDateTime::Now(); + + /** Date at which this slot was loaded. */ + UPROPERTY(BlueprintReadOnly, Transient, Category = SaveSlot) + FDateTime LoadDate; +}; + + +/** + * USaveSlot stores information that needs to be accessible WITHOUT loading the game. + * Works like a common SaveGame object + * E.g: Dates, played time, progress, level + */ +UCLASS(ClassGroup = SaveExtension, hideCategories = ("Activation", "Actor Tick", "Actor", "Input", + "Rendering", "Replication", "Socket", "Thumbnail")) +class SAVEEXTENSION_API USaveSlot : public USaveGame +{ + GENERATED_BODY() + +public: + /** Begin Settings */ + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Slot") + TSubclassOf DataClass = USaveSlotData::StaticClass(); + + /** If checked, will attempt to Save Game to first Slot found, timed event. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Automatic") + bool bPeriodicSave = false; + + /** Interval in seconds for auto saving */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Automatic", + meta = (EditCondition = "bPeriodicSave", UIMin = "15", UIMax = "3600")) + int32 PeriodicSaveInterval = 120.f; + + /** If checked, will attempt to Save Game to current Slot found, timed event. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Automatic") + bool bSaveOnClose = false; + + /** If checked, will attempt to Load Game from last Slot found, when game starts */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Automatic") + bool bLoadOnStart = false; + + /** If true will store the game instance */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Serialization") + bool bStoreGameInstance = true; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Serialization") + FSEClassFilter ActorFilter{AActor::StaticClass()}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Serialization") + FSEClassFilter ComponentFilter{UActorComponent::StaticClass()}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Serialization") + FSEClassFilter SubsystemFilter{USubsystem::StaticClass()}; + + /** If true, will save and load sub-levels when they are shown or hidden. + * This includes level streaming and world composition. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Serialization") + bool bStoreSublevels = true; + + /** If true save files will be compressed + * Performance: Can add from 10ms to 20ms to loading and saving (estimate) but reduce file sizes making + * them up to 30x smaller + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Files") + bool bUseCompression = true; + + /** Serialization will be multi-threaded between all available cores. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Async") + ESEAsyncMode MultithreadedSerialization = ESEAsyncMode::SaveAndLoadSync; + + /** Split serialization between multiple frames. Ignored if MultithreadedSerialization is used + * Currently only implemented on Loading + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Async") + ESEAsyncMode FrameSplittedSerialization = ESEAsyncMode::SaveAndLoadSync; + + /** Max milliseconds to use every frame in an asynchronous operation. + * If running at 60Fps (16.6ms), loading or saving asynchronously will add MaxFrameMS: + * 16.6ms + 5MS = 21.6ms -> 46Fps + * This means gameplay will not be stopped nor have frame drops while saving or loading. Works best for + * non multi-threaded platforms + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Async", + meta = (UIMin = "3", UIMax = "10", + EditCondition = "FrameSplittedSerialization != ESEAsyncMode::SaveAndLoadSync")) + float MaxFrameMs = 5.f; + + /** Files will be loaded or saved on a secondary thread while game continues */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Async") + ESEAsyncMode MultithreadedFiles = ESEAsyncMode::SaveAndLoadAsync; + + /** + * If checked, will print messages to Log, and Viewport if DebugInScreen is enabled. + * Ignored in package or Shipping mode. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug", AdvancedDisplay) + bool bDebug = false; + + /** + * If checked and Debug is enabled, will print messages to Viewport. + * Ignored in package or Shipping mode. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug", AdvancedDisplay, + meta = (EditCondition = "bDebug")) + bool bDebugInScreen = true; + + /** End Settings */ + + +public: + /** Slot where this SaveInfo and its saveData are saved */ + UPROPERTY(BlueprintReadWrite, Category = SaveSlot) + FName Name = TEXT("Default"); + + UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) + FText DisplayName; + + /** Root Level where this Slot was saved */ + UPROPERTY(SaveGame, BlueprintReadOnly, Category = SaveSlot) + FName Map; + + UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) + FSaveSlotStats Stats; + + UPROPERTY(BlueprintReadWrite, Transient, Category = SaveSlot) // Saved + TObjectPtr Thumbnail; + +protected: + UPROPERTY(Transient) + bool bCapturingThumbnail = false; + FSEOnThumbnailCaptured CapturedThumbnailDelegate; + + UPROPERTY(Transient, BlueprintReadOnly, Category = SaveSlot) + TObjectPtr Data; + + +public: + void PostInitProperties() override; + void Serialize(FArchive& Ar) override; + + /** Captures a thumbnail for the current slot + * @return true if thumbnail was requested. Only one can be requested at the same time. + */ + void CaptureThumbnail(FSEOnThumbnailCaptured Callback, const int32 Width = 640, const int32 Height = 360); + + /** Captures a thumbnail for the current slot */ + UFUNCTION(BlueprintCallable, Category = SaveSlot, meta = (DisplayName = "Capture Thumbnail")) + void BPCaptureThumbnail(const int32 Width = 640, const int32 Height = 360) + { + CaptureThumbnail({}, Width, Height); + } + + USaveSlotData* GetData() const + { + return Data; + } + + // Internal use only recommended + void AssignData(USaveSlotData* NewData) + { + Data = NewData; + } + + // Gets the data instance, and creates it if wasn't already + UFUNCTION(BlueprintCallable, Category = SaveSlot) + USaveSlotData* AssureData(); + + bool ShouldDeserializeAsync() const; + bool ShouldSerializeAsync() const; + + ESEAsyncMode GetFrameSplitSerialization() const; + float GetMaxFrameMs() const; + + bool IsFrameSplitLoad() const; + bool IsFrameSplitSave() const; + + bool ShouldLoadFileAsync() const; + bool ShouldSaveFileAsync() const; + + UFUNCTION(BlueprintPure, Category = SaveSlot) + bool IsLoadingOrSaving() const; + + // Called for every level before being saved or loaded + UFUNCTION(BlueprintNativeEvent, Category = SaveSlot) + void GetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) const; + +private: + // Called for every level before being saved or loaded + virtual void OnGetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) const; + + void OnThumbnailCaptured(int32 InSizeX, int32 InSizeY, const TArray& InImageData); +}; diff --git a/Source/SaveExtension/Public/SaveSlotData.h b/Source/SaveExtension/Public/SaveSlotData.h new file mode 100644 index 0000000..7a26919 --- /dev/null +++ b/Source/SaveExtension/Public/SaveSlotData.h @@ -0,0 +1,63 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + + +#pragma once + +#include "Serialization/LevelRecords.h" +#include "Serialization/Records.h" + +#include +#include +#include +#include + +#include "SaveSlotData.generated.h" + + +struct FUniqueNetIdRepl; + + +/** + * USaveSlotData stores all information that can be accessible only while the game is loaded. + * Works like a common SaveGame object + * E.g: Items, Quests, Enemies, World Actors, AI, Physics + */ +UCLASS(Blueprintable, BlueprintType, ClassGroup = SaveExtension, hideCategories = ("Activation", "Actor Tick", "Actor", "Input", + "Rendering", "Replication", "Socket", "Thumbnail")) +class SAVEEXTENSION_API USaveSlotData : public UObject +{ + GENERATED_BODY() + +public: + /** Game world time since game started in seconds */ + UPROPERTY(SaveGame, Category = SaveSlotData, BlueprintReadOnly) + float TimeSeconds; + + /** Records + * All serialized information to be saved or loaded + * Serialized manually for performance + */ + FObjectRecord GameInstance; + TArray GameInstanceSubsystems; + + TArray WorldSubsystems; + + FPersistentLevelRecord RootLevel; + TArray SubLevels; + + TArray Players; + + + void CleanRecords(bool bKeepSublevels); + + /** Using manual serialization. It's way faster than reflection serialization */ + virtual void Serialize(FArchive& Ar) override; + + UFUNCTION(BlueprintPure, Category = SaveSlotData) + FPlayerRecord& FindOrAddPlayerRecord(const FUniqueNetIdRepl& UniqueId); + FPlayerRecord* FindPlayerRecord(const FUniqueNetIdRepl& UniqueId); + UFUNCTION(BlueprintPure, Category = SaveSlotData) + bool FindPlayerRecord(const FUniqueNetIdRepl& UniqueId, UPARAM(Ref) FPlayerRecord& Record); + UFUNCTION(BlueprintPure, Category = SaveSlotData) + bool RemovePlayerRecord(const FUniqueNetIdRepl& UniqueId); +}; diff --git a/Source/SaveExtension/Public/Serialization/LevelRecords.h b/Source/SaveExtension/Public/Serialization/LevelRecords.h index 4dbcf9a..232adae 100644 --- a/Source/SaveExtension/Public/Serialization/LevelRecords.h +++ b/Source/SaveExtension/Public/Serialization/LevelRecords.h @@ -1,14 +1,15 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once +#include "LevelFilter.h" +#include "Records.h" + #include -#include #include +#include -#include "Records.h" -#include "LevelFilter.h" #include "LevelRecords.generated.h" @@ -18,22 +19,26 @@ struct FLevelRecord : public FBaseRecord { GENERATED_BODY() - bool bOverrideGeneralFilter = false; - // Filter is used if bOverrideGeneralFilter is true - FSELevelFilter Filter; - /** Record of the Level Script Actor */ FActorRecord LevelScript; /** Records of the World Actors */ TArray Actors; + /** Not-serialized. Assigned before loading and saving by the SaveSlot */ + FSELevelFilter Filter; + + /** Not-serialized. During saving or loading points to the live actor */ + TArray>> RecordsToActors; FLevelRecord() : Super() {} virtual bool Serialize(FArchive& Ar) override; - bool IsValid() const { return !Name.IsNone(); } + bool IsValid() const + { + return !Name.IsNone(); + } void CleanRecords(); }; diff --git a/Source/SaveExtension/Public/Serialization/MTTask.h b/Source/SaveExtension/Public/Serialization/MTTask.h deleted file mode 100644 index d29eeae..0000000 --- a/Source/SaveExtension/Public/Serialization/MTTask.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" - -#include -#include -#include -#include - -#include "SavePreset.h" -#include "LevelFilter.h" - - -///////////////////////////////////////////////////// -// FSlotDataActorsTask -// Async task to serialize actors from a level. -class FMTTask : public FNonAbandonableTask { -protected: - - /** Used only if Sync */ - const UWorld* const World; - USlotData* SlotData; - - // Locally cached settings - const FSELevelFilter& Filter; - - - FMTTask(const bool bIsloading, const UWorld* InWorld, USlotData* InSlotData, const FSELevelFilter& Filter) - : World(InWorld) - , SlotData(InSlotData) - , Filter(Filter) - {} -}; diff --git a/Source/SaveExtension/Public/Serialization/MTTask_SerializeActors.h b/Source/SaveExtension/Public/Serialization/MTTask_SerializeActors.h deleted file mode 100644 index 702c6d3..0000000 --- a/Source/SaveExtension/Public/Serialization/MTTask_SerializeActors.h +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "MTTask_SerializeActors.h" - -#include -#include -#include -#include - -#include "SavePreset.h" - -#include "MTTask.h" -#include "Serialization/Records.h" -#include "Serialization/LevelRecords.h" - - -class USlotData; - -/** Called when game has been saved - * @param SlotInfo the saved slot. Null if save failed - */ -DECLARE_DELEGATE_OneParam(FOnGameSaved, USlotInfo*); - - -///////////////////////////////////////////////////// -// FMTTask_SerializeActors -// Async task to serialize actors from a level. -class FMTTask_SerializeActors : public FMTTask -{ - const TArray* const LevelActors; - const int32 StartIndex; - const int32 Num; - const bool bStoreGameInstance = false; - - /** USE ONLY FOR DUMPING DATA */ - FLevelRecord* LevelRecord = nullptr; - - FActorRecord LevelScriptRecord; - TArray ActorRecords; - - -public: - FMTTask_SerializeActors(const UWorld* World, USlotData* SlotData, - const TArray* const InLevelActors, const int32 InStartIndex, const int32 InNum, bool bStoreGameInstance, - FLevelRecord* InLevelRecord, const FSELevelFilter& Filter) - : FMTTask(false, World, SlotData, Filter) - , LevelActors(InLevelActors) - , StartIndex(InStartIndex) - , Num(InNum) - , bStoreGameInstance(bStoreGameInstance) - , LevelRecord(InLevelRecord) - , LevelScriptRecord{} - , ActorRecords{} - { - // No apparent performance benefit - // ActorRecords.Reserve(Num); - } - - void DoWork(); - - /** Called after task has completed to recover resulting information */ - void DumpData() { - if (LevelScriptRecord.IsValid()) - LevelRecord->LevelScript = LevelScriptRecord; - - // Shrink not needed. Move wont keep reserved space - LevelRecord->Actors.Append(MoveTemp(ActorRecords)); - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FMTTask_SerializeActors, STATGROUP_ThreadPoolAsyncTasks); - } - -private: - - void SerializeGameInstance(); - - /** Serializes an actor into this Actor Record */ - bool SerializeActor(const AActor* Actor, FActorRecord& Record) const; - - /** Serializes the components of an actor into a provided Actor Record */ - inline void SerializeActorComponents(const AActor* Actor, FActorRecord& ActorRecord, int8 indent = 0) const; -}; diff --git a/Source/SaveExtension/Public/Serialization/Records.h b/Source/SaveExtension/Public/Serialization/Records.h index c41c40b..dd76414 100644 --- a/Source/SaveExtension/Public/Serialization/Records.h +++ b/Source/SaveExtension/Public/Serialization/Records.h @@ -1,15 +1,16 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include -#include -#include +#include #include "Records.generated.h" -class USlotData; + +struct FSEClassFilter; +class USaveSlotData; +class APlayerState; +class USubsystem; USTRUCT() @@ -31,11 +32,19 @@ struct FBaseRecord virtual ~FBaseRecord() {} }; -template<> +template <> struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2 -{ enum { WithSerializer = true }; }; +{ + enum + { + WithSerializer = true + }; +}; -FORCEINLINE bool operator==(const FBaseRecord& A, const FBaseRecord& B) { return A.Name == B.Name; } +inline bool operator==(const FBaseRecord& A, const FBaseRecord& B) +{ + return A.Name == B.Name; +} /** Represents a serialized Object */ @@ -58,10 +67,10 @@ struct FObjectRecord : public FBaseRecord bool IsValid() const { - return !Name.IsNone() && Class && Data.Num() > 0; + return !Name.IsNone() && Class; } - FORCEINLINE bool operator== (const UObject* Other) const + bool operator==(const UObject* Other) const { return Other && Name == Other->GetFName() && Class == Other->GetClass(); } @@ -77,6 +86,8 @@ struct FComponentRecord : public FObjectRecord FTransform Transform; + FComponentRecord() : Super() {} + FComponentRecord(const UActorComponent* Component) : Super(Component) {} virtual bool Serialize(FArchive& Ar) override; }; @@ -98,6 +109,56 @@ struct FActorRecord : public FObjectRecord FActorRecord() : Super() {} FActorRecord(const AActor* Actor) : Super(Actor) {} - virtual bool Serialize(FArchive& Ar) override; }; + + +/** Represents a serialized Subsystem */ +USTRUCT() +struct FSubsystemRecord : public FObjectRecord +{ + GENERATED_BODY() + + FSubsystemRecord() : Super() {} + FSubsystemRecord(const USubsystem* Subsystem); +}; + +USTRUCT(BlueprintType) +struct FPlayerRecord +{ + GENERATED_BODY() + + FUniqueNetIdRepl UniqueId; + + FActorRecord PlayerState; + FActorRecord Controller; + FActorRecord Pawn; + + + FPlayerRecord() = default; + FPlayerRecord(const FUniqueNetIdRepl& UniqueId) : UniqueId(UniqueId) {} + bool operator==(const FPlayerRecord& Other) const; +}; + + +namespace SERecords +{ + extern const FName TagNoTransform; + extern const FName TagNoPhysics; + extern const FName TagNoTags; + + + void SerializeActor(const AActor* Actor, FActorRecord& Record, const FSEClassFilter& ComponentFilter); + bool DeserializeActor(AActor* Actor, const FActorRecord& Record, const FSEClassFilter& ComponentFilter); + void SerializePlayer( + const APlayerState* PlayerState, FPlayerRecord& Record, const FSEClassFilter& ComponentFilter); + void DeserializePlayer( + APlayerState* PlayerState, const FPlayerRecord& Record, const FSEClassFilter& ComponentFilter); + + bool IsSaveTag(const FName& Tag); + bool StoresTransform(const AActor* Actor); + bool StoresPhysics(const AActor* Actor); + bool StoresTags(const AActor* Actor); + bool IsProcedural(const AActor* Actor); + bool StoresTags(const UActorComponent* Component); +} // namespace SERecords diff --git a/Source/SaveExtension/Public/Serialization/SEArchive.h b/Source/SaveExtension/Public/Serialization/SEArchive.h index f0d9100..ef7905c 100644 --- a/Source/SaveExtension/Public/Serialization/SEArchive.h +++ b/Source/SaveExtension/Public/Serialization/SEArchive.h @@ -9,7 +9,6 @@ struct FSEArchive : public FObjectAndNameAsStringProxyArchive { public: - FSEArchive(FArchive &InInnerArchive, bool bInLoadIfFindFails) : FObjectAndNameAsStringProxyArchive(InInnerArchive,bInLoadIfFindFails) { diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask.h b/Source/SaveExtension/Public/Serialization/SEDataTask.h new file mode 100644 index 0000000..ebd701e --- /dev/null +++ b/Source/SaveExtension/Public/Serialization/SEDataTask.h @@ -0,0 +1,89 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "LevelFilter.h" +#include "SaveSlotData.h" + +#include +#include +#include + + +class USaveManager; +class USaveSlotData; + + +enum class ESETaskType : uint8 +{ + None, + Load, + Save +}; + +/** + * Base class for managing the data of SaveSlot file + */ +struct FSEDataTask +{ + ESETaskType Type = ESETaskType::None; + +private: + bool bRunning = false; + bool bFinished = false; + bool bSucceeded = false; + +protected: + TObjectPtr Manager; + + +public: + FSEDataTask(USaveManager* Manager, ESETaskType Type) : Type(Type), Manager(Manager) {} + virtual ~FSEDataTask() = default; + + FSEDataTask& Start(); + + virtual void Tick(float DeltaTime) {} + + void Finish(bool bSuccess); + + bool IsRunning() const + { + return bRunning; + } + bool IsFinished() const + { + return bFinished; + } + bool IsSucceeded() const + { + return IsFinished() && bSucceeded; + } + bool IsFailed() const + { + return IsFinished() && !bSucceeded; + } + bool IsScheduled() const; + + virtual void OnTick(float DeltaTime) {} + +protected: + virtual void OnStart() {} + + virtual void OnFinish(bool bSuccess) {} + + void BakeAllFilters(); + + FLevelRecord* FindLevelRecord(USaveSlotData& Data, const ULevelStreaming* Level) const; + + float GetTimeMilliseconds() const + { + return FPlatformTime::ToMilliseconds(FPlatformTime::Cycles()); + } + + UWorld* GetWorld() const; + + +public: + static FString GetWorldName(const UWorld* World); +}; diff --git a/Source/SaveExtension/Public/Serialization/SlotDataTask_Loader.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h similarity index 50% rename from Source/SaveExtension/Public/Serialization/SlotDataTask_Loader.h rename to Source/SaveExtension/Public/Serialization/SEDataTask_Load.h index 7e5e911..1f9f9df 100644 --- a/Source/SaveExtension/Public/Serialization/SlotDataTask_Loader.h +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h @@ -1,22 +1,26 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once -#include "Delegates.h" -#include "ISaveExtension.h" -#include "Multithreading/LoadFileTask.h" -#include "SavePreset.h" -#include "SlotInfo.h" -#include "SlotData.h" -#include "SlotDataTask.h" +#include "SEDataTask.h" #include +#include #include #include -#include #include +#include + -#include "SlotDataTask_Loader.generated.h" +class USaveManager; +class USaveSlot; +class USaveSlotData; + + +/** Called when game has been loaded + * @param SaveSlot the loaded slot. Null if load failed + */ +DECLARE_DELEGATE_OneParam(FOnGameLoaded, USaveSlot*); enum class ELoadDataTaskState : uint8 @@ -32,22 +36,21 @@ enum class ELoadDataTaskState : uint8 }; /** -* Manages the loading process of a SaveData file -*/ -UCLASS() -class USlotDataTask_Loader : public USlotDataTask + * Manages the loading process of a SaveData file + */ +struct FSEDataTask_Load : public FSEDataTask { - GENERATED_BODY() - +protected: FName SlotName; - UPROPERTY() - USlotInfo* NewSlotInfo; + TObjectPtr Slot; + TObjectPtr SlotData; + float MaxFrameMs = 0.f; + FSEClassFilter SubsystemFilter; FOnGameLoaded Delegate; protected: - // Async variables TWeakObjectPtr CurrentLevel; TWeakObjectPtr CurrentSLevel; @@ -55,49 +58,43 @@ class USlotDataTask_Loader : public USlotDataTask int32 CurrentActorIndex = 0; TArray> CurrentLevelActors; - /** Start AsyncTasks */ - FAsyncTask* LoadDataTask; - /** End AsyncTasks */ + UE::Tasks::TTask LoadFileTask; ELoadDataTaskState LoadState = ELoadDataTaskState::NotStarted; public: + FSEDataTask_Load(USaveManager* Manager, USaveSlot* Slot); + ~FSEDataTask_Load(); - USlotDataTask_Loader() : Super() {} - - auto Setup(FName InSlotName) + auto& Setup(FName InSlotName) { SlotName = InSlotName; - return this; + return *this; } - auto Bind(const FOnGameLoaded& OnLoaded) { Delegate = OnLoaded; return this; } + auto& Bind(const FOnGameLoaded& OnLoaded) + { + Delegate = OnLoaded; + return *this; + } void OnMapLoaded(); private: - virtual void OnStart() override; virtual void Tick(float DeltaTime) override; virtual void OnFinish(bool bSuccess) override; - virtual void BeginDestroy() override; void StartDeserialization(); /** Spawns Actors hat were saved but which actors are not in the world. */ - void RespawnActors(const TArray& Records, const ULevel* Level); + void RespawnActors(const TArray& Records, const ULevel* Level, FLevelRecord& LevelRecord); protected: - - //~ Begin Files - void StartLoadingData(); - - USlotData* GetLoadedData() const; - FORCEINLINE const bool IsDataLoaded() const { return LoadDataTask && LoadDataTask->IsDone(); }; - //~ End Files - + void StartLoadingFile(); + bool CheckFileLoaded(); /** BEGIN Deserialization */ void BeforeDeserialize(); @@ -114,21 +111,6 @@ class USlotDataTask_Loader : public USlotDataTask void PrepareAllLevels(); void PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord); - /** Deserializes all Level actors. */ - inline void DeserializeLevel_Actor(AActor* const Actor, const FLevelRecord& LevelRecord, const FSELevelFilter& Filter); - void FindNextAsyncLevel(ULevelStreaming*& OutLevelStreaming) const; - -private: - - /** Deserializes Game Instance Object and its Properties. - Requires 'SaveGameMode' flag to be used. */ - void DeserializeGameInstance(); - - /** Serializes an actor into this Actor Record */ - bool DeserializeActor(AActor* Actor, const FActorRecord& Record, const FSELevelFilter& Filter); - - /** Deserializes the components of an actor from a provided Record */ - void DeserializeActorComponents(AActor* Actor, const FActorRecord& ActorRecord, const FSELevelFilter& Filter, int8 indent = 0); /** END Deserialization */ }; \ No newline at end of file diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_LoadLevel.h b/Source/SaveExtension/Public/Serialization/SEDataTask_LoadLevel.h new file mode 100644 index 0000000..f575fc1 --- /dev/null +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_LoadLevel.h @@ -0,0 +1,31 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SEDataTask_Load.h" + + +/** + * Manages the serializing process of a single level + */ +struct FSEDataTask_LoadLevel : public FSEDataTask_Load +{ + TObjectPtr StreamingLevel; + + +public: + FSEDataTask_LoadLevel(USaveManager* Manager, USaveSlot* Slot) + : FSEDataTask_Load(Manager, Slot) + {} + + auto& Setup(ULevelStreaming* InStreamingLevel) + { + StreamingLevel = InStreamingLevel; + return *this; + } + +private: + virtual void OnStart() override; + + virtual void DeserializeASyncLoop(float StartMS = 0.0f) override; +}; diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h new file mode 100644 index 0000000..6852ebc --- /dev/null +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h @@ -0,0 +1,85 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SEDataTask.h" + +#include +#include +#include +#include +#include +#include +#include + + +class USaveManager; +class USaveSlot; +class USaveSlotData; + + +/** Called when game has been saved + * @param SaveSlot the saved slot. Null if save failed + */ +DECLARE_DELEGATE_OneParam(FOnGameSaved, USaveSlot*); + + +/** + * Manages the saving process of a SaveData file + */ +struct FSEDataTask_Save : public FSEDataTask +{ + bool bOverride = false; + bool bCaptureThumbnail = false; + FName SlotName; + int32 Width = 0; + int32 Height = 0; + + FOnGameSaved Delegate; + +protected: + TObjectPtr Slot; + TObjectPtr SlotData; + FSEClassFilter SubsystemFilter; + + + UE::Tasks::TTask SaveFileTask; + + bool bWaitingThumbnail = false; + +public: + FSEDataTask_Save(USaveManager* Manager, USaveSlot* Slot); + ~FSEDataTask_Save(); + + auto& Setup( + FName InSlotName, bool bInOverride, bool bInSaveThumbnail, const int32 InWidth, const int32 InHeight) + { + SlotName = InSlotName; + bOverride = bInOverride; + bCaptureThumbnail = bInSaveThumbnail; + Width = InWidth; + Height = InHeight; + + return *this; + } + + auto& Bind(const FOnGameSaved& OnSaved) + { + Delegate = OnSaved; + return *this; + } + + // Where all magic happens + virtual void OnStart() override; + virtual void Tick(float DelLoadtaTime) override; + virtual void OnFinish(bool bSuccess) override; + +protected: + /** Serializes all world actors. */ + void SerializeWorld(); + void PrepareAllLevels(const TArray& Levels); + void PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord); + void SerializeLevel(const ULevel* Level, const ULevelStreaming* StreamingLevel = nullptr); + + void SaveFile(); +}; diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_SaveLevel.h b/Source/SaveExtension/Public/Serialization/SEDataTask_SaveLevel.h new file mode 100644 index 0000000..e03d0c2 --- /dev/null +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_SaveLevel.h @@ -0,0 +1,30 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include "SEDataTask_Save.h" + + +/** + * Manages the serializing process of a single level + */ +struct FSEDataTask_SaveLevel : public FSEDataTask_Save +{ + TObjectPtr StreamingLevel; + + +public: + FSEDataTask_SaveLevel(USaveManager* Manager, USaveSlot* Slot) + : FSEDataTask_Save(Manager, Slot) + {} + + auto& Setup(ULevelStreaming* InStreamingLevel) + { + StreamingLevel = InStreamingLevel; + return *this; + } + +private: + virtual void OnStart() override; + virtual void OnFinish(bool bSuccess) override; +}; diff --git a/Source/SaveExtension/Public/Serialization/SlotDataTask.h b/Source/SaveExtension/Public/Serialization/SlotDataTask.h deleted file mode 100644 index 387ab59..0000000 --- a/Source/SaveExtension/Public/Serialization/SlotDataTask.h +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" - -#include -#include -#include - -#include "SlotData.h" -#include "LevelFilter.h" - -#include "SlotDataTask.generated.h" - -class USaveManager; - - -/** -* Base class for managing a SaveData file -*/ -UCLASS() -class USlotDataTask : public UObject -{ - GENERATED_BODY() - -private: - - uint8 bRunning : 1; - uint8 bFinished : 1; - uint8 bSucceeded : 1; - -protected: - - UPROPERTY() - USlotData* SlotData; - - UPROPERTY() - const USavePreset* Preset; - - UPROPERTY() - float MaxFrameMs = 0.f; - -public: - - USlotDataTask() : Super(), bRunning(false), bFinished(false) {} - - void Prepare(USlotData* InSaveData, const USavePreset& InPreset) - { - SlotData = InSaveData; - Preset = &InPreset; - MaxFrameMs = Preset->GetMaxFrameMs(); - } - - USlotDataTask* Start(); - - virtual void Tick(float DeltaTime) {} - - void Finish(bool bSuccess); - - bool IsRunning() const { return bRunning; } - bool IsFinished() const { return bFinished; } - bool IsSucceeded() const { return IsFinished() && bSucceeded; } - bool IsFailed() const { return IsFinished() && !bSucceeded; } - bool IsScheduled() const; - - virtual void OnTick(float DeltaTime) {} - -protected: - - virtual void OnStart() {} - - virtual void OnFinish(bool bSuccess) {} - - USaveManager* GetManager() const; - - void BakeAllFilters(); - - const FSELevelFilter& GetGeneralFilter() const; - const FSELevelFilter& GetLevelFilter(const FLevelRecord& Level) const; - - FLevelRecord* FindLevelRecord(const ULevelStreaming* Level) const; - - //~ Begin UObject Interface - virtual UWorld* GetWorld() const override; - //~ End UObject Interface - - FORCEINLINE float GetTimeMilliseconds() const { - return FPlatformTime::ToMilliseconds(FPlatformTime::Cycles()); - } -}; - - -///////////////////////////////////////////////////// -// FSlotDataActorsTask -// Async task to serialize actors from a level. -class FSlotDataActorsTask : public FNonAbandonableTask { -protected: - - const bool bIsSync; - /** USE ONLY IF SYNC */ - const UWorld* const World; - /** USE ONLY IF SYNC */ - USlotData* SlotData; - - const FSELevelFilter& Filter; - - - FSlotDataActorsTask(const bool bInIsSync, const UWorld* InWorld, USlotData* InSlotData, const FSELevelFilter& Filter) : - bIsSync(bInIsSync), - World(InWorld), - SlotData(InSlotData), - Filter(Filter) - {} -}; diff --git a/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelLoader.h b/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelLoader.h deleted file mode 100644 index 17d0bd1..0000000 --- a/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelLoader.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" - -#include "SavePreset.h" - -#include "SlotDataTask_Loader.h" -#include "SlotDataTask_LevelLoader.generated.h" - - -/** -* Manages the serializing process of a single level -*/ -UCLASS() -class USlotDataTask_LevelLoader : public USlotDataTask_Loader -{ - GENERATED_BODY() - - - UPROPERTY() - ULevelStreaming* StreamingLevel; - -public: - - auto Setup(ULevelStreaming* InStreamingLevel) - { - StreamingLevel = InStreamingLevel; - return this; - } - -private: - - virtual void OnStart() override; - - virtual void DeserializeASyncLoop(float StartMS = 0.0f) override; -}; diff --git a/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelSaver.h b/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelSaver.h deleted file mode 100644 index 63f1c88..0000000 --- a/Source/SaveExtension/Public/Serialization/SlotDataTask_LevelSaver.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" - -#include "SavePreset.h" - -#include "SlotDataTask_Saver.h" -#include "SlotDataTask_LevelSaver.generated.h" - - -/** -* Manages the serializing process of a single level -*/ -UCLASS() -class USlotDataTask_LevelSaver : public USlotDataTask_Saver -{ - GENERATED_BODY() - - - UPROPERTY() - ULevelStreaming* StreamingLevel; - -public: - - auto Setup(ULevelStreaming* InStreamingLevel) - { - StreamingLevel = InStreamingLevel; - return this; - } - -private: - - virtual void OnStart() override; - virtual void OnFinish(bool bSuccess) override { - SELog(Preset, "Finished Serializing level", FColor::Green); - } -}; diff --git a/Source/SaveExtension/Public/Serialization/SlotDataTask_Saver.h b/Source/SaveExtension/Public/Serialization/SlotDataTask_Saver.h deleted file mode 100644 index 46fb9b0..0000000 --- a/Source/SaveExtension/Public/Serialization/SlotDataTask_Saver.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include "Delegates.h" -#include "ISaveExtension.h" -#include "MTTask_SerializeActors.h" -#include "Multithreading/SaveFileTask.h" -#include "SavePreset.h" -#include "SlotData.h" -#include "SlotDataTask.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "SlotDataTask_Saver.generated.h" - - -/** -* Manages the saving process of a SaveData file -*/ -UCLASS() -class USlotDataTask_Saver : public USlotDataTask -{ - GENERATED_BODY() - - bool bOverride; - bool bSaveThumbnail; - FName SlotName; - int32 Width; - int32 Height; - - FOnGameSaved Delegate; - -protected: - - UPROPERTY() - USlotInfo* SlotInfo; - - /** Start Async variables */ - TWeakObjectPtr CurrentLevel; - TWeakObjectPtr CurrentSLevel; - int32 CurrentActorIndex; - TArray> CurrentLevelActors; - /** End Async variables */ - - /** Begin AsyncTasks */ - TArray> Tasks; - FAsyncTask* SaveTask; - /** End AsyncTasks */ - - -public: - - USlotDataTask_Saver() - : USlotDataTask() - , SaveTask(nullptr) - {} - - auto* Setup(FName InSlotName, bool bInOverride, bool bInSaveThumbnail, const int32 InWidth, const int32 InHeight) - { - SlotName = InSlotName; - bOverride = bInOverride; - bSaveThumbnail = bInSaveThumbnail; - Width = InWidth; - Height = InHeight; - - return this; - } - - auto* Bind(const FOnGameSaved& OnSaved) { Delegate = OnSaved; return this; } - - // Where all magic happens - virtual void OnStart() override; - virtual void Tick(float DeltaTime) override; - virtual void OnFinish(bool bSuccess) override; - virtual void BeginDestroy() override; - -protected: - - /** BEGIN Serialization */ - /** Serializes all world actors. */ - void SerializeWorld(); - - void PrepareAllLevels(const TArray& Levels); - - void SerializeLevelSync(const ULevel* Level, int32 AssignedThreads, const ULevelStreaming* StreamingLevel = nullptr); - - /** END Serialization */ - - void RunScheduledTasks(); - -private: - - /** BEGIN FileSaving */ - void SaveFile(); - /** End FileSaving */ -}; diff --git a/Source/SaveExtension/Public/SlotData.h b/Source/SaveExtension/Public/SlotData.h deleted file mode 100644 index 1cf8f1d..0000000 --- a/Source/SaveExtension/Public/SlotData.h +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - - -#pragma once - -#include "ISaveExtension.h" - - #include -#include -#include -#include -#include - -#include "Serialization/Records.h" -#include "Serialization/LevelRecords.h" - -#include "SlotData.generated.h" - - -/** - * USaveData stores all information that can be accessible only while the game is loaded. - * Works like a common SaveGame object - * E.g: Items, Quests, Enemies, World Actors, AI, Physics - */ -UCLASS(ClassGroup = SaveExtension, hideCategories = ("Activation", "Actor Tick", "Actor", "Input", "Rendering", "Replication", "Socket", "Thumbnail")) -class SAVEEXTENSION_API USlotData : public USaveGame -{ - GENERATED_BODY() - -public: - - USlotData() : Super() {} - - - /** Full Name of the Map where this SlotData was saved */ - UPROPERTY(Category = SaveData, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) - FName Map; - - /** Game world time since game started in seconds */ - UPROPERTY(Category = SaveData, BlueprintReadOnly) - float TimeSeconds; - - /** Records - * All serialized information to be saved or loaded - * Serialized manually for performance - */ - bool bStoreGameInstance = false; - FObjectRecord GameInstance; - - FSELevelFilter GeneralLevelFilter; - FPersistentLevelRecord MainLevel; - TArray SubLevels; - - - void CleanRecords(bool bKeepSublevels); - - /** Using manual serialization. It's way faster than reflection serialization */ - virtual void Serialize(FArchive& Ar) override; -}; diff --git a/Source/SaveExtension/Public/SlotInfo.h b/Source/SaveExtension/Public/SlotInfo.h deleted file mode 100644 index 115b18b..0000000 --- a/Source/SaveExtension/Public/SlotInfo.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include - -#include "SlotInfo.generated.h" - - -/** - * USaveInfo stores information that needs to be accessible WITHOUT loading the game. - * Works like a common SaveGame object - * E.g: Dates, played time, progress, level - */ -UCLASS(ClassGroup = SaveExtension, hideCategories = ("Activation", "Actor Tick", "Actor", "Input", "Rendering", "Replication", "Socket", "Thumbnail")) -class SAVEEXTENSION_API USlotInfo : public USaveGame -{ - GENERATED_BODY() - -public: - - USlotInfo() : Super() - , PlayedTime(FTimespan::Zero()) - , SlotPlayedTime(FTimespan::Zero()) - , SaveDate(FDateTime::Now()) - {} - - - /** Slot where this SaveInfo and its saveData are saved */ - UPROPERTY(BlueprintReadOnly, Category = SlotInfo) - FName FileName; - - UPROPERTY(BlueprintReadWrite, Category = SlotInfo) - FText DisplayName; - - /** Played time since this saved game was started. Not related to slots, slots can change */ - UPROPERTY(BlueprintReadOnly, Category = SlotInfo) - FTimespan PlayedTime; - - /** Played time since this saved game was created */ - UPROPERTY(BlueprintReadOnly, Category = SlotInfo) - FTimespan SlotPlayedTime; - - /** Last date this slot was saved. */ - UPROPERTY(BlueprintReadOnly, Category = SlotInfo) - FDateTime SaveDate; - - /** Date at which this slot was loaded. */ - UPROPERTY(BlueprintReadOnly, Transient, Category = SlotInfo) - FDateTime LoadDate; - - /** Root Level where this Slot was saved */ - UPROPERTY(BlueprintReadOnly, Category = SlotInfo) - FName Map; - -private: - - UPROPERTY() - FString ThumbnailPath; - - /** Thumbnail gets cached here the first time it is requested */ - UPROPERTY(Transient) - UTexture2D* CachedThumbnail; - - -public: - - /** Returns this slot's thumbnail if any */ - UFUNCTION(BlueprintCallable, Category = SlotInfo) - UTexture2D* GetThumbnail() const; - - /** Captures a thumbnail for the current slot */ - bool CaptureThumbnail(const int32 Width = 640, const int32 Height = 360); - - - /** Internal Usage. Will be called when an screenshot is captured */ - void _SetThumbnailPath(const FString& Path); - - /** Internal Usage. Will be called to remove previous thumbnail */ - FString _GetThumbnailPath() { return ThumbnailPath; } -}; diff --git a/Source/SaveExtension/SaveExtension.Build.cs b/Source/SaveExtension/SaveExtension.Build.cs index c09e149..00c330f 100644 --- a/Source/SaveExtension/SaveExtension.Build.cs +++ b/Source/SaveExtension/SaveExtension.Build.cs @@ -3,27 +3,35 @@ using UnrealBuildTool; using System.IO; -namespace UnrealBuildTool.Rules { - -public class SaveExtension : ModuleRules +namespace UnrealBuildTool.Rules { - public SaveExtension(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bEnforceIWYU = true; - PublicDependencyModuleNames.AddRange(new string[] + public class SaveExtension : ModuleRules + { + public SaveExtension(ReadOnlyTargetRules Target) : base(Target) { - "Core", - "Engine", - "Foliage", - "AIModule", - "CoreUObject", - "DeveloperSettings", - "ImageWrapper", - "NavigationSystem" - }); + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + IWYUSupport = IWYUSupport.Full; + + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "Engine", + "AIModule", + "CoreUObject", + "DeveloperSettings" + }); + + PrivateDependencyModuleNames.AddRange(new string[] { }); + + if (Target.Type == TargetType.Editor) + { + PrivateDependencyModuleNames.AddRange(new string[] + { + "UnrealEd" + }); + } + } } -} } diff --git a/Source/Test/Private/Automatron.h b/Source/Test/Private/Automatron.h index 98afce1..7f2d967 100644 --- a/Source/Test/Private/Automatron.h +++ b/Source/Test/Private/Automatron.h @@ -38,8 +38,8 @@ #pragma once #include -#include #include +#include #include #include #include @@ -88,7 +88,6 @@ namespace Automatron class TRegister : public FRegister { public: - // Just by existing, this instance will define the class and register the spec static TRegister Instance; @@ -99,7 +98,6 @@ namespace Automatron } private: - static void Setup() { static T Spec{}; @@ -631,9 +629,10 @@ namespace Automatron uint32 Flags = 0; bool bInitializedWorld = false; -# if WITH_EDITOR +#if WITH_EDITOR bool bInitializedPIE = false; -# endif + FDelegateHandle PIEStartedHandle; +#endif TWeakObjectPtr MainWorld; @@ -731,28 +730,28 @@ namespace Automatron // GENERATION MACROS #define GENERATE_SPEC(TClass, PrettyName, TFlags) \ - GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) + GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) #define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ - private: \ - void Setup() \ - { \ - FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ - } \ - static Automatron::Spec::TRegister& __meta_register() \ - { \ - return Automatron::Spec::TRegister::Instance; \ - } \ - friend Automatron::Spec::TRegister; \ - \ - virtual void Define() override +private: \ + void Setup() \ + { \ + FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ + } \ + static Automatron::Spec::TRegister& __meta_register() \ + { \ + return Automatron::Spec::TRegister::Instance; \ + } \ + friend Automatron::Spec::TRegister; \ + \ + virtual void Define() override #define SPEC(TClass, TParent, PrettyName, TFlags) \ - class TClass : public TParent \ - { \ - GENERATE_SPEC(TClass, PrettyName, TFlags); \ - }; \ - void TClass::Define() + class TClass : public TParent \ + { \ + GENERATE_SPEC(TClass, PrettyName, TFlags); \ + }; \ + void TClass::Define() //////////////////////////////////////////////////////////////// @@ -1215,14 +1214,12 @@ namespace Automatron UWorld* SelectedWorld = FindGameWorld(); -# if WITH_EDITOR +#if WITH_EDITOR // If there was no PIE world, start it and try again if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) { - FDelegateHandle PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda( - [this, OnWorldReady, PIEStartedHandle](const bool bIsSimulating) { - FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); - + PIEStartedHandle = + FEditorDelegates::PostPIEStarted.AddLambda([this, OnWorldReady](const bool bIsSimulating) { UWorld* SelectedWorld = FindGameWorld(); bInitializedPIE = SelectedWorld != nullptr; bInitializedWorld = bInitializedPIE; @@ -1232,7 +1229,7 @@ namespace Automatron FEditorPromotionTestUtilities::StartPIE(false); return; } -# endif +#endif if (!SelectedWorld) { @@ -1253,7 +1250,8 @@ namespace Automatron return; } -# if WITH_EDITOR +#if WITH_EDITOR + FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); if (bInitializedPIE) { FEditorPromotionTestUtilities::EndPIE(); @@ -1261,7 +1259,7 @@ namespace Automatron bInitializedWorld = false; return; } -# endif +#endif if (!bInitializedWorld) { @@ -1352,7 +1350,7 @@ namespace Automatron inline UGameInstance* FTestSpec::CreateGameInstance(const FTestWorldSettings& Settings, UObject* Context) { UClass* GameInstanceClass = Settings.GameInstance.Get(); - if(!GameInstanceClass) + if (!GameInstanceClass) { FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; GameInstanceClass = GameInstanceClassName.TryLoadClass(); @@ -1420,7 +1418,8 @@ namespace Automatron inline bool FTestSpec::SetGameMode(UWorld* World, FTestWorldSettings& Settings) { - if ((!World->IsNetMode(NM_DedicatedServer) && !World->IsNetMode(NM_ListenServer)) || World->GetAuthGameMode()) + if ((!World->IsNetMode(NM_DedicatedServer) && !World->IsNetMode(NM_ListenServer)) || + World->GetAuthGameMode()) { return false; } diff --git a/Source/Test/Private/Files.spec.cpp b/Source/Test/Private/Files.spec.cpp index 6218601..dcb3f54 100644 --- a/Source/Test/Private/Files.spec.cpp +++ b/Source/Test/Private/Files.spec.cpp @@ -1,9 +1,10 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "Automatron.h" #include "Helpers/TestActor.h" -#include "SaveManager.h" -#include "FileAdapter.h" + +#include +#include class FSaveSpec_Files : public Automatron::FTestSpec @@ -13,7 +14,6 @@ class FSaveSpec_Files : public Automatron::FTestSpec USaveManager* SaveManager = nullptr; ATestActor* TestActor = nullptr; - USavePreset* TestPreset = nullptr; // Helper for some test delegates bool bFinishTick = false; @@ -34,32 +34,31 @@ void FSaveSpec_Files::Define() SaveManager->bTickWithGameWorld = true; - // Set test preset - TestPreset = SaveManager->SetActivePreset(USavePreset::StaticClass()); - TestPreset->MultithreadedSerialization = ESaveASyncMode::OnlySync; + SaveManager->GetActiveSlot()->MultithreadedSerialization = ESEAsyncMode::SaveAndLoadSync; }); It("Can save files synchronously", [this]() { - TestPreset->MultithreadedFiles = ESaveASyncMode::OnlySync; + SaveManager->GetActiveSlot()->MultithreadedFiles = ESEAsyncMode::SaveAndLoadSync; TestTrue("Saved", SaveManager->SaveSlot(0)); - TestTrue("Info File exists in disk", FFileAdapter::DoesFileExist(TEXT("0"))); + TestTrue("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); }); It("Can save files asynchronously", [this]() { - TestPreset->MultithreadedFiles = ESaveASyncMode::SaveAsync; + SaveManager->GetActiveSlot()->MultithreadedFiles = ESEAsyncMode::SaveAsync; bFinishTick = false; - bool bSaving = SaveManager->SaveSlot(0, true, false, {}, FOnGameSaved::CreateLambda([this](auto* Info) { - // Notified that files have been saved asynchronously - TestTrue("Info File exists in disk", FFileAdapter::DoesFileExist(TEXT("0"))); - bFinishTick = true; - })); + bool bSaving = + SaveManager->SaveSlot(0, true, false, {}, FOnGameSaved::CreateLambda([this](auto* Info) { + // Notified that files have been saved asynchronously + TestTrue("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); + bFinishTick = true; + })); TestTrue("Started Saving", bSaving); // Files shouldn't exist yet - TestFalse("Info File exists in disk", FFileAdapter::DoesFileExist(TEXT("0"))); + TestFalse("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; @@ -67,24 +66,22 @@ void FSaveSpec_Files::Define() }); It("Can load files synchronously", [this]() { - TestPreset->MultithreadedFiles = ESaveASyncMode::OnlySync; + SaveManager->GetActiveSlot()->MultithreadedFiles = ESEAsyncMode::SaveAndLoadSync; TestTrue("Saved", SaveManager->SaveSlot(0)); - USlotInfo* Info = nullptr; - USlotData* Data = nullptr; - TestTrue("File was loaded", FFileAdapter::LoadFile(TEXT("0"), Info, Data, true, SaveManager)); - TestNotNull("Info is valid", Info); - TestNotNull("Data is valid", Data); + USaveSlot* Slot = FSEFileHelpers::LoadFileSync(TEXT("0"), nullptr, true, SaveManager); + TestNotNull("Slot is valid", Slot); + TestNotNull("Data is valid", Slot->GetData()); }); AfterEach([this]() { if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; }); diff --git a/Source/Test/Private/GameInstance.spec.cpp b/Source/Test/Private/GameInstanceSpec.cpp similarity index 63% rename from Source/Test/Private/GameInstance.spec.cpp rename to Source/Test/Private/GameInstanceSpec.cpp index 0b72cfd..34c0888 100644 --- a/Source/Test/Private/GameInstance.spec.cpp +++ b/Source/Test/Private/GameInstanceSpec.cpp @@ -1,7 +1,8 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "GameInstanceSpec.h" #include "Automatron.h" -#include "Helpers/TestGameInstance.h" #include "SaveManager.h" @@ -11,7 +12,6 @@ class FSaveSpec_GameInstance : public Automatron::FTestSpec EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter); USaveManager* SaveManager = nullptr; - USavePreset* TestPreset = nullptr; // Helper for some test delegates bool bFinishTick = false; @@ -22,7 +22,7 @@ class FSaveSpec_GameInstance : public Automatron::FTestSpec bCanUsePIEWorld = false; DefaultWorldSettings.bShouldTick = true; - DefaultWorldSettings.GameInstance = UTestGameInstance::StaticClass(); + DefaultWorldSettings.GameInstance = USETestGameInstance::StaticClass(); } }; @@ -34,16 +34,11 @@ void FSaveSpec_GameInstance::Define() SaveManager->bTickWithGameWorld = true; - TestPreset = SaveManager->SetActivePreset(USavePreset::StaticClass()); - TestPreset->bStoreGameInstance = true; - - TestPreset->MultithreadedFiles = ESaveASyncMode::OnlySync; - TestPreset->MultithreadedSerialization = ESaveASyncMode::OnlySync; + SaveManager->AssureActiveSlot(UTestSaveSlot::StaticClass(), true); }); - It("GameInstance can be saved", [this]() - { - auto* GI = GetMainWorld()->GetGameInstance(); + It("GameInstance can be saved", [this]() { + auto* GI = GetMainWorld()->GetGameInstance(); GI->bMyBool = true; SaveManager->SaveSlot(0); @@ -56,15 +51,13 @@ void FSaveSpec_GameInstance::Define() TestTrue("Saved variable loaded", GI->bMyBool); }); - AfterEach([this]() - { + AfterEach([this]() { if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() - { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; diff --git a/Source/Test/Private/GameInstanceSpec.h b/Source/Test/Private/GameInstanceSpec.h new file mode 100644 index 0000000..c92785a --- /dev/null +++ b/Source/Test/Private/GameInstanceSpec.h @@ -0,0 +1,23 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. +#pragma once + +#include "Helpers/SETestGameInstance.h" + +#include + +#include "GameInstanceSpec.generated.h" + + +UCLASS() +class UTestSaveSlot : public USaveSlot +{ + GENERATED_BODY() + + UTestSaveSlot() : Super() + { + bStoreGameInstance = true; + + MultithreadedFiles = ESEAsyncMode::SaveAndLoadSync; + MultithreadedSerialization = ESEAsyncMode::SaveAndLoadSync; + } +}; diff --git a/Source/Test/Private/Helpers/SETestGameInstance.h b/Source/Test/Private/Helpers/SETestGameInstance.h new file mode 100644 index 0000000..1399aee --- /dev/null +++ b/Source/Test/Private/Helpers/SETestGameInstance.h @@ -0,0 +1,20 @@ + +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#pragma once + +#include +#include + +#include "SETestGameInstance.generated.h" + + +UCLASS() +class USETestGameInstance : public UGameInstance +{ + GENERATED_BODY() + +public: + UPROPERTY(SaveGame) + bool bMyBool = false; +}; diff --git a/Source/Test/Private/Helpers/TestActor.h b/Source/Test/Private/Helpers/TestActor.h index 610d7f6..f380d13 100644 --- a/Source/Test/Private/Helpers/TestActor.h +++ b/Source/Test/Private/Helpers/TestActor.h @@ -1,61 +1,62 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once #include #include + #include "TestActor.generated.h" + USTRUCT() struct FTestSaveStruct { - GENERATED_BODY() + GENERATED_BODY() }; UCLASS() class ATestActor : public AActor { - GENERATED_BODY() + GENERATED_BODY() public: + UPROPERTY(SaveGame) + bool bMyBool = false; - UPROPERTY(SaveGame) - bool bMyBool = false; - - UPROPERTY(SaveGame) - float MyFloat = 0.f; + UPROPERTY(SaveGame) + float MyFloat = 0.f; - // INTEGERS + // INTEGERS - UPROPERTY(SaveGame) - uint8 MyU8 = 0.f; + UPROPERTY(SaveGame) + uint8 MyU8 = 0.f; - UPROPERTY(SaveGame) - uint16 MyU16 = 0; + UPROPERTY(SaveGame) + uint16 MyU16 = 0; - UPROPERTY(SaveGame) - uint32 MyU32 = 0; + UPROPERTY(SaveGame) + uint32 MyU32 = 0; - UPROPERTY(SaveGame) - uint64 MyU64 = 0; + UPROPERTY(SaveGame) + uint64 MyU64 = 0; - UPROPERTY(SaveGame) - int8 MyI8 = 0.f; + UPROPERTY(SaveGame) + int8 MyI8 = 0.f; - UPROPERTY(SaveGame) - int16 MyI16 = 0; + UPROPERTY(SaveGame) + int16 MyI16 = 0; - UPROPERTY(SaveGame) - int32 MyI32 = 0; + UPROPERTY(SaveGame) + int32 MyI32 = 0; - UPROPERTY(SaveGame) - int64 MyI64 = 0; + UPROPERTY(SaveGame) + int64 MyI64 = 0; - UPROPERTY(SaveGame) - FTestSaveStruct MyStruct; + UPROPERTY(SaveGame) + FTestSaveStruct MyStruct; }; diff --git a/Source/Test/Private/Helpers/TestGameInstance.h b/Source/Test/Private/Helpers/TestGameInstance.h deleted file mode 100644 index 7ede425..0000000 --- a/Source/Test/Private/Helpers/TestGameInstance.h +++ /dev/null @@ -1,19 +0,0 @@ - -// Copyright 2015-2020 Piperift. All Rights Reserved. - -#pragma once - -#include -#include -#include "TestGameInstance.generated.h" - -UCLASS() -class UTestGameInstance : public UGameInstance -{ - GENERATED_BODY() - -public: - - UPROPERTY(SaveGame) - bool bMyBool = false; -}; diff --git a/Source/Test/Private/SaveExtensionTest.cpp b/Source/Test/Private/SaveExtensionTest.cpp index 69ff67e..ce019a9 100644 --- a/Source/Test/Private/SaveExtensionTest.cpp +++ b/Source/Test/Private/SaveExtensionTest.cpp @@ -1,9 +1,11 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #include "SaveExtensionTest.h" + #include "Automatron.h" + IMPLEMENT_MODULE(FSaveExtensionTest, SaveExtensionTest); void FSaveExtensionTest::StartupModule() diff --git a/Source/Test/Private/Save.spec.cpp b/Source/Test/Private/SavingSpec.cpp similarity index 82% rename from Source/Test/Private/Save.spec.cpp rename to Source/Test/Private/SavingSpec.cpp index 3da63a9..58413a6 100644 --- a/Source/Test/Private/Save.spec.cpp +++ b/Source/Test/Private/SavingSpec.cpp @@ -1,7 +1,8 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. + +#include "SavingSpec.h" #include "Automatron.h" -#include "Helpers/TestActor.h" #include "SaveManager.h" @@ -12,7 +13,6 @@ class FSaveSpec_Preset : public Automatron::FTestSpec USaveManager* SaveManager = nullptr; ATestActor* TestActor = nullptr; - USavePreset* TestPreset = nullptr; // Helper for some test delegates bool bFinishTick = false; @@ -57,18 +57,12 @@ void FSaveSpec_Preset::Define() Describe("Serialization", [this]() { BeforeEach([this]() { - TestPreset = SaveManager->SetActivePreset(USavePreset::StaticClass()); - TestPreset->ActorFilter.ClassFilter.AllowedClasses.Add(ATestActor::StaticClass()); - - // We don't need Async files are tested independently - TestPreset->MultithreadedFiles = ESaveASyncMode::OnlySync; + SaveManager->AssureActiveSlot(UTestSaveSlot_SyncSaving::StaticClass(), true); TestActor = GetMainWorld()->SpawnActor(); }); It("Can save an actor synchronously", [this]() { - TestPreset->MultithreadedSerialization = ESaveASyncMode::OnlySync; - TestTrue("Saved", SaveManager->SaveSlot(0)); TestTrue("Loaded", SaveManager->LoadSlot(0)); }); @@ -78,12 +72,7 @@ void FSaveSpec_Preset::Define() }); Describe("Properties", [this]() { - BeforeEach([this]() { - TestPreset->MultithreadedSerialization = ESaveASyncMode::OnlySync; - }); - - It("bool", [this]() - { + It("bool", [this]() { TestActor->bMyBool = true; SaveManager->SaveSlot(0); TestTrue("bool didn't change after save", TestActor->bMyBool); @@ -95,8 +84,7 @@ void FSaveSpec_Preset::Define() TestTrue("bool was saved", TestActor->bMyBool); }); - It("uint8", [this]() - { + It("uint8", [this]() { TestActor->MyU8 = 34; SaveManager->SaveSlot(0); TestEqual("uint8 didn't change after save", TestActor->MyU8, 34); @@ -106,8 +94,7 @@ void FSaveSpec_Preset::Define() TestEqual("uint8 was saved", TestActor->MyU8, 34); }); - It("uint16", [this]() - { + It("uint16", [this]() { TestActor->MyU16 = 34; SaveManager->SaveSlot(0); TestEqual("uint16 didn't change after save", TestActor->MyU16, 34); @@ -117,8 +104,7 @@ void FSaveSpec_Preset::Define() TestEqual("uint16 was saved", TestActor->MyU16, 34); }); - It("uint32", [this]() - { + It("uint32", [this]() { TestActor->MyU32 = 34; SaveManager->SaveSlot(0); TestEqual("uint32 didn't change after save", TestActor->MyU32, 34); @@ -128,8 +114,7 @@ void FSaveSpec_Preset::Define() TestEqual("uint32 was saved", TestActor->MyU32, 34); }); - It("uint64", [this]() - { + It("uint64", [this]() { TestActor->MyU64 = 34; SaveManager->SaveSlot(0); TestEqual("uint16 didn't change after save", TestActor->MyU64, 34); @@ -139,8 +124,7 @@ void FSaveSpec_Preset::Define() TestEqual("uint16 was saved", TestActor->MyU64, 34); }); - It("int8", [this]() - { + It("int8", [this]() { TestActor->MyI8 = 34; SaveManager->SaveSlot(0); TestEqual("int8 didn't change after save", TestActor->MyI8, 34); @@ -150,8 +134,7 @@ void FSaveSpec_Preset::Define() TestEqual("int8 was saved", TestActor->MyI8, 34); }); - It("int16", [this]() - { + It("int16", [this]() { TestActor->MyI16 = 34; SaveManager->SaveSlot(0); TestEqual("int16 didn't change after save", TestActor->MyI16, 34); @@ -161,8 +144,7 @@ void FSaveSpec_Preset::Define() TestEqual("int16 was saved", TestActor->MyI16, 34); }); - It("int32", [this]() - { + It("int32", [this]() { TestActor->MyI32 = 34; SaveManager->SaveSlot(0); TestEqual("int32 didn't change after save", TestActor->MyI32, 34); @@ -172,8 +154,7 @@ void FSaveSpec_Preset::Define() TestEqual("int32 was saved", TestActor->MyI32, 34); }); - It("int64", [this]() - { + It("int64", [this]() { TestActor->MyI64 = 34; SaveManager->SaveSlot(0); TestEqual("int64 didn't change after save", TestActor->MyI64, 34); @@ -197,9 +178,9 @@ void FSaveSpec_Preset::Define() if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; diff --git a/Source/Test/Private/SavingSpec.h b/Source/Test/Private/SavingSpec.h new file mode 100644 index 0000000..92b07b7 --- /dev/null +++ b/Source/Test/Private/SavingSpec.h @@ -0,0 +1,24 @@ +// Copyright 2015-2024 Piperift. All Rights Reserved. +#pragma once + +#include "Helpers/TestActor.h" + +#include + +#include "SavingSpec.generated.h" + + +UCLASS() +class UTestSaveSlot_SyncSaving : public USaveSlot +{ + GENERATED_BODY() + + UTestSaveSlot_SyncSaving() : Super() + { + bStoreGameInstance = true; + + MultithreadedFiles = ESEAsyncMode::SaveAndLoadSync; + MultithreadedSerialization = ESEAsyncMode::SaveAndLoadSync; + ActorFilter.AllowedClasses.Add(ATestActor::StaticClass()); + } +}; diff --git a/Source/Test/Public/SaveExtensionTest.h b/Source/Test/Public/SaveExtensionTest.h index e13d993..4c347c2 100644 --- a/Source/Test/Public/SaveExtensionTest.h +++ b/Source/Test/Public/SaveExtensionTest.h @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Piperift. All Rights Reserved. +// Copyright 2015-2024 Piperift. All Rights Reserved. #pragma once diff --git a/Source/Test/SaveExtensionTest.Build.cs b/Source/Test/SaveExtensionTest.Build.cs index 5281610..5297223 100644 --- a/Source/Test/SaveExtensionTest.Build.cs +++ b/Source/Test/SaveExtensionTest.Build.cs @@ -5,11 +5,12 @@ namespace UnrealBuildTool.Rules { - public class SaveExtensionTest : ModuleRules { + public class SaveExtensionTest : ModuleRules + { public SaveExtensionTest(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - bEnforceIWYU = true; + IWYUSupport = IWYUSupport.Full; PublicDependencyModuleNames.AddRange(new string[] { @@ -20,10 +21,10 @@ public SaveExtensionTest(ReadOnlyTargetRules Target) : base(Target) "EngineSettings" }); - if (Target.bBuildEditor == true) - { - PrivateDependencyModuleNames.Add("UnrealEd"); - } - } + if (Target.bBuildEditor == true) + { + PrivateDependencyModuleNames.Add("UnrealEd"); + } + } } } \ No newline at end of file