412 lines
13 KiB
C++
412 lines
13 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "EleriGameState.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "MyCharacter.h"
|
|
#include "../System/Subsystem/ObjectPersistenceSubsystem.h"
|
|
#include "EngineUtils.h"
|
|
#include "TimeOfDayManager.h"
|
|
#include "Components/StatForgeComponent.h"
|
|
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
|
|
#include "ProjectEleri/GameObjects/EleriBaseActor.h"
|
|
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
|
|
#include "ProjectEleri/GameObjects/BaseCharacter.h"
|
|
|
|
namespace
|
|
{
|
|
/** The record a save holds for an actor, looked up by its id and then by its name.
|
|
*
|
|
* The id is the real key, and the name is the fallback for a save written before the ids were
|
|
* stable: such a save filed every one of these actors under an id that was rolled again on the next
|
|
* launch, so that id can never match again, and the actor name is the only thing it recorded. Set
|
|
* onto a record whose id is therefore a dead end. No caller is left worse off by it either, because
|
|
* the load that follows checks the name it has just matched against the actor's own name.
|
|
*
|
|
* Walked rather than keyed by name in a map of its own, because this is a miss path: on a save
|
|
* written since the ids became stable the id lookup answers and the walk never happens. */
|
|
const FActorSaveData* FindActorSaveData(const TMap<FGuid, FActorSaveData>& ActorSaveData, const UObject& Actor, const FGuid& PersistentId)
|
|
{
|
|
if (const FActorSaveData* FoundData = ActorSaveData.Find(PersistentId))
|
|
{
|
|
return FoundData;
|
|
}
|
|
|
|
for (const TPair<FGuid, FActorSaveData>& Entry : ActorSaveData)
|
|
{
|
|
if (Entry.Value.ActorName == Actor.GetFName())
|
|
{
|
|
return &Entry.Value;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
AEleriGameState* AEleriGameState::Get(const UObject* WorldContext)
|
|
{
|
|
if (WorldContext && WorldContext->GetWorld() && WorldContext->GetWorld()->GetGameState())
|
|
{
|
|
return Cast<AEleriGameState>(WorldContext->GetWorld()->GetGameState());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int32 AEleriGameState::GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag)
|
|
{
|
|
if (!Get(WorldContext)) return 0;
|
|
|
|
if (const int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag))
|
|
{
|
|
return *Value;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32 AEleriGameState::ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount)
|
|
{
|
|
if (!Get(WorldContext)) return 0;
|
|
|
|
if (int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag))
|
|
{
|
|
*Value += Amount;
|
|
return *Value;
|
|
}
|
|
else
|
|
{
|
|
Get(WorldContext)->FriendshipData.Add(NpcTag, Amount);
|
|
return Amount;
|
|
}
|
|
}
|
|
|
|
void AEleriGameState::PropagateAllSeedData()
|
|
{
|
|
ItemDatabase = GetWorld()->GetGameInstance()->GetSubsystem<UItemDatabase>();
|
|
if (!ItemDatabase) return;
|
|
TArray<AActor*> OutActors;
|
|
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
|
|
for (AActor* Actor : OutActors)
|
|
{
|
|
if (ASeedBedActor* SeedActor = Cast<ASeedBedActor>(Actor))
|
|
{
|
|
if (FSeedBedSaveData* SeedSaveData = SaveData.SeedBedData.Find(SeedActor->SeedBedIndex))
|
|
{
|
|
SeedActor->PropagateSeedSaveData(*SeedSaveData);
|
|
}
|
|
else
|
|
{
|
|
CacheSeedData(SeedActor->SeedBedIndex, SeedActor->PrepareSeedSaveData());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
check(InSaveData);
|
|
AMyCharacter* PlayerCharacter = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
|
|
check(PlayerCharacter);
|
|
|
|
SaveData = InSaveData->SaveData;
|
|
|
|
//The clock goes back first, because everything below it, the NPC schedules in particular, is a
|
|
//function of the time of day
|
|
ATimeOfDayManager* Clock = Cast<ATimeOfDayManager>(
|
|
UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
|
if (Clock != nullptr && SaveData.TimeOfDaySave.IsValidTime())
|
|
{
|
|
Clock->LoadSaveData(SaveData.TimeOfDaySave);
|
|
|
|
// LoadSaveData writes the fields silently, so the clock is then set to the value it just
|
|
// adopted. That is what broadcasts OnTimePassed and OnMinutesAdded, which is how everything
|
|
// already listening hears that the time jumped instead of having to read the clock back itself.
|
|
Clock->SetTimeOfDay(Clock->GetCurrentFloatMinutes());
|
|
}
|
|
|
|
TArray<AActor*> OutActors;
|
|
|
|
// Load all inventories
|
|
UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors);
|
|
for (int32 i = 0; i < OutActors.Num(); i++)
|
|
{
|
|
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(
|
|
OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass())))
|
|
{
|
|
Inv->LoadFromSave(SaveData.InventoryData[FName(OutActors[i]->GetName())]);
|
|
}
|
|
}
|
|
|
|
//Load placed objects
|
|
if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<
|
|
UObjectPersistenceSubsystem>())
|
|
{
|
|
ObjectPersistenceSubsystem->LoadSavedData(SaveData.PlacedActors);
|
|
}
|
|
|
|
//Promote actor data
|
|
for (FActorIterator It(GetWorld()); It; ++It)
|
|
{
|
|
AActor* Actor = *It;
|
|
if (!Actor->IsValidLowLevel() || Actor->IsPendingKillPending() || !Cast<ISaveableObjectInterface>(Actor) || !
|
|
Cast<IActorPersistentIdInterface>(Actor))
|
|
{
|
|
continue;
|
|
}
|
|
if (const FActorSaveData* ActorSaveData = FindActorSaveData(
|
|
SaveData.ActorSaveData, *Actor, Cast<IActorPersistentIdInterface>(Actor)->GetPersistentId()))
|
|
{
|
|
// Copied, because RequestLoad takes the record by mutable reference while the one inside the
|
|
// save is only ever read from here.
|
|
FActorSaveData RecordToLoad = *ActorSaveData;
|
|
Cast<ISaveableObjectInterface>(Actor)->RequestLoad(RecordToLoad);
|
|
}
|
|
}
|
|
|
|
//Put every scheduled NPC back where it was, on the assignment it was on. This comes after the clock
|
|
//above, so an assignment resolves against the time the save was made at.
|
|
for (TActorIterator<ABaseCharacter> It(GetWorld()); It; ++It)
|
|
{
|
|
ABaseCharacter* Npc = *It;
|
|
if (!IsValid(Npc)) continue;
|
|
|
|
UNpcScheduleComponent* Schedule = Npc->GetScheduleComponent();
|
|
if (Schedule == nullptr || !Schedule->NpcTag.IsValid()) continue;
|
|
|
|
FNpcScheduleSaveData NpcSaveData;
|
|
if (RequestNpcScheduleData(Schedule->NpcTag, NpcSaveData))
|
|
{
|
|
Schedule->ApplySaveData(NpcSaveData);
|
|
}
|
|
}
|
|
|
|
//Game event data
|
|
if (GetWorld()->GetGameInstance())
|
|
{
|
|
if (UGameEventSubsystem* GES = GetWorld()->GetGameInstance()->GetSubsystem<UGameEventSubsystem>())
|
|
{
|
|
GES->LoadGameEventMapData(SaveData.EventDataMap);
|
|
}
|
|
}
|
|
|
|
//Load player data
|
|
PlayerCharacter->SetActorTransform(SaveData.PlayerTransform);
|
|
if (UStatForgeComponent* StatForgeComponent =
|
|
UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(GetWorld()))
|
|
{
|
|
StatForgeComponent->RemoveAllGameplayStatEffects();
|
|
|
|
for (const FStatRuntimeDef& Stat : SaveData.StatData)
|
|
{
|
|
FGameplayStatEffectMod Mod;
|
|
Mod.AffectedStat = Stat.StatDef.StatName;
|
|
Mod.CalculationType = EGameplayStatEffectModCalculation::Flat;
|
|
Mod.FlatValue = Stat.BaseValue;
|
|
Mod.ModOperation = EModOperation::Override;
|
|
TMap<FName, float> DataValueMap;
|
|
StatForgeComponent->ApplyModToStat(Mod, DataValueMap);
|
|
}
|
|
|
|
for (const FActiveGameplayStatEffect_SerializedData& SavedGe : SaveData.GameEffects)
|
|
{
|
|
const FGuid Id = StatForgeComponent->ApplyGameplayStatEffect(
|
|
SavedGe.GameplayEffectClass, SavedGe.DataValueMap, SavedGe.StackCount);
|
|
StatForgeComponent->ModifyGeDuration(Id, SavedGe.RemainingDuration, SavedGe.NextTickDuration);
|
|
}
|
|
}
|
|
|
|
//Seed delete and spawn should always go last
|
|
PropagateAllSeedData();
|
|
|
|
//Announced only once everything above has been read, so a listener that reacts to the load sees a
|
|
//finished world rather than a half restored one
|
|
OnPropagateSaveDataComplete.Broadcast();
|
|
}
|
|
|
|
const FSaveData& AEleriGameState::PrepareSaveData()
|
|
{
|
|
//SAVE TIME OF DAY
|
|
ATimeOfDayManager* TimeOfDay = Cast<ATimeOfDayManager>(
|
|
UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
|
check(TimeOfDay);
|
|
SaveData.TimeOfDaySave = TimeOfDay->GetSaveData();
|
|
|
|
//SAVE BOOK
|
|
const ABookOfAlchemyManager* Book = Cast<ABookOfAlchemyManager>(
|
|
UGameplayStatics::GetActorOfClass(GetWorld(), ABookOfAlchemyManager::StaticClass()));
|
|
check(Book);
|
|
SaveData.SetBookProgressionData(Book->BookProgression);
|
|
|
|
TArray<AActor*> OutActors;
|
|
|
|
//SAVE INVENTORIES
|
|
UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors);
|
|
for (int32 i = 0; i < OutActors.Num(); i++)
|
|
{
|
|
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(
|
|
OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass())))
|
|
{
|
|
SaveData.SetInventoryDataForActor(OutActors[i], Inv);
|
|
}
|
|
}
|
|
|
|
//SAVE PLACED OBJECTS
|
|
if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<
|
|
UObjectPersistenceSubsystem>())
|
|
{
|
|
SaveData.SetPlacedObjectsData(ObjectPersistenceSubsystem->PrepareSaveData());
|
|
}
|
|
|
|
//SAVE SEEDS
|
|
OutActors.Empty();
|
|
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
|
|
for (AActor* SeedActor : OutActors)
|
|
{
|
|
if (ASeedBedActor* SeedBedActor = Cast<ASeedBedActor>(SeedActor))
|
|
{
|
|
FSeedBedSaveData SeedSaveData = SeedBedActor->PrepareSeedSaveData();
|
|
CacheSeedData(SeedSaveData.Index, SeedSaveData);
|
|
}
|
|
}
|
|
|
|
//SAVE ACTORS
|
|
SaveData.CollectSaveableActorsData(GetWorld());
|
|
|
|
//SAVE NPC SCHEDULES
|
|
SaveData.CollectNpcScheduleData(GetWorld());
|
|
|
|
//SAVE GAME EVENTS
|
|
SaveData.SetGameEventData(GetWorld());
|
|
|
|
//SAVE PLAYER STUFF
|
|
SaveData.SetPlayerData(GetWorld());
|
|
SaveData.SetAbilitySystemData(GetWorld());
|
|
|
|
return SaveData;
|
|
}
|
|
|
|
void AEleriGameState::CacheActorData(AEleriBaseActor* EleriActor)
|
|
{
|
|
if (!IsValid(EleriActor)) return;
|
|
|
|
const FGuid PersistentId = EleriActor->GetPersistentId();
|
|
const FActorSaveData ActorData = Cast<ISaveableObjectInterface>(EleriActor)->RequestSave();
|
|
|
|
if (FActorSaveData* FoundActorData = SaveData.ActorSaveData.Find(PersistentId))
|
|
{
|
|
*FoundActorData = ActorData;
|
|
return;
|
|
}
|
|
|
|
// The id lookup above is the whole of the normal path. What the walk below is for is a save written
|
|
// before the ids were stable: it filed this actor under an id that was rolled again on the next
|
|
// launch, and simply adding the actor under today's id on top of that would leave both records in
|
|
// the save, one of them something nothing will ever read again. The stale one is moved over instead.
|
|
// A name is only stable for as long as the level is, so this is the fallback and never the first
|
|
// choice.
|
|
FGuid StaleKey;
|
|
for (const TPair<FGuid, FActorSaveData>& Entry : SaveData.ActorSaveData)
|
|
{
|
|
if (Entry.Value.ActorName == ActorData.ActorName)
|
|
{
|
|
StaleKey = Entry.Key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (StaleKey.IsValid())
|
|
{
|
|
SaveData.ActorSaveData.Remove(StaleKey);
|
|
}
|
|
|
|
SaveData.ActorSaveData.Add(PersistentId, ActorData);
|
|
}
|
|
|
|
bool AEleriGameState::RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const
|
|
{
|
|
if (!IsValid(EleriActor)) return false;
|
|
|
|
// Looked up by id and then by name, see FindActorSaveData. The same two passes the promote loop in
|
|
// PropagateSaveData uses, so an actor is restored however the load reaches it: through that loop, or
|
|
// through its own BeginPlay when it enters the world after the load has already been applied.
|
|
const FActorSaveData* FoundActorData =
|
|
FindActorSaveData(SaveData.ActorSaveData, *EleriActor, EleriActor->GetPersistentId());
|
|
|
|
if (FoundActorData != nullptr)
|
|
{
|
|
OutData = *FoundActorData;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void AEleriGameState::CacheSeedData(int32 Index, const FSeedBedSaveData& Data)
|
|
{
|
|
SaveData.SetPlantedSeedData(Index, Data);
|
|
}
|
|
|
|
bool AEleriGameState::GetSeedData(int32 Index, FSeedBedSaveData& OutData)
|
|
{
|
|
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
|
|
{
|
|
OutData = *FoundData;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool AEleriGameState::RequestNpcScheduleData(FGameplayTag NpcTag, FNpcScheduleSaveData& OutData) const
|
|
{
|
|
if (!NpcTag.IsValid()) return false;
|
|
|
|
// A linear scan, because one entry per scheduled NPC is a handful and a tag keyed map would only
|
|
// buy a hash for that.
|
|
for (const FNpcScheduleSaveData& NpcData : SaveData.NpcScheduleData)
|
|
{
|
|
if (NpcData.NpcTag == NpcTag)
|
|
{
|
|
OutData = NpcData;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|