Files
ProjectEleri/Source/ProjectEleri/Private/EleriGameState.cpp

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;
}