From 9a0a3608fbeac8025c7edf0dfebce1099ce12497 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 11 May 2026 01:00:41 -0700 Subject: [PATCH] Add placed actor persistence foundation --- AGRARIAN_FOUNDATION_STATUS.md | 5 +- .../AgrarianPersistenceSubsystem.cpp | 121 ++++++++++++++++++ .../AgrarianPersistenceSubsystem.h | 19 +++ .../AgrarianPersistentActorComponent.cpp | 44 +++++++ .../AgrarianPersistentActorComponent.h | 38 ++++++ Source/AgrarianGame/AgrarianShelterActor.cpp | 4 + Source/AgrarianGame/AgrarianShelterActor.h | 4 + 7 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 Source/AgrarianGame/AgrarianPersistentActorComponent.cpp create mode 100644 Source/AgrarianGame/AgrarianPersistentActorComponent.h diff --git a/AGRARIAN_FOUNDATION_STATUS.md b/AGRARIAN_FOUNDATION_STATUS.md index 75a02a1..ef39838 100644 --- a/AGRARIAN_FOUNDATION_STATUS.md +++ b/AGRARIAN_FOUNDATION_STATUS.md @@ -24,6 +24,9 @@ - [x] Resource nodes can use item definition assets for harvest yields. - [x] Building placement component added. - [x] Character now owns a building placement component. +- [x] Persistent actor component added. +- [x] Persistence subsystem can capture and restore saveable world actors. +- [x] Primitive shelter actor is marked as a persistent world actor. ## Next Unreal Editor Tasks @@ -45,6 +48,6 @@ - [x] Add building placement component. - [x] Add simple crafting recipe defaults or data asset pipeline. - [x] Add item definition data asset class. -- [ ] Add save/load capture for placed actors. +- [x] Add save/load capture for placed actors. - [ ] Add admin/dev console commands. - [ ] Add wildlife base actor. diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp index 564b633..3256f10 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp @@ -1,7 +1,10 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianPersistenceSubsystem.h" +#include "AgrarianPersistentActorComponent.h" #include "AgrarianSaveGame.h" +#include "EngineUtils.h" +#include "Engine/World.h" #include "Kismet/GameplayStatics.h" UAgrarianSaveGame* UAgrarianPersistenceSubsystem::CreateEmptySave() const @@ -31,3 +34,121 @@ bool UAgrarianPersistenceSubsystem::DoesSaveExist() const { return UGameplayStatics::DoesSaveGameExist(DefaultSlotName, UserIndex); } + +void UAgrarianPersistenceSubsystem::RegisterWorldActorClass(FName ActorTypeId, TSubclassOf ActorClass) +{ + if (ActorTypeId != NAME_None && ActorClass) + { + WorldActorClassRegistry.Add(ActorTypeId, ActorClass); + } +} + +int32 UAgrarianPersistenceSubsystem::CaptureWorldActors(UAgrarianSaveGame* SaveGame) const +{ + if (!SaveGame) + { + return 0; + } + + TArray PersistentComponents; + FindPersistentComponents(PersistentComponents); + + SaveGame->WorldActors.Reset(); + for (const UAgrarianPersistentActorComponent* Component : PersistentComponents) + { + if (Component && Component->IsSaveable()) + { + SaveGame->WorldActors.Add(Component->CaptureSaveState()); + } + } + + return SaveGame->WorldActors.Num(); +} + +int32 UAgrarianPersistenceSubsystem::RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors) const +{ + UWorld* World = GetWorld(); + if (!World || !SaveGame) + { + return 0; + } + + if (bClearExistingActors) + { + TArray ExistingComponents; + FindPersistentComponents(ExistingComponents); + + for (UAgrarianPersistentActorComponent* Component : ExistingComponents) + { + if (AActor* Owner = Component ? Component->GetOwner() : nullptr) + { + Owner->Destroy(); + } + } + } + + int32 RestoredCount = 0; + for (const FAgrarianSavedWorldActor& SavedActor : SaveGame->WorldActors) + { + const TSubclassOf* ActorClass = WorldActorClassRegistry.Find(SavedActor.ActorTypeId); + if (!ActorClass || !(*ActorClass)) + { + continue; + } + + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AActor* SpawnedActor = World->SpawnActor(*ActorClass, SavedActor.Transform, SpawnParams); + if (!SpawnedActor) + { + continue; + } + + if (UAgrarianPersistentActorComponent* Component = SpawnedActor->FindComponentByClass()) + { + Component->ApplySaveState(SavedActor); + } + + RestoredCount++; + } + + return RestoredCount; +} + +bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const +{ + UAgrarianSaveGame* SaveGame = LoadOrCreateSave(); + if (!SaveGame) + { + return false; + } + + CaptureWorldActors(SaveGame); + return WriteSave(SaveGame); +} + +void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray& OutComponents) const +{ + OutComponents.Reset(); + + UWorld* World = GetWorld(); + if (!World) + { + return; + } + + for (TActorIterator ActorIt(World); ActorIt; ++ActorIt) + { + AActor* Actor = *ActorIt; + if (!Actor || Actor->IsPendingKillPending()) + { + continue; + } + + if (UAgrarianPersistentActorComponent* Component = Actor->FindComponentByClass()) + { + OutComponents.Add(Component); + } + } +} diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h index c412d01..1ecaa5d 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.h +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.h @@ -7,6 +7,7 @@ #include "AgrarianPersistenceSubsystem.generated.h" class UAgrarianSaveGame; +class UAgrarianPersistentActorComponent; UCLASS() class UAgrarianPersistenceSubsystem : public UGameInstanceSubsystem @@ -20,6 +21,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") int32 UserIndex = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + TMap> WorldActorClassRegistry; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") UAgrarianSaveGame* CreateEmptySave() const; @@ -31,4 +35,19 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") bool DoesSaveExist() const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + void RegisterWorldActorClass(FName ActorTypeId, TSubclassOf ActorClass); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + int32 CaptureWorldActors(UAgrarianSaveGame* SaveGame) const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + int32 RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors = true) const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + bool SaveCurrentWorld() const; + +protected: + void FindPersistentComponents(TArray& OutComponents) const; }; diff --git a/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp b/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp new file mode 100644 index 0000000..345e930 --- /dev/null +++ b/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp @@ -0,0 +1,44 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianPersistentActorComponent.h" + +UAgrarianPersistentActorComponent::UAgrarianPersistentActorComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +bool UAgrarianPersistentActorComponent::IsSaveable() const +{ + return ActorTypeId != NAME_None && GetOwner() != nullptr; +} + +FAgrarianSavedWorldActor UAgrarianPersistentActorComponent::CaptureSaveState() const +{ + FAgrarianSavedWorldActor SavedActor; + SavedActor.ActorTypeId = ActorTypeId; + SavedActor.StringState = StringState; + SavedActor.NumberState = NumberState; + + if (bSaveTransform && GetOwner()) + { + SavedActor.Transform = GetOwner()->GetActorTransform(); + } + + return SavedActor; +} + +void UAgrarianPersistentActorComponent::ApplySaveState(const FAgrarianSavedWorldActor& SavedActor) +{ + StringState = SavedActor.StringState; + NumberState = SavedActor.NumberState; + + if (ActorTypeId == NAME_None) + { + ActorTypeId = SavedActor.ActorTypeId; + } + + if (bSaveTransform && GetOwner()) + { + GetOwner()->SetActorTransform(SavedActor.Transform); + } +} diff --git a/Source/AgrarianGame/AgrarianPersistentActorComponent.h b/Source/AgrarianGame/AgrarianPersistentActorComponent.h new file mode 100644 index 0000000..69ed381 --- /dev/null +++ b/Source/AgrarianGame/AgrarianPersistentActorComponent.h @@ -0,0 +1,38 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "AgrarianSaveGame.h" +#include "AgrarianPersistentActorComponent.generated.h" + +UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class UAgrarianPersistentActorComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UAgrarianPersistentActorComponent(); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + FName ActorTypeId = NAME_None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + bool bSaveTransform = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + TMap StringState; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence") + TMap NumberState; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + bool IsSaveable() const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + FAgrarianSavedWorldActor CaptureSaveState() const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence") + void ApplySaveState(const FAgrarianSavedWorldActor& SavedActor); +}; diff --git a/Source/AgrarianGame/AgrarianShelterActor.cpp b/Source/AgrarianGame/AgrarianShelterActor.cpp index 48033a8..b0a6fcc 100644 --- a/Source/AgrarianGame/AgrarianShelterActor.cpp +++ b/Source/AgrarianGame/AgrarianShelterActor.cpp @@ -1,6 +1,7 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianShelterActor.h" +#include "AgrarianPersistentActorComponent.h" #include "Components/BoxComponent.h" #include "Components/StaticMeshComponent.h" @@ -15,4 +16,7 @@ AAgrarianShelterActor::AAgrarianShelterActor() ProtectionVolume->SetupAttachment(RootComponent); ProtectionVolume->SetBoxExtent(FVector(250.0f, 250.0f, 180.0f)); ProtectionVolume->SetCollisionProfileName(TEXT("OverlapAllDynamic")); + + PersistentActorComponent = CreateDefaultSubobject(TEXT("PersistentActorComponent")); + PersistentActorComponent->ActorTypeId = TEXT("primitive_shelter"); } diff --git a/Source/AgrarianGame/AgrarianShelterActor.h b/Source/AgrarianGame/AgrarianShelterActor.h index 2ded45a..371371a 100644 --- a/Source/AgrarianGame/AgrarianShelterActor.h +++ b/Source/AgrarianGame/AgrarianShelterActor.h @@ -7,6 +7,7 @@ #include "AgrarianShelterActor.generated.h" class UBoxComponent; +class UAgrarianPersistentActorComponent; class UStaticMeshComponent; UCLASS(Blueprintable) @@ -23,6 +24,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Shelter") TObjectPtr ProtectionVolume; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Shelter") + TObjectPtr PersistentActorComponent; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Shelter", meta = (ClampMin = "0", ClampMax = "1")) float WeatherProtection = 0.65f; };