diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index e340425..3fdb74e 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -594,7 +594,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe particle component placeholder that activates and hides with replicated fire state, leaving final visual assets assignable later. - [x] Add replication. -- [ ] Add persistence. +- [x] Add persistence. Added campfire persistent actor support for transform, + lit state, fuel seconds, and cooking placeholder progress using the shared + world-actor persistence path. - [x] Connect fire to body temperature. - [ ] Connect rain/weather to fire behavior. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 60d9010..5dc7e1e 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -302,6 +302,12 @@ placeholder. It is attached above the fire, starts inactive, and follows the same replicated lit-state visual update path as fire light intensity so final smoke or ember assets can be assigned later without changing gameplay code. +Campfire persistence uses the shared `UAgrarianPersistentActorComponent` world +actor path. `AAgrarianCampfire` implements the persistence-state provider hook +to write lit state, remaining fuel, cooking enabled state, required cook time, +and cooking progress into numeric save state, then restores those values before +reapplying the fire visual state on load. + The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the current temperature, daily low/high, precipitation, wind, humidity, cloud cover, diff --git a/Scripts/verify_fire_persistence.py b/Scripts/verify_fire_persistence.py new file mode 100644 index 0000000..4a40115 --- /dev/null +++ b/Scripts/verify_fire_persistence.py @@ -0,0 +1,58 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +REQUIRED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianPersistentStateProvider.h": [ + "class IAgrarianPersistentStateProvider", + "void CapturePersistentState(UAgrarianPersistentActorComponent* PersistentComponent) const;", + "void ApplyPersistentState(UAgrarianPersistentActorComponent* PersistentComponent);", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianPersistentActorComponent.cpp": [ + "IAgrarianPersistentStateProvider::Execute_CapturePersistentState", + "IAgrarianPersistentStateProvider::Execute_ApplyPersistentState", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h": [ + "public IAgrarianPersistentStateProvider", + "TObjectPtr PersistentActorComponent;", + "CapturePersistentState_Implementation", + "ApplyPersistentState_Implementation", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp": [ + "PersistentActorComponent = CreateDefaultSubobject(TEXT(\"PersistentActorComponent\"));", + "PersistentActorComponent->ActorTypeId = TEXT(\"campfire\");", + "NumberState.Add(TEXT(\"lit\")", + "NumberState.Add(TEXT(\"fuel_seconds\")", + "NumberState.Add(TEXT(\"cooking_progress_seconds\")", + "void AAgrarianCampfire::ApplyPersistentState_Implementation", + "SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [ + "Persistence->RegisterWorldActorClass(TEXT(\"campfire\"), AAgrarianCampfire::StaticClass());", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "- [x] Add persistence.", + ], + ROOT / "Docs" / "TechnicalDesignDocument.md": [ + "Campfire persistence uses the shared `UAgrarianPersistentActorComponent` world", + ], +} + + +def main(): + missing = [] + for path, snippets in REQUIRED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}") + + if missing: + raise SystemExit("Campfire persistence verification failed:\n" + "\n".join(missing)) + + print("PASS: campfire persistence is implemented and documented.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianCampfire.cpp b/Source/AgrarianGame/AgrarianCampfire.cpp index cc62d7c..806223d 100644 --- a/Source/AgrarianGame/AgrarianCampfire.cpp +++ b/Source/AgrarianGame/AgrarianCampfire.cpp @@ -3,6 +3,7 @@ #include "AgrarianCampfire.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" +#include "AgrarianPersistentActorComponent.h" #include "AgrarianSurvivalComponent.h" #include "Particles/ParticleSystemComponent.h" #include "Components/PointLightComponent.h" @@ -29,6 +30,9 @@ AAgrarianCampfire::AAgrarianCampfire() SmokeEffect->bAutoActivate = false; SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f)); SmokeEffect->SetVisibility(false); + + PersistentActorComponent = CreateDefaultSubobject(TEXT("PersistentActorComponent")); + PersistentActorComponent->ActorTypeId = TEXT("campfire"); } void AAgrarianCampfire::Tick(float DeltaSeconds) @@ -86,6 +90,56 @@ void AAgrarianCampfire::Interact_Implementation(AAgrarianGameCharacter* Interact } } +void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) const +{ + if (!PersistentComponent) + { + return; + } + + PersistentComponent->NumberState.Add(TEXT("lit"), bLit ? 1.0f : 0.0f); + PersistentComponent->NumberState.Add(TEXT("fuel_seconds"), FuelSeconds); + PersistentComponent->NumberState.Add(TEXT("cooking_placeholder_enabled"), bCookingPlaceholderEnabled ? 1.0f : 0.0f); + PersistentComponent->NumberState.Add(TEXT("cooking_seconds_required"), CookingSecondsRequired); + PersistentComponent->NumberState.Add(TEXT("cooking_progress_seconds"), CookingProgressSeconds); +} + +void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) +{ + if (!HasAuthority() || !PersistentComponent) + { + return; + } + + const float* SavedFuelSeconds = PersistentComponent->NumberState.Find(TEXT("fuel_seconds")); + const float* SavedCookingEnabled = PersistentComponent->NumberState.Find(TEXT("cooking_placeholder_enabled")); + const float* SavedCookingRequired = PersistentComponent->NumberState.Find(TEXT("cooking_seconds_required")); + const float* SavedCookingProgress = PersistentComponent->NumberState.Find(TEXT("cooking_progress_seconds")); + const float* SavedLit = PersistentComponent->NumberState.Find(TEXT("lit")); + + if (SavedFuelSeconds) + { + FuelSeconds = FMath::Max(0.0f, *SavedFuelSeconds); + } + + if (SavedCookingEnabled) + { + bCookingPlaceholderEnabled = *SavedCookingEnabled > 0.5f; + } + + if (SavedCookingRequired) + { + CookingSecondsRequired = FMath::Max(0.0f, *SavedCookingRequired); + } + + if (SavedCookingProgress) + { + CookingProgressSeconds = FMath::Clamp(*SavedCookingProgress, 0.0f, CookingSecondsRequired); + } + + SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f); +} + void AAgrarianCampfire::AddFuel(float Seconds) { if (HasAuthority()) diff --git a/Source/AgrarianGame/AgrarianCampfire.h b/Source/AgrarianGame/AgrarianCampfire.h index 34e3c57..7145fcf 100644 --- a/Source/AgrarianGame/AgrarianCampfire.h +++ b/Source/AgrarianGame/AgrarianCampfire.h @@ -5,14 +5,16 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "AgrarianInteractable.h" +#include "AgrarianPersistentStateProvider.h" #include "AgrarianCampfire.generated.h" class UPointLightComponent; class UParticleSystemComponent; +class UAgrarianPersistentActorComponent; class UStaticMeshComponent; UCLASS(Blueprintable) -class AAgrarianCampfire : public AActor, public IAgrarianInteractable +class AAgrarianCampfire : public AActor, public IAgrarianInteractable, public IAgrarianPersistentStateProvider { GENERATED_BODY() @@ -31,6 +33,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Effects") TObjectPtr SmokeEffect; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Persistence") + TObjectPtr PersistentActorComponent; + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_FireState, Category = "Agrarian|Fire") bool bLit = false; @@ -55,6 +60,8 @@ public: virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; + virtual void CapturePersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) const override; + virtual void ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) override; UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire") void AddFuel(float Seconds); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index bc19540..5a08c32 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -2,6 +2,7 @@ #include "AgrarianGamePlayerController.h" +#include "AgrarianCampfire.h" #include "AgrarianCraftingComponent.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" @@ -296,6 +297,7 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation() } Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); + Persistence->RegisterWorldActorClass(TEXT("campfire"), AAgrarianCampfire::StaticClass()); int32 RestoredPlayerCount = 0; int32 RestoredActorCount = 0; const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount); diff --git a/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp b/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp index 345e930..2bde0ac 100644 --- a/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp +++ b/Source/AgrarianGame/AgrarianPersistentActorComponent.cpp @@ -1,6 +1,7 @@ // Copyright Pacificao. All Rights Reserved. #include "AgrarianPersistentActorComponent.h" +#include "AgrarianPersistentStateProvider.h" UAgrarianPersistentActorComponent::UAgrarianPersistentActorComponent() { @@ -14,6 +15,13 @@ bool UAgrarianPersistentActorComponent::IsSaveable() const FAgrarianSavedWorldActor UAgrarianPersistentActorComponent::CaptureSaveState() const { + if (const AActor* Owner = GetOwner(); Owner && Owner->Implements()) + { + IAgrarianPersistentStateProvider::Execute_CapturePersistentState( + const_cast(Owner), + const_cast(this)); + } + FAgrarianSavedWorldActor SavedActor; SavedActor.ActorTypeId = ActorTypeId; SavedActor.StringState = StringState; @@ -41,4 +49,9 @@ void UAgrarianPersistentActorComponent::ApplySaveState(const FAgrarianSavedWorld { GetOwner()->SetActorTransform(SavedActor.Transform); } + + if (AActor* Owner = GetOwner(); Owner && Owner->Implements()) + { + IAgrarianPersistentStateProvider::Execute_ApplyPersistentState(Owner, this); + } } diff --git a/Source/AgrarianGame/AgrarianPersistentStateProvider.h b/Source/AgrarianGame/AgrarianPersistentStateProvider.h new file mode 100644 index 0000000..50938b6 --- /dev/null +++ b/Source/AgrarianGame/AgrarianPersistentStateProvider.h @@ -0,0 +1,27 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "AgrarianPersistentStateProvider.generated.h" + +class UAgrarianPersistentActorComponent; + +UINTERFACE(BlueprintType) +class UAgrarianPersistentStateProvider : public UInterface +{ + GENERATED_BODY() +}; + +class IAgrarianPersistentStateProvider +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent, Category = "Agrarian|Persistence") + void CapturePersistentState(UAgrarianPersistentActorComponent* PersistentComponent) const; + + UFUNCTION(BlueprintNativeEvent, Category = "Agrarian|Persistence") + void ApplyPersistentState(UAgrarianPersistentActorComponent* PersistentComponent); +};