diff --git a/SPUD.uplugin b/SPUD.uplugin index 65e3e41..537a8e2 100644 --- a/SPUD.uplugin +++ b/SPUD.uplugin @@ -32,12 +32,5 @@ "LoadingPhase": "Default" } - ], - "Plugins" : - [ - { - "Name" : "StructUtils", - "Enabled" : true - } ] } \ No newline at end of file diff --git a/Source/SPUD/Private/SpudData.cpp b/Source/SPUD/Private/SpudData.cpp index cfedee1..e375bd0 100644 --- a/Source/SPUD/Private/SpudData.cpp +++ b/Source/SPUD/Private/SpudData.cpp @@ -1147,7 +1147,7 @@ void FSpudSaveData::DeleteAllLevelDataFiles(const FString& LevelPath) FString FSpudSaveData::GetLevelDataPath(const FString& LevelPath, const FString& LevelName) { - return FString::Printf(TEXT("%s%s.lvl"), *LevelPath, *LevelName); + return FString::Printf(TEXT("%s%s.lvl"), *LevelPath, *LevelName); } void FSpudSaveData::WriteLevelData(FSpudLevelData& LevelData, const FString& LevelName, const FString& LevelPath) @@ -1183,7 +1183,8 @@ bool FSpudSaveData::ReadSaveInfoFromArchive(FSpudChunkedDataArchive& Ar, FSpudSa if (Hdr.Magic != FSpudChunkHeader::EncodeMagic(SPUDDATA_SAVEGAME_MAGIC)) { - UE_LOG(LogSpudData, Error, TEXT("Cannot get info for save game, file is not a save game")) + // This is actually not an error, there might be other external saved files in folder + UE_LOG(LogSpudData, VeryVerbose, TEXT("Cannot get info for save game, file is not a save game")) return false; } diff --git a/Source/SPUD/Private/SpudPropertyUtil.cpp b/Source/SPUD/Private/SpudPropertyUtil.cpp index f00d723..08da338 100644 --- a/Source/SPUD/Private/SpudPropertyUtil.cpp +++ b/Source/SPUD/Private/SpudPropertyUtil.cpp @@ -2,9 +2,9 @@ #include #include "EngineUtils.h" -#include "InstancedStruct.h" #include "ISpudObject.h" #include "..\Public\SpudMemoryReaderWriter.h" +#include "StructUtils/InstancedStruct.h" DEFINE_LOG_CATEGORY(LogSpudProps) diff --git a/Source/SPUD/Private/SpudState.cpp b/Source/SPUD/Private/SpudState.cpp index 48e090d..a8cbd2e 100644 --- a/Source/SPUD/Private/SpudState.cpp +++ b/Source/SPUD/Private/SpudState.cpp @@ -1,6 +1,5 @@ #include "SpudState.h" -#include "EngineUtils.h" #include "ISpudObject.h" #include "SpudPropertyUtil.h" #include "SpudSubsystem.h" @@ -9,9 +8,7 @@ #include "GameFramework/GameModeBase.h" #include "GameFramework/GameStateBase.h" #include "GameFramework/MovementComponent.h" -#include "Kismet/GameplayStatics.h" #include "ImageUtils.h" -#include "..\Public\SpudMemoryReaderWriter.h" #include "GameFramework/PlayerState.h" DEFINE_LOG_CATEGORY(LogSpudState) @@ -104,24 +101,16 @@ void USpudState::StorePropertyVisitor::StoreNestedUObjectIfNeeded(UObject* RootO // assets from that in a post-load hook, otherwise it just makes your saves fragile / bloated to store derived data checkf(!Obj->IsAsset(), TEXT("Cannot store %s from property %s/%s - Storing links to assets is not supported"), *Obj->GetName(), *RootObject->GetName(), *Property->GetNameCPP()); - - const bool IsCallback = Obj->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass()); - - if (IsCallback) - { - ISpudObjectCallback::Execute_SpudPreStore(Obj, ParentState); - } + + ISpudObject::Execute_SpudPreStore(Obj, ParentState); const uint32 NewPrefixID = GetNestedPrefix(Property, CurrentPrefixID); ParentState->StoreObjectProperties(Obj, NewPrefixID, PropertyOffsets, Meta, Out, Depth+1); - if (IsCallback) - { - // No custom data callbacks for nested UObjects, only root ones - // This is because nested UObjects don't get their own data package, and could be null sometimes etc, - // could interfere with data packing in nasty ways - // I *could* store UObjects in their own data wrappers but that becomes cumbersome so don't for now - ISpudObjectCallback::Execute_SpudPostStore(Obj, ParentState); - } + // No custom data callbacks for nested UObjects, only root ones + // This is because nested UObjects don't get their own data package, and could be null sometimes etc, + // could interfere with data packing in nasty ways + // I *could* store UObjects in their own data wrappers but that becomes cumbersome so don't for now + ISpudObject::Execute_SpudPostStore(Obj, ParentState); } } } @@ -343,6 +332,15 @@ FSpudNamedObjectData* USpudState::GetGlobalObjectData(const FString& ID, bool Au return Ret; } +bool USpudState::ShouldActorSkipDuringLevelRestore(AActor* Actor) const +{ + if (Actor->Implements()) + { + return ISpudObject::Execute_ShouldSkipDuringLevelRestore(Actor); + } + return false; +} + void USpudState::StoreGlobalObject(UObject* Obj) { @@ -360,7 +358,6 @@ void USpudState::StoreGlobalObject(UObject* Obj, FSpudNamedObjectData* Data) if (Data) { FSpudClassMetadata& Meta = SaveData.GlobalData.Metadata; - const bool bIsCallback = Obj->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass()); if (Obj->Implements() && ISpudObject::Execute_ShouldSkip(Obj)) { @@ -370,22 +367,19 @@ void USpudState::StoreGlobalObject(UObject* Obj, FSpudNamedObjectData* Data) UE_LOG(LogSpudState, Verbose, TEXT("* STORE Global object: %s"), *Obj->GetName()); - if (bIsCallback) - ISpudObjectCallback::Execute_SpudPreStore(Obj, this); + ISpudObject::Execute_SpudPreStore(Obj, this); StoreObjectProperties(Obj, Data->Properties, Meta); - if (bIsCallback) + if (ISpudObject::Execute_HasCustomData(Obj)) { Data->CustomData.Data.Empty(); FSpudMemoryWriter CustomDataWriter(Data->CustomData.Data); auto CustomDataStruct = NewObject(); CustomDataStruct->Init(&CustomDataWriter); - ISpudObjectCallback::Execute_SpudStoreCustomData(Obj, this, CustomDataStruct); - - ISpudObjectCallback::Execute_SpudPostStore(Obj, this); + ISpudObject::Execute_SpudStoreCustomData(Obj, this, CustomDataStruct); } - + ISpudObject::Execute_SpudPostStore(Obj, this); } } @@ -415,7 +409,17 @@ void USpudState::StoreObjectProperties(UObject* Obj, uint32 PrefixID, TArrayActors) { - if (SpudPropertyUtil::IsPersistentObject(Actor)) + if (SpudPropertyUtil::IsPersistentObject(Actor) && !ShouldActorSkipDuringLevelRestore(Actor)) { RestoreActor(Actor, LevelData, &RuntimeObjectsByGuid); auto Guid = SpudPropertyUtil::GetGuidProperty(Actor); @@ -474,6 +478,30 @@ bool USpudState::PreLoadLevelData(const FString& LevelName) return Data != nullptr; } +bool USpudState::CanRestoreWorld(UWorld* World, const FString& OnlyLevelName) +{ + if (!IsValid(World)) + { + return false; + } + + bool bNeedsRestore = false; + for (auto& Level : World->GetLevels()) + { + if (!OnlyLevelName.IsEmpty() && GetLevelName(Level) != OnlyLevelName) + { + continue; + } + + if (CanRestoreLevel(Level)) + { + bNeedsRestore = true; + break; + } + } + return bNeedsRestore; +} + void USpudState::RestoreActor(AActor* Actor) { if (Actor->HasAnyFlags(RF_ClassDefaultObject|RF_ArchetypeObject|RF_BeginDestroyed)) @@ -484,7 +512,7 @@ void USpudState::RestoreActor(AActor* Actor) auto LevelData = GetLevelData(LevelName, false); if (!LevelData.IsValid()) { - UE_LOG(LogSpudState, Error, TEXT("Unable to restore Actor %s, missing level data"), *Actor->GetName()); + UE_LOG(LogSpudState, Verbose, TEXT("Nothing to load for Actor %s"), *Actor->GetName()); return; } @@ -625,29 +653,25 @@ void USpudState::RestoreActor(AActor* Actor, FSpudSaveData::TLevelDataPtr LevelD void USpudState::PreRestoreObject(UObject* Obj, uint32 StoredUserVersion) { - if(Obj->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass())) - { - if (GCurrentUserDataModelVersion != StoredUserVersion) - ISpudObjectCallback::Execute_SpudPreRestoreDataModelUpgrade(Obj, this, StoredUserVersion, GCurrentUserDataModelVersion); - - ISpudObjectCallback::Execute_SpudPreRestore(Obj, this); - - } + if (GCurrentUserDataModelVersion != StoredUserVersion) + ISpudObject::Execute_SpudPreRestoreDataModelUpgrade(Obj, this, StoredUserVersion, GCurrentUserDataModelVersion); + + ISpudObject::Execute_SpudPreRestore(Obj, this); } void USpudState::PostRestoreObject(UObject* Obj, const FSpudCustomData& FromCustomData, uint32 StoredUserVersion) { - if (Obj->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass())) - { - if (GCurrentUserDataModelVersion != StoredUserVersion) - ISpudObjectCallback::Execute_SpudPostRestoreDataModelUpgrade(Obj, this, StoredUserVersion, GCurrentUserDataModelVersion); + if (GCurrentUserDataModelVersion != StoredUserVersion) + ISpudObject::Execute_SpudPostRestoreDataModelUpgrade(Obj, this, StoredUserVersion, GCurrentUserDataModelVersion); + if (ISpudObject::Execute_HasCustomData(Obj)) + { FSpudMemoryReader Reader(FromCustomData.Data); auto CustomData = NewObject(); CustomData->Init(&Reader); - ISpudObjectCallback::Execute_SpudRestoreCustomData(Obj, this, CustomData); - ISpudObjectCallback::Execute_SpudPostRestore(Obj, this); + ISpudObject::Execute_SpudRestoreCustomData(Obj, this, CustomData); } + ISpudObject::Execute_SpudPostRestore(Obj, this); } void USpudState::RestoreCoreActorData(AActor* Actor, const FSpudCoreActorData& FromData) @@ -688,8 +712,7 @@ void USpudState::RestoreCoreActorData(AActor* Actor, const FSpudCoreActorData& F auto Pawn = Cast(Actor); - if (Pawn && Pawn->IsPlayerControlled() && - !GetSpudSubsystem(Pawn->GetWorld())->IsLoadingGame()) + if (Pawn && Pawn->IsPlayerControlled() && !GetSpudSubsystem(Pawn->GetWorld())->IsLoadingGame()) { // This is a player-controlled pawn, and we're not loading the game // That means this was a map transition. In this case we do NOT want to reset the pawn's position @@ -700,9 +723,10 @@ void USpudState::RestoreCoreActorData(AActor* Actor, const FSpudCoreActorData& F } + const bool bRestoreActorTransform = ShouldActorTransformBeRestored(Actor); + const auto RootComp = Actor->GetRootComponent(); - if (RootComp && RootComp->Mobility == EComponentMobility::Movable && - ShouldActorTransformBeRestored(Actor)) + if (RootComp && RootComp->Mobility == EComponentMobility::Movable && bRestoreActorTransform) { // Only set the actor transform if movable, to avoid editor warnings about static/stationary objects Actor->SetActorTransform(XForm, false, nullptr, ETeleportType::ResetPhysics); @@ -728,7 +752,7 @@ void USpudState::RestoreCoreActorData(AActor* Actor, const FSpudCoreActorData& F } } - if (Pawn) + if (bRestoreActorTransform && Pawn) { if (auto Controller = Pawn->GetController()) { @@ -832,23 +856,15 @@ void USpudState::RestorePropertyVisitor::RestoreNestedUObjectIfNeeded(UObject* R // property before this contains the class (or null) if (Obj) { - const bool IsCallback = Obj->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass()); - - if (IsCallback) - { - ISpudObjectCallback::Execute_SpudPreRestore(Obj, ParentState); - } + ISpudObject::Execute_SpudPreRestore(Obj, ParentState); const uint32 NewPrefixID = GetNestedPrefix(Property, CurrentPrefixID); ParentState->RestoreObjectProperties(Obj, DataIn, Meta, RuntimeObjects, Depth+1); - if (IsCallback) - { - // No custom data callbacks for nested UObjects, only root ones - // This is because nested UObjects don't get their own data package, and could be null sometimes etc, - // could interfere with data packing in nasty ways - // I *could* store UObjects in their own data wrappers but that becomes cumbersome so don't for now - ISpudObjectCallback::Execute_SpudPostRestore(Obj, ParentState); - } + // No custom data callbacks for nested UObjects, only root ones + // This is because nested UObjects don't get their own data package, and could be null sometimes etc, + // could interfere with data packing in nasty ways + // I *could* store UObjects in their own data wrappers but that becomes cumbersome so don't for now + ISpudObject::Execute_SpudPostRestore(Obj, ParentState); } } } @@ -922,12 +938,7 @@ bool USpudState::RestoreSlowPropertyVisitor::VisitProperty(UObject* RootObject, return true; } -void USpudState::RestoreLoadedWorld(UWorld* World) -{ - RestoreLoadedWorld(World, false); -} - -void USpudState::RestoreLoadedWorld(UWorld* World, bool bSingleLevel, const FString& OnlyLevel) +void USpudState::RestoreLoadedWorld(UWorld* World, const FString& OnlyLevel) { // So that we don't need to check every instance of a class for matching stored / runtime class properties // we will keep a cache of whether to use the fast or slow path. It's only valid for this specific load @@ -938,7 +949,7 @@ void USpudState::RestoreLoadedWorld(UWorld* World, bool bSingleLevel, const FStr if (!IsValid(Level)) continue; - if (bSingleLevel && GetLevelName(Level) != OnlyLevel) + if (!OnlyLevel.IsEmpty() && GetLevelName(Level) != OnlyLevel) continue; RestoreLevel(Level); @@ -1045,11 +1056,8 @@ void USpudState::StoreActor(AActor* Actor, FSpudSaveData::TLevelDataPtr LevelDat UE_LOG(LogSpudState, Verbose, TEXT(" * STORE Runtime Actor: %s (%s)"), *Guid.ToString(EGuidFormats::DigitsWithHyphens), *Name) else UE_LOG(LogSpudState, Verbose, TEXT(" * STORE Level Actor: %s/%s"), *LevelData->Name, *Name); - - bool bIsCallback = Actor->GetClass()->ImplementsInterface(USpudObjectCallback::StaticClass()); - - if (bIsCallback) - ISpudObjectCallback::Execute_SpudPreStore(Actor, this); + + ISpudObject::Execute_SpudPreStore(Actor, this); // Core data first pDestCoreData->Empty(); @@ -1059,7 +1067,7 @@ void USpudState::StoreActor(AActor* Actor, FSpudSaveData::TLevelDataPtr LevelDat // Now properties, visit all and write out StoreObjectProperties(Actor, *pDestProperties, Meta); - if (bIsCallback) + if (ISpudObject::Execute_HasCustomData(Actor)) { if (pDestCustomData) { @@ -1067,11 +1075,11 @@ void USpudState::StoreActor(AActor* Actor, FSpudSaveData::TLevelDataPtr LevelDat FSpudMemoryWriter CustomDataWriter(*pDestCustomData); auto CustomDataStruct = NewObject(); CustomDataStruct->Init(&CustomDataWriter); - ISpudObjectCallback::Execute_SpudStoreCustomData(Actor, this, CustomDataStruct); - } - - ISpudObjectCallback::Execute_SpudPostStore(Actor, this); + ISpudObject::Execute_SpudStoreCustomData(Actor, this, CustomDataStruct); + } } + + ISpudObject::Execute_SpudPostStore(Actor, this); } diff --git a/Source/SPUD/Private/SpudStreamingVolume.cpp b/Source/SPUD/Private/SpudStreamingVolume.cpp deleted file mode 100644 index 8a5c068..0000000 --- a/Source/SPUD/Private/SpudStreamingVolume.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "SpudStreamingVolume.h" - -#include "Engine/CollisionProfile.h" -#include "SpudSubsystem.h" -#include "Components/BrushComponent.h" -#include "Kismet/GameplayStatics.h" - -ASpudStreamingVolume::ASpudStreamingVolume(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ - // unlike the standard streaming volume, we're going to use pawns *and* cameras to load - auto BC = GetBrushComponent(); - BC->SetCollisionProfileName(UCollisionProfile::CustomCollisionProfileName); - BC->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); - BC->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap); - BC->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Overlap); - BC->bAlwaysCreatePhysicsState = true; - - bColored = true; - BrushColor.R = 255; - BrushColor.G = 165; - BrushColor.B = 0; - BrushColor.A = 255; -} - -void ASpudStreamingVolume::BeginPlay() -{ - Super::BeginPlay(); - - // So, there's a problem with pawns. It's possible that a pawn has its collision enabled when it's not currently - // possessed, which triggers the overlap event, but we don't care about it yet because it's not player controlled. - // Similarly if a possessed pawn which is overlapping this volume is then unpossessed, we need to unsub the level. - // So we need to be told when a pawn is is possessed or unpossessed to be able to close this loophole. - auto GI = GetWorld()->GetGameInstance(); - if (GI) - { - GI->GetOnPawnControllerChanged().AddDynamic(this, &ASpudStreamingVolume::OnPawnControllerChanged); - } - -} - -void ASpudStreamingVolume::EndPlay(const EEndPlayReason::Type EndPlayReason) -{ - Super::EndPlay(EndPlayReason); - - auto GI = GetWorld()->GetGameInstance(); - if (GI) - { - GI->GetOnPawnControllerChanged().RemoveDynamic(this, &ASpudStreamingVolume::OnPawnControllerChanged); - } - -} - -bool ASpudStreamingVolume::IsRelevantActor(AActor* Actor) const -{ - // This gets called for Cameras and Pawns (I just prefer this to cameras-only for 3rd person setups, having to - // worry about a distant camera poking out of the volume when the character is still in it) - // However, only consider player-controlled pawns to avoid AI keeping levels alive - if (auto Pawn = Cast(Actor)) - { - return Pawn->IsPlayerControlled(); - } - - // Must be a camera - return true; - -} - -void ASpudStreamingVolume::OnPawnControllerChanged(APawn* Pawn, AController* NewCtrl) -{ - // If player controlled and already overlapping... - // This means becoming the possessed pawn when pawn already overlapped so was potentially previously ignored - // If already overlapping, this might change the decision of whether relevant - if (PawnsInVolume.Contains(Pawn)) - { - if (IsRelevantActor(Pawn)) - AddRelevantActor(Pawn); - else - RemoveRelevantActor(Pawn); - } - -} - -void ASpudStreamingVolume::NotifyActorBeginOverlap(AActor* OtherActor) -{ - if (auto Pawn = Cast(OtherActor)) - { - // We need to track ALL pawns in the area in case they get possessed / unpossessed - PawnsInVolume.Add(Pawn); - } - - if (!IsRelevantActor(OtherActor)) - return; - - AddRelevantActor(OtherActor); -} - -void ASpudStreamingVolume::NotifyActorEndOverlap(AActor* OtherActor) -{ - if (auto Pawn = Cast(OtherActor)) - { - // We need to track ALL pawns in the area in case they get possessed / unpossessed - PawnsInVolume.Remove(Pawn); - } - - if (!IsRelevantActor(OtherActor)) - return; - - RemoveRelevantActor(OtherActor); - -} - - -void ASpudStreamingVolume::AddRelevantActor(AActor* Actor) -{ - const int OldNum = RelevantActorsInVolume.Num(); - - RelevantActorsInVolume.AddUnique(Actor); - - // Shouldn't need to listen in to actor destruction, that will trigger end overlap - - if (OldNum == 0) - { - auto PS = GetSpudSubsystem(GetWorld()); - if (PS) - { - for (auto Level : StreamingLevels) - { - if (!Level.IsNull()) - { - // Can't use GetAssetPathName in PIE because it gets prefixed with UEDPIE_0_ for uniqueness with editor version - const FName LevelName = FName(Level.GetAssetName()); - //UE_LOG(LogTemp, Verbose, TEXT("Requesting Stream Load: %s"), *Level.GetAssetName()); - PS->AddRequestForStreamingLevel(this, LevelName, false); - } - } - } - } -} - -void ASpudStreamingVolume::RemoveRelevantActor(AActor* Actor) -{ - const int Removed = RelevantActorsInVolume.Remove(Actor); - - if (Removed > 0 && RelevantActorsInVolume.Num() == 0) - { - auto PS = GetSpudSubsystem(GetWorld()); - if (PS) - { - for (auto Level : StreamingLevels) - { - if (!Level.IsNull()) - { - // Can't use GetAssetPathName in PIE because it gets prefixed with UEDPIE_0_ for uniqueness with editor version - const FName LevelName = FName(Level.GetAssetName()); - //UE_LOG(LogTemp, Verbose, TEXT("Withdrawing Stream Level Request: %s"), *LevelName.ToString()); - PS->WithdrawRequestForStreamingLevel(this, LevelName); - } - } - } - - } -} diff --git a/Source/SPUD/Private/SpudSubsystem.cpp b/Source/SPUD/Private/SpudSubsystem.cpp index ce188ea..90cbdea 100644 --- a/Source/SPUD/Private/SpudSubsystem.cpp +++ b/Source/SPUD/Private/SpudSubsystem.cpp @@ -1,5 +1,4 @@ #include "SpudSubsystem.h" -#include "EngineUtils.h" #include "SpudState.h" #include "Engine/LevelStreaming.h" #include "Engine/LocalPlayer.h" @@ -8,23 +7,32 @@ #include "TimerManager.h" #include "HAL/FileManager.h" #include "Async/Async.h" +#include "Streaming/LevelStreamingDelegates.h" -DEFINE_LOG_CATEGORY(LogSpudSubsystem) +#define USE_UE_SAVE_SYSTEM (PREFER_UE_SAVE_SYSTEM || PLATFORM_PS5) +#if USE_UE_SAVE_SYSTEM +#include "PlatformFeatures.h" +#include "SaveGameSystem.h" +#endif -#define SPUD_QUICKSAVE_SLOTNAME "__QuickSave__" -#define SPUD_AUTOSAVE_SLOTNAME "__AutoSave__" +DEFINE_LOG_CATEGORY(LogSpudSubsystem) +static bool bEnableSPUD = true; +static FAutoConsoleVariableRef CVarEnableSPUD(TEXT("SPUD.Enable"), bEnableSPUD, TEXT("Can be used to debug disable state of plugin by setting to false"), ECVF_Cheat); void USpudSubsystem::Initialize(FSubsystemCollectionBase& Collection) { bIsTearingDown = false; // Note: this will register for clients too, but callbacks will be ignored // We can't call ServerCheck() here because GameMode won't be valid (which is what we use to determine server mode) - OnPostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &USpudSubsystem::OnPostLoadMap); - OnPreLoadMapHandle = FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USpudSubsystem::OnPreLoadMap); - - OnSeamlessTravelHandle = FWorldDelegates::OnSeamlessTravelTransition.AddUObject(this, &USpudSubsystem::OnSeamlessTravelTransition); + FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &USpudSubsystem::OnPostLoadMap); + FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USpudSubsystem::OnPreLoadMap); + + FLevelStreamingDelegates::OnLevelBeginMakingVisible.AddUObject(this, &USpudSubsystem::OnLevelBeginMakingVisible); + FLevelStreamingDelegates::OnLevelBeginMakingInvisible.AddUObject(this, &USpudSubsystem::OnLevelBeginMakingInvisible); + + FWorldDelegates::OnSeamlessTravelStart.AddUObject(this, &USpudSubsystem::OnSeamlessTravelStart); #if WITH_EDITORONLY_DATA // The one problem we have is that in PIE mode, PostLoadMap doesn't get fired for the current map you're on @@ -35,16 +43,12 @@ void USpudSubsystem::Initialize(FSubsystemCollectionBase& Collection) auto World = GetWorld(); if (World && World->WorldType == EWorldType::PIE) { - FTimerHandle TempHandle; - GetWorld()->GetTimerManager().SetTimer(TempHandle,[this]() + GetWorld()->GetTimerManager().SetTimerForNextTick([this]() { // TODO: make this more configurable, use a known save etc - NewGame(false); - - }, 0.2, false); + NewGame(true); + }); } - - #endif } @@ -53,22 +57,11 @@ void USpudSubsystem::Deinitialize() Super::Deinitialize(); bIsTearingDown = true; - FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(OnPostLoadMapHandle); - FCoreUObjectDelegates::PreLoadMap.Remove(OnPreLoadMapHandle); - FWorldDelegates::OnSeamlessTravelTransition.Remove(OnSeamlessTravelHandle); - - // Clean up streaming level event listeners, as they may fire after we've been destroyed - for (auto It = MonitoredStreamingLevels.CreateIterator(); It; ++It) - { - ULevelStreaming* const Level = It.Key(); - if (ensure(Level)) - { - USpudStreamingLevelWrapper* const Wrapper = It.Value(); - Level->OnLevelShown.RemoveAll(Wrapper); - Level->OnLevelHidden.RemoveAll(Wrapper); - It.RemoveCurrent(); - } - } + FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); + FCoreUObjectDelegates::PreLoadMap.RemoveAll(this); + FLevelStreamingDelegates::OnLevelBeginMakingVisible.RemoveAll(this); + FLevelStreamingDelegates::OnLevelBeginMakingInvisible.RemoveAll(this); + FWorldDelegates::OnSeamlessTravelStart.RemoveAll(this); } @@ -95,6 +88,11 @@ void USpudSubsystem::NewGame(bool bCheckServerOnly, bool bAfterLevelLoad) bool USpudSubsystem::ServerCheck(bool LogWarning) const { + if (!bEnableSPUD) + { + return false; + } + // Note: must only call this when game mode is present! Don't call when unloading // On missing world etc we just assume true for safety auto GI = GetGameInstance(); @@ -118,12 +116,12 @@ void USpudSubsystem::EndGame() UnsubscribeAllLevelObjectEvents(); CurrentState = ESpudSystemState::Disabled; - IsRestoringState = false; + bIsRestoringState = false; } void USpudSubsystem::AutoSaveGame(FText Title, bool bTakeScreenshot, const USpudCustomSaveInfo* ExtraInfo) { - SaveGame(SPUD_AUTOSAVE_SLOTNAME, + SaveGame(AutoSaveSlotName, Title.IsEmpty() ? NSLOCTEXT("Spud", "AutoSaveTitle", "Autosave") : Title, bTakeScreenshot, ExtraInfo); @@ -131,26 +129,26 @@ void USpudSubsystem::AutoSaveGame(FText Title, bool bTakeScreenshot, const USpud void USpudSubsystem::QuickSaveGame(FText Title, bool bTakeScreenshot, const USpudCustomSaveInfo* ExtraInfo) { - SaveGame(SPUD_QUICKSAVE_SLOTNAME, + SaveGame(QuickSaveSlotName, Title.IsEmpty() ? NSLOCTEXT("Spud", "QuickSaveTitle", "Quick Save") : Title, bTakeScreenshot, ExtraInfo); } -void USpudSubsystem::QuickLoadGame(const FString& TravelOptions) +void USpudSubsystem::QuickLoadGame(bool bAutoTravelLevel, const FString& TravelOptions) { - LoadGame(SPUD_QUICKSAVE_SLOTNAME, TravelOptions); + LoadGame(QuickSaveSlotName, bAutoTravelLevel, TravelOptions); } bool USpudSubsystem::IsQuickSave(const FString& SlotName) { - return SlotName == SPUD_QUICKSAVE_SLOTNAME; + return SlotName == QuickSaveSlotName; } bool USpudSubsystem::IsAutoSave(const FString& SlotName) { - return SlotName == SPUD_AUTOSAVE_SLOTNAME; + return SlotName == AutoSaveSlotName; } void USpudSubsystem::NotifyLevelLoadedExternally(FName LevelName) @@ -163,25 +161,21 @@ void USpudSubsystem::NotifyLevelUnloadedExternally(ULevel* Level) HandleLevelUnloaded(Level); } -void USpudSubsystem::LoadLatestSaveGame(const FString& TravelOptions) +void USpudSubsystem::LoadLatestSaveGame(bool bAutoTravelLevel, const FString& TravelOptions) { auto Latest = GetLatestSaveGame(); if (Latest) - LoadGame(Latest->SlotName, TravelOptions); + LoadGame(Latest->SlotName, bAutoTravelLevel, TravelOptions); } void USpudSubsystem::OnPreLoadMap(const FString& MapName) { if (!ServerCheck(false)) + { return; + } PreTravelToNewMap.Broadcast(MapName); - // All streaming maps will be unloaded by travelling, so remove all - LevelRequests.Empty(); - StopUnloadTimer(); - MonitoredStreamingLevels.Empty(); - - FirstStreamRequestSinceMapLoad = true; // When we transition out of a map while enabled, save contents if (CurrentState == ESpudSystemState::RunningIdle) @@ -191,20 +185,35 @@ void USpudSubsystem::OnPreLoadMap(const FString& MapName) const auto World = GetWorld(); if (IsValid(World)) { - UE_LOG(LogSpudSubsystem, Verbose, TEXT("OnPreLoadMap saving: %s"), *UGameplayStatics::GetCurrentLevelName(World)); - // Map and all streaming level data will be released. - // Block while doing it so they all get written predictably - StoreWorld(World, true, true); + if (bSaveLevelStateWhileTraveling) + { + UE_LOG(LogSpudSubsystem, Verbose, TEXT("OnPreLoadMap saving: %s"), *UGameplayStatics::GetCurrentLevelName(World)); + // Map and all streaming level data will be released. + // Block while doing it so they all get written predictably + StoreWorld(World, true, true); + } + else + { + UE_LOG(LogSpudSubsystem, Verbose, TEXT("OnPreLoadMap releasing data: %s"), *UGameplayStatics::GetCurrentLevelName(World)); + for (auto && Level : World->GetLevels()) + { + GetActiveState()->ReleaseLevelData(USpudState::GetLevelName(Level), true); + } + } } } } -void USpudSubsystem::OnSeamlessTravelTransition(UWorld* World) +void USpudSubsystem::OnSeamlessTravelStart(UWorld* World, const FString& MapName) { + if (!ServerCheck(false)) + { + return; + } + if (IsValid(World)) { - FString MapName = UGameplayStatics::GetCurrentLevelName(World); - UE_LOG(LogSpudSubsystem, Verbose, TEXT("OnSeamlessTravelTransition: %s"), *MapName); + UE_LOG(LogSpudSubsystem, Verbose, TEXT("OnSeamlessTravelStart: %s"), *MapName); // Just before seamless travel, do the same thing as pre load map on OpenLevel OnPreLoadMap(MapName); } @@ -213,8 +222,9 @@ void USpudSubsystem::OnSeamlessTravelTransition(UWorld* World) void USpudSubsystem::OnPostLoadMap(UWorld* World) { if (!ServerCheck(false)) + { return; - + } switch(CurrentState) { @@ -232,45 +242,101 @@ void USpudSubsystem::OnPostLoadMap(UWorld* World) } break; case ESpudSystemState::RunningIdle: + // We need to subscribe to ALL currently loaded levels, because of "AlwaysLoaded" sublevels + SubscribeAllLevelObjectEvents(); + break; case ESpudSystemState::LoadingGame: // This is called when a new map is loaded // In all cases, we try to load the state if (IsValid(World)) // nullptr seems possible if load is aborted or something? { const FString LevelName = UGameplayStatics::GetCurrentLevelName(World); - UE_LOG(LogSpudSubsystem, - Verbose, - TEXT("OnPostLoadMap restore: %s"), - *LevelName); + if (CanRestoreWorld(World)) + { + UE_LOG(LogSpudSubsystem, + Verbose, + TEXT("OnPostLoadMap restore: %s"), + *LevelName); - IsRestoringState = true; + bIsRestoringState = true; - const auto State = GetActiveState(); - PreLevelRestore.Broadcast(LevelName); - State->RestoreLoadedWorld(World); - PostLevelRestore.Broadcast(LevelName, true); - - IsRestoringState = false; - // We need to subscribe to ALL currently loaded levels, because of "AlwaysLoaded" sublevels - SubscribeAllLevelObjectEvents(); - } + const auto State = GetActiveState(); + PreLevelRestore.Broadcast(LevelName); + // Only load main level. If there are initially visible sub-levels in world, they should be loaded with streamed level callbacks. Otherwise those levels will get loaded twice. + State->RestoreLoadedWorld(World, LevelName); + PostLevelRestore.Broadcast(LevelName, true); - // If we were loading, this is the completion - if (CurrentState == ESpudSystemState::LoadingGame) - { - LoadComplete(SlotNameInProgress, true); - UE_LOG(LogSpudSubsystem, Log, TEXT("Load: Success")); - } + bIsRestoringState = false; + + // We need to subscribe to ALL currently loaded levels, because of "AlwaysLoaded" sublevels + SubscribeAllLevelObjectEvents(); + + LoadComplete(SlotNameInProgress, true); + UE_LOG(LogSpudSubsystem, Log, TEXT("Load: Success")); + } + else + { + UE_LOG(LogSpudState, Log, TEXT("Skipping restore of world %s, no saved data."), *LevelName); + + // We need to subscribe to ALL currently loaded levels, because of "AlwaysLoaded" sublevels + SubscribeAllLevelObjectEvents(); + + LoadComplete(SlotNameInProgress, false); + UE_LOG(LogSpudSubsystem, Log, TEXT("Load: Skipped")); + } + } break; default: break; - } PostTravelToNewMap.Broadcast(); } +void USpudSubsystem::LoadActorData(AActor* Actor, bool bAsGameLoad) +{ + if (!ServerCheck(false)) + { + return; + } + + const ESpudSystemState PrevState = CurrentState; + + if (bAsGameLoad) + { + CurrentState = ESpudSystemState::LoadingGame; + } + + UE_LOG(LogSpudSubsystem, + Verbose, + TEXT("LoadActorData restore: %s"), + *GetNameSafe(Actor)); + + bIsRestoringState = true; + const auto State = GetActiveState(); + State->RestoreActor(Actor); + bIsRestoringState = false; + + CurrentState = PrevState; +} + +void USpudSubsystem::MarkActorDestroyed(AActor* Actor) +{ + if (!ServerCheck(false)) + { + return; + } + OnActorDestroyed(Actor); +} + +bool USpudSubsystem::IsStreamedLevelRestoring(ULevel* Level) const +{ + const FString LevelName = USpudState::GetLevelName(Level); + const bool* FoundState = LevelStreamingRestoreStates.Find(FName(LevelName)); + return FoundState && *FoundState; +} + void USpudSubsystem::SaveGame(const FString& SlotName, const FText& Title, bool bTakeScreenshot, const USpudCustomSaveInfo* ExtraInfo) { if (!ServerCheck(true)) @@ -305,14 +371,10 @@ void USpudSubsystem::SaveGame(const FString& SlotName, const FText& Title, bool SlotNameInProgress = SlotName; TitleInProgress = Title; ExtraInfoInProgress = ExtraInfo; - UGameViewportClient* ViewportClient = UGameplayStatics::GetPlayerController(GetWorld(), 0)->GetLocalPlayer()->ViewportClient; + UGameViewportClient* ViewportClient = GetGameInstance()->GetGameViewportClient(); + check(ViewportClient); OnScreenshotHandle = ViewportClient->OnScreenshotCaptured().AddUObject(this, &USpudSubsystem::OnScreenshotCaptured); FScreenshotRequest::RequestScreenshot(false); - // OnScreenShotCaptured will finish - // EXCEPT that if a Widget BP is open in the editor, this request will disappear into nowhere!! (4.26.1) - // So we need a failsafe - // Wait for 1 second. Can't use FTimerManager because there's no option for those to tick while game paused (which is common in saves!) - ScreenshotTimeout = 1; } else { @@ -320,25 +382,8 @@ void USpudSubsystem::SaveGame(const FString& SlotName, const FText& Title, bool } } - -void USpudSubsystem::ScreenshotTimedOut() -{ - // We failed to get a screenshot back in time - // This is mostly likely down to a weird fecking issue in PIE where if ANY Widget Blueprint is open while a screenshot - // is requested, that request is never fulfilled - - UE_LOG(LogSpudSubsystem, Error, TEXT("Request for save screenshot timed out. This is most likely a UE4 bug: " - "Widget Blueprints being open in the editor during PIE seems to break screenshots. Completing save game without a screenshot.")) - - ScreenshotTimeout = 0; - FinishSaveGame(SlotNameInProgress, TitleInProgress, ExtraInfoInProgress, nullptr); - -} - void USpudSubsystem::OnScreenshotCaptured(int32 Width, int32 Height, const TArray& Colours) { - ScreenshotTimeout = 0; - UGameViewportClient* ViewportClient = UGameplayStatics::GetPlayerController(GetWorld(), 0)->GetLocalPlayer()->ViewportClient; ViewportClient->OnScreenshotCaptured().Remove(OnScreenshotHandle); OnScreenshotHandle.Reset(); @@ -394,10 +439,36 @@ void USpudSubsystem::FinishSaveGame(const FString& SlotName, const FText& Title, // Plus it writes it all to memory first, which we don't need another copy of. Write direct to file // I'm not sure if the save game system doesn't do this because of some console hardware issues, but // I'll worry about that at some later point + + bool SaveOK = false; + +#if USE_UE_SAVE_SYSTEM + if (ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem()) + { + // We need to convert the data to a TArray first + TArray Data; + FMemoryWriter MemoryWriter(Data); + State->SaveToArchive(MemoryWriter); + + MemoryWriter.Close(); + + if (MemoryWriter.IsError() || MemoryWriter.IsCriticalError()) + { + UE_LOG(LogSpudSubsystem, Error, TEXT("Error while saving game to %s"), *SlotName); + SaveOK = false; + } + else + { + // Save to slot + SaveOK = SaveSystem->SaveGame(false, *SlotName, 0, Data); + if(SaveOK) + UE_LOG(LogSpudSubsystem, Log, TEXT("Save to slot %s: Success"), *SlotName); + } + } +#else IFileManager& FileMgr = IFileManager::Get(); auto Archive = TUniquePtr(FileMgr.CreateFileWriter(*GetSaveGameFilePath(SlotName))); - bool SaveOK; if(Archive) { State->SaveToArchive(*Archive); @@ -420,9 +491,9 @@ void USpudSubsystem::FinishSaveGame(const FString& SlotName, const FText& Title, UE_LOG(LogSpudSubsystem, Error, TEXT("Error while creating save game for slot %s"), *SlotName); SaveOK = false; } +#endif SaveComplete(SlotName, SaveOK); - } void USpudSubsystem::SaveComplete(const FString& SlotName, bool bSuccess) @@ -444,15 +515,9 @@ void USpudSubsystem::HandleLevelLoaded(FName LevelName) AsyncTask(ENamedThreads::GameThread, [this, LevelName]() { - // But also add a slight delay so we get a tick in between so physics works - FTimerHandle H; - if (UWorld* World = GetWorld()) - { - World->GetTimerManager().SetTimer(H, [this, LevelName]() - { - PostLoadStreamLevelGameThread(LevelName); - }, 0.01, false); - } + PostLoadStreamLevelGameThread(LevelName); + LevelStreamingRestoreStates.Add(LevelName, false); + PostLoadStreamingLevel.Broadcast(LevelName); }); } @@ -491,7 +556,7 @@ void USpudSubsystem::StoreLevel(ULevel* Level, bool bRelease, bool bBlocking) PostLevelStore.Broadcast(LevelName, true); } -void USpudSubsystem::LoadGame(const FString& SlotName, const FString& TravelOptions) +void USpudSubsystem::LoadGame(const FString& SlotName, bool bAutoTravelLevel, const FString& TravelOptions) { if (!ServerCheck(true)) { @@ -504,11 +569,12 @@ void USpudSubsystem::LoadGame(const FString& SlotName, const FString& TravelOpti // TODO: ignore or queue? UE_LOG(LogSpudSubsystem, Error, TEXT("TODO: Overlapping calls to save/load, resolve this")); LoadComplete(SlotName, false); + return; } CurrentState = ESpudSystemState::LoadingGame; - IsRestoringState = true; + bIsRestoringState = true; PreLoadGame.Broadcast(SlotName); UE_LOG(LogSpudSubsystem, Verbose, TEXT("Loading Game from slot %s"), *SlotName); @@ -519,10 +585,44 @@ void USpudSubsystem::LoadGame(const FString& SlotName, const FString& TravelOpti // TODO: async load +#if USE_UE_SAVE_SYSTEM + if (ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem()) + { + TArray Data; + FMemoryReader MemoryReader(Data, false); + MemoryReader.Seek(0); + if(SaveSystem->LoadGame(false, *SlotName, 0, Data)) + { + // Load the data from the memory reader + State->LoadFromArchive(MemoryReader, false); + // Close Buffer for cache errors + MemoryReader.Close(); + if (MemoryReader.IsError() || MemoryReader.IsCriticalError()) + { + UE_LOG(LogSpudSubsystem, Error, TEXT("Error while loading game from %s"), *SlotName); + LoadComplete(SlotName, false); + return; + } + } + else + { + Data.Empty(); + MemoryReader.Close(); + UE_LOG(LogSpudSubsystem, Error, TEXT("LoadGame: Load Game Returned false, check for inner errors")); + LoadComplete(SlotName, false); + return; + } + } + else + { + UE_LOG(LogSpudSubsystem, Error, TEXT("LoadGame: Platform save system null, cannot load game")); + LoadComplete(SlotName, false); + return; + } +#else IFileManager& FileMgr = IFileManager::Get(); - auto Archive = TUniquePtr(FileMgr.CreateFileReader(*GetSaveGameFilePath(SlotName))); - if(Archive) + if(auto Archive = TUniquePtr(FileMgr.CreateFileReader(*GetSaveGameFilePath(SlotName)))) { // Load only global data and page in level data as needed State->LoadFromArchive(*Archive, false); @@ -541,6 +641,7 @@ void USpudSubsystem::LoadGame(const FString& SlotName, const FString& TravelOpti LoadComplete(SlotName, false); return; } +#endif // Just do the reverse of what we did // Global objects first before map, these should be only objects which survive map load @@ -557,16 +658,19 @@ void USpudSubsystem::LoadGame(const FString& SlotName, const FString& TravelOpti // This is deferred, final load process will happen in PostLoadMap SlotNameInProgress = SlotName; - UE_LOG(LogSpudSubsystem, Verbose, TEXT("(Re)loading map: %s"), *State->GetPersistentLevel()); - - UGameplayStatics::OpenLevel(GetWorld(), FName(State->GetPersistentLevel()), true, TravelOptions); + + if (bAutoTravelLevel) + { + UE_LOG(LogSpudSubsystem, Verbose, TEXT("(Re)loading map: %s"), *State->GetPersistentLevel()); + UGameplayStatics::OpenLevel(GetWorld(), FName(State->GetPersistentLevel()), true, TravelOptions); + } } void USpudSubsystem::LoadComplete(const FString& SlotName, bool bSuccess) { CurrentState = ESpudSystemState::RunningIdle; - IsRestoringState = false; + bIsRestoringState = false; SlotNameInProgress = ""; PostLoadGame.Broadcast(SlotName, bSuccess); } @@ -576,8 +680,18 @@ bool USpudSubsystem::DeleteSave(const FString& SlotName) if (!ServerCheck(true)) return false; +#if USE_UE_SAVE_SYSTEM + if (ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem()) + return SaveSystem->DeleteGame(false, *SlotName, 0); + else + { + UE_LOG(LogSpudSubsystem, Error, TEXT("DeleteSave: Platform save system null, cannot delete game")); + return false; + } +#else IFileManager& FileMgr = IFileManager::Get(); return FileMgr.Delete(*GetSaveGameFilePath(SlotName), false, true); +#endif } void USpudSubsystem::AddPersistentGlobalObject(UObject* Obj) @@ -607,153 +721,6 @@ void USpudSubsystem::ClearLevelState(const FString& LevelName) } -void USpudSubsystem::AddRequestForStreamingLevel(UObject* Requester, FName LevelName, bool BlockingLoad) -{ - if (!ServerCheck(false)) - return; - - auto && Request = LevelRequests.FindOrAdd(LevelName); - const int PrevRequesters = Request.Requesters.Num(); - Request.Requesters.AddUnique(Requester); - if (Request.bPendingUnload) - { - Request.bPendingUnload = false; // no load required, just flip the unload flag - Request.LastRequestExpiredTime = 0; - } - else if (PrevRequesters == 0) - { - // Load on the first request only - LoadStreamLevel(LevelName, BlockingLoad); - } -} - -void USpudSubsystem::WithdrawRequestForStreamingLevel(UObject* Requester, FName LevelName) -{ - if (!ServerCheck(false)) - return; - - if (auto Request = LevelRequests.Find(LevelName)) - { - const int Removed = Request->Requesters.Remove(Requester); - if (Removed > 0 && Request->Requesters.Num() == 0) - { - // This level can be unloaded after time delay - Request->bPendingUnload = true; - Request->LastRequestExpiredTime = UGameplayStatics::GetTimeSeconds(GetWorld()); - StartUnloadTimer(); - } - } -} - -void USpudSubsystem::StartUnloadTimer() -{ - if (!StreamLevelUnloadTimerHandle.IsValid()) - { - // Set up a timer which repeatedly checks for actual unload - // This doesn't need to be every tick, just every 0.5s - GetWorld()->GetTimerManager().SetTimer(StreamLevelUnloadTimerHandle, this, &USpudSubsystem::CheckStreamUnload, 0.5, true); - } -} - - -void USpudSubsystem::StopUnloadTimer() -{ - if (StreamLevelUnloadTimerHandle.IsValid()) - { - GetWorld()->GetTimerManager().ClearTimer(StreamLevelUnloadTimerHandle); - } -} - -void USpudSubsystem::CheckStreamUnload() -{ - const float UnloadBeforeTime = UGameplayStatics::GetTimeSeconds(GetWorld()) - StreamLevelUnloadDelay; - bool bAnyStillWaiting = false; - for (auto && Pair : LevelRequests) - { - const FName& LevelName = Pair.Key; - FStreamLevelRequests& Request = Pair.Value; - if (Request.bPendingUnload) - { - if (Request.Requesters.Num() == 0 && - Request.LastRequestExpiredTime <= UnloadBeforeTime) - { - Request.bPendingUnload = false; - UnloadStreamLevel(LevelName); - } - else - bAnyStillWaiting = true; - } - } - - // Only run the timer while we have something to do - if (!bAnyStillWaiting) - StopUnloadTimer(); -} - - - - -void USpudSubsystem::LoadStreamLevel(FName LevelName, bool Blocking) -{ - FScopeLock PendingLoadLock(&LevelsPendingLoadMutex); - PreLoadStreamingLevel.Broadcast(LevelName); - - FLatentActionInfo Latent; - Latent.ExecutionFunction = "PostLoadStreamLevel"; - Latent.CallbackTarget = this; - int32 RequestID = LoadUnloadRequests++; // overflow is OK - Latent.UUID = RequestID; // this eliminates duplicate calls so should be unique - Latent.Linkage = RequestID; - LevelsPendingLoad.Add(RequestID, LevelName); - - // Upgrade to a blocking call if this is the first streaming level since map change (ensure appears in time) - if (FirstStreamRequestSinceMapLoad) - { - Blocking = true; - FirstStreamRequestSinceMapLoad = false; - } - - // We don't make the level visible until the post-load callback - UGameplayStatics::LoadStreamLevel(GetWorld(), LevelName, false, Blocking, Latent); -} - -void USpudSubsystem::PostLoadStreamLevel(int32 LinkID) -{ - FScopeLock PendingLoadLock(&LevelsPendingLoadMutex); - - // We should be able to obtain the level name - if (LevelsPendingLoad.Contains(LinkID)) - { - FName LevelName = LevelsPendingLoad.FindAndRemoveChecked(LinkID); - - // This might look odd but for physics restoration to work properly we need a very specific - // set of circumstances: - // 1. Level must be made visible first - // 2. We need to wait for all the objects to be ticked at least once - // 3. Then we restore - // - // Failure to do this means SetPhysicsLinearVelocity etc just does *nothing* silently - - // Make visible - auto StreamLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LevelName); - if (StreamLevel) - { - StreamLevel->SetShouldBeVisible(true); - } - - if (!bSupportWorldPartition) - { - // When supporting WP, shown event will trigger this - HandleLevelLoaded(LevelName); - } - } - else - { - UE_LOG(LogSpudSubsystem, Error, TEXT("PostLoadStreamLevel called but not for a level we loaded??")); - } -} - - void USpudSubsystem::PostLoadStreamLevelGameThread(FName LevelName) { PostLoadStreamingLevel.Broadcast(LevelName); @@ -762,13 +729,14 @@ void USpudSubsystem::PostLoadStreamLevelGameThread(FName LevelName) if (StreamLevel) { ULevel* Level = StreamLevel->GetLoadedLevel(); + if (!Level) { UE_LOG(LogSpudSubsystem, Log, TEXT("PostLoadStreamLevel called for %s but level is null; probably unloaded again?"), *LevelName.ToString()); return; } - IsRestoringState = true; + bIsRestoringState = true; PreLevelRestore.Broadcast(LevelName.ToString()); // It's important to note that this streaming level won't be added to UWorld::Levels yet @@ -785,48 +753,14 @@ void USpudSubsystem::PostLoadStreamLevelGameThread(FName LevelName) SubscribeLevelObjectEvents(Level); PostLevelRestore.Broadcast(LevelName.ToString(), true); - IsRestoringState = false; + bIsRestoringState = false; } } -void USpudSubsystem::UnloadStreamLevel(FName LevelName) -{ - auto StreamLevel = UGameplayStatics::GetStreamingLevel(GetWorld(), LevelName); - - if (StreamLevel) - { - ULevel* Level = StreamLevel->GetLoadedLevel(); - if (!Level) - { - // Already unloaded - return; - } - PreUnloadStreamingLevel.Broadcast(LevelName); - - if (!bSupportWorldPartition) - { - // If using WP, the hidden event will trigger this instead - HandleLevelUnloaded(Level); - } - - // Now unload - FScopeLock PendingUnloadLock(&LevelsPendingUnloadMutex); - - FLatentActionInfo Latent; - Latent.ExecutionFunction = "PostUnloadStreamLevel"; - Latent.CallbackTarget = this; - int32 RequestID = LoadUnloadRequests++; // overflow is OK - Latent.UUID = RequestID; // this eliminates duplicate calls so should be unique - Latent.Linkage = RequestID; - LevelsPendingUnload.Add(RequestID, LevelName); - UGameplayStatics::UnloadStreamLevel(GetWorld(), LevelName, Latent, false); - } -} - void USpudSubsystem::ForceReset() { CurrentState = ESpudSystemState::RunningIdle; - IsRestoringState = false; + bIsRestoringState = false; } void USpudSubsystem::SetUserDataModelVersion(int32 Version) @@ -840,19 +774,6 @@ int32 USpudSubsystem::GetUserDataModelVersion() const return GCurrentUserDataModelVersion; } -void USpudSubsystem::PostUnloadStreamLevel(int32 LinkID) -{ - FScopeLock PendingUnloadLock(&LevelsPendingUnloadMutex); - - const FName LevelName = LevelsPendingUnload.FindAndRemoveChecked(LinkID); - - // Pass back to the game thread, streaming calls happen in loading thread? - AsyncTask(ENamedThreads::GameThread, [this, LevelName]() - { - PostUnloadStreamLevelGameThread(LevelName); - }); -} - void USpudSubsystem::PostUnloadStreamLevelGameThread(FName LevelName) { @@ -883,6 +804,52 @@ void USpudSubsystem::UnsubscribeAllLevelObjectEvents() } } +bool USpudSubsystem::CanRestoreLevel(ULevel* Level) +{ + return GetActiveState()->CanRestoreLevel(Level); +} + +bool USpudSubsystem::CanRestoreWorld(UWorld* World) +{ + return GetActiveState()->CanRestoreWorld(World); +} + +void USpudSubsystem::OnLevelBeginMakingInvisible(UWorld* World, const ULevelStreaming* StreamingLevel, ULevel* LoadedLevel) +{ + if (!ServerCheck(true) || World->IsNetMode(NM_Client)) + { + return; + } + + const FString LevelName = USpudState::GetLevelName(LoadedLevel); + UE_LOG(LogSpudSubsystem, Verbose, TEXT("Level hidden: %s"), *LevelName); + PreUnloadStreamingLevel.Broadcast(FName(LevelName)); + HandleLevelUnloaded(LoadedLevel); + PostUnloadStreamingLevel.Broadcast(FName(LevelName)); +} + +void USpudSubsystem::OnLevelBeginMakingVisible(UWorld* World, const ULevelStreaming* StreamingLevel, ULevel* LoadedLevel) +{ + if (!ServerCheck(true) || World->IsNetMode(NM_Client)) + { + return; + } + + const FString LevelNameStr = USpudState::GetLevelName(LoadedLevel); + UE_LOG(LogSpudSubsystem, Verbose, TEXT("Level shown: %s"), *LevelNameStr); + + // Early return if we do not have anything to load. So we won't change load state + if (!CanRestoreLevel(LoadedLevel)) + { + UE_LOG(LogSpudState, Log, TEXT("Skipping restore of streaming level %s, no saved data."), *LevelNameStr); + return; + } + + const FName LevelName = FName(LevelNameStr); + LevelStreamingRestoreStates.Add(LevelName, true); + PreLoadStreamingLevel.Broadcast(LevelName); + HandleLevelLoaded(LevelName); +} void USpudSubsystem::SubscribeLevelObjectEvents(ULevel* Level) { @@ -955,23 +922,25 @@ struct FSaveSorter TArray USpudSubsystem::GetSaveGameList(bool bIncludeQuickSave, bool bIncludeAutoSave, ESpudSaveSorting Sorting) { - TArray SaveFiles; ListSaveGameFiles(SaveFiles); TArray Ret; - for (auto && File : SaveFiles) + for (auto&& File : SaveFiles) { +#if PLATFORM_PS5 + FString SlotName = File; // Because consoles doesn't have an extension +#else FString SlotName = FPaths::GetBaseFilename(File); +#endif - if ((!bIncludeQuickSave && SlotName == SPUD_QUICKSAVE_SLOTNAME) || - (!bIncludeAutoSave && SlotName == SPUD_AUTOSAVE_SLOTNAME)) + if ((!bIncludeQuickSave && SlotName == QuickSaveSlotName) || + (!bIncludeAutoSave && SlotName == AutoSaveSlotName)) { - continue; + continue; } - auto Info = GetSaveGameInfo(SlotName); - if (Info) + if (auto Info = GetSaveGameInfo(SlotName)) Ret.Add(Info); } @@ -985,6 +954,30 @@ TArray USpudSubsystem::GetSaveGameList(bool bIncludeQuickSav USpudSaveGameInfo* USpudSubsystem::GetSaveGameInfo(const FString& SlotName) { +#if USE_UE_SAVE_SYSTEM + if (ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem()) + { + TArray Data; + FMemoryReader MemoryReader(Data, false); + MemoryReader.Seek(0); + if (SaveSystem->LoadGame(false, *SlotName, 0, Data)) + { + auto Info = NewObject(); + const bool bResult = USpudState::LoadSaveInfoFromArchive(MemoryReader, *Info); + Info->SlotName = SlotName; + + return bResult ? Info : nullptr; + } + + //Load Failed + MemoryReader.FlushCache(); + Data.Empty(); + MemoryReader.Close(); + } + //Platform Save System is null + UE_LOG(LogSpudSubsystem, Error, TEXT("GetSaveGameInfo: Platform save system is null, cannot load game")); + return nullptr; +#else IFileManager& FM = IFileManager::Get(); // We want to parse just the very first part of the file, not all of it FString AbsoluteFilename = FPaths::Combine(GetSaveGameDirectory(), SlotName + ".sav"); @@ -995,14 +988,15 @@ USpudSaveGameInfo* USpudSubsystem::GetSaveGameInfo(const FString& SlotName) UE_LOG(LogSpudSubsystem, Error, TEXT("Unable to open %s for reading info"), *AbsoluteFilename); return nullptr; } - + auto Info = NewObject(); Info->SlotName = SlotName; - USpudState::LoadSaveInfoFromArchive(*Archive, *Info); + const bool bResult = USpudState::LoadSaveInfoFromArchive(*Archive, *Info); Archive->Close(); - - return Info; + + return bResult ? Info : nullptr; +#endif } USpudSaveGameInfo* USpudSubsystem::GetLatestSaveGame() @@ -1020,12 +1014,12 @@ USpudSaveGameInfo* USpudSubsystem::GetLatestSaveGame() USpudSaveGameInfo* USpudSubsystem::GetQuickSaveGame() { - return GetSaveGameInfo(SPUD_QUICKSAVE_SLOTNAME); + return GetSaveGameInfo(QuickSaveSlotName); } USpudSaveGameInfo* USpudSubsystem::GetAutoSaveGame() { - return GetSaveGameInfo(SPUD_AUTOSAVE_SLOTNAME); + return GetSaveGameInfo(AutoSaveSlotName); } FString USpudSubsystem::GetSaveGameDirectory() @@ -1040,9 +1034,16 @@ FString USpudSubsystem::GetSaveGameFilePath(const FString& SlotName) void USpudSubsystem::ListSaveGameFiles(TArray& OutSaveFileList) { +#if USE_UE_SAVE_SYSTEM + if (ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem()) + { + SaveSystem->GetSaveGameNames(OutSaveFileList,0); + } +#else IFileManager& FM = IFileManager::Get(); - FM.FindFiles(OutSaveFileList, *GetSaveGameDirectory(), TEXT(".sav")); + FM.FindFiles(OutSaveFileList, *GetSaveGameDirectory(), TEXT(".sav")); +#endif } FString USpudSubsystem::GetActiveGameFolder() @@ -1183,117 +1184,3 @@ USpudCustomSaveInfo* USpudSubsystem::CreateCustomSaveInfo() { return NewObject(); } - - - -// FTickableGameObject begin - - -void USpudSubsystem::Tick(float DeltaTime) -{ - if (ScreenshotTimeout > 0) - { - ScreenshotTimeout -= DeltaTime; - if (ScreenshotTimeout <= 0) - { - ScreenshotTimeout = 0; - ScreenshotTimedOut(); - } - } - - if (bSupportWorldPartition) - { - auto world = GetWorld(); - if (world) - { - TSet streamingLevels(world->GetStreamingLevels()); - - // Find newly added levels. - for (const auto level : streamingLevels) - { - if (!MonitoredStreamingLevels.Contains(level)) - { - UE_LOG(LogSpudSubsystem, Verbose, TEXT("Loaded streaming level: %s"), *GetNameSafe(level)); - auto wrapper = NewObject(world); - wrapper->LevelStreaming = level; - MonitoredStreamingLevels.Add(level, wrapper); - level->OnLevelShown.AddUniqueDynamic(wrapper, &USpudStreamingLevelWrapper::OnLevelShown); - level->OnLevelHidden.AddUniqueDynamic(wrapper, &USpudStreamingLevelWrapper::OnLevelHidden); - if (level->IsLevelVisible()) - wrapper->OnLevelShown(); - } - } - - // Discard unloaded levels. - for (auto it = MonitoredStreamingLevels.CreateIterator(); it; ++it) - { - if (!streamingLevels.Contains(it.Key())) - { - UE_LOG(LogSpudSubsystem, Verbose, TEXT("Unloaded streaming level: %s"), *GetNameSafe(it.Key())); - check(!it.Key()->IsLevelVisible()); - it.Key()->OnLevelShown.RemoveAll(it.Value()); - it.Key()->OnLevelHidden.RemoveAll(it.Value()); - it.RemoveCurrent(); - } - } - } - } -} - -ETickableTickType USpudSubsystem::GetTickableTickType() const -{ - // This is for timeout purposes - return ETickableTickType::Always; -} - -bool USpudSubsystem::IsTickableWhenPaused() const -{ - // We need the screenshot failsafe timeout even when paused - return true; -} - -TStatId USpudSubsystem::GetStatId() const -{ - RETURN_QUICK_DECLARE_CYCLE_STAT(USpudSubsystem, STATGROUP_Tickables); -} - - -// FTickableGameObject end - -void USpudStreamingLevelWrapper::OnLevelShown() -{ - const auto level = LevelStreaming->GetLoadedLevel(); - if (level) - { - UE_LOG(LogSpudSubsystem, Verbose, TEXT("Level shown: %s"), *USpudState::GetLevelName(level)); - - auto spud = UGameInstance::GetSubsystem(GetWorld()->GetGameInstance()); - if (ensureMsgf(spud, TEXT("Unable to find SpudSubsystem, so cannot load the state of level: %s"), *USpudState::GetLevelName(level))) - { - spud->HandleLevelLoaded(level); - } - } - else - UE_LOG(LogSpudSubsystem, Verbose, TEXT("No loaded level")); -} - -void USpudStreamingLevelWrapper::OnLevelHidden() -{ - const auto level = LevelStreaming->GetLoadedLevel(); - if (level) - { - const auto levelName = USpudState::GetLevelName(level); - UE_LOG(LogSpudSubsystem, Verbose, TEXT("Level hidden: %s"), *levelName); - - // We no longer crash, but we still need to know when this happens, as we really should be - // storing the state of the unloaded level - auto spud = UGameInstance::GetSubsystem(GetWorld()->GetGameInstance()); - if (ensureMsgf(spud, TEXT("Unable to find SpudSubsystem, so cannot save the state of level: %s"), *levelName)) - { - spud->PreUnloadStreamingLevel.Broadcast(FName(levelName)); - spud->HandleLevelUnloaded(level); - } - } - else - UE_LOG(LogSpudSubsystem, Verbose, TEXT("No loaded level")); -} diff --git a/Source/SPUD/Public/ISpudObject.h b/Source/SPUD/Public/ISpudObject.h index 0716e1c..b5233bd 100644 --- a/Source/SPUD/Public/ISpudObject.h +++ b/Source/SPUD/Public/ISpudObject.h @@ -1,10 +1,10 @@ #pragma once -#include "CoreMinimal.h" -#include "SpudState.h" - +#include "UObject/Interface.h" #include "ISpudObject.generated.h" +class USpudState; + UINTERFACE(MinimalAPI) class USpudObject : public UInterface { @@ -63,23 +63,14 @@ class SPUD_API ISpudObject /// Allows deciding if an object should be skipped at runtime. UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "SPUD Interface") bool ShouldSkip() const; virtual bool ShouldSkip_Implementation() const { return false; } -}; - -UINTERFACE(MinimalAPI) -class USpudObjectCallback : public UInterface -{ - GENERATED_BODY() -}; -/** -* Interface for fine control of persistence. Implement this in your objects to be notified when they are persisted or -* restored individually, and to include custom data in your stored records if you want. -*/ -class SPUD_API ISpudObjectCallback -{ - GENERATED_BODY() + // Allows skipping loading actors during level restore. Use this if you need to restore an actor individually after actual load process. + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "SPUD Interface") + bool ShouldSkipDuringLevelRestore() const; virtual bool ShouldSkipDuringLevelRestore_Implementation() const { return false; } -public: + // Allows calling SpudStoreCustomData / SpudRestoreCustomData interface functions during save/load. + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "SPUD Interface") + bool HasCustomData() const; virtual bool HasCustomData_Implementation() const { return false; } // --- IMPORTANT --- // WEIRD ASS PROBLEM: Passing USpudState to any of these interface methods, when it was a USaveGame with @@ -140,5 +131,4 @@ class SPUD_API ISpudObjectCallback /// This is called for root objects and nested UObjects UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "SPUD") void SpudPostRestore(const USpudState* State); - }; diff --git a/Source/SPUD/Public/SpudState.h b/Source/SPUD/Public/SpudState.h index c419ba4..d3a55ce 100644 --- a/Source/SPUD/Public/SpudState.h +++ b/Source/SPUD/Public/SpudState.h @@ -31,10 +31,10 @@ class SPUD_API USpudSaveGameInfo : public UObject FString SlotName; /// Thumbnail screenshot (may be blank if one wasn't included in the save game) UPROPERTY(BlueprintReadOnly) - UTexture2D* Thumbnail; + TObjectPtr Thumbnail; /// Custom fields that you chose to store with the save header information specifically for your game UPROPERTY(BlueprintReadOnly) - USpudCustomSaveInfo* CustomInfo; + TObjectPtr CustomInfo; }; @@ -103,6 +103,7 @@ class SPUD_API USpudState : public UObject FSpudNamedObjectData* GetGlobalObjectData(const UObject* Obj, bool AutoCreate); FSpudNamedObjectData* GetGlobalObjectData(const FString& ID, bool AutoCreate); + bool ShouldActorSkipDuringLevelRestore(AActor* Actor) const; bool ShouldActorBeRespawnedOnRestore(AActor* Actor) const; bool ShouldActorTransformBeRestored(AActor* Actor) const; bool ShouldActorVelocityBeRestored(AActor* Actor) const; @@ -111,9 +112,7 @@ class SPUD_API USpudState : public UObject void StoreGlobalObject(UObject* Obj, FSpudNamedObjectData* Data); void StoreObjectProperties(UObject* Obj, FSpudPropertyData& Properties, FSpudClassMetadata& Meta, int StartDepth = 0); void StoreObjectProperties(UObject* Obj, uint32 PrefixID, TArray& PropertyOffsets, FSpudClassMetadata& Meta, FSpudMemoryWriter& Out, int StartDepth = 0); - - // Actually restores the world, on the assumption that it's already loaded into the correct map - void RestoreLoadedWorld(UWorld* World, bool bSingleLevel, const FString& OnlyLevelName = ""); + // Returns whether this is an actor which is not technically in a level, but is auto-created so doesn't need to be // spawned by the restore process. E.g. GameMode, Pawns bool ShouldRespawnRuntimeActor(const AActor* Actor) const; @@ -241,6 +240,9 @@ class SPUD_API USpudState : public UObject /// Does NOT restore any global object state (see RestoreGlobalObject). void RestoreLevel(UWorld* World, const FString& LevelName); + /// Checks if we can restore a level. Returns false if level data does not exist. + bool CanRestoreLevel(ULevel* Level); + /// Specialised function for restoring a specific level by reference void RestoreLevel(ULevel* Level); @@ -248,8 +250,11 @@ class SPUD_API USpudState : public UObject /// Useful for pre-caching before RestoreLevel bool PreLoadLevelData(const FString& LevelName); + /// Checks if we can restore a world. Returns false if no saved data exist for any of levels in world. + bool CanRestoreWorld(UWorld* World, const FString& OnlyLevelName = ""); + // Restores the world and all levels currently in it, on the assumption that it's already loaded into the correct map - void RestoreLoadedWorld(UWorld* World); + void RestoreLoadedWorld(UWorld* World, const FString& OnlyLevelName = ""); /// Restores a single actor from this state. Does not require the actor to implement ISpudObject. /// NOTE: this is a limited function, it's less efficient than using RestoreLevel for multiple actors, and it diff --git a/Source/SPUD/Public/SpudStreamingVolume.h b/Source/SPUD/Public/SpudStreamingVolume.h deleted file mode 100644 index 0ffd0cf..0000000 --- a/Source/SPUD/Public/SpudStreamingVolume.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/Volume.h" -#include "SpudStreamingVolume.generated.h" - -/// Drop-in replacement for ALevelStreamingVolume which has a number of advantages: -/// 1. Communicates with SpudSubsystem to organise the streaming in/out of levels such that they get persisted correctly -/// 2. Responds to both cameras and player-controlled pawns, making setup easier for 3rd person cameras (less prone to camera popping out of intuitive volumes) -/// 3. Linked streaming levels are editable directly in the volume, instead of the weird backwards system of pointing the level at the volume -UCLASS(Blueprintable, ClassGroup="SPUD", HideCategories=(Advanced, Attachment, Collision, Volume, Navigation)) -class SPUD_API ASpudStreamingVolume : public AVolume -{ - GENERATED_BODY() - -protected: - - UPROPERTY(Category=LevelStreamingVolume, EditAnywhere, BlueprintReadOnly, meta=(DisplayName = "Streaming Levels", AllowedClasses="/Script/Engine.World")) - TArray StreamingLevels; - - UPROPERTY() - TArray RelevantActorsInVolume; - - UPROPERTY() - TArray PawnsInVolume; - - ASpudStreamingVolume(const FObjectInitializer& ObjectInitializer); - - virtual void BeginPlay() override; - virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; - bool IsRelevantActor(AActor* Actor) const; - void AddRelevantActor(AActor* Actor); - void RemoveRelevantActor(AActor* Actor); - - UFUNCTION() - void OnPawnControllerChanged(APawn* Pawn, AController* NewCtrl); - -public: - - virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; - virtual void NotifyActorEndOverlap(AActor* OtherActor) override; -}; diff --git a/Source/SPUD/Public/SpudSubsystem.h b/Source/SPUD/Public/SpudSubsystem.h index e415012..7a816a1 100644 --- a/Source/SPUD/Public/SpudSubsystem.h +++ b/Source/SPUD/Public/SpudSubsystem.h @@ -5,30 +5,29 @@ #include "SpudCustomSaveInfo.h" #include "SpudState.h" #include "Subsystems/GameInstanceSubsystem.h" -#include "Tickable.h" #include "Engine/World.h" #include "SpudSubsystem.generated.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpudSubsystem, Verbose, Verbose); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreLoadGame, const FString&, SlotName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpudPostLoadGame, const FString&, SlotName, bool, bSuccess); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreSaveGame, const FString&, SlotName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpudPostSaveGame, const FString&, SlotName, bool, bSuccess); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreLoadGame, const FString& /** SlotName */); +DECLARE_MULTICAST_DELEGATE_TwoParams(FSpudPostLoadGame, const FString& /** SlotName */, bool /** bSuccess */); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreSaveGame, const FString& /** SlotName */); +DECLARE_MULTICAST_DELEGATE_TwoParams(FSpudPostSaveGame, const FString& /** SlotName */, bool /** bSuccess */); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreLevelStore, const FString&, LevelName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpudPostLevelStore, const FString&, LevelName, bool, bSuccess); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreLevelRestore, const FString&, LevelName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpudPostLevelRestore, const FString&, LevelName, bool, bSuccess); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreLevelStore, const FString& /** LevelName */); +DECLARE_MULTICAST_DELEGATE_TwoParams(FSpudPostLevelStore, const FString& /** LevelName */, bool /** bSuccess */); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreLevelRestore, const FString& /** LevelName */); +DECLARE_MULTICAST_DELEGATE_TwoParams(FSpudPostLevelRestore, const FString& /** LevelName */, bool /** bSuccess */); /// Helper delegates to allow blueprints to listen in on map transitions & streaming if they want -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreTravelToNewMap, const FString&, NextMapName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSpudPostTravelToNewMap); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreLoadStreamingLevel, const FName&, LevelName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPostLoadStreamingLevel, const FName&, LevelName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPreUnloadStreamingLevel, const FName&, LevelName); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpudPostUnloadStreamingLevel, const FName&, LevelName); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreTravelToNewMap, const FString& /** NextMapName */); +DECLARE_MULTICAST_DELEGATE(FSpudPostTravelToNewMap); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreLoadStreamingLevel, const FName& /** LevelName */); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPostLoadStreamingLevel, const FName& /** LevelName */); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPreUnloadStreamingLevel, const FName& /** LevelName */); +DECLARE_MULTICAST_DELEGATE_OneParam(FSpudPostUnloadStreamingLevel, const FName& /** LevelName */); // Callbacks passed to functions DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(bool, FSpudUpgradeSaveDelegate, class USpudState*, SaveState); @@ -61,80 +60,44 @@ enum class ESpudSaveSorting : uint8 Title }; -UCLASS(Transient) -class SPUD_API USpudStreamingLevelWrapper : public UObject -{ - GENERATED_BODY() - -public: - UPROPERTY() - ULevelStreaming* LevelStreaming; - - UFUNCTION() - void OnLevelShown(); - UFUNCTION() - void OnLevelHidden(); -}; - /// Subsystem which controls our save games, and also the active game's persistent state (for streaming levels) UCLASS(Config=Engine) -class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableGameObject +class SPUD_API USpudSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() - friend USpudStreamingLevelWrapper; - public: /// Event fired just before a game is loaded - UPROPERTY(BlueprintAssignable) FSpudPreLoadGame PreLoadGame; /// Event fired just after a game has finished loading - UPROPERTY(BlueprintAssignable) FSpudPostLoadGame PostLoadGame; /// Event fired just before a game is saved - UPROPERTY(BlueprintAssignable) FSpudPreSaveGame PreSaveGame; /// Event fired just after a game finished saving - UPROPERTY(BlueprintAssignable) FSpudPostSaveGame PostSaveGame; /// Event fired just before we write the contents of a level to the state database - UPROPERTY(BlueprintAssignable) FSpudPreLevelStore PreLevelStore; /// Event fired just after we've written the contents of a level to the state database - UPROPERTY(BlueprintAssignable) FSpudPostLevelStore PostLevelStore; /// Event fired just before we're about to populate a loaded level from the state database - UPROPERTY(BlueprintAssignable) FSpudPreLevelRestore PreLevelRestore; /// Event fired just after we've finished populating a loaded level from the state database - UPROPERTY(BlueprintAssignable) FSpudPostLevelRestore PostLevelRestore; /// Event fired just prior to travelling to a new map (convenience for blueprints mainly, who don't have access to FCoreDelegates) - UPROPERTY(BlueprintAssignable) FSpudPreTravelToNewMap PreTravelToNewMap; /// Event fired just after travelling to a new map (convenience for blueprints mainly, who don't have access to FCoreDelegates) - UPROPERTY(BlueprintAssignable) FSpudPostTravelToNewMap PostTravelToNewMap; /// Event fired just before this subsystem loads a streaming level - UPROPERTY(BlueprintAssignable) FSpudPreLoadStreamingLevel PreLoadStreamingLevel; /// Event fired just after a streaming level has loaded, but BEFORE any state has been restored - UPROPERTY(BlueprintAssignable) FSpudPostLoadStreamingLevel PostLoadStreamingLevel; /// Event fired just before this subsystem unloads a streaming level, BEFORE any state has been stored if needed /// This is ALMOST the same as PreLevelStore, except when loading a game that's not called, but this is - UPROPERTY(BlueprintAssignable) FSpudPreUnloadStreamingLevel PreUnloadStreamingLevel; /// Event fired just after a streaming level has unloaded - UPROPERTY(BlueprintAssignable) FSpudPostUnloadStreamingLevel PostUnloadStreamingLevel; - /// The time delay after the last request for a streaming level is withdrawn, that the level will be unloaded - /// This is used to reduce load/unload thrashing at boundaries - UPROPERTY(BlueprintReadWrite, Config) - float StreamLevelUnloadDelay = 3; - /// The desired width of screenshots taken for save games UPROPERTY(BlueprintReadWrite, Config) int32 ScreenshotWidth = 240; @@ -143,28 +106,21 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG int32 ScreenshotHeight = 135; FDelegateHandle OnScreenshotHandle; - /// If true, use the show/hide events of streaming levels to save/load, which is compatible with World Partition - /// You can set this to false to change to the legacy mode which requires ASpudStreamingVolume + // If false, we won't try to save current persistent level's state while jumping/traveling into another persistent level. UPROPERTY(BlueprintReadWrite, Config) - bool bSupportWorldPartition = true; + bool bSaveLevelStateWhileTraveling = false; + + UPROPERTY(BlueprintReadOnly) + FString QuickSaveSlotName = FString("__QuickSave__"); + UPROPERTY(BlueprintReadOnly) + FString AutoSaveSlotName = FString("__AutoSave__"); protected: - FDelegateHandle OnPreLoadMapHandle; - FDelegateHandle OnPostLoadMapHandle; - FDelegateHandle OnSeamlessTravelHandle; - int32 LoadUnloadRequests = 0; - bool FirstStreamRequestSinceMapLoad = true; - TMap LevelsPendingLoad; - TMap LevelsPendingUnload; - FCriticalSection LevelsPendingLoadMutex; - FCriticalSection LevelsPendingUnloadMutex; - FTimerHandle StreamLevelUnloadTimerHandle; - float ScreenshotTimeout = 0; FString SlotNameInProgress; FText TitleInProgress; UPROPERTY() - const USpudCustomSaveInfo* ExtraInfoInProgress; + TObjectPtr ExtraInfoInProgress; UPROPERTY() TArray> GlobalObjects; @@ -176,15 +132,18 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG // True while restoring game state, either by loading a game or restoring the state of a streamed-in level. UPROPERTY(BlueprintReadOnly) - bool IsRestoringState = false; + bool bIsRestoringState = false; /// True when system shutdown has been started UPROPERTY(BlueprintReadOnly) bool bIsTearingDown = false; + UPROPERTY(BlueprintReadOnly) + TMap LevelStreamingRestoreStates; + // The currently active game state UPROPERTY() - USpudState* ActiveState; + TObjectPtr ActiveState; USpudState* GetActiveState() { @@ -194,29 +153,12 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG return ActiveState; } - struct FStreamLevelRequests - { - TArray> Requesters; - bool bPendingUnload; - float LastRequestExpiredTime; - - FStreamLevelRequests(): bPendingUnload(false), LastRequestExpiredTime(0) - { - } - }; - - // Map of streaming level names to the requests to load them - TMap LevelRequests; - - UPROPERTY() - TMap MonitoredStreamingLevels; - bool ServerCheck(bool LogWarning) const; UFUNCTION() void OnPreLoadMap(const FString& MapName); UFUNCTION() - void OnSeamlessTravelTransition(UWorld* World); + void OnSeamlessTravelStart(UWorld* World, const FString& MapName); UFUNCTION() void OnPostLoadMap(UWorld* World); UFUNCTION() @@ -225,13 +167,17 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG void SubscribeLevelObjectEvents(ULevel* Level); void UnsubscribeLevelObjectEvents(ULevel* Level); void UnsubscribeAllLevelObjectEvents(); + + bool CanRestoreLevel(ULevel* Level); + bool CanRestoreWorld(UWorld* World); + + UFUNCTION() + void OnLevelBeginMakingInvisible(UWorld* World, const ULevelStreaming* StreamingLevel, ULevel* LoadedLevel); + UFUNCTION() + void OnLevelBeginMakingVisible(UWorld* World, const ULevelStreaming* StreamingLevel, ULevel* LoadedLevel); // This is a latent callback and has to be BlueprintCallable UFUNCTION(BlueprintCallable) - void PostLoadStreamLevel(int32 LinkID); - UFUNCTION(BlueprintCallable) - void PostUnloadStreamLevel(int32 LinkID); - UFUNCTION(BlueprintCallable) void PostLoadStreamLevelGameThread(FName LevelName); UFUNCTION(BlueprintCallable) void PostUnloadStreamLevelGameThread(FName LevelName); @@ -240,8 +186,6 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG void StoreLevel(ULevel* Level, bool bRelease, bool bBlocking); UFUNCTION() - void ScreenshotTimedOut(); - UFUNCTION() void OnScreenshotCaptured(int32 Width, int32 Height, const TArray& Colours); void FinishSaveGame(const FString& SlotName, const FText& Title, const USpudCustomSaveInfo* ExtraInfo, TArray* ScreenshotData); @@ -252,20 +196,28 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG void HandleLevelLoaded(ULevel* Level) { HandleLevelLoaded(FName(USpudState::GetLevelName(Level))); } void HandleLevelUnloaded(ULevel* Level); - void LoadStreamLevel(FName LevelName, bool Blocking); - void StartUnloadTimer(); - void StopUnloadTimer(); - void CheckStreamUnload(); - void UnloadStreamLevel(FName LevelName); - public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; + // Loads specific actor from active state. If bAsGameLoad is true, we'll try to load the actor as we were loading a full level save. + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) + void LoadActorData(AActor* Actor, bool bAsGameLoad = false); + + // Manually mark actor as destroyed + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) + void MarkActorDestroyed(AActor* Actor); + + UFUNCTION(BlueprintPure) + bool IsRestoringState() const { return bIsRestoringState; } + UFUNCTION(BlueprintPure) bool IsLoadingGame() const { return CurrentState == ESpudSystemState::LoadingGame; } + UFUNCTION(BlueprintPure) + bool IsStreamedLevelRestoring(ULevel* Level) const; + UFUNCTION(BlueprintPure) bool IsSavingGame() const { return CurrentState == ESpudSystemState::SavingGame; } @@ -308,17 +260,18 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG /** * Quick load the game from the last player-requested Quick Save slot (NOT the last autosave or manual save) + * @param bAutoTravelLevel Initiates travel automatically after load * @param TravelOptions Options string to include in the travel URL e.g. "Listen" */ UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) - void QuickLoadGame(const FString& TravelOptions = FString(TEXT(""))); + void QuickLoadGame(bool bAutoTravelLevel = true, const FString& TravelOptions = FString(TEXT(""))); /** * Continue a game from the latest save of any kind - autosave, quick save, manual save. The same as calling LoadGame on the most recent. * @param TravelOptions Options string to include in the travel URL e.g. "Listen" */ UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) - void LoadLatestSaveGame(const FString& TravelOptions = FString(TEXT(""))); + void LoadLatestSaveGame(bool bAutoTravelLevel = true, const FString& TravelOptions = FString(TEXT(""))); /// Create a save game descriptor which you can use to store additional descriptive information about a save game. /// Fill the returned object in then pass it to the SaveGame call to have additional info to display on save/load screens @@ -339,10 +292,11 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG /** * Load the game in a given slot name. Asynchronous, use the PostLoadGame event to determine when load is complete (and success) * @param SlotName The slot name of the save to load + * @param bAutoTravelLevel Initiates travel automatically after load * @param TravelOptions Options string to include in the travel URL e.g. "Listen" */ UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) - void LoadGame(const FString& SlotName, const FString& TravelOptions = FString(TEXT(""))); + void LoadGame(const FString& SlotName, bool bAutoTravelLevel = true, const FString& TravelOptions = FString(TEXT(""))); /// Delete the save game in a given slot UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) @@ -396,15 +350,6 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) void ClearLevelState(const FString& LevelName); - /// Make a request that a streaming level is loaded. Won't load if already loaded, but will - /// record the request count so that unloading is done when all requests are withdrawn. - UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) - void AddRequestForStreamingLevel(UObject* Requester, FName LevelName, bool BlockingLoad); - /// Withdraw a request for a streaming level. Once all requesters have rescinded their requests, the - /// streaming level will be considered ready to be unloaded. - UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) - void WithdrawRequestForStreamingLevel(UObject* Requester, FName LevelName); - /// Get the list of the save games with metadata UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly) TArray GetSaveGameList(bool bIncludeQuickSave = true, bool bIncludeAutoSave = true, ESpudSaveSorting Sorting = ESpudSaveSorting::None); @@ -517,15 +462,6 @@ class SPUD_API USpudSubsystem : public UGameInstanceSubsystem, public FTickableG static void ListSaveGameFiles(TArray& OutSaveFileList); static FString GetActiveGameFolder(); static FString GetActiveGameFilePath(const FString& Name); - - - // FTickableGameObject begin - virtual void Tick(float DeltaTime) override; - virtual ETickableTickType GetTickableTickType() const override; - virtual bool IsTickableWhenPaused() const override; - virtual TStatId GetStatId() const override; - // FTickableGameObject end - }; inline USpudSubsystem* GetSpudSubsystem(UWorld* WorldContext) diff --git a/Source/SPUD/SPUD.Build.cs b/Source/SPUD/SPUD.Build.cs index 00f8ba0..d54e1df 100644 --- a/Source/SPUD/SPUD.Build.cs +++ b/Source/SPUD/SPUD.Build.cs @@ -6,6 +6,7 @@ public class SPUD : ModuleRules public SPUD(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + bAllowConfidentialPlatformDefines = true; PublicIncludePaths.AddRange( new string[] { @@ -31,7 +32,6 @@ public SPUD(ReadOnlyTargetRules Target) : base(Target) PrivateDependencyModuleNames.AddRange( new string[] { - "StructUtils" } ); @@ -41,5 +41,8 @@ public SPUD(ReadOnlyTargetRules Target) : base(Target) { } ); + + // Prefer UE save system to keep multi-platform compatibility between files + PublicDefinitions.Add("PREFER_UE_SAVE_SYSTEM=1"); } } diff --git a/Source/SPUDEditor/Private/SpudEditorModule.cpp b/Source/SPUDEditor/Private/SpudEditorModule.cpp index 39782c8..a442652 100644 --- a/Source/SPUDEditor/Private/SpudEditorModule.cpp +++ b/Source/SPUDEditor/Private/SpudEditorModule.cpp @@ -3,6 +3,7 @@ #include "ISettingsModule.h" #include "ISettingsSection.h" +#include "FileHelpers.h" #include "SPUDEditor/Public/SpudPluginSettings.h" IMPLEMENT_GAME_MODULE(FSpudEditorModule, SPUDEditor); diff --git a/Source/SPUDEditor/Private/SpudEditorModule.h b/Source/SPUDEditor/Private/SpudEditorModule.h index c889e6c..573b362 100644 --- a/Source/SPUDEditor/Private/SpudEditorModule.h +++ b/Source/SPUDEditor/Private/SpudEditorModule.h @@ -4,7 +4,6 @@ #include "Modules/ModuleInterface.h" #include "Modules/ModuleManager.h" -#include "UnrealEd.h" DECLARE_LOG_CATEGORY_EXTERN(LogSpudEditor, All, All) diff --git a/Source/SPUDTest/Private/SpudTest.cpp b/Source/SPUDTest/Private/SpudTest.cpp index 4b5d9fd..00ad230 100644 --- a/Source/SPUDTest/Private/SpudTest.cpp +++ b/Source/SPUDTest/Private/SpudTest.cpp @@ -1,7 +1,9 @@ #include "Misc/AutomationTest.h" -#include "Engine.h" #include "SpudState.h" #include "TestSaveObject.h" +#include "Engine/PointLight.h" +#include "Engine/StaticMeshActor.h" +#include "StructUtils/InstancedStruct.h" template @@ -169,7 +171,7 @@ void CheckAllTypes(FAutomationTestBase* Test, const FString& Prefix, const T& Ac Test->TestEqual(Prefix + "StringVal should match", Actual.StringVal, Expected.StringVal); Test->TestEqual(Prefix + "TextVal should match", Actual.TextVal.ToString(), Expected.TextVal.ToString()); - Test->TestNotNull(Prefix + "UObject shouldn't be null", Actual.UObjectVal); + Test->TestNotNull(Prefix + "UObject shouldn't be null", Actual.UObjectVal.Get()); if (Actual.UObjectVal) { Test->TestEqual(Prefix + "UObject String should match", Actual.UObjectVal->NestedStringVal, Expected.UObjectVal->NestedStringVal); @@ -380,11 +382,11 @@ bool FTestNestedObject::RunTest(const FString& Parameters) auto LoadedObj = NewObject(); State->RestoreGlobalObject(LoadedObj, "TestObject"); - TestNotNull("UObject1 shouldn't be null", LoadedObj->UObjectVal1); - TestNotNull("UObject2 shouldn't be null", LoadedObj->UObjectVal2); - TestNotNull("UObject3 shouldn't be null", LoadedObj->UObjectVal3); - TestNotNull("UObject4 shouldn't be null", LoadedObj->UObjectVal4); - TestNotNull("UObject5 shouldn't be null", LoadedObj->UObjectVal5); + TestNotNull("UObject1 shouldn't be null", LoadedObj->UObjectVal1.Get()); + TestNotNull("UObject2 shouldn't be null", LoadedObj->UObjectVal2.Get()); + TestNotNull("UObject3 shouldn't be null", LoadedObj->UObjectVal3.Get()); + TestNotNull("UObject4 shouldn't be null", LoadedObj->UObjectVal4.Get()); + TestNotNull("UObject5 shouldn't be null", LoadedObj->UObjectVal5.Get()); return true; } diff --git a/Source/SPUDTest/Private/TestSaveObject.h b/Source/SPUDTest/Private/TestSaveObject.h index 3b67154..85859e1 100644 --- a/Source/SPUDTest/Private/TestSaveObject.h +++ b/Source/SPUDTest/Private/TestSaveObject.h @@ -3,8 +3,8 @@ #pragma once #include "CoreMinimal.h" -#include "InstancedStruct.h" #include "ISpudObject.h" +#include "StructUtils/InstancedStruct.h" #include "UObject/Object.h" #include "TestSaveObject.generated.h" @@ -83,7 +83,7 @@ struct FTestAllTypesStruct FText TextVal; UPROPERTY(SaveGame) - UTestNestedUObject* UObjectVal = nullptr; + TObjectPtr UObjectVal = nullptr; UPROPERTY(SaveGame) TObjectPtr TObjectPtrVal = nullptr; @@ -99,7 +99,7 @@ struct FTestAllTypesStruct // sadly we can't test actor refs easily here; test example world does that though UPROPERTY(SaveGame) - TMap UObjectMap; + TMap> UObjectMap; // Arrays of the above UPROPERTY(SaveGame) @@ -206,7 +206,7 @@ class SPUDTEST_API UTestSaveObjectBasic : public UObject FText TextVal; UPROPERTY(SaveGame) - UTestNestedUObject* UObjectVal; + TObjectPtr UObjectVal; UPROPERTY(SaveGame) TObjectPtr TObjectPtrVal = nullptr; @@ -221,7 +221,7 @@ class SPUDTEST_API UTestSaveObjectBasic : public UObject TArray< TSubclassOf > ActorSubclassArray; UPROPERTY(SaveGame) - TMap UObjectMap; + TMap> UObjectMap; // sadly we can't test actor refs easily here; test example world does that though @@ -281,7 +281,7 @@ class SPUDTEST_API UTestSaveObjectStructs : public UObject }; UCLASS() -class SPUDTEST_API UTestSaveObjectCustomData : public UObject, public ISpudObjectCallback +class SPUDTEST_API UTestSaveObjectCustomData : public UObject, public ISpudObject { GENERATED_BODY() public: @@ -303,9 +303,10 @@ class SPUDTEST_API UTestSaveObjectCustomData : public UObject, public ISpudObjec static const FString TestChunkID1; static const FString TestChunkID2; - - virtual void SpudStoreCustomData_Implementation(const USpudState* State, USpudStateCustomData* CustomData) override; - virtual void SpudRestoreCustomData_Implementation(USpudState* State, USpudStateCustomData* CustomData) override; + + virtual bool HasCustomData_Implementation() const override { return true; } + virtual void SpudStoreCustomData_Implementation(const USpudState* State, class USpudStateCustomData* CustomData) override; + virtual void SpudRestoreCustomData_Implementation(USpudState* State, class USpudStateCustomData* CustomData) override; }; @@ -361,19 +362,19 @@ class SPUDTEST_API UTestSaveObjectParent : public UObject GENERATED_BODY() public: UPROPERTY(SaveGame) - UTestNestedChild1* UObjectVal1; + TObjectPtr UObjectVal1; UPROPERTY(SaveGame) - UTestNestedChild2* UObjectVal2; + TObjectPtr UObjectVal2; UPROPERTY(SaveGame) - UTestNestedChild3* UObjectVal3; + TObjectPtr UObjectVal3; UPROPERTY(SaveGame) - UTestNestedChild4* UObjectVal4; + TObjectPtr UObjectVal4; UPROPERTY(SaveGame) - UTestNestedChild5* UObjectVal5; + TObjectPtr UObjectVal5; }; USTRUCT(BlueprintType) diff --git a/Source/SPUDTest/SPUDTest.Build.cs b/Source/SPUDTest/SPUDTest.Build.cs index 7bda68d..44067fe 100644 --- a/Source/SPUDTest/SPUDTest.Build.cs +++ b/Source/SPUDTest/SPUDTest.Build.cs @@ -14,8 +14,7 @@ public SPUDTest(ReadOnlyTargetRules Target) : base(Target) "Core", "CoreUObject", "Engine", - "SPUD", - "StructUtils" + "SPUD" } );