Add campfire persistence state

This commit is contained in:
2026-05-17 19:09:55 -07:00
parent c60b975294
commit 7291c4844b
8 changed files with 171 additions and 2 deletions
+3 -1
View File
@@ -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 particle component placeholder that activates and hides with replicated fire
state, leaving final visual assets assignable later. state, leaving final visual assets assignable later.
- [x] Add replication. - [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. - [x] Connect fire to body temperature.
- [ ] Connect rain/weather to fire behavior. - [ ] Connect rain/weather to fire behavior.
+6
View File
@@ -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 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. 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 The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
current temperature, daily low/high, precipitation, wind, humidity, cloud cover, current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
+58
View File
@@ -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<UAgrarianPersistentActorComponent> PersistentActorComponent;",
"CapturePersistentState_Implementation",
"ApplyPersistentState_Implementation",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp": [
"PersistentActorComponent = CreateDefaultSubobject<UAgrarianPersistentActorComponent>(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()
+54
View File
@@ -3,6 +3,7 @@
#include "AgrarianCampfire.h" #include "AgrarianCampfire.h"
#include "AgrarianGameCharacter.h" #include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianSurvivalComponent.h" #include "AgrarianSurvivalComponent.h"
#include "Particles/ParticleSystemComponent.h" #include "Particles/ParticleSystemComponent.h"
#include "Components/PointLightComponent.h" #include "Components/PointLightComponent.h"
@@ -29,6 +30,9 @@ AAgrarianCampfire::AAgrarianCampfire()
SmokeEffect->bAutoActivate = false; SmokeEffect->bAutoActivate = false;
SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f)); SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f));
SmokeEffect->SetVisibility(false); SmokeEffect->SetVisibility(false);
PersistentActorComponent = CreateDefaultSubobject<UAgrarianPersistentActorComponent>(TEXT("PersistentActorComponent"));
PersistentActorComponent->ActorTypeId = TEXT("campfire");
} }
void AAgrarianCampfire::Tick(float DeltaSeconds) 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) void AAgrarianCampfire::AddFuel(float Seconds)
{ {
if (HasAuthority()) if (HasAuthority())
+8 -1
View File
@@ -5,14 +5,16 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "AgrarianInteractable.h" #include "AgrarianInteractable.h"
#include "AgrarianPersistentStateProvider.h"
#include "AgrarianCampfire.generated.h" #include "AgrarianCampfire.generated.h"
class UPointLightComponent; class UPointLightComponent;
class UParticleSystemComponent; class UParticleSystemComponent;
class UAgrarianPersistentActorComponent;
class UStaticMeshComponent; class UStaticMeshComponent;
UCLASS(Blueprintable) UCLASS(Blueprintable)
class AAgrarianCampfire : public AActor, public IAgrarianInteractable class AAgrarianCampfire : public AActor, public IAgrarianInteractable, public IAgrarianPersistentStateProvider
{ {
GENERATED_BODY() GENERATED_BODY()
@@ -31,6 +33,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Effects") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Effects")
TObjectPtr<UParticleSystemComponent> SmokeEffect; TObjectPtr<UParticleSystemComponent> SmokeEffect;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Persistence")
TObjectPtr<UAgrarianPersistentActorComponent> PersistentActorComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_FireState, Category = "Agrarian|Fire") UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_FireState, Category = "Agrarian|Fire")
bool bLit = false; bool bLit = false;
@@ -55,6 +60,8 @@ public:
virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) 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") UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire")
void AddFuel(float Seconds); void AddFuel(float Seconds);
@@ -2,6 +2,7 @@
#include "AgrarianGamePlayerController.h" #include "AgrarianGamePlayerController.h"
#include "AgrarianCampfire.h"
#include "AgrarianCraftingComponent.h" #include "AgrarianCraftingComponent.h"
#include "AgrarianGameCharacter.h" #include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
@@ -296,6 +297,7 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation()
} }
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass()); Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass());
Persistence->RegisterWorldActorClass(TEXT("campfire"), AAgrarianCampfire::StaticClass());
int32 RestoredPlayerCount = 0; int32 RestoredPlayerCount = 0;
int32 RestoredActorCount = 0; int32 RestoredActorCount = 0;
const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount); const bool bLoaded = Persistence->LoadCurrentWorld(RestoredPlayerCount, RestoredActorCount);
@@ -1,6 +1,7 @@
// Copyright Pacificao. All Rights Reserved. // Copyright Pacificao. All Rights Reserved.
#include "AgrarianPersistentActorComponent.h" #include "AgrarianPersistentActorComponent.h"
#include "AgrarianPersistentStateProvider.h"
UAgrarianPersistentActorComponent::UAgrarianPersistentActorComponent() UAgrarianPersistentActorComponent::UAgrarianPersistentActorComponent()
{ {
@@ -14,6 +15,13 @@ bool UAgrarianPersistentActorComponent::IsSaveable() const
FAgrarianSavedWorldActor UAgrarianPersistentActorComponent::CaptureSaveState() const FAgrarianSavedWorldActor UAgrarianPersistentActorComponent::CaptureSaveState() const
{ {
if (const AActor* Owner = GetOwner(); Owner && Owner->Implements<UAgrarianPersistentStateProvider>())
{
IAgrarianPersistentStateProvider::Execute_CapturePersistentState(
const_cast<AActor*>(Owner),
const_cast<UAgrarianPersistentActorComponent*>(this));
}
FAgrarianSavedWorldActor SavedActor; FAgrarianSavedWorldActor SavedActor;
SavedActor.ActorTypeId = ActorTypeId; SavedActor.ActorTypeId = ActorTypeId;
SavedActor.StringState = StringState; SavedActor.StringState = StringState;
@@ -41,4 +49,9 @@ void UAgrarianPersistentActorComponent::ApplySaveState(const FAgrarianSavedWorld
{ {
GetOwner()->SetActorTransform(SavedActor.Transform); GetOwner()->SetActorTransform(SavedActor.Transform);
} }
if (AActor* Owner = GetOwner(); Owner && Owner->Implements<UAgrarianPersistentStateProvider>())
{
IAgrarianPersistentStateProvider::Execute_ApplyPersistentState(Owner, this);
}
} }
@@ -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);
};