Watering system implement
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include "SeedBedActor.h"
|
#include "SeedBedActor.h"
|
||||||
|
|
||||||
#include "EleriGameState.h"
|
#include "EleriGameState.h"
|
||||||
|
#include "TimeOfDayManager.h"
|
||||||
#include "Components/DecalComponent.h"
|
#include "Components/DecalComponent.h"
|
||||||
#include "Components/SceneComponent.h"
|
#include "Components/SceneComponent.h"
|
||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
@@ -90,6 +91,12 @@ namespace
|
|||||||
return IsValid(Slot.SlotData.ItemDataAsset) || Slot.SlotData.ItemAssetID.IsValid();
|
return IsValid(Slot.SlotData.ItemDataAsset) || Slot.SlotData.ItemAssetID.IsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A dead plant keeps its id until the slot is cleared, but it neither grows nor blocks the slot anymore */
|
||||||
|
bool SlotHasLivingPlant(const FSeedBedSeedSlot& Slot)
|
||||||
|
{
|
||||||
|
return SlotHasPlant(Slot) && !Slot.SlotData.bDied;
|
||||||
|
}
|
||||||
|
|
||||||
/** Growth stages are set up by hand, so a zeroed scale should never make a plant invisible */
|
/** Growth stages are set up by hand, so a zeroed scale should never make a plant invisible */
|
||||||
FVector SanitizeScale(const FVector& Scale)
|
FVector SanitizeScale(const FVector& Scale)
|
||||||
{
|
{
|
||||||
@@ -126,18 +133,40 @@ void ASeedBedActor::BeginPlay()
|
|||||||
|
|
||||||
ClearSeedBedGrid();
|
ClearSeedBedGrid();
|
||||||
GenerateSeedBedGrid();
|
GenerateSeedBedGrid();
|
||||||
|
|
||||||
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
|
// The bed is streamed in and out together with its world partition cell, so it tracks the days itself
|
||||||
ensure(GameState);
|
BindToTimeOfDay();
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
FSeedBedSaveData SaveData;
|
|
||||||
if (GameState->GetSeedData(SeedBedIndex, SaveData))
|
#if WITH_EDITOR
|
||||||
|
// Beds are addressed by SeedBedIndex in the save data, two beds sharing one would overwrite each other
|
||||||
|
TArray<AActor*> OtherSeedBeds;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OtherSeedBeds);
|
||||||
|
for (AActor* OtherSeedBed : OtherSeedBeds)
|
||||||
{
|
{
|
||||||
PropagateSeedSaveData(SaveData);
|
const ASeedBedActor* OtherBed = Cast<ASeedBedActor>(OtherSeedBed);
|
||||||
|
if (OtherBed && OtherBed != this && OtherBed->SeedBedIndex == SeedBedIndex)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s shares SeedBedIndex %d with %s, their save data overwrite each other"),
|
||||||
|
*GetName(), SeedBedIndex, *OtherBed->GetName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
#endif
|
||||||
|
|
||||||
|
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
|
||||||
{
|
{
|
||||||
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
FSeedBedSaveData SaveData;
|
||||||
|
if (GameState->GetSeedData(SeedBedIndex, SaveData))
|
||||||
|
{
|
||||||
|
// The bed was in the world before, so its plants have to catch up on the time that passed without it
|
||||||
|
PropagateSeedSaveData(SaveData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastUpdateTime = LastKnownTimeOfDay;
|
||||||
|
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bed can be built in the editor, but slots added through blueprints need their visuals as well
|
// The bed can be built in the editor, but slots added through blueprints need their visuals as well
|
||||||
@@ -146,9 +175,19 @@ void ASeedBedActor::BeginPlay()
|
|||||||
|
|
||||||
void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
{
|
{
|
||||||
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
|
// This is the moment the bed stops receiving broadcasts, so the resume math starts from the time of the day here
|
||||||
ensure(GameState);
|
if (IsValid(TimeOfDayManager))
|
||||||
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
{
|
||||||
|
TimeOfDayManager->OnDayPassed.RemoveDynamic(this, &ASeedBedActor::HandleDayPassed);
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
|
LastUpdateTime = LastKnownTimeOfDay;
|
||||||
|
|
||||||
|
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
|
||||||
|
{
|
||||||
|
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
||||||
|
}
|
||||||
|
|
||||||
Super::EndPlay(EndPlayReason);
|
Super::EndPlay(EndPlayReason);
|
||||||
}
|
}
|
||||||
@@ -158,6 +197,166 @@ void ASeedBedActor::Tick(float DeltaTime)
|
|||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
}
|
}
|
||||||
//------------------------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------------------------
|
||||||
|
// TIME
|
||||||
|
//------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void ASeedBedActor::BindToTimeOfDay()
|
||||||
|
{
|
||||||
|
TimeOfDayManager = Cast<ATimeOfDayManager>(UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
||||||
|
if (!IsValid(TimeOfDayManager))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUniqueDynamic only ever leaves one binding of the bed on the delegate, even after a reload
|
||||||
|
TimeOfDayManager->OnDayPassed.AddUniqueDynamic(this, &ASeedBedActor::HandleDayPassed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::CacheCurrentTimeOfDay()
|
||||||
|
{
|
||||||
|
if (IsValid(TimeOfDayManager))
|
||||||
|
{
|
||||||
|
LastKnownTimeOfDay = TimeOfDayManager->GetTimeOfDay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::HandleDayPassed()
|
||||||
|
{
|
||||||
|
AdvanceSeedBedByDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::AdvanceSeedBedByDays(int32 Days)
|
||||||
|
{
|
||||||
|
if (Days <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
|
const FTimeOfDayData Now = LastKnownTimeOfDay;
|
||||||
|
|
||||||
|
// Plants that die are reported after the map has been walked, listeners are free to change the bed
|
||||||
|
TArray<FIntVector2> DiedSlots;
|
||||||
|
|
||||||
|
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||||
|
{
|
||||||
|
const bool bWasAlive = SlotHasLivingPlant(SlotPair.Value);
|
||||||
|
ApplyDayPassedToSlot(SlotPair.Value, Now);
|
||||||
|
|
||||||
|
if (bWasAlive && !SlotHasLivingPlant(SlotPair.Value))
|
||||||
|
{
|
||||||
|
DiedSlots.Add(SlotPair.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastUpdateTime = Now;
|
||||||
|
RefreshAllSlotVisuals();
|
||||||
|
|
||||||
|
for (const FIntVector2& SlotCoordinate : DiedSlots)
|
||||||
|
{
|
||||||
|
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now)
|
||||||
|
{
|
||||||
|
FSeedBedSeedSlotData& SlotData = Slot.SlotData;
|
||||||
|
|
||||||
|
// The moisture of the soil only lasts for the day it was watered on
|
||||||
|
const bool bWasWatered = SlotData.bWateredToday;
|
||||||
|
SlotData.bWateredToday = false;
|
||||||
|
|
||||||
|
if (!SlotHasLivingPlant(Slot))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plant that was dry for too long never grows again, so the death check comes before the growth
|
||||||
|
const int32 DaysSinceWatering = SlotData.LastWateredTime.IsValidTime()
|
||||||
|
? Now.ToAbsoluteDays() - SlotData.LastWateredTime.ToAbsoluteDays()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (DaysSinceWatering >= FMath::Max(1, DaysWithoutWaterToDie))
|
||||||
|
{
|
||||||
|
SlotData.bDied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a watered plant grows, and it never grows past its own grow time
|
||||||
|
if (bWasWatered)
|
||||||
|
{
|
||||||
|
SlotData.SeedAge = FMath::Min(SlotData.SeedAge + 1, FMath::Max(0, SlotData.GrowTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::ApplyOfflineProgression()
|
||||||
|
{
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
|
|
||||||
|
// Without a time of day actor there is nothing to compare against, the bed stays exactly as it was saved
|
||||||
|
if (!LastKnownTimeOfDay.IsValidTime())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A save from before the stamps existed cannot be measured, the countdown simply starts now
|
||||||
|
if (!LastUpdateTime.IsValidTime())
|
||||||
|
{
|
||||||
|
LastUpdateTime = LastKnownTimeOfDay;
|
||||||
|
|
||||||
|
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||||
|
{
|
||||||
|
if (SlotHasLivingPlant(SlotPair.Value) && !SlotPair.Value.SlotData.LastWateredTime.IsValidTime())
|
||||||
|
{
|
||||||
|
SlotPair.Value.SlotData.LastWateredTime = LastKnownTimeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 DaysPassed = LastKnownTimeOfDay.ToAbsoluteDays() - LastUpdateTime.ToAbsoluteDays();
|
||||||
|
if (DaysPassed > 0)
|
||||||
|
{
|
||||||
|
// Growth is linear and nothing can be watered while the bed is gone, so a single pass is enough
|
||||||
|
AdvanceSeedBedByDays(DaysPassed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastUpdateTime = LastKnownTimeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::WaterSeedInSlot(FIntVector2 SlotCoordinate)
|
||||||
|
{
|
||||||
|
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
|
||||||
|
if (!Slot || !SlotHasLivingPlant(*Slot))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
|
|
||||||
|
// Wetness is a per day state, the stamp is what the death check measures the days since
|
||||||
|
Slot->SlotData.bWateredToday = true;
|
||||||
|
Slot->SlotData.WateredTimes++;
|
||||||
|
Slot->SlotData.LastWateredTime = LastKnownTimeOfDay;
|
||||||
|
|
||||||
|
RefreshSlotVisual(SlotCoordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASeedBedActor::WaterAllSeedSlots()
|
||||||
|
{
|
||||||
|
// The slots are collected first, a listener of the refresh is free to change the map while we water
|
||||||
|
TArray<FIntVector2> SlotCoordinates;
|
||||||
|
SeedBedSeedSlotsMap.GetKeys(SlotCoordinates);
|
||||||
|
|
||||||
|
for (const FIntVector2& SlotCoordinate : SlotCoordinates)
|
||||||
|
{
|
||||||
|
WaterSeedInSlot(SlotCoordinate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//------------------------------------------------------------------------------------------------
|
||||||
// GRID GENERATION
|
// GRID GENERATION
|
||||||
//------------------------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -233,7 +432,10 @@ FVector ASeedBedActor::GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const
|
|||||||
// The grid is centered on the actor, so the coordinates are shifted by half a cell
|
// The grid is centered on the actor, so the coordinates are shifted by half a cell
|
||||||
if (const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate))
|
if (const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate))
|
||||||
{
|
{
|
||||||
return Slot->SeedBedDecal->GetRelativeLocation();
|
if (IsValid(Slot->SeedBedDecal))
|
||||||
|
{
|
||||||
|
return Slot->SeedBedDecal->GetRelativeLocation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return FVector::ZeroVector;
|
return FVector::ZeroVector;
|
||||||
@@ -243,6 +445,7 @@ FSeedBedSaveData ASeedBedActor::PrepareSeedSaveData()
|
|||||||
{
|
{
|
||||||
FSeedBedSaveData SaveData;
|
FSeedBedSaveData SaveData;
|
||||||
SaveData.Index = SeedBedIndex;
|
SaveData.Index = SeedBedIndex;
|
||||||
|
SaveData.LastUpdateTime = LastUpdateTime;
|
||||||
for (const auto& Slot : SeedBedSeedSlotsMap)
|
for (const auto& Slot : SeedBedSeedSlotsMap)
|
||||||
{
|
{
|
||||||
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
|
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
|
||||||
@@ -257,6 +460,7 @@ void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
|
|||||||
ensure(ItemDatabase);
|
ensure(ItemDatabase);
|
||||||
|
|
||||||
SeedBedIndex = SeedSaveData.Index;
|
SeedBedIndex = SeedSaveData.Index;
|
||||||
|
LastUpdateTime = SeedSaveData.LastUpdateTime;
|
||||||
for (const auto& SavedSlot : SeedSaveData.SlotData)
|
for (const auto& SavedSlot : SeedSaveData.SlotData)
|
||||||
{
|
{
|
||||||
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key))
|
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key))
|
||||||
@@ -266,6 +470,9 @@ void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
|
|||||||
SeedSlot->SlotData.ItemDataAsset = ItemDatabase->GetDataAssetFromId(SeedSlot->SlotData.ItemAssetID);
|
SeedSlot->SlotData.ItemDataAsset = ItemDatabase->GetDataAssetFromId(SeedSlot->SlotData.ItemAssetID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Time kept running while the bed was not in the world, every plant is brought up to date in one pass
|
||||||
|
ApplyOfflineProgression();
|
||||||
|
|
||||||
RefreshAllSlotVisuals();
|
RefreshAllSlotVisuals();
|
||||||
}
|
}
|
||||||
@@ -293,6 +500,17 @@ FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRe
|
|||||||
Offset.Y + Y + SeedBedDecalSize.Y,
|
Offset.Y + Y + SeedBedDecalSize.Y,
|
||||||
0.f));
|
0.f));
|
||||||
|
|
||||||
|
// The plant mesh of a slot lives as long as the slot does, the visuals only ever show or hide it
|
||||||
|
if (!IsValid(Slot.PlantStaticMesh))
|
||||||
|
{
|
||||||
|
Slot.PlantStaticMesh = CreateSlotMeshComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Plant")));
|
||||||
|
if (IsValid(Slot.PlantStaticMesh))
|
||||||
|
{
|
||||||
|
Slot.PlantStaticMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Slot.PlantStaticMesh->SetVisibility(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApplySlotVisuals(SlotCoordinate, Slot);
|
ApplySlotVisuals(SlotCoordinate, Slot);
|
||||||
|
|
||||||
return Slot;
|
return Slot;
|
||||||
@@ -309,37 +527,52 @@ void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlo
|
|||||||
if (IsValid(Slot.SeedBedDecal))
|
if (IsValid(Slot.SeedBedDecal))
|
||||||
{
|
{
|
||||||
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid());
|
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid());
|
||||||
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday);
|
|
||||||
|
if (IsValid(Slot.SeedBedDynamicMat))
|
||||||
|
{
|
||||||
|
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday ? 1.f : 0.f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PLANT ----------------------------------------------------------------------------------------
|
// PLANT ----------------------------------------------------------------------------------------
|
||||||
const bool bHasPlant = SlotHasPlant(Slot) && !Slot.SlotData.bDied;
|
const bool bHasPlant = SlotHasLivingPlant(Slot);
|
||||||
|
|
||||||
if (IsValid(Slot.PlantStaticMesh))
|
if (!IsValid(Slot.PlantStaticMesh))
|
||||||
{
|
{
|
||||||
UStaticMesh* PlantMesh = nullptr;
|
return;
|
||||||
FVector PlantOffset = FVector::ZeroVector;
|
|
||||||
FVector PlantScale = FVector::OneVector;
|
|
||||||
|
|
||||||
if (bHasPlant)
|
|
||||||
{
|
|
||||||
const int32 StageIndex = ResolveGrowthStageIndex(Slot);
|
|
||||||
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
|
|
||||||
if (IsValid(SeedData) && SeedData->SeedStages.IsValidIndex(StageIndex))
|
|
||||||
{
|
|
||||||
const FSeedStage& Stage = SeedData->SeedStages[StageIndex];
|
|
||||||
PlantMesh = ResolveSoftObject(Stage.Mesh);
|
|
||||||
PlantOffset = Stage.MeshOffset;
|
|
||||||
PlantScale = SanitizeScale(Stage.MeshScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A seed without stage meshes still has to show up, the fallback keeps the plant visible
|
|
||||||
if (!PlantMesh)
|
|
||||||
{
|
|
||||||
PlantMesh = ResolveSoftObject(DefaultPlantMesh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bHasPlant)
|
||||||
|
{
|
||||||
|
// A plant that died or was harvested keeps its mesh, the slot is only emptied when it is cleared for real
|
||||||
|
Slot.PlantStaticMesh->SetVisibility(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMesh* PlantMesh = nullptr;
|
||||||
|
FVector PlantOffset = FVector::ZeroVector;
|
||||||
|
FVector PlantScale = FVector::OneVector;
|
||||||
|
|
||||||
|
const int32 StageIndex = ResolveGrowthStageIndex(Slot);
|
||||||
|
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
|
||||||
|
if (IsValid(SeedData) && SeedData->SeedStages.IsValidIndex(StageIndex))
|
||||||
|
{
|
||||||
|
const FSeedStage& Stage = SeedData->SeedStages[StageIndex];
|
||||||
|
PlantMesh = ResolveSoftObject(Stage.Mesh);
|
||||||
|
PlantOffset = Stage.MeshOffset;
|
||||||
|
PlantScale = SanitizeScale(Stage.MeshScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A seed without stage meshes still has to show up, the fallback keeps the plant visible
|
||||||
|
if (!PlantMesh)
|
||||||
|
{
|
||||||
|
PlantMesh = ResolveSoftObject(DefaultPlantMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot.PlantStaticMesh->SetStaticMesh(PlantMesh);
|
||||||
|
Slot.PlantStaticMesh->SetRelativeLocation(SlotLocation + PlantOffset);
|
||||||
|
Slot.PlantStaticMesh->SetRelativeScale3D(PlantScale);
|
||||||
|
Slot.PlantStaticMesh->SetVisibility(PlantMesh != nullptr);
|
||||||
}
|
}
|
||||||
void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
|
void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
|
||||||
{
|
{
|
||||||
@@ -351,7 +584,7 @@ void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
|
|||||||
|
|
||||||
ApplySlotVisuals(SlotCoordinate, *Slot);
|
ApplySlotVisuals(SlotCoordinate, *Slot);
|
||||||
|
|
||||||
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasPlant(*Slot));
|
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasLivingPlant(*Slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ASeedBedActor::RefreshAllSlotVisuals()
|
void ASeedBedActor::RefreshAllSlotVisuals()
|
||||||
@@ -397,7 +630,8 @@ bool ASeedBedActor::IsSlotFree(FIntVector2 SlotCoordinate) const
|
|||||||
{
|
{
|
||||||
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
|
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
|
||||||
|
|
||||||
return Slot != nullptr && !SlotHasPlant(*Slot);
|
// A dead plant still holds its mesh, but the slot can be planted again
|
||||||
|
return Slot != nullptr && !SlotHasLivingPlant(*Slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 ASeedBedActor::GetFreeSlotCount() const
|
int32 ASeedBedActor::GetFreeSlotCount() const
|
||||||
@@ -406,7 +640,7 @@ int32 ASeedBedActor::GetFreeSlotCount() const
|
|||||||
|
|
||||||
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||||
{
|
{
|
||||||
if (!SlotHasPlant(SlotPair.Value))
|
if (!SlotHasLivingPlant(SlotPair.Value))
|
||||||
{
|
{
|
||||||
++FreeSlotCount;
|
++FreeSlotCount;
|
||||||
}
|
}
|
||||||
@@ -421,7 +655,7 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
|
|||||||
|
|
||||||
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||||
{
|
{
|
||||||
if (SlotHasPlant(SlotPair.Value))
|
if (!SlotHasLivingPlant(SlotPair.Value))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -498,6 +732,7 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A fresh plant starts at day zero, everything that is stored about it is the state of the new seed
|
// A fresh plant starts at day zero, everything that is stored about it is the state of the new seed
|
||||||
|
CacheCurrentTimeOfDay();
|
||||||
Slot->SlotData.ItemDataAsset = SeedData;
|
Slot->SlotData.ItemDataAsset = SeedData;
|
||||||
Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId();
|
Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId();
|
||||||
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
|
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
|
||||||
@@ -507,8 +742,14 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
|
|||||||
Slot->SlotData.bWateredToday = false;
|
Slot->SlotData.bWateredToday = false;
|
||||||
Slot->SlotData.bDied = false;
|
Slot->SlotData.bDied = false;
|
||||||
Slot->SlotData.bHighQuality = bHighQuality;
|
Slot->SlotData.bHighQuality = bHighQuality;
|
||||||
|
|
||||||
Slot->SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), 0.f);
|
// A plant that is never watered has to dry out from the day it was planted on
|
||||||
|
Slot->SlotData.LastWateredTime = LastKnownTimeOfDay;
|
||||||
|
|
||||||
|
if (IsValid(Slot->SeedBedDynamicMat))
|
||||||
|
{
|
||||||
|
Slot->SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), 0.f);
|
||||||
|
}
|
||||||
|
|
||||||
RefreshSlotVisual(SlotCoordinate);
|
RefreshSlotVisual(SlotCoordinate);
|
||||||
|
|
||||||
@@ -532,6 +773,7 @@ void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePl
|
|||||||
Slot->SlotData.bWateredToday = false;
|
Slot->SlotData.bWateredToday = false;
|
||||||
Slot->SlotData.bDied = false;
|
Slot->SlotData.bDied = false;
|
||||||
Slot->SlotData.bHighQuality = false;
|
Slot->SlotData.bHighQuality = false;
|
||||||
|
Slot->SlotData.LastWateredTime = FTimeOfDayData();
|
||||||
|
|
||||||
if (bRemovePlantMesh)
|
if (bRemovePlantMesh)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "GameFramework/Actor.h"
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "ProjectEleri/System/TimeOfDayStruct.h"
|
||||||
#include "SeedBedActor.generated.h"
|
#include "SeedBedActor.generated.h"
|
||||||
|
|
||||||
class ATimeOfDayManager;
|
class ATimeOfDayManager;
|
||||||
class UDecalComponent;
|
class UDecalComponent;
|
||||||
|
class UMaterialInstanceDynamic;
|
||||||
class UMaterialInterface;
|
class UMaterialInterface;
|
||||||
class UStaticMesh;
|
class UStaticMesh;
|
||||||
class UStaticMeshComponent;
|
class UStaticMeshComponent;
|
||||||
@@ -47,6 +49,10 @@ struct FSeedBedSeedSlotData
|
|||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
bool bHighQuality = false;
|
bool bHighQuality = false;
|
||||||
|
|
||||||
|
/** In game time of the last watering, the death check measures the days that passed since then */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
|
FTimeOfDayData LastWateredTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
USTRUCT(BlueprintType)
|
USTRUCT(BlueprintType)
|
||||||
@@ -77,6 +83,10 @@ struct FSeedBedSaveData
|
|||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||||
TMap<FIntVector2, FSeedBedSeedSlotData> SlotData;
|
TMap<FIntVector2, FSeedBedSeedSlotData> SlotData;
|
||||||
|
|
||||||
|
/** In game time the bed last applied growth to its slots, the resume math starts from this stamp */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
|
FTimeOfDayData LastUpdateTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
@@ -168,6 +178,14 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||||
void ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh = true);
|
void ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh = true);
|
||||||
|
|
||||||
|
/** Waters a single slot: refills the soil and restarts the countdown the death check measures */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||||
|
void WaterSeedInSlot(FIntVector2 SlotCoordinate);
|
||||||
|
|
||||||
|
/** Waters every planted slot of the bed */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||||
|
void WaterAllSeedSlots();
|
||||||
|
|
||||||
/** Finds the free slot that is closest to the given world location */
|
/** Finds the free slot that is closest to the given world location */
|
||||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||||
bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const;
|
bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const;
|
||||||
@@ -185,6 +203,18 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed")
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed")
|
||||||
bool HasFreeSlot() const { return GetFreeSlotCount() > 0; }
|
bool HasFreeSlot() const { return GetFreeSlotCount() > 0; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------------------------
|
||||||
|
// TIME
|
||||||
|
//------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** The in game time the bed last applied growth for, the stamp the resume math continues from */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed|Time")
|
||||||
|
FTimeOfDayData GetLastUpdateTime() const { return LastUpdateTime; }
|
||||||
|
|
||||||
|
/** Applies the given amount of in game days to every plant of the bed, used by the day tick and the resume catch up */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Seed Bed|Time")
|
||||||
|
void AdvanceSeedBedByDays(int32 Days);
|
||||||
|
|
||||||
//------------------------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------------------------
|
||||||
// VISUALS
|
// VISUALS
|
||||||
//------------------------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------------------------
|
||||||
@@ -228,6 +258,22 @@ protected:
|
|||||||
|
|
||||||
FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const;
|
FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const;
|
||||||
|
|
||||||
|
/** Subscribes to the time of day actor, the bed ages its plants on every day that passes */
|
||||||
|
void BindToTimeOfDay();
|
||||||
|
|
||||||
|
/** Applies one in game day of growth, moisture handling and death checks to a single slot */
|
||||||
|
void ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now);
|
||||||
|
|
||||||
|
/** Catches the plants up on the in game time that passed while the bed was not in the world */
|
||||||
|
void ApplyOfflineProgression();
|
||||||
|
|
||||||
|
/** Stores the current in game time, the manager may already be gone when the bed is torn down */
|
||||||
|
void CacheCurrentTimeOfDay();
|
||||||
|
|
||||||
|
/** Day tick of the time of day actor */
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleDayPassed();
|
||||||
|
|
||||||
|
|
||||||
/** Whether the highlight decals of the bed are currently shown */
|
/** Whether the highlight decals of the bed are currently shown */
|
||||||
bool bPlantingHighlightActive = false;
|
bool bPlantingHighlightActive = false;
|
||||||
@@ -238,7 +284,19 @@ protected:
|
|||||||
/** Decal used to highlight this slot when the player is looking for a place to plant a seed */
|
/** Decal used to highlight this slot when the player is looking for a place to plant a seed */
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||||
UDecalComponent* HighlightDecal = nullptr;
|
UDecalComponent* HighlightDecal = nullptr;
|
||||||
|
|
||||||
|
/** Days a plant survives without being watered before it dies */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Time")
|
||||||
|
int32 DaysWithoutWaterToDie = 2;
|
||||||
|
|
||||||
|
/** In game time the bed last applied growth for, written on every day and again when the bed leaves the world */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
FTimeOfDayData LastUpdateTime;
|
||||||
|
|
||||||
|
/** Copy of the last time the manager reported, used when the bed is torn down after the manager is gone */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
FTimeOfDayData LastKnownTimeOfDay;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
ATimeOfDayManager* TimeOfDayManager = nullptr;
|
ATimeOfDayManager* TimeOfDayManager = nullptr;
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,41 @@ void AEleriGameState::PropagateAllSeedData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AEleriGameState::WaterAllSeedBeds()
|
||||||
|
{
|
||||||
|
const ATimeOfDayManager* TimeOfDay = Cast<ATimeOfDayManager>(
|
||||||
|
UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
||||||
|
if (!TimeOfDay) return;
|
||||||
|
|
||||||
|
TArray<AActor*> OutActors;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
|
||||||
|
|
||||||
|
TArray<ASeedBedActor*> LoadedSeedBeds;
|
||||||
|
LoadedSeedBeds.Reserve(OutActors.Num());
|
||||||
|
|
||||||
|
// Only the cached data also covers the beds that are streamed out, so it is the source of truth.
|
||||||
|
// Loaded beds know more than the cache does, so they are cached first, exactly like the save does it.
|
||||||
|
for (AActor* Actor : OutActors)
|
||||||
|
{
|
||||||
|
ASeedBedActor* SeedBedActor = Cast<ASeedBedActor>(Actor);
|
||||||
|
if (!SeedBedActor) continue;
|
||||||
|
|
||||||
|
LoadedSeedBeds.Add(SeedBedActor);
|
||||||
|
CacheSeedData(SeedBedActor->SeedBedIndex, SeedBedActor->PrepareSeedSaveData());
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveData.WaterAllPlantedSeeds(TimeOfDay->GetTimeOfDay());
|
||||||
|
|
||||||
|
// The loaded beds adopt the watered state right away, the others pick it up when they are streamed in
|
||||||
|
for (ASeedBedActor* SeedBedActor : LoadedSeedBeds)
|
||||||
|
{
|
||||||
|
if (const FSeedBedSaveData* SeedBedData = SaveData.SeedBedData.Find(SeedBedActor->SeedBedIndex))
|
||||||
|
{
|
||||||
|
SeedBedActor->PropagateSeedSaveData(*SeedBedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
|
void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
|
||||||
{
|
{
|
||||||
check(InSaveData);
|
check(InSaveData);
|
||||||
@@ -248,6 +283,7 @@ bool AEleriGameState::GetSeedData(int32 Index, FSeedBedSaveData& OutData)
|
|||||||
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
|
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
|
||||||
{
|
{
|
||||||
OutData = *FoundData;
|
OutData = *FoundData;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -53,6 +53,27 @@ void FSaveData::SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSave
|
|||||||
SeedBedData.Add(Index, SeedSaveData);
|
SeedBedData.Add(Index, SeedSaveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FSaveData::WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime)
|
||||||
|
{
|
||||||
|
for (TPair<int32, FSeedBedSaveData>& SeedBedPair : SeedBedData)
|
||||||
|
{
|
||||||
|
for (TPair<FIntVector2, FSeedBedSeedSlotData>& SlotPair : SeedBedPair.Value.SlotData)
|
||||||
|
{
|
||||||
|
FSeedBedSeedSlotData& SlotData = SlotPair.Value;
|
||||||
|
|
||||||
|
// Rain refills the soil of living plants, it neither plants nor revives anything
|
||||||
|
if (!SlotData.ItemAssetID.IsValid() || SlotData.bDied)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotData.bWateredToday = true;
|
||||||
|
SlotData.WateredTimes++;
|
||||||
|
SlotData.LastWateredTime = WateredTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors)
|
void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors)
|
||||||
{
|
{
|
||||||
PlacedActors = InPlacedActors;
|
PlacedActors = InPlacedActors;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "Kismet/KismetMaterialLibrary.h"
|
#include "Kismet/KismetMaterialLibrary.h"
|
||||||
#include "Materials/MaterialParameterCollection.h"
|
#include "Materials/MaterialParameterCollection.h"
|
||||||
#include "ProjectEleri/Data/WeatherPresetDataAsset.h"
|
#include "ProjectEleri/Data/WeatherPresetDataAsset.h"
|
||||||
|
#include "EleriGameState.h"
|
||||||
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
||||||
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
|
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
|
||||||
|
|
||||||
@@ -120,6 +121,15 @@ void ATimeOfDayManager::Tick(float DeltaTime)
|
|||||||
CurrentWeaterFadeAlpha = 1.f;
|
CurrentWeaterFadeAlpha = 1.f;
|
||||||
QueuedWeatherPreset = GetWeatherDistributionValue();
|
QueuedWeatherPreset = GetWeatherDistributionValue();
|
||||||
K2_WeatherFadeStart(QueuedWeatherPreset->bIsRain);
|
K2_WeatherFadeStart(QueuedWeatherPreset->bIsRain);
|
||||||
|
|
||||||
|
if (QueuedWeatherPreset->bIsRain)
|
||||||
|
{
|
||||||
|
// Rain refills the soil of every plant, the game state also reaches the beds that are streamed out
|
||||||
|
if (AEleriGameState* GameState = AEleriGameState::Get(this))
|
||||||
|
{
|
||||||
|
GameState->WaterAllSeedBeds();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ public:
|
|||||||
void CacheSeedData(int32 Index, const FSeedBedSaveData& Data);
|
void CacheSeedData(int32 Index, const FSeedBedSaveData& Data);
|
||||||
bool GetSeedData(int32 Index, FSeedBedSaveData& OutData);
|
bool GetSeedData(int32 Index, FSeedBedSaveData& OutData);
|
||||||
|
|
||||||
|
/** Waters every planted seed of every seed bed, loaded or not. Called when it starts raining. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Game State")
|
||||||
|
void WaterAllSeedBeds();
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FOnPropagateSaveDataComplete OnPropagateSaveDataComplete;
|
FOnPropagateSaveDataComplete OnPropagateSaveDataComplete;
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ struct FSaveData {
|
|||||||
void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression);
|
void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression);
|
||||||
void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory);
|
void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory);
|
||||||
void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData);
|
void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData);
|
||||||
|
/** Refills the soil of every planted seed of the cached seed beds, used by rain */
|
||||||
|
void WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime);
|
||||||
void SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors);
|
void SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors);
|
||||||
void CollectSaveableActorsData(const UWorld* World);
|
void CollectSaveableActorsData(const UWorld* World);
|
||||||
void SetGameEventData(const UWorld* World);
|
void SetGameEventData(const UWorld* World);
|
||||||
|
|||||||
@@ -31,4 +31,20 @@ public:
|
|||||||
bool operator==(const FTimeOfDayData& Other) const {
|
bool operator==(const FTimeOfDayData& Other) const {
|
||||||
return Year == Other.Year && Month == Other.Month && Day == Other.Day && Hour == Other.Hour && Minute == Other.Minute;
|
return Year == Other.Year && Month == Other.Month && Day == Other.Day && Hour == Other.Hour && Minute == Other.Minute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sums up years, months and days into a single monotonic day counter so two stamps can be compared.
|
||||||
|
* The formula mirrors ATimeOfDayManager::GetAllDaysPassed(), keep both in sync. */
|
||||||
|
int32 ToAbsoluteDays() const {
|
||||||
|
return ((Year - 1) * 4 * 28) + ((Month - 1) * 28) + Day;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The same counter in minutes, for the places that need a finer resolution than whole days */
|
||||||
|
int32 ToAbsoluteMinutes() const {
|
||||||
|
return (ToAbsoluteDays() * 24 * 60) + (Hour * 60) + Minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A stamp that was never written, for example a save from before the stamp existed, is all zeroes */
|
||||||
|
bool IsValidTime() const {
|
||||||
|
return Year > 0 || Month > 0 || Day > 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user