Files
ProjectEleri/Source/ProjectEleri/QuestSystem/QuestSubsystem.cpp

1479 lines
44 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "QuestSubsystem.h"
#include "Engine/AssetManager.h"
#include "Engine/World.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "GameplayTagAssetInterface.h"
#include "Kismet/GameplayStatics.h"
#include "TimerManager.h"
#include "InventoryComponent.h"
#include "MyCharacter.h"
#include "ProjectEleri/Ai/NpcScheduleComponent.h"
#include "ProjectEleri/DialogueSystem/DialogueSceneSubsystem.h"
#include "ProjectEleri/DialogueSystem/DialogueSubsystem.h"
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
#include "ProjectEleri/Items/DataAssets/ItemDataAsset.h"
#include "ProjectEleri/Public/EleriGameState.h"
#include "ProjectEleri/QuestSystem/QuestDataAsset.h"
#include "ProjectEleri/Settings/EleriGameSettings.h"
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
#include "Subsystem/InteractionSubsystem.h"
#include "TimeOfDayManager.h"
UQuestSubsystem* UQuestSubsystem::Get(const UObject* WorldContext)
{
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContext);
if (GameInstance == nullptr)
{
return nullptr;
}
UQuestSubsystem* Subsystem = GameInstance->GetSubsystem<UQuestSubsystem>();
// Asking for the quest subsystem is also what wakes it up. Everything a quest watches in a level is
// bound here rather than only at the start of play, so a subsystem that missed a world because it
// was created after that world had already begun still picks it up on the first query.
if (Subsystem != nullptr && WorldContext != nullptr)
{
Subsystem->EnsureGameplayBound(WorldContext->GetWorld());
}
return Subsystem;
}
void UQuestSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
bQuestsLoaded = false;
// The registry sweep is asynchronous because it goes through the asset manager, the same way the
// event and dialogue subsystems read theirs, and it is the only thing here that costs anything at
// startup.
if (UAssetManager* AssetManager = UAssetManager::GetIfInitialized())
{
FStreamableManager& StreamableManager = AssetManager->GetStreamableManager();
TArray<FAssetData> AssetDataList;
AssetManager->GetAssetRegistry().GetAssetsByClass(
FTopLevelAssetPath(UQuestDataAsset::StaticClass()->GetPathName()), AssetDataList, false);
AssetPathsToLoad.Reset();
for (const FAssetData& AssetData : AssetDataList)
{
AssetPathsToLoad.AddUnique(AssetData.GetSoftObjectPath());
}
if (AssetPathsToLoad.Num() == 0)
{
// No quests in the project yet. Saying so now rather than leaving the subsystem waiting on
// a load that has nothing in it keeps "no quests" and "quests not read yet" apart.
QuestAssetsLoaded();
}
else
{
StreamableManager.RequestAsyncLoad(
AssetPathsToLoad, FStreamableDelegate::CreateUObject(this, &UQuestSubsystem::QuestAssetsLoaded));
}
}
PostWorldInitializationHandle = FWorldDelegates::OnPostWorldInitialization.AddUObject(
this, &UQuestSubsystem::HandlePostWorldInitialization);
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
this, &UQuestSubsystem::HandleWorldCleanup);
}
void UQuestSubsystem::Deinitialize()
{
ReleaseGameplayBindings();
if (PostWorldInitializationHandle.IsValid())
{
FWorldDelegates::OnPostWorldInitialization.Remove(PostWorldInitializationHandle);
PostWorldInitializationHandle.Reset();
}
if (WorldCleanupHandle.IsValid())
{
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
WorldCleanupHandle.Reset();
}
Super::Deinitialize();
}
void UQuestSubsystem::QuestAssetsLoaded()
{
QuestAssets.Reset();
for (const FSoftObjectPath& Path : AssetPathsToLoad)
{
UQuestDataAsset* QuestAsset = Cast<UQuestDataAsset>(Path.ResolveObject());
if (QuestAsset == nullptr)
{
continue;
}
FString Error;
if (!QuestAsset->IsValidQuest(Error))
{
UE_LOG(LogTemp, Warning, TEXT("Quest asset '%s' is not usable: %s."), *QuestAsset->GetName(), *Error);
continue;
}
if (const UQuestDataAsset* Existing = GetQuestByTag(QuestAsset->QuestTag))
{
UE_LOG(LogTemp, Warning, TEXT("Quest asset '%s' uses tag %s, which '%s' already uses. It is ignored."),
*QuestAsset->GetName(), *QuestAsset->QuestTag.ToString(), *Existing->GetName());
continue;
}
QuestAssets.Add(QuestAsset);
}
AssetPathsToLoad.Reset();
bQuestsLoaded = true;
UE_LOG(LogTemp, Log, TEXT("Quest subsystem loaded %d quests."), QuestAssets.Num());
OnQuestAssetsLoaded.Broadcast();
}
UQuestDataAsset* UQuestSubsystem::GetQuestByTag(FGameplayTag QuestTag) const
{
if (!QuestTag.IsValid())
{
return nullptr;
}
for (const TObjectPtr<UQuestDataAsset>& Quest : QuestAssets)
{
if (Quest != nullptr && Quest->QuestTag == QuestTag)
{
return Quest;
}
}
return nullptr;
}
void UQuestSubsystem::HandlePostWorldInitialization(UWorld* World, const FWorldInitializationValues IVS)
{
if (World == nullptr || !World->IsGameWorld())
{
return;
}
// The previous world, if there is one, is left to its cleanup: this subsystem follows the player,
// and a listen server's other worlds are not where the quests are being played.
if (UWorld* PreviousWorld = BoundWorld.Get())
{
if (WorldBeginPlayHandle.IsValid())
{
PreviousWorld->OnWorldBeginPlay.Remove(WorldBeginPlayHandle);
WorldBeginPlayHandle.Reset();
}
}
BoundWorld = World;
// Begin play rather than the initialization this is called from, because the player controller and
// its pawn, which every binding below hangs off, do not exist yet at this point.
WorldBeginPlayHandle = World->OnWorldBeginPlay.AddUObject(this, &UQuestSubsystem::HandleWorldBeginPlay);
}
void UQuestSubsystem::HandleWorldBeginPlay()
{
EnsureGameplayBound(BoundWorld.Get());
}
void UQuestSubsystem::HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources)
{
if (World == nullptr || World != BoundWorld.Get())
{
return;
}
if (WorldBeginPlayHandle.IsValid())
{
World->OnWorldBeginPlay.Remove(WorldBeginPlayHandle);
WorldBeginPlayHandle.Reset();
}
ReleaseGameplayBindings();
BoundWorld.Reset();
}
void UQuestSubsystem::EnsureGameplayBound(UWorld* World)
{
if (World == nullptr || !World->IsGameWorld() || World != BoundWorld.Get())
{
// A world that was never announced has no begin play to carry the first binding, and there is
// nothing to bind to in a world that is not the player's.
return;
}
APlayerController* PlayerController = World->GetFirstPlayerController();
AMyCharacter* Character = PlayerController != nullptr ? Cast<AMyCharacter>(PlayerController->GetPawn()) : nullptr;
UInventoryComponent* Inventory = Character != nullptr ? Character->GetComponentByClass<UInventoryComponent>() : nullptr;
const bool bRebind = BoundPlayerController.Get() != PlayerController
|| BoundCharacter.Get() != Character
|| BoundInventory.Get() != Inventory;
if (!bRebind)
{
return;
}
ReleaseGameplayBindings();
BoundPlayerController = PlayerController;
BoundCharacter = Character;
BoundInventory = Inventory;
// A new controller means new dynamic bindings all round: the controller announces pawn changes,
// the pawn announces interactions, the inventory announces its own contents, and none of those
// survive the pawn being replaced.
if (PlayerController != nullptr)
{
PlayerController->OnPossessedPawnChanged.AddDynamic(this, &UQuestSubsystem::HandlePossessedPawnChanged);
}
if (Character != nullptr)
{
Character->OnInteract.AddDynamic(this, &UQuestSubsystem::HandlePlayerInteract);
}
if (Inventory != nullptr)
{
Inventory->OnInventoryUpdated.AddDynamic(this, &UQuestSubsystem::HandleInventoryUpdated);
}
// The dialogue subsystem is a game instance subsystem and so survives travel, but it is bound here
// because this is where it is guaranteed to be alive: nothing about a conversation needs a world.
if (UGameInstance* GameInstance = GetGameInstance())
{
if (UDialogueSubsystem* DialogueSubsystem = GameInstance->GetSubsystem<UDialogueSubsystem>())
{
DialogueSubsystem->OnDialogueSpeakerChanged.AddDynamic(this, &UQuestSubsystem::HandleDialogueSpeakerChanged);
DialogueSubsystem->OnDialogueGameplayEvent.AddDynamic(this, &UQuestSubsystem::HandleDialogueGameplayEvent);
}
}
// The clock is respawned with the level, so its day change is bound per level even though the
// quests that measure deadlines against it are not.
if (ATimeOfDayManager* TimeOfDayManager = GetTimeOfDayManager())
{
TimeOfDayManager->OnDayPassed.AddDynamic(this, &UQuestSubsystem::HandleDayPassed);
}
UE_LOG(LogTemp, Log, TEXT("Quest subsystem bound in world '%s' (controller: %s, character: %s)."),
*World->GetName(),
PlayerController != nullptr ? *PlayerController->GetName() : TEXT("none"),
Character != nullptr ? *Character->GetName() : TEXT("none"));
// Whatever is already being carried counts towards the steps that ask for an item, and a quest that
// was part way through when the player travelled needs its steps re-read in the new world.
RefreshActiveQuests();
}
void UQuestSubsystem::ReleaseGameplayBindings()
{
if (APlayerController* PlayerController = BoundPlayerController.Get())
{
PlayerController->OnPossessedPawnChanged.RemoveAll(this);
}
if (AMyCharacter* Character = BoundCharacter.Get())
{
Character->OnInteract.RemoveAll(this);
}
if (UInventoryComponent* Inventory = BoundInventory.Get())
{
Inventory->OnInventoryUpdated.RemoveAll(this);
}
if (UGameInstance* GameInstance = GetGameInstance())
{
if (UDialogueSubsystem* DialogueSubsystem = GameInstance->GetSubsystem<UDialogueSubsystem>())
{
DialogueSubsystem->OnDialogueSpeakerChanged.RemoveAll(this);
DialogueSubsystem->OnDialogueGameplayEvent.RemoveAll(this);
}
}
if (ATimeOfDayManager* TimeOfDayManager = CachedTimeOfDayManager.Get())
{
TimeOfDayManager->OnDayPassed.RemoveAll(this);
}
BoundPlayerController.Reset();
BoundCharacter.Reset();
BoundInventory.Reset();
}
UObject* UQuestSubsystem::GetTestContext() const
{
if (const AMyCharacter* Character = BoundCharacter.Get())
{
return const_cast<AMyCharacter*>(Character);
}
// Before a pawn exists, or after a respawn the binding has not caught up with, the player
// character of the world is the same thing the event system hands its own requirements.
const UWorld* World = GetBoundWorld();
return World != nullptr ? UGameplayStatics::GetPlayerCharacter(World, 0) : nullptr;
}
UInventoryComponent* UQuestSubsystem::GetPlayerInventory() const
{
return BoundInventory.Get();
}
ATimeOfDayManager* UQuestSubsystem::GetTimeOfDayManager()
{
if (CachedTimeOfDayManager.IsValid())
{
return CachedTimeOfDayManager.Get();
}
UWorld* World = GetBoundWorld();
if (World == nullptr)
{
return nullptr;
}
// Cached, because this is asked for on every deadline check and GetActorOfClass walks the level.
ATimeOfDayManager* Found = Cast<ATimeOfDayManager>(UGameplayStatics::GetActorOfClass(World, ATimeOfDayManager::StaticClass()));
CachedTimeOfDayManager = Found;
return Found;
}
int32 UQuestSubsystem::GetCurrentDay()
{
const ATimeOfDayManager* TimeOfDayManager = GetTimeOfDayManager();
return TimeOfDayManager != nullptr ? TimeOfDayManager->GetAllDaysPassed() : 0;
}
FGameplayTag UQuestSubsystem::ResolveActorNpcTag(const AActor* Actor)
{
if (Actor == nullptr)
{
return FGameplayTag();
}
// Scheduled characters carry their tag on their schedule component, which is the tag their day
// plan and their friendship are keyed by, so a talk step names the same thing a schedule does.
if (const UNpcScheduleComponent* Schedule = Actor->FindComponentByClass<UNpcScheduleComponent>())
{
if (Schedule->NpcTag.IsValid())
{
return Schedule->NpcTag;
}
}
// Anything else that owns gameplay tags is matched by the first of its tags under NpcTags, which
// is how a door or a chest can be the target of a step without having a schedule at all.
if (const IGameplayTagAssetInterface* TagInterface = Cast<IGameplayTagAssetInterface>(Actor))
{
static const FGameplayTag NpcTagRoot = FGameplayTag::RequestGameplayTag(FName(TEXT("NpcTags")), false);
FGameplayTagContainer OwnedTags;
TagInterface->GetOwnedGameplayTags(OwnedTags);
for (const FGameplayTag& Tag : OwnedTags)
{
if (NpcTagRoot.IsValid() && Tag.MatchesTag(NpcTagRoot))
{
return Tag;
}
}
}
return FGameplayTag();
}
EQuestStatus UQuestSubsystem::GetQuestStatus(FGameplayTag QuestTag) const
{
if (const FQuestRuntimeState* State = QuestStates.Find(QuestTag))
{
// An offer is nothing more than the requirements holding, so a quest that has been put forward
// and has since stopped qualifying stops being on offer. Nothing is written back for this: the
// entry keeps saying Available and the answer here is what changes, which is what makes the
// offer come back by itself if the conditions come back.
if (State->Status == EQuestStatus::Available)
{
const UQuestDataAsset* Quest = GetQuestByTag(QuestTag);
if (Quest == nullptr || !Quest->AreOfferRequirementsMet(GetTestContext()))
{
return EQuestStatus::Unavailable;
}
}
return State->Status;
}
// Nothing has happened to this quest yet, so the useful answer is whether the player could take it.
// That is worked out from the offer requirements every time rather than remembered, which is what
// lets a quest become takeable because a friendship grew or a day passed, with nothing having to
// announce it.
const UQuestDataAsset* Quest = GetQuestByTag(QuestTag);
if (Quest == nullptr)
{
return EQuestStatus::Unavailable;
}
return Quest->AreOfferRequirementsMet(GetTestContext()) ? EQuestStatus::Available : EQuestStatus::Unavailable;
}
bool UQuestSubsystem::IsQuestInProgress(FGameplayTag QuestTag) const
{
const EQuestStatus Status = GetQuestStatus(QuestTag);
return Status == EQuestStatus::Active || Status == EQuestStatus::ReadyToTurnIn;
}
bool UQuestSubsystem::CanAcceptQuest(FGameplayTag QuestTag) const
{
const UQuestDataAsset* Quest = GetQuestByTag(QuestTag);
if (Quest == nullptr)
{
return false;
}
const EQuestStatus Status = GetQuestStatus(QuestTag);
// A quest already being worked on cannot be taken twice, and one that has been handed in is
// finished with. Failed and abandoned quests are left takeable, which is what makes a second
// attempt possible without anything having to reset the save.
if (Status == EQuestStatus::Active || Status == EQuestStatus::ReadyToTurnIn || Status == EQuestStatus::Completed)
{
return false;
}
return Quest->AreOfferRequirementsMet(GetTestContext());
}
int32 UQuestSubsystem::GetObjectiveProgress(FGameplayTag QuestTag, FName ObjectiveId) const
{
if (const FQuestRuntimeState* State = QuestStates.Find(QuestTag))
{
if (const FQuestObjectiveProgress* Progress = State->FindProgress(ObjectiveId))
{
return Progress->CurrentCount;
}
}
return 0;
}
FQuestJournalEntry UQuestSubsystem::BuildJournalEntry(const UQuestDataAsset* Quest) const
{
FQuestJournalEntry Entry;
if (Quest == nullptr)
{
return Entry;
}
Entry.Quest = const_cast<UQuestDataAsset*>(Quest);
Entry.QuestTag = Quest->QuestTag;
Entry.QuestName = Quest->QuestName;
Entry.QuestDescription = Quest->QuestDescription;
Entry.Status = GetQuestStatus(Quest->QuestTag);
Entry.bReadyToTurnIn = Entry.Status == EQuestStatus::ReadyToTurnIn;
const FQuestRuntimeState* State = QuestStates.Find(Quest->QuestTag);
for (const FQuestObjective& Objective : Quest->Objectives)
{
// Hidden steps are tracked but never shown, which is how a quest keeps a secret count of
// something the player is not supposed to know it is watching.
if (Objective.bHidden)
{
continue;
}
FQuestObjectiveView View;
View.ObjectiveId = Objective.ObjectiveId;
View.Description = Objective.Description;
View.RequiredCount = FMath::Max(1, Objective.RequiredCount);
View.bOptional = Objective.bOptional;
if (const FQuestObjectiveProgress* Progress = State != nullptr ? State->FindProgress(Objective.ObjectiveId) : nullptr)
{
View.CurrentCount = Progress->CurrentCount;
View.bComplete = Progress->bComplete;
}
Entry.Objectives.Add(View);
}
return Entry;
}
TArray<FQuestJournalEntry> UQuestSubsystem::GetTrackedQuestEntries() const
{
TArray<FQuestJournalEntry> Entries;
for (const TPair<FGameplayTag, FQuestRuntimeState>& Pair : QuestStates)
{
if (Pair.Value.Quest == nullptr)
{
continue;
}
if (Pair.Value.Status == EQuestStatus::Active || Pair.Value.Status == EQuestStatus::ReadyToTurnIn)
{
Entries.Add(BuildJournalEntry(Pair.Value.Quest));
}
}
// A map has no order of its own, and a journal that reshuffles itself between two openings is
// worse than one in no particular order, so the list is sorted into the one order that is stable
// whatever the player has been doing.
Entries.Sort([](const FQuestJournalEntry& A, const FQuestJournalEntry& B)
{
return A.QuestTag.ToString() < B.QuestTag.ToString();
});
return Entries;
}
TArray<FQuestJournalEntry> UQuestSubsystem::GetOfferableQuestEntries() const
{
return GetQuestEntries(EQuestStatus::Available);
}
TArray<FQuestJournalEntry> UQuestSubsystem::GetQuestEntries(EQuestStatus Status) const
{
TArray<FQuestJournalEntry> Entries;
for (const TObjectPtr<UQuestDataAsset>& Quest : QuestAssets)
{
if (Quest != nullptr && GetQuestStatus(Quest->QuestTag) == Status)
{
Entries.Add(BuildJournalEntry(Quest));
}
}
return Entries;
}
FQuestRuntimeState& UQuestSubsystem::FindOrAddState(FGameplayTag QuestTag)
{
if (FQuestRuntimeState* Existing = QuestStates.Find(QuestTag))
{
return *Existing;
}
FQuestRuntimeState& State = QuestStates.Add(QuestTag);
State.Quest = GetQuestByTag(QuestTag);
State.Status = EQuestStatus::Unavailable;
return State;
}
void UQuestSubsystem::SetQuestStatus(FQuestRuntimeState& State, EQuestStatus NewStatus)
{
if (State.Status == NewStatus)
{
return;
}
State.Status = NewStatus;
// The asset is read out before the broadcast, because a listener is free to start or finish another
// quest, which can reallocate the map this state lives in.
UQuestDataAsset* Quest = State.Quest;
OnQuestStatusChanged.Broadcast(Quest, NewStatus);
// The stage's cutscene comes after the broadcast rather than before it, so a listener that reacts to
// the new status has already reacted by the time the scene takes the player's controls, and so the
// quest asset is not being read while that listener may still be writing to the quest map.
PlayStatusScene(Quest, NewStatus);
}
void UQuestSubsystem::PlayStatusScene(const UQuestDataAsset* Quest, EQuestStatus NewStatus)
{
if (Quest == nullptr)
{
return;
}
const TSoftObjectPtr<ULevelSequence>* Scene = Quest->StatusScenes.Find(NewStatus);
if (Scene == nullptr || Scene->IsNull())
{
// Most quests have no scene for most stages, which is the ordinary case and not a fault.
return;
}
if (UDialogueSceneSubsystem* SceneSubsystem = UDialogueSceneSubsystem::Get(this))
{
// The player goes in as the instigator, because a quest is not an actor and the thing a scene
// listener wants to know is whose scene this is.
SceneSubsystem->PlayScene(*Scene, Cast<AActor>(GetTestContext()));
}
}
bool UQuestSubsystem::OfferQuest(FGameplayTag QuestTag)
{
const UQuestDataAsset* Quest = GetQuestByTag(QuestTag);
if (Quest == nullptr || !Quest->AreOfferRequirementsMet(GetTestContext()))
{
return false;
}
const EQuestStatus Status = GetQuestStatus(QuestTag);
// A quest nobody has accepted is already available the moment its requirements hold, so the only
// thing this does is make the offer something the game can announce. A quest that was failed or
// abandoned is left alone: taking it up again is StartQuest's business, because that is the call
// that resets its progress.
if (Status == EQuestStatus::Available || Status == EQuestStatus::Unavailable)
{
SetQuestStatus(FindOrAddState(QuestTag), EQuestStatus::Available);
return true;
}
return false;
}
bool UQuestSubsystem::StartQuest(FGameplayTag QuestTag)
{
UQuestDataAsset* Quest = GetQuestByTag(QuestTag);
if (Quest == nullptr || !CanAcceptQuest(QuestTag))
{
return false;
}
FQuestRuntimeState& State = FindOrAddState(QuestTag);
State.Quest = Quest;
State.AcceptedDay = GetCurrentDay();
State.CompletedDay = 0;
State.Objectives.Reset();
// Every step starts at zero in the order the asset lists them, so the journal lists the quest the
// way it was written from the first frame it is visible.
for (const FQuestObjective& Objective : Quest->Objectives)
{
FQuestObjectiveProgress Progress;
Progress.ObjectiveId = Objective.ObjectiveId;
Progress.CurrentCount = 0;
Progress.bComplete = false;
Progress.bCountedFromInventory = Objective.StepType == EQuestStepType::AcquireItems
|| Objective.StepType == EQuestStepType::DeliverItems;
State.Objectives.Add(Progress);
}
SetQuestStatus(State, EQuestStatus::Active);
// A quest can be handed to a player who is already carrying what it asks for, and a quest handed
// over by a conversation may have been finished by that same conversation, so the steps are read
// once here rather than waiting for the next thing the player does.
RefreshActiveQuests();
return true;
}
bool UQuestSubsystem::AbandonQuest(FGameplayTag QuestTag)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || !IsQuestInProgress(QuestTag))
{
return false;
}
// How far it had got is kept, so an abandoned quest that is taken up again reads as one the player
// has already made progress on rather than as one that was never touched.
SetQuestStatus(*State, EQuestStatus::Abandoned);
return true;
}
bool UQuestSubsystem::FailQuest(FGameplayTag QuestTag)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Status == EQuestStatus::Failed || State->Status == EQuestStatus::Completed)
{
return false;
}
SetQuestStatus(*State, EQuestStatus::Failed);
return true;
}
bool UQuestSubsystem::CompleteQuest(FGameplayTag QuestTag)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Quest == nullptr || State->Status != EQuestStatus::ReadyToTurnIn)
{
// Asking to hand in a quest that is not finished, or one that was handed in already, is a
// script that got ahead of the player rather than a fault: nothing happens either way.
return false;
}
if (!State->Quest->AreTurnInRequirementsMet(GetTestContext()))
{
return false;
}
CompleteQuestInternal(*State);
return true;
}
void UQuestSubsystem::CompleteQuestInternal(FQuestRuntimeState& State)
{
UQuestDataAsset* Quest = State.Quest;
if (Quest == nullptr)
{
return;
}
const FGameplayTag QuestTag = Quest->QuestTag;
// The delivery is taken first, so that a reward of the same item cannot be mistaken for the item
// the player was asked to bring.
if (Quest->bConsumeDeliveredItems && Quest->HasDeliverStep())
{
if (UInventoryComponent* Inventory = GetPlayerInventory())
{
TArray<TPair<FPrimaryAssetId, int32>> Deliverables;
Quest->CollectDeliverables(State, Deliverables);
for (const TPair<FPrimaryAssetId, int32>& Deliverable : Deliverables)
{
Inventory->RemoveItemFromInventory(Deliverable.Key, Deliverable.Value);
}
}
}
State.CompletedDay = GetCurrentDay();
// The inventory is deaf while the rewards are handed over and every running quest is read once
// afterwards instead, so that a reward cannot count towards a step of a quest still in flight.
bGrantingRewards = true;
GrantRewards(*Quest);
bGrantingRewards = false;
// Re-found rather than reused: granting a reward changes the inventory, which the inventory binding
// turns into a re-read of every running quest, and that can add to the map this state lives in.
if (FQuestRuntimeState* Refreshed = QuestStates.Find(QuestTag))
{
SetQuestStatus(*Refreshed, EQuestStatus::Completed);
}
RefreshActiveQuests();
}
void UQuestSubsystem::GrantRewards(const UQuestDataAsset& Quest)
{
const FQuestRewardBundle& Rewards = Quest.Rewards;
if (UInventoryComponent* Inventory = GetPlayerInventory())
{
for (const FQuestItemReward& Reward : Rewards.Items)
{
const FPrimaryAssetId ItemId = Reward.GetItemId();
if (!ItemId.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Quest '%s' rewards an item that could not be resolved, so it was not given."),
*Quest.GetName());
continue;
}
Inventory->AddItemToInventory(ItemId, FMath::Max(1, Reward.Amount), Reward.bHighQuality);
}
for (const TPair<FGameplayTag, int32>& SpecialItem : Rewards.SpecialItems)
{
// Gold and the crystals are items like any other, addressed by tag through the same map the
// shop and the alchemy bench use, so a quest reward needs no path of its own to them.
const FPrimaryAssetId ItemId = UEleriGameSettings::GetSpecialItemId(SpecialItem.Key);
if (!ItemId.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Quest '%s' rewards '%s', which is not in the special items map, so nothing was given."),
*Quest.GetName(), *SpecialItem.Key.ToString());
continue;
}
Inventory->AddItemToInventory(ItemId, FMath::Max(1, SpecialItem.Value));
}
}
for (const TPair<FGameplayTag, int32>& Friendship : Rewards.Friendship)
{
AEleriGameState::ModifyFriendshipAmount(this, Friendship.Key, Friendship.Value);
}
for (const TPair<EExpType, float>& Experience : Rewards.Experience)
{
UMainBlueprintFunctionLibrary::ModifyExp(this, Experience.Key, Experience.Value);
}
if (UGameInstance* GameInstance = GetGameInstance())
{
if (UGameEventSubsystem* EventSubsystem = GameInstance->GetSubsystem<UGameEventSubsystem>())
{
for (const TSoftObjectPtr<UGameEventDataAsset>& Event : Rewards.EventsToStart)
{
if (Event.IsNull())
{
continue;
}
// Started through the event subsystem, so an event a quest unlocks is still subject to
// its own requirements and cannot be forced open by a reward.
EventSubsystem->StartEvent(Event.ToSoftObjectPath());
}
}
}
}
bool UQuestSubsystem::AreRequiredObjectivesComplete(const FQuestRuntimeState& State) const
{
const UQuestDataAsset* Quest = State.Quest;
if (Quest == nullptr)
{
return false;
}
for (const FQuestObjective& Objective : Quest->Objectives)
{
if (Objective.bOptional)
{
continue;
}
const FQuestObjectiveProgress* Progress = State.FindProgress(Objective.ObjectiveId);
if (Progress == nullptr || !Progress->bComplete)
{
return false;
}
}
return Quest->CountRequiredObjectives() > 0;
}
bool UQuestSubsystem::AdvanceObjective(FQuestRuntimeState& State, FName ObjectiveId, int32 Amount)
{
if (Amount <= 0)
{
return false;
}
UQuestDataAsset* Quest = State.Quest;
if (Quest == nullptr)
{
return false;
}
const FQuestObjective* Objective = Quest->FindObjective(ObjectiveId);
FQuestObjectiveProgress* Progress = State.FindProgress(ObjectiveId);
if (Objective == nullptr || Progress == nullptr || Progress->bComplete)
{
return false;
}
// A quest that asks for its steps in order does not hear about the ones still waiting behind the
// step the player is on, which is what stops a later step from being counted by something done on
// the way to the earlier one.
if (!Quest->IsObjectiveUnlocked(Quest->FindObjectiveIndex(ObjectiveId), State))
{
return false;
}
// The conditions are asked again every time the step is about to count, so a step written as "in
// the rain" is not counted while the sun is out even if its trigger fires.
if (!Objective->TestConditions(GetTestContext()))
{
return false;
}
const int32 Required = FMath::Max(1, Objective->RequiredCount);
const int32 NewCount = FMath::Min(Required, Progress->CurrentCount + Amount);
if (NewCount == Progress->CurrentCount)
{
return false;
}
Progress->CurrentCount = NewCount;
Progress->bComplete = NewCount >= Required;
const bool bNowComplete = Progress->bComplete;
const FName CountedObjectiveId = Objective->ObjectiveId;
OnQuestObjectiveProgress.Broadcast(Quest, CountedObjectiveId, NewCount, Required);
if (bNowComplete)
{
OnQuestObjectiveCompleted.Broadcast(Quest, CountedObjectiveId);
}
// Both broadcasts and everything they started have been and gone by now, so the state the caller
// handed in is not touched again: whatever happens next looks the quest up by tag.
if (bNowComplete)
{
UpdateQuestStatuses();
}
return true;
}
bool UQuestSubsystem::RecountInventoryObjectives(FQuestRuntimeState& State)
{
UQuestDataAsset* Quest = State.Quest;
if (Quest == nullptr)
{
return false;
}
UInventoryComponent* Inventory = GetPlayerInventory();
if (Inventory == nullptr)
{
return false;
}
bool bChanged = false;
for (int32 Index = 0; Index < Quest->Objectives.Num(); ++Index)
{
const FQuestObjective& Objective = Quest->Objectives[Index];
if (Objective.StepType != EQuestStepType::AcquireItems && Objective.StepType != EQuestStepType::DeliverItems)
{
continue;
}
const FPrimaryAssetId ItemId = Objective.TargetItem;
if (!ItemId.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Quest '%s' step '%s' asks for an item that could not be resolved, so it can never be done."),
*Quest->GetName(), *Objective.ObjectiveId.ToString());
continue;
}
// The state is looked up for every step, because broadcasting a step's progress lets a listener
// start, finish or abandon another quest, which can reallocate the map.
FQuestRuntimeState* CurrentState = QuestStates.Find(Quest->QuestTag);
if (CurrentState == nullptr)
{
return bChanged;
}
// A step behind another step of a sequential quest is not read at all, so what the player is
// already carrying only starts counting once the quest has actually asked for it.
const int32 Held = Quest->IsObjectiveUnlocked(Index, *CurrentState)
? Inventory->GetStackForItem(ItemId, Objective.bHighQuality)
: 0;
FQuestObjectiveProgress* Progress = CurrentState->FindProgress(Objective.ObjectiveId);
if (Progress == nullptr)
{
continue;
}
const int32 Required = FMath::Max(1, Objective.RequiredCount);
const int32 NewCount = FMath::Clamp(Held, 0, Required);
const bool bNowComplete = NewCount >= Required;
if (NewCount == Progress->CurrentCount && bNowComplete == Progress->bComplete)
{
continue;
}
Progress->CurrentCount = NewCount;
Progress->bComplete = bNowComplete;
bChanged = true;
OnQuestObjectiveProgress.Broadcast(Quest, Objective.ObjectiveId, NewCount, Required);
if (bNowComplete)
{
OnQuestObjectiveCompleted.Broadcast(Quest, Objective.ObjectiveId);
}
}
return bChanged;
}
void UQuestSubsystem::UpdateQuestStatuses()
{
// A copy of the keys rather than a walk of the map: moving a quest on announces it, and a listener
// is free to start another quest in response, which can reallocate the map being walked.
TArray<FGameplayTag> QuestTags;
QuestStates.GetKeys(QuestTags);
for (const FGameplayTag& QuestTag : QuestTags)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Quest == nullptr)
{
continue;
}
if (State->Status != EQuestStatus::Active && State->Status != EQuestStatus::ReadyToTurnIn)
{
continue;
}
// The deadline is asked first: a quest that has run out of days is gone even if the step that
// would have finished it was performed on the way.
const int32 Deadline = State->Quest->DeadlineInDays;
if (Deadline > 0 && GetCurrentDay() - State->AcceptedDay > Deadline)
{
SetQuestStatus(*State, EQuestStatus::Failed);
continue;
}
// Ready to hand in only when the steps are done and the conditions for the hand-in still hold.
// A quest whose conditions lapsed in the meantime drops back to being under way, which is what
// the turn-in requirements are for.
const bool bCanHandIn = AreRequiredObjectivesComplete(*State)
&& State->Quest->AreTurnInRequirementsMet(GetTestContext());
SetQuestStatus(*State, bCanHandIn ? EQuestStatus::ReadyToTurnIn : EQuestStatus::Active);
}
}
void UQuestSubsystem::RefreshActiveQuests()
{
TArray<FGameplayTag> QuestTags;
QuestStates.GetKeys(QuestTags);
for (const FGameplayTag& QuestTag : QuestTags)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Quest == nullptr)
{
continue;
}
if (State->Status != EQuestStatus::Active && State->Status != EQuestStatus::ReadyToTurnIn)
{
continue;
}
RecountInventoryObjectives(*State);
}
// Last, because counting an item step can be the thing that finishes a quest: the status of every
// running quest is settled once everything has had its say.
UpdateQuestStatuses();
}
void UQuestSubsystem::HandleObjectiveTrigger(EQuestStepType StepType, AActor* Target, FGameplayTag GameplayTag)
{
if (QuestStates.Num() == 0)
{
return;
}
TArray<FGameplayTag> QuestTags;
QuestStates.GetKeys(QuestTags);
for (const FGameplayTag& QuestTag : QuestTags)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Quest == nullptr)
{
continue;
}
if (State->Status != EQuestStatus::Active && State->Status != EQuestStatus::ReadyToTurnIn)
{
continue;
}
// The asset is kept, the state is not: a step that completes overrides the status of its own
// quest, and a listener of that can start another quest, which can reallocate the map the state
// above lives in.
UQuestDataAsset* Quest = State->Quest;
for (const FQuestObjective& Objective : Quest->Objectives)
{
if (Objective.StepType != StepType)
{
continue;
}
bool bMatched = false;
if (StepType == EQuestStepType::DialogueEvent)
{
// A dialogue event is matched by tag alone: the line fired the event the step named,
// whoever said it. That is what makes a quest count a conversation it did not start.
bMatched = GameplayTag.IsValid()
&& Objective.TargetGameplayTag.IsValid()
&& GameplayTag.MatchesTag(Objective.TargetGameplayTag);
}
else if (StepType == EQuestStepType::TalkToNpc)
{
// A talk step is satisfied by holding the conversation with the NPC it names, and also
// by a line that fired the event it names, which is how a conversation with several
// outcomes is one step rather than several.
bMatched = (Target != nullptr && DoesActorMatchObjective(Objective, Target))
|| (GameplayTag.IsValid()
&& Objective.TargetGameplayTag.IsValid()
&& GameplayTag.MatchesTag(Objective.TargetGameplayTag));
}
else
{
bMatched = Target != nullptr && DoesActorMatchObjective(Objective, Target);
}
if (!bMatched)
{
continue;
}
if (FQuestRuntimeState* CurrentState = QuestStates.Find(QuestTag))
{
AdvanceObjective(*CurrentState, Objective.ObjectiveId);
}
}
}
}
bool UQuestSubsystem::DoesActorMatchObjective(const FQuestObjective& Objective, const AActor* Actor) const
{
if (Actor == nullptr)
{
return false;
}
// The actor the step names, when it names one. Asked both ways round: a resolved reference is the
// cheap path, and the path is what a step that points at an actor in a level which is not loaded
// right now can still be matched against.
if (Objective.TargetActors.ContainsByPredicate([Actor](const TSoftObjectPtr<AActor>& OtherActor)
{
return OtherActor.ToSoftObjectPath() == FSoftObjectPath(Actor);
}))
{
return true;
}
// The marker the step names, which is how a LocationArrival step matching a marker that is not the
// player's own is recognised.
if (!Objective.TargetLocationMarker.IsNull()
&& (Objective.TargetLocationMarker.Get() == Actor || Objective.TargetLocationMarker.ToSoftObjectPath() == FSoftObjectPath(Actor)))
{
return true;
}
// The NPC tag the actor carries, when the step names one
if (Objective.TargetNpcTag.IsValid())
{
const FGameplayTag ActorTag = ResolveActorNpcTag(Actor);
if (ActorTag.IsValid() && ActorTag.MatchesTag(Objective.TargetNpcTag))
{
return true;
}
}
// A step that names nothing is satisfied by anything, which is how "use one of the wells" is
// written without having to list every well in the level.
return Objective.TargetActors.IsEmpty()
&& Objective.TargetLocationMarker.IsNull()
&& !Objective.TargetNpcTag.IsValid();
}
void UQuestSubsystem::NotifyQuestSignal(const FQuestSignal& Signal, AActor* Source)
{
if (!Signal.SignalTag.IsValid())
{
return;
}
TArray<FGameplayTag> QuestTags;
QuestStates.GetKeys(QuestTags);
for (const FGameplayTag& QuestTag : QuestTags)
{
FQuestRuntimeState* State = QuestStates.Find(QuestTag);
if (State == nullptr || State->Quest == nullptr)
{
continue;
}
if (State->Status != EQuestStatus::Active && State->Status != EQuestStatus::ReadyToTurnIn)
{
continue;
}
UQuestDataAsset* Quest = State->Quest;
const FGameplayTag SourceTag = Source != nullptr ? ResolveActorNpcTag(Source) : FGameplayTag();
for (const FQuestObjective& Objective : Quest->Objectives)
{
if (!Objective.IsCustomStep() || !Objective.CustomSignalTag.IsValid())
{
continue;
}
if (!Signal.SignalTag.MatchesTag(Objective.CustomSignalTag))
{
continue;
}
// A signal may name the one step it is about, which is what a quest with two similar custom
// steps uses so that the two cannot be counted by the same event.
if (!Signal.ObjectiveId.IsNone() && Signal.ObjectiveId != Objective.ObjectiveId)
{
continue;
}
// A step that names a speaker only takes a signal that can be traced to that speaker, and a
// signal that names one only takes what came from it. Both are the same question asked from
// either end, and a caller that passes no source at all answers neither.
if (Objective.TargetNpcTag.IsValid()
&& (!SourceTag.IsValid() || !SourceTag.MatchesTag(Objective.TargetNpcTag)))
{
continue;
}
if (Signal.SourceNpcTag.IsValid()
&& (!SourceTag.IsValid() || !SourceTag.MatchesTag(Signal.SourceNpcTag)))
{
continue;
}
if (FQuestRuntimeState* CurrentState = QuestStates.Find(QuestTag))
{
AdvanceObjective(*CurrentState, Objective.ObjectiveId);
}
}
}
}
void UQuestSubsystem::NotifyInteraction(AActor* Interactable, AActor* Interactor)
{
if (Interactable == nullptr)
{
return;
}
HandleObjectiveTrigger(EQuestStepType::InteractWith, Interactable, FGameplayTag());
}
void UQuestSubsystem::NotifyDialogueStarted(AActor* Speaker)
{
if (Speaker == nullptr)
{
return;
}
HandleObjectiveTrigger(EQuestStepType::TalkToNpc, Speaker, FGameplayTag());
}
void UQuestSubsystem::NotifyDialogueEvent(FGameplayTag EventTag, AActor* Speaker)
{
if (!EventTag.IsValid())
{
return;
}
// The event is offered to both kinds of talk step: one written against a conversation and one
// written against a line that can come out of it both mean "this conversation happened".
HandleObjectiveTrigger(EQuestStepType::DialogueEvent, Speaker, EventTag);
HandleObjectiveTrigger(EQuestStepType::TalkToNpc, Speaker, EventTag);
}
void UQuestSubsystem::NotifyInventoryChanged()
{
if (bGrantingRewards)
{
return;
}
RefreshActiveQuests();
}
void UQuestSubsystem::NotifyLocationReached(AActor* Marker)
{
if (Marker == nullptr)
{
return;
}
HandleObjectiveTrigger(EQuestStepType::LocationArrival, Marker, FGameplayTag());
}
void UQuestSubsystem::HandlePossessedPawnChanged(APawn* OldPawn, APawn* NewPawn)
{
// Whatever kind of pawn the player ends up in, what a quest watches is the character they are
// driving, so the bindings are remade rather than a second pawn being bound alongside the first.
EnsureGameplayBound(GetBoundWorld());
}
void UQuestSubsystem::HandleDialogueSpeakerChanged(AActor* Speaker)
{
// Fired both when a conversation starts and when it ends: the end comes with no speaker, and a
// conversation nobody is being held with is not something a talk step can count.
NotifyDialogueStarted(Speaker);
}
void UQuestSubsystem::HandleDialogueGameplayEvent(FGameplayTag GameplayTag)
{
// A dialogue event does not name who fired it, so whoever is being talked to at this moment, if
// anyone, is where it came from.
AActor* Speaker = nullptr;
if (UGameInstance* GameInstance = GetGameInstance())
{
if (const UDialogueSubsystem* DialogueSubsystem = GameInstance->GetSubsystem<UDialogueSubsystem>())
{
Speaker = DialogueSubsystem->GetActiveSpeaker();
}
}
NotifyDialogueEvent(GameplayTag, Speaker);
}
void UQuestSubsystem::HandleInventoryUpdated()
{
NotifyInventoryChanged();
}
void UQuestSubsystem::HandlePlayerInteract()
{
UWorld* World = GetBoundWorld();
if (World == nullptr)
{
return;
}
// The player character's interaction delegate carries nothing, so the actor is taken from the
// interaction system, which is the one the player had under the crosshair when they pressed the key.
// Anything that knows exactly what was used calls NotifyInteraction directly instead.
AActor* Interactable = UInteractionSubsystem::GetClosestInteractable(World);
NotifyInteraction(Interactable, BoundCharacter.Get());
}
void UQuestSubsystem::HandleDayPassed()
{
// Deadlines are counted in days, and a day only turns over on this one call.
RefreshActiveQuests();
}
void UQuestSubsystem::PrepareSaveData(TArray<FQuestSaveEntry>& OutEntries) const
{
OutEntries.Reset();
for (const TPair<FGameplayTag, FQuestRuntimeState>& Pair : QuestStates)
{
const FQuestRuntimeState& State = Pair.Value;
if (State.Quest == nullptr)
{
continue;
}
// An unoffered quest is nothing the player has done, so it is not written: whether it can be
// taken is worked out from its requirements the moment the save is loaded, which is also what
// keeps a save from freezing a quest into a state its conditions have since moved past.
if (State.Status == EQuestStatus::Unavailable)
{
continue;
}
FQuestSaveEntry Entry;
Entry.QuestPath = FSoftObjectPath(State.Quest);
Entry.Status = State.Status;
Entry.AcceptedDay = State.AcceptedDay;
Entry.CompletedDay = State.CompletedDay;
for (const FQuestObjectiveProgress& Progress : State.Objectives)
{
FQuestObjectiveSave ObjectiveSave;
ObjectiveSave.ObjectiveId = Progress.ObjectiveId;
ObjectiveSave.CurrentCount = Progress.CurrentCount;
ObjectiveSave.bComplete = Progress.bComplete;
Entry.Objectives.Add(ObjectiveSave);
}
OutEntries.Add(Entry);
}
}
void UQuestSubsystem::ApplySaveData(const TArray<FQuestSaveEntry>& Entries)
{
QuestStates.Reset();
for (const FQuestSaveEntry& Entry : Entries)
{
// Loaded by path rather than by pointer, and a quest that is no longer in the project is left
// out with a word in the log: an author who deletes a quest should not be able to break a save
// that was made while it existed.
UQuestDataAsset* Quest = Cast<UQuestDataAsset>(Entry.QuestPath.TryLoad());
if (Quest == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("A quest in this save ('%s') is no longer in the project. Its progress is dropped."),
*Entry.QuestPath.ToString());
continue;
}
if (Entry.Status == EQuestStatus::Unavailable || Entry.Status == EQuestStatus::Available)
{
// Nothing was accepted, so there is nothing to put back: the offer is a question asked of
// the requirements, and it is asked again the first time anybody looks.
continue;
}
FQuestRuntimeState State;
State.Quest = Quest;
State.Status = Entry.Status;
State.AcceptedDay = Entry.AcceptedDay;
State.CompletedDay = Entry.CompletedDay;
for (const FQuestObjectiveSave& ObjectiveSave : Entry.Objectives)
{
FQuestObjectiveProgress Progress;
Progress.ObjectiveId = ObjectiveSave.ObjectiveId;
Progress.CurrentCount = ObjectiveSave.CurrentCount;
Progress.bComplete = ObjectiveSave.bComplete;
// Filled in from the asset below, so that a step added to a quest after this save was made
// is not left without a record of its own.
Progress.bCountedFromInventory = false;
State.Objectives.Add(Progress);
}
// A step that is in the asset but not in the save, because it was added after the save was
// taken, starts where a new quest starts: at zero.
for (const FQuestObjective& Objective : Quest->Objectives)
{
FQuestObjectiveProgress* Progress = State.FindProgress(Objective.ObjectiveId);
if (Progress == nullptr)
{
FQuestObjectiveProgress NewProgress;
NewProgress.ObjectiveId = Objective.ObjectiveId;
State.Objectives.Add(NewProgress);
Progress = &State.Objectives.Last();
}
Progress->bCountedFromInventory = Objective.StepType == EQuestStepType::AcquireItems
|| Objective.StepType == EQuestStepType::DeliverItems;
}
QuestStates.Add(Quest->QuestTag, State);
}
// The steps counted from the inventory are read again from what the player is actually carrying: a
// save taken half way through a delivery must not remember an item the player has since sold. A
// load that happens before the player exists leaves that to the next refresh, which the world
// binding performs as soon as there is a pawn to read.
RefreshActiveQuests();
UE_LOG(LogTemp, Log, TEXT("Quest subsystem restored %d quests from a save."), QuestStates.Num());
}