#include #include #include #include #include #include #include #include #include #include #include static TESQuest* FindQuestByNameId(const String& name) { auto& questRegistry = ModManager::Get()->quests; auto it = std::find_if(questRegistry.begin(), questRegistry.end(), [name](auto* it) { return std::strcmp(it->idName.AsAscii(), name.c_str()); }); return it != questRegistry.end() ? *it : nullptr; } QuestService::QuestService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) { m_joinedConnection = aDispatcher.sink().connect<&QuestService::OnConnected>(this); m_questUpdateConnection = aDispatcher.sink().connect<&QuestService::OnQuestUpdate>(this); // A note about the Gameevents: // TESQuestStageItemDoneEvent gets fired to late, we instead use TESQuestStageEvent, because it responds immediately. // TESQuestInitEvent can be instead managed by start stop quest management. // bind game event listeners auto* pEventList = EventDispatcherManager::Get(); pEventList->questStartStopEvent.RegisterSink(this); pEventList->questStageEvent.RegisterSink(this); } void QuestService::OnConnected(const ConnectedEvent&) noexcept { // TODO: this should be followed with whatever the quest leader selected /* // deselect any active quests auto* pPlayer = PlayerCharacter::Get(); for (auto& objective : pPlayer->objectives) { if (auto* pQuest = objective.instance->quest) pQuest->SetActive(false); } */ } BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, const EventDispatcher*) { if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) return BSTEventResult::kOk; spdlog::info("Quest start/stop event: {:X}", apEvent->formId); if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) { if (IsNonSyncableQuest(pQuest)) return BSTEventResult::kOk; if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) { // Perhaps redundant, but necessary. We need the logging and // the lambda coming up is queued and runs later GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(pQuest->formID, Id)) { spdlog::info(__FUNCTION__ ": queuing type none/misc quest gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}", Id.LogFormat(), pQuest->currentStage, pQuest->IsStopped() ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started, static_cast>(pQuest->type), pQuest->formID, pQuest->fullName.value.AsAscii()); } } m_world.GetRunner().Queue( [&, formId = pQuest->formID, stageId = pQuest->currentStage, stopped = pQuest->IsStopped(), type = pQuest->type]() { GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(formId, Id)) { RequestQuestUpdate update; update.Id = Id; update.Stage = stageId; update.Status = stopped ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started; update.ClientQuestType = static_cast>(type); m_world.GetTransport().Send(update); } }); } return BSTEventResult::kOk; } BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const EventDispatcher*) { if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) return BSTEventResult::kOk; spdlog::info("Quest stage event: {:X}, stage: {}", apEvent->formId, apEvent->stageId); // there is no reason to even fetch the quest object, since the event provides everything already.... if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) { if (IsNonSyncableQuest(pQuest)) return BSTEventResult::kOk; if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) { // Perhaps redundant, but necessary. We need the logging and // the lambda coming up is queued and runs later GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(pQuest->formID, Id)) { spdlog::info(__FUNCTION__ ": queuing type none/misc quest gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}", Id.LogFormat(), pQuest->currentStage, RequestQuestUpdate::StageUpdate, static_cast>(pQuest->type), pQuest->formID, pQuest->fullName.value.AsAscii()); } } m_world.GetRunner().Queue( [&, formId = apEvent->formId, stageId = apEvent->stageId, type = pQuest->type]() { GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(formId, Id)) { RequestQuestUpdate update; update.Id = Id; update.Stage = stageId; update.Status = RequestQuestUpdate::StageUpdate; update.ClientQuestType = static_cast>(type); m_world.GetTransport().Send(update); } }); } return BSTEventResult::kOk; } void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { ModSystem& modSystem = World::Get().GetModSystem(); uint32_t formId = modSystem.GetGameId(aUpdate.Id); TESQuest* pQuest = Cast(TESForm::GetById(formId)); if (!pQuest) { spdlog::error("Failed to find quest, base id: {:X}, mod id: {:X}", aUpdate.Id.BaseId, aUpdate.Id.ModId); return; } if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) { spdlog::info(__FUNCTION__ ": receiving type none/misc quest update gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}", aUpdate.Id.LogFormat(), aUpdate.Stage, aUpdate.Status, aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii()); } bool bResult = false; switch (aUpdate.Status) { case NotifyQuestUpdate::Started: { pQuest->ScriptSetStage(aUpdate.Stage); pQuest->SetActive(true); bResult = true; spdlog::info("Remote quest started: {:X}, stage: {}", formId, aUpdate.Stage); break; } case NotifyQuestUpdate::StageUpdate: pQuest->ScriptSetStage(aUpdate.Stage); bResult = true; spdlog::info("Remote quest updated: {:X}, stage: {}", formId, aUpdate.Stage); break; case NotifyQuestUpdate::Stopped: bResult = StopQuest(formId); spdlog::info("Remote quest stopped: {:X}, stage: {}", formId, aUpdate.Stage); break; default: break; } if (!bResult) spdlog::error("Failed to update the client quest state, quest: {:X}, stage: {}, status: {}", formId, aUpdate.Stage, aUpdate.Status); } bool QuestService::StopQuest(uint32_t aformId) { TESQuest* pQuest = Cast(TESForm::GetById(aformId)); if (pQuest) { pQuest->SetActive(false); pQuest->SetStopped(); return true; } return false; } static constexpr std::array kNonSyncableQuestIds = std::to_array({ 0x2BA16, // Werewolf transformation quest 0x20071D0, // Vampire transformation quest 0x3AC44, // MS13BleakFallsBarrowLeverScene // 0xFE014801, // Unknown dynamic ID, kept as note, maybe lookup correct ID this game? 0xF2593 // Skill experience quest }); bool QuestService::IsNonSyncableQuest(TESQuest* apQuest) { // Quests with no quest stages are never synced. Most TESQues::Type:: quests should // be synced, including Type::None and Type::Miscellaneous, but there are a few // known exceptions that should be excluded that are in the table. return apQuest->stages.Empty() || std::find(kNonSyncableQuestIds.begin(), kNonSyncableQuestIds.end(), apQuest->formID) != kNonSyncableQuestIds.end(); } void QuestService::DebugDumpQuests() { auto& quests = ModManager::Get()->quests; for (TESQuest* pQuest : quests) spdlog::info("{:X}|{}|{}|{}", pQuest->formID, (uint8_t)pQuest->type, pQuest->priority, pQuest->idName.AsAscii()); }