diff --git a/Code/client/Games/Animation.cpp b/Code/client/Games/Animation.cpp index 178caef13..4e54f8f16 100644 --- a/Code/client/Games/Animation.cpp +++ b/Code/client/Games/Animation.cpp @@ -20,6 +20,9 @@ static TPerformAction* RealPerformAction; // TODO: make scoped override thread_local bool g_forceAnimation = false; +// This is where the Actors AI is enabled/disabled: almost all of NPC AI/behavior is +// determined by Actions that are run on them. + uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction) { auto pActor = apAction->actor; diff --git a/Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp b/Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp index 884b859a9..67f5ec195 100644 --- a/Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp +++ b/Code/client/Games/Skyrim/AI/Movement/PlayerControls.cpp @@ -19,6 +19,13 @@ void PlayerControls::SetCamSwitch(bool aSet) noexcept Data.remapMode = aSet; } +bool PlayerControls::IsMovementControlsEnabled() noexcept +{ + using TIsMovementControlsEnabled = bool(); + POINTER_SKYRIMSE(TIsMovementControlsEnabled, s_isMovementControlsEnabled, 55485); + return s_isMovementControlsEnabled.Get()(); +} + BSInputEnableManager* BSInputEnableManager::Get() { POINTER_SKYRIMSE(BSInputEnableManager*, s_instance, 400863); diff --git a/Code/client/Games/Skyrim/AI/Movement/PlayerControls.h b/Code/client/Games/Skyrim/AI/Movement/PlayerControls.h index 17c1f8101..4ce1c4d04 100644 --- a/Code/client/Games/Skyrim/AI/Movement/PlayerControls.h +++ b/Code/client/Games/Skyrim/AI/Movement/PlayerControls.h @@ -42,6 +42,8 @@ struct PlayerControls void SetCamSwitch(bool aSet) noexcept; + static bool IsMovementControlsEnabled() noexcept; + public: char pad0[0x20]; PlayerControlsData Data; diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 931c2bdf1..de47e1602 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -1215,8 +1215,6 @@ static TSpeakSoundFunction* RealSpeakSoundFunction = nullptr; bool TP_MAKE_THISCALL(HookSpeakSoundFunction, Actor, const char* apName, uint32_t* a3, uint32_t a4, uint32_t a5, uint32_t a6, uint64_t a7, uint64_t a8, uint64_t a9, bool a10, uint64_t a11, bool a12, bool a13, bool a14) { - spdlog::debug("a3: {:X}, a4: {}, a5: {}, a6: {}, a7: {}, a8: {:X}, a9: {:X}, a10: {}, a11: {:X}, a12: {}, a13: {}, a14: {}", (uint64_t)a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14); - if (apThis->GetExtension()->IsLocal()) World::Get().GetRunner().Trigger(DialogueEvent(apThis->formID, apName)); diff --git a/Code/client/Games/Skyrim/Events/EventDispatcher.h b/Code/client/Games/Skyrim/Events/EventDispatcher.h index e7a9a8d96..e7663c6f5 100644 --- a/Code/client/Games/Skyrim/Events/EventDispatcher.h +++ b/Code/client/Games/Skyrim/Events/EventDispatcher.h @@ -190,14 +190,29 @@ struct TESResolveNPCTemplatesEvent struct TESSceneEvent { + void* ref; + uint32_t sceneFormId; + uint32_t sceneType; // BEGIN (0) or END (1) }; +// RE'd fields in TESSceneActionEvent and TESScenePhaseEvent are probably incorrect + struct TESSceneActionEvent { + void* ref; + uint32_t sceneFormId; + uint32_t actionIndex; + uint32_t questFormId; + uint32_t actorAliasId; }; struct TESScenePhaseEvent { + uint32_t sceneFormId; + uint32_t phaseIndex; + uint32_t sceneType; // BEGIN (0) or END (1) + uint16_t questStageId; + void* callback; }; struct TESSellEvent diff --git a/Code/client/Games/Skyrim/Forms/TESQuest.cpp b/Code/client/Games/Skyrim/Forms/TESQuest.cpp index 6f4feeb4b..dc834b8fc 100644 --- a/Code/client/Games/Skyrim/Forms/TESQuest.cpp +++ b/Code/client/Games/Skyrim/Forms/TESQuest.cpp @@ -54,13 +54,9 @@ void TESQuest::SetActive(bool toggle) bool TESQuest::IsStageDone(uint16_t stageIndex) { - for (Stage* it : stages) - { - if (it->stageIndex == stageIndex) - return it->IsDone(); - } - - return false; + TP_THIS_FUNCTION(TIsStageDone, bool, TESQuest, uint16_t); + POINTER_SKYRIMSE(TIsStageDone, IsStageDone, 25011); + return IsStageDone(this, stageIndex); } bool TESQuest::Kill() @@ -88,23 +84,28 @@ bool TESQuest::EnsureQuestStarted(bool& success, bool force) return SetRunning(this, &success, force); } -bool TESQuest::SetStage(uint16_t newStage) +bool TESQuest::SetStage(uint16_t stageIndex) { ScopedQuestOverride _; TP_THIS_FUNCTION(TSetStage, bool, TESQuest, uint16_t); POINTER_SKYRIMSE(TSetStage, SetStage, 25004); - return SetStage(this, newStage); + return SetStage(this, stageIndex); } void TESQuest::ScriptSetStage(uint16_t stageIndex) { + spdlog::debug(__FUNCTION__ ": called with a value of {}", stageIndex); if (currentStage == stageIndex || IsStageDone(stageIndex)) + { + spdlog::debug("Stage {} is already done, not calling SetCurrentStageID", stageIndex); return; + } using Quest = TESQuest; PAPYRUS_FUNCTION(void, Quest, SetCurrentStageID, int); s_pSetCurrentStageID(this, stageIndex); + spdlog::debug(__FUNCTION__ ": stage has been set to {}", stageIndex); } void TESQuest::SetStopped() @@ -113,6 +114,24 @@ void TESQuest::SetStopped() MarkChanged(2); } +bool TESQuest::IsAnyCutscenePlaying() +{ + for (const auto& scene : scenes) + { + if (scene->isPlaying) + return true; + } + return false; +} + +void BGSScene::ScriptForceStart() +{ + using Scene = BGSScene; + PAPYRUS_FUNCTION(void, Scene, ForceStart); + s_pForceStart(this); + spdlog::debug(__FUNCTION__ ": force started scene {:X}, isPlaying? {}", formID, isPlaying); +} + static TiltedPhoques::Initializer s_questInitHooks( []() { diff --git a/Code/client/Games/Skyrim/Forms/TESQuest.h b/Code/client/Games/Skyrim/Forms/TESQuest.h index 6c121e202..d82f2a22f 100644 --- a/Code/client/Games/Skyrim/Forms/TESQuest.h +++ b/Code/client/Games/Skyrim/Forms/TESQuest.h @@ -3,14 +3,42 @@ #include #include #include +#include #include +struct BGSSceneAction +{ + virtual ~BGSSceneAction(); + + uint32_t actorID; + uint16_t startPhase; + uint16_t endPhase; + uint32_t flags; + uint8_t status; + + void Start() { this->status |= 1u; } +}; + +static_assert(offsetof(BGSSceneAction, flags) == 0x10); + struct BGSScene : TESForm { GameArray phases; GameArray actorIds; + GameArray actorFlags; + GameArray actorProgressionFlags; + GameArray actions; + TESQuest* owningQuest; + uint32_t flags; + uint32_t padA4; + TESCondition conditions; + bool isPlaying; + + void ScriptForceStart(); }; +static_assert(offsetof(BGSScene, isPlaying) == 0xB0); + struct TESQuest : BGSStoryManagerTreeForm { enum class State : uint8_t @@ -76,6 +104,10 @@ struct TESQuest : BGSStoryManagerTreeForm uint16_t stageIndex; uint8_t flags; + operator bool() const + { + return *reinterpret_cast(this) != 0; + } inline bool IsDone() { return flags & 1; } }; @@ -90,11 +122,9 @@ struct TESQuest : BGSStoryManagerTreeForm Type type; // 0x00DF int32_t scopedStatus; // 0x00E0 default init: -1, if not -1 outside of story manager scope uint32_t padE4; - GameList stages; - /* - GameList* pExecutedStages; // 0x00E8 - GameList* pWaitingStages; // 0x00F0 - */ + //GameList stages; + GameValueList* pExecutedStages; // 0x00E8 + GameValueList* pWaitingStages; // 0x00F0 GameList objectives; // 0x00F8 char pad108[0x100]; // 0x0108 GameArray scenes; // 0x0208 @@ -126,15 +156,16 @@ struct TESQuest : BGSStoryManagerTreeForm bool EnsureQuestStarted(bool& succeded, bool force); - bool SetStage(uint16_t stage); + bool SetStage(uint16_t stageIndex); void ScriptSetStage(uint16_t stage); void SetStopped(); + bool IsAnyCutscenePlaying(); }; static_assert(sizeof(TESQuest) == 0x268); static_assert(offsetof(TESQuest, fullName) == 0x28); static_assert(offsetof(TESQuest, flags) == 0xDC); -static_assert(offsetof(TESQuest, stages) == 0xE8); +static_assert(offsetof(TESQuest, pExecutedStages) == 0xE8); static_assert(offsetof(TESQuest, objectives) == 0xF8); static_assert(offsetof(TESQuest, currentStage) == 0x228); static_assert(offsetof(TESQuest, unkFlags) == 0x248); diff --git a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp index ed5978c13..58593c0b8 100644 --- a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp +++ b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp @@ -68,7 +68,7 @@ bool MagicTarget::AddTargetData::IsForbiddenEffect(Actor* apTarget) if (apTarget != PlayerCharacter::Get()) return false; - return pEffectItem->IsNightVisionEffect(); + return pEffectItem->IsNightVisionEffect() || pEffectItem->IsSlowEffect(); } Actor* MagicTarget::GetTargetAsActor() diff --git a/Code/client/Services/Debug/Views/QuestDebugView.cpp b/Code/client/Services/Debug/Views/QuestDebugView.cpp index 383f6e30a..9f2cc4220 100644 --- a/Code/client/Services/Debug/Views/QuestDebugView.cpp +++ b/Code/client/Services/Debug/Views/QuestDebugView.cpp @@ -53,8 +53,9 @@ void DebugService::DrawQuestDebugView() if (ImGui::CollapsingHeader("Stages")) { - for (auto* pStage : pQuest->stages) + for (auto& stage : *pQuest->pExecutedStages) { + auto pStage = &stage; ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex, pStage->IsDone() ? "true" : "false"); char setStage[64]; @@ -64,6 +65,36 @@ void DebugService::DrawQuestDebugView() pQuest->ScriptSetStage(pStage->stageIndex); } } + if (ImGui::CollapsingHeader("Waiting Stages")) + { + for (auto& pStage : *pQuest->pWaitingStages) + { + ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Stage: %d, is done? %s", pStage->stageIndex, pStage->IsDone() ? "true" : "false"); + + char setStage[64]; + sprintf_s(setStage, std::size(setStage), "Set stage (%d)", pStage->stageIndex); + + if (ImGui::Button(setStage)) + pQuest->ScriptSetStage(pStage->stageIndex); + } + } + + if (ImGui::CollapsingHeader("Scenes")) + { + for (auto& pScene : pQuest->scenes) + { + ImGui::TextColored({0.f, 255.f, 255.f, 255.f}, "Scene Form ID: %x, is playing? %s", pScene->formID, pScene->isPlaying ? "true" : "false"); + + ImGui::Text("Scene actions:"); + for (int i = 0; i < pScene->actions.length; ++i) + { + char startAction[64]; + sprintf_s(startAction, std::size(startAction), "Start action %d", i); + if (ImGui::Button(startAction)) + pScene->actions[i]->Start(); + } + } + } if (ImGui::CollapsingHeader("Actors")) { diff --git a/Code/client/Services/Generic/InputService.cpp b/Code/client/Services/Generic/InputService.cpp index 558ead849..cd191bc11 100644 --- a/Code/client/Services/Generic/InputService.cpp +++ b/Code/client/Services/Generic/InputService.cpp @@ -189,8 +189,6 @@ void ProcessKeyboard(uint16_t aKey, uint16_t aScanCode, cef_key_event_type_t aTy const auto active = overlay.GetActive(); - spdlog::debug("ProcessKey, type: {}, key: {}, active: {}", aType, aKey, active); - if (aType != KEYEVENT_CHAR && (IsToggleKey(aKey) || (IsDisableKey(aKey) && active))) { if (!overlay.GetInGame()) diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index d5d5bb45f..bdb7ac528 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -358,7 +358,9 @@ void MagicService::OnAddTargetEvent(const AddTargetEvent& acEvent) noexcept m_transport.Send(request); - spdlog::debug("Sending effect sync request"); + spdlog::debug("Sending effect sync request. SpellId={:X} ({:X} / {:X}), EffectId={:X} ({:X} / {:X})", + request.SpellId.LogFormat(), request.SpellId.BaseId, request.SpellId.ModId, + request.EffectId.LogFormat(), request.EffectId.BaseId, request.EffectId.ModId); } void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept diff --git a/Code/client/Services/Generic/QuestService.cpp b/Code/client/Services/Generic/QuestService.cpp index b32f501be..cbee74a55 100644 --- a/Code/client/Services/Generic/QuestService.cpp +++ b/Code/client/Services/Generic/QuestService.cpp @@ -9,11 +9,14 @@ #include #include #include +#include #include #include +#include #include +#include static TESQuest* FindQuestByNameId(const String& name) { @@ -28,6 +31,7 @@ QuestService::QuestService(World& aWorld, entt::dispatcher& aDispatcher) { m_joinedConnection = aDispatcher.sink().connect<&QuestService::OnConnected>(this); m_questUpdateConnection = aDispatcher.sink().connect<&QuestService::OnQuestUpdate>(this); + m_questSceneUpdateConnection = aDispatcher.sink().connect<&QuestService::OnQuestSceneUpdate>(this); // A note about the Gameevents: // TESQuestStageItemDoneEvent gets fired to late, we instead use TESQuestStageEvent, because it responds immediately. @@ -36,6 +40,10 @@ QuestService::QuestService(World& aWorld, entt::dispatcher& aDispatcher) auto* pEventList = EventDispatcherManager::Get(); pEventList->questStartStopEvent.RegisterSink(this); pEventList->questStageEvent.RegisterSink(this); + + pEventList->scenePhaseEvent.RegisterSink(this); + pEventList->sceneActionEvent.RegisterSink(this); + pEventList->sceneEvent.RegisterSink(this); } void QuestService::OnConnected(const ConnectedEvent&) noexcept @@ -54,16 +62,19 @@ void QuestService::OnConnected(const ConnectedEvent&) noexcept BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, const EventDispatcher*) { - if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) + if (!m_world.Get().GetPartyService().IsInParty()) + { + spdlog::debug("(Local) TESQuestStartStopEvent: not in party, quest stage advancement won't be sent. Returning."); return BSTEventResult::kOk; + } - spdlog::info("Quest start/stop event: {:X}", apEvent->formId); + spdlog::info("(Local) TESQuestStartStopEvent: 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 @@ -72,13 +83,12 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons 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()); + 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]() { @@ -90,7 +100,7 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons update.Id = Id; update.Stage = stageId; update.Status = stopped ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started; - update.ClientQuestType = static_cast>(type); + update.ClientQuestType = static_cast>(type); m_world.GetTransport().Send(update); } @@ -102,10 +112,18 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const EventDispatcher*) { - if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) + if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) + { + spdlog::info("(Local) TESQuestStageEvent: \"{}\" - in cutscene? {}", pQuest->GetName(), pQuest->IsAnyCutscenePlaying()); + } + + if (!CanAdvanceQuestForParty()) + { + spdlog::warn("(Local) TESQuestStageEvent: quest stage advancement won't be sent: either not in party, or a non-leader with disabled controls."); return BSTEventResult::kOk; + } - spdlog::info("Quest stage event: {:X}, stage: {}", apEvent->formId, apEvent->stageId); + spdlog::info("(Local) TESQuestStageEvent: {:X}, stage: {}. Sending to server.", 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))) @@ -121,11 +139,9 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev 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()); + 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()); } } @@ -150,6 +166,60 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev return BSTEventResult::kOk; } +BSTEventResult QuestService::OnEvent(const TESSceneEvent* apEvent, const EventDispatcher*) +{ + const String sceneType = apEvent->sceneType == 0 ? "Begin" : "End"; + spdlog::info("(Local) TESSceneEvent event: scene formID {:X}, type {}", apEvent->sceneFormId, sceneType); + + if (!m_world.GetPartyService().IsInParty() || !m_world.GetPartyService().IsLeader()) + { + spdlog::warn("(Local) TESSceneEvent: scene update won't be sent: either not in party or not leader!"); + return BSTEventResult::kAbort; + } + + if (BGSScene* pScene = Cast(TESForm::GetById(apEvent->sceneFormId))) + { + m_world.GetRunner().Queue( + [&, sceneId = apEvent->sceneFormId, questId = pScene->owningQuest->formID]() + { + GameId sceneGameId; + GameId questGameId; + auto& modSys = m_world.GetModSystem(); + if (modSys.GetServerModId(sceneId, sceneGameId) && modSys.GetServerModId(questId, questGameId)) + { + RequestQuestSceneUpdate update; + update.SceneId = sceneGameId; + update.QuestId = questGameId; + + m_world.GetTransport().Send(update); + } + }); + } + + return BSTEventResult::kOk; +} + +BSTEventResult QuestService::OnEvent(const TESSceneActionEvent* apEvent, const EventDispatcher*) +{ + spdlog::info("TESSceneActionEvent: scene id {:X}, action index {}", apEvent->sceneFormId, apEvent->actionIndex); + + if (!m_world.GetPartyService().IsLeader()) + return BSTEventResult::kAbort; + + return BSTEventResult::kOk; +} + +BSTEventResult QuestService::OnEvent(const TESScenePhaseEvent* apEvent, const EventDispatcher*) +{ + const String sceneType = apEvent->sceneType == 0 ? "Begin" : "End"; + spdlog::info("TESScenePhaseEvent event: quest stage {}, phase index {}, type {}", apEvent->questStageId, apEvent->phaseIndex, sceneType); + + if (!m_world.GetPartyService().IsLeader()) + return BSTEventResult::kAbort; + + return BSTEventResult::kOk; +} + void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { ModSystem& modSystem = World::Get().GetModSystem(); @@ -163,9 +233,7 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept 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()); + 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; @@ -173,20 +241,20 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { case NotifyQuestUpdate::Started: { + spdlog::info("(NotifyQuestUpdate) Remote quest started. Starting local quest {:X}, stage: {}", formId, aUpdate.Stage); pQuest->ScriptSetStage(aUpdate.Stage); pQuest->SetActive(true); bResult = true; - spdlog::info("Remote quest started: {:X}, stage: {}", formId, aUpdate.Stage); break; } case NotifyQuestUpdate::StageUpdate: + spdlog::info("(NotifyQuestUpdate) Remote quest updated. Updating local quest {:X}, stage: {}", formId, aUpdate.Stage); pQuest->ScriptSetStage(aUpdate.Stage); bResult = true; - spdlog::info("Remote quest updated: {:X}, stage: {}", formId, aUpdate.Stage); break; case NotifyQuestUpdate::Stopped: + spdlog::info("(NotifyQuestUpdate) Remote quest stopped. Stopping local quest {:X}, stage: {}", formId, aUpdate.Stage); bResult = StopQuest(formId); - spdlog::info("Remote quest stopped: {:X}, stage: {}", formId, aUpdate.Stage); break; default: break; } @@ -195,6 +263,36 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept spdlog::error("Failed to update the client quest state, quest: {:X}, stage: {}, status: {}", formId, aUpdate.Stage, aUpdate.Status); } +void QuestService::OnQuestSceneUpdate(const NotifyQuestSceneUpdate& aUpdate) noexcept +{ + ModSystem& modSystem = World::Get().GetModSystem(); + TESQuest* pQuest = Cast(TESForm::GetById(modSystem.GetGameId(aUpdate.QuestId))); + if (!pQuest) + { + spdlog::error("Failed to find quest, base id: {:X}, mod id: {:X}", aUpdate.QuestId.BaseId, aUpdate.QuestId.ModId); + return; + } + BGSScene* pScene = Cast(TESForm::GetById(modSystem.GetGameId(aUpdate.SceneId))); + if (!pScene) + { + spdlog::error("Failed to find scene, base id: {:X}, mod id: {:X}", aUpdate.SceneId.BaseId, aUpdate.SceneId.ModId); + return; + } + + spdlog::debug("Force starting scene {}", aUpdate.QuestId.LogFormat()); + pScene->ScriptForceStart(); +} + +bool QuestService::CanAdvanceQuestForParty() const noexcept +{ + const bool isInParty = m_world.Get().GetPartyService().IsInParty(); + // Party leaders can always advance quests. + // Members can only advance quest stages when their controls are enabled (needed for scripted cutscenes to work properly) + const bool canAdvanceQuestStages = m_world.Get().GetPartyService().IsLeader() || PlayerControls::IsMovementControlsEnabled(); + + return isInParty && canAdvanceQuestStages; +} + bool QuestService::StopQuest(uint32_t aformId) { TESQuest* pQuest = Cast(TESForm::GetById(aformId)); @@ -221,8 +319,7 @@ 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(); + return apQuest->pExecutedStages->Empty() || std::find(kNonSyncableQuestIds.begin(), kNonSyncableQuestIds.end(), apQuest->formID) != kNonSyncableQuestIds.end(); } void QuestService::DebugDumpQuests() diff --git a/Code/client/Services/QuestService.h b/Code/client/Services/QuestService.h index b4f1b58fc..1fff9432c 100644 --- a/Code/client/Services/QuestService.h +++ b/Code/client/Services/QuestService.h @@ -1,19 +1,18 @@ #pragma once -#include #include #include +#include struct NotifyQuestUpdate; +struct NotifyQuestSceneUpdate; struct TESQuest; /** * @brief Handles quest sync - * - * This service is currently not in use. */ -class QuestService final : public BSTEventSink, BSTEventSink +class QuestService final : public BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink { public: QuestService(World&, entt::dispatcher&); @@ -30,12 +29,19 @@ class QuestService final : public BSTEventSink, BSTEvent BSTEventResult OnEvent(const TESQuestStartStopEvent*, const EventDispatcher*) override; BSTEventResult OnEvent(const TESQuestStageEvent*, const EventDispatcher*) override; + BSTEventResult OnEvent(const TESSceneEvent*, const EventDispatcher*) override; + BSTEventResult OnEvent(const TESSceneActionEvent*, const EventDispatcher*) override; + BSTEventResult OnEvent(const TESScenePhaseEvent*, const EventDispatcher*) override; void OnQuestUpdate(const NotifyQuestUpdate&) noexcept; + void OnQuestSceneUpdate(const NotifyQuestSceneUpdate&) noexcept; + + bool CanAdvanceQuestForParty() const noexcept; World& m_world; entt::scoped_connection m_joinedConnection; entt::scoped_connection m_leftConnection; entt::scoped_connection m_questUpdateConnection; + entt::scoped_connection m_questSceneUpdateConnection; }; diff --git a/Code/encoding/Messages/ClientMessageFactory.h b/Code/encoding/Messages/ClientMessageFactory.h index 30e7984cd..d90eb0d22 100644 --- a/Code/encoding/Messages/ClientMessageFactory.h +++ b/Code/encoding/Messages/ClientMessageFactory.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +67,7 @@ struct ClientMessageFactory template static auto Visit(T&& func) { auto s_visitor = CreateMessageVisitor< - AuthenticationRequest, AssignCharacterRequest, CancelAssignmentRequest, ClientReferencesMoveRequest, EnterInteriorCellRequest, RequestInventoryChanges, RequestFactionsChanges, RequestQuestUpdate, PartyInviteRequest, PartyAcceptInviteRequest, PartyLeaveRequest, PartyCreateRequest, + AuthenticationRequest, AssignCharacterRequest, CancelAssignmentRequest, ClientReferencesMoveRequest, EnterInteriorCellRequest, RequestInventoryChanges, RequestFactionsChanges, RequestQuestUpdate, RequestQuestSceneUpdate, PartyInviteRequest, PartyAcceptInviteRequest, PartyLeaveRequest, PartyCreateRequest, PartyChangeLeaderRequest, PartyKickRequest, RequestActorValueChanges, RequestActorMaxValueChanges, EnterExteriorCellRequest, RequestHealthChangeBroadcast, ActivateRequest, LockChangeRequest, AssignObjectsRequest, RequestDeathStateChange, ShiftGridCellRequest, RequestOwnershipTransfer, RequestOwnershipClaim, RequestObjectInventoryChanges, SpellCastRequest, ProjectileLaunchRequest, InterruptCastRequest, AddTargetRequest, ScriptAnimationRequest, DrawWeaponRequest, MountRequest, NewPackageRequest, RequestRespawn, SyncExperienceRequest, RequestEquipmentChanges, SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, diff --git a/Code/encoding/Messages/NotifyQuestSceneUpdate.cpp b/Code/encoding/Messages/NotifyQuestSceneUpdate.cpp new file mode 100644 index 000000000..aba3c91d7 --- /dev/null +++ b/Code/encoding/Messages/NotifyQuestSceneUpdate.cpp @@ -0,0 +1,16 @@ + +#include +#include + +void NotifyQuestSceneUpdate::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + SceneId.Serialize(aWriter); + QuestId.Serialize(aWriter); +} + +void NotifyQuestSceneUpdate::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + SceneId.Deserialize(aReader); + QuestId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyQuestSceneUpdate.h b/Code/encoding/Messages/NotifyQuestSceneUpdate.h new file mode 100644 index 000000000..f00f54a73 --- /dev/null +++ b/Code/encoding/Messages/NotifyQuestSceneUpdate.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Message.h" +#include + +struct NotifyQuestSceneUpdate final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyQuestSceneUpdate; + + NotifyQuestSceneUpdate() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyQuestSceneUpdate& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && SceneId == acRhs.SceneId && QuestId == acRhs.QuestId; } + + GameId SceneId; + GameId QuestId; +}; diff --git a/Code/encoding/Messages/RequestQuestSceneUpdate.cpp b/Code/encoding/Messages/RequestQuestSceneUpdate.cpp new file mode 100644 index 000000000..f31682d2e --- /dev/null +++ b/Code/encoding/Messages/RequestQuestSceneUpdate.cpp @@ -0,0 +1,16 @@ + +#include +#include + +void RequestQuestSceneUpdate::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + SceneId.Serialize(aWriter); + QuestId.Serialize(aWriter); +} + +void RequestQuestSceneUpdate::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + SceneId.Deserialize(aReader); + QuestId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RequestQuestSceneUpdate.h b/Code/encoding/Messages/RequestQuestSceneUpdate.h new file mode 100644 index 000000000..c00a1bfc6 --- /dev/null +++ b/Code/encoding/Messages/RequestQuestSceneUpdate.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Message.h" +#include + +struct RequestQuestSceneUpdate final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestQuestSceneUpdate; + + RequestQuestSceneUpdate() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestQuestSceneUpdate& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && SceneId == acRhs.SceneId && QuestId == acRhs.QuestId; } + + GameId SceneId; + GameId QuestId; +}; diff --git a/Code/encoding/Messages/ServerMessageFactory.h b/Code/encoding/Messages/ServerMessageFactory.h index 942c30fec..d0ed6437b 100644 --- a/Code/encoding/Messages/ServerMessageFactory.h +++ b/Code/encoding/Messages/ServerMessageFactory.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -70,7 +71,7 @@ struct ServerMessageFactory template static auto Visit(T&& func) { auto s_visitor = CreateMessageVisitor< - AuthenticationResponse, AssignCharacterResponse, ServerReferencesMoveRequest, ServerTimeSettings, CharacterSpawnRequest, NotifyInventoryChanges, StringCacheUpdate, NotifyFactionsChanges, NotifyRemoveCharacter, NotifyQuestUpdate, NotifyPlayerList, NotifyPartyInfo, NotifyPartyInvite, + AuthenticationResponse, AssignCharacterResponse, ServerReferencesMoveRequest, ServerTimeSettings, CharacterSpawnRequest, NotifyInventoryChanges, StringCacheUpdate, NotifyFactionsChanges, NotifyRemoveCharacter, NotifyQuestUpdate, NotifyQuestSceneUpdate, NotifyPlayerList, NotifyPartyInfo, NotifyPartyInvite, NotifyActorValueChanges, NotifyPartyJoined, NotifyPartyLeft, NotifyActorMaxValueChanges, NotifyHealthChangeBroadcast, NotifySpawnData, NotifyActivate, NotifyLockChange, AssignObjectsResponse, NotifyDeathStateChange, NotifyOwnershipTransfer, NotifyObjectInventoryChanges, NotifySpellCast, NotifyProjectileLaunch, NotifyInterruptCast, NotifyAddTarget, NotifyScriptAnimation, NotifyDrawWeapon, NotifyMount, NotifyNewPackage, NotifyRespawn, NotifySyncExperience, NotifyEquipmentChanges, NotifyChatMessageBroadcast, TeleportCommandResponse, NotifyPlayerRespawn, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyRelinquishControl, NotifyPlayerLeft, NotifyPlayerJoined, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, diff --git a/Code/encoding/Opcodes.h b/Code/encoding/Opcodes.h index 8f0b10592..c6b0b97fd 100644 --- a/Code/encoding/Opcodes.h +++ b/Code/encoding/Opcodes.h @@ -10,6 +10,7 @@ enum ClientOpcode : unsigned char kEnterInteriorCellRequest, kRequestFactionsChanges, kRequestQuestUpdate, + kRequestQuestSceneUpdate, kPartyInviteRequest, kPartyAcceptInviteRequest, kPartyLeaveRequest, @@ -67,6 +68,7 @@ enum ServerOpcode : unsigned char kNotifyFactionsChanges, kNotifyRemoveCharacter, kNotifyQuestUpdate, + kNotifyQuestSceneUpdate, kNotifyPlayerList, kNotifyPartyInfo, kNotifyPartyInvite, diff --git a/Code/server/Services/QuestService.cpp b/Code/server/Services/QuestService.cpp index 52133e74c..0d8bcb079 100644 --- a/Code/server/Services/QuestService.cpp +++ b/Code/server/Services/QuestService.cpp @@ -5,19 +5,21 @@ #include #include +#include #include +#include #include namespace { Console::Setting bEnableMiscQuestSync{"Gameplay:bEnableMiscQuestSync", "(Experimental) Syncs miscellaneous quests when possible", false}; - } QuestService::QuestService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) { m_questUpdateConnection = aDispatcher.sink>().connect<&QuestService::OnQuestChanges>(this); + m_questSceneUpdateConnection = aDispatcher.sink>().connect<&QuestService::OnQuestSceneChanges>(this); } void QuestService::OnQuestChanges(const PacketEvent& acMessage) noexcept @@ -96,3 +98,21 @@ void QuestService::OnQuestChanges(const PacketEvent& acMessa GameServer::Get()->SendToParty(notify, partyComponent, acMessage.GetSender()); } + +void QuestService::OnQuestSceneChanges(const PacketEvent& acMessage) noexcept +{ + const auto& message = acMessage.Packet; + + auto* pPlayer = acMessage.pPlayer; + NotifyQuestSceneUpdate notify{}; + notify.SceneId = message.SceneId; + notify.QuestId = message.QuestId; + + const auto& partyComponent = acMessage.pPlayer->GetParty(); + if (!partyComponent.JoinedPartyId.has_value()) + return; + + spdlog::info(__FUNCTION__ ": sending scene update {} of quest {}", notify.SceneId.LogFormat(), notify.QuestId.LogFormat()); + + GameServer::Get()->SendToParty(notify, partyComponent, acMessage.GetSender()); +} diff --git a/Code/server/Services/QuestService.h b/Code/server/Services/QuestService.h index 1e6a90539..622689921 100644 --- a/Code/server/Services/QuestService.h +++ b/Code/server/Services/QuestService.h @@ -6,11 +6,10 @@ struct World; struct UpdateEvent; struct RequestQuestUpdate; +struct RequestQuestSceneUpdate; /** * @brief Dispatch quest sync messages. - * - * This service is currently not in use. */ class QuestService { @@ -19,10 +18,12 @@ class QuestService private: void OnQuestChanges(const PacketEvent& aChanges) noexcept; + void OnQuestSceneChanges(const PacketEvent& aChanges) noexcept; World& m_world; entt::scoped_connection m_questUpdateConnection; + entt::scoped_connection m_questSceneUpdateConnection; entt::scoped_connection m_updateConnection; entt::scoped_connection m_joinConnection; };