From 46b5bb277e1ed91b7a6f90e576a8c08d24958563 Mon Sep 17 00:00:00 2001 From: Daniil Zakharov Date: Mon, 12 Jan 2026 02:16:11 +0300 Subject: [PATCH 1/3] WIP: Some work to improve how heavily scripted cutscenes play out --- .../Skyrim/AI/Movement/PlayerControls.cpp | 7 ++ .../Games/Skyrim/AI/Movement/PlayerControls.h | 2 + Code/client/Games/Skyrim/Actor.cpp | 2 +- .../Games/Skyrim/Events/EventDispatcher.h | 15 +++++ Code/client/Games/Skyrim/Forms/TESQuest.cpp | 19 +++--- Code/client/Games/Skyrim/Forms/TESQuest.h | 2 +- .../client/Games/Skyrim/Magic/MagicTarget.cpp | 2 +- .../Services/Generic/CalendarService.cpp | 5 +- Code/client/Services/Generic/InputService.cpp | 2 +- Code/client/Services/Generic/MagicService.cpp | 6 +- Code/client/Services/Generic/QuestService.cpp | 65 +++++++++++++++++-- Code/client/Services/QuestService.h | 11 +++- Code/client/TiltedOnlineApp.cpp | 1 + 13 files changed, 111 insertions(+), 28 deletions(-) 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..3e23b5aa6 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -1215,7 +1215,7 @@ 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); + //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..5a5b621f9 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 { + uint32_t sceneFormId; + uint32_t sceneType; // BEGIN or END + uint16_t questStageId; + void* callback; }; struct TESSceneActionEvent { + uint32_t sceneFormId; + uint32_t actionId; + uint32_t questFormId; + uint32_t refAliasId; + uint16_t questStageId; + void* callback; }; struct TESScenePhaseEvent { + uint32_t sceneFormId; + uint32_t phaseIndex; + uint32_t sceneType; // BEGIN or END + 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..0b83704bc 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::info("ScriptSetStage called with a value of {}", stageIndex); if (currentStage == stageIndex || IsStageDone(stageIndex)) + { + spdlog::warn("\tThis stage {} is already done, not calling SetCurrentStageID", stageIndex); return; + } using Quest = TESQuest; PAPYRUS_FUNCTION(void, Quest, SetCurrentStageID, int); s_pSetCurrentStageID(this, stageIndex); + spdlog::info("ScriptSetStage: successfully set to {}", stageIndex); } void TESQuest::SetStopped() diff --git a/Code/client/Games/Skyrim/Forms/TESQuest.h b/Code/client/Games/Skyrim/Forms/TESQuest.h index 6c121e202..bbad98b76 100644 --- a/Code/client/Games/Skyrim/Forms/TESQuest.h +++ b/Code/client/Games/Skyrim/Forms/TESQuest.h @@ -126,7 +126,7 @@ 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(); }; 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/Generic/CalendarService.cpp b/Code/client/Services/Generic/CalendarService.cpp index 585d034dc..092937afd 100644 --- a/Code/client/Services/Generic/CalendarService.cpp +++ b/Code/client/Services/Generic/CalendarService.cpp @@ -21,9 +21,10 @@ bool CalendarService::AllowGameTick() noexcept CalendarService::CalendarService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) : m_world(aWorld), m_transport(aTransport) { - m_timeUpdateConnection = aDispatcher.sink().connect<&CalendarService::OnTimeUpdate>(this); + // temporarily commented out time sync for ease of testing + /*m_timeUpdateConnection = aDispatcher.sink().connect<&CalendarService::OnTimeUpdate>(this); m_updateConnection = aDispatcher.sink().connect<&CalendarService::HandleUpdate>(this); - m_disconnectedConnection = aDispatcher.sink().connect<&CalendarService::OnDisconnected>(this); + m_disconnectedConnection = aDispatcher.sink().connect<&CalendarService::OnDisconnected>(this);*/ } void CalendarService::OnTimeUpdate(const ServerTimeSettings& acMessage) noexcept diff --git a/Code/client/Services/Generic/InputService.cpp b/Code/client/Services/Generic/InputService.cpp index 558ead849..efd5175cd 100644 --- a/Code/client/Services/Generic/InputService.cpp +++ b/Code/client/Services/Generic/InputService.cpp @@ -189,7 +189,7 @@ 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); + //spdlog::debug("ProcessKey, type: {}, key: {}, active: {}", aType, aKey, active); if (aType != KEYEVENT_CHAR && (IsToggleKey(aKey) || (IsDisableKey(aKey) && active))) { diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index d5d5bb45f..045cb4d62 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -358,7 +358,7 @@ 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 @@ -424,8 +424,8 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept pActor->GetExtension()->GraphDescriptorHash = AnimationGraphDescriptor_VampireLordBehavior::m_key; // This hack is here because slow time seems to be twice as slow when cast by an npc - if (pEffect->IsSlowEffect()) - pActor = PlayerCharacter::Get(); + //if (pEffect->IsSlowEffect()) + // pActor = PlayerCharacter::Get(); pActor->magicTarget.AddTarget(data, acMessage.ApplyHealPerkBonus, acMessage.ApplyStaminaPerkBonus); spdlog::debug("Applied remote magic effect"); diff --git a/Code/client/Services/Generic/QuestService.cpp b/Code/client/Services/Generic/QuestService.cpp index b32f501be..f36ac9f78 100644 --- a/Code/client/Services/Generic/QuestService.cpp +++ b/Code/client/Services/Generic/QuestService.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -36,6 +37,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,10 +59,13 @@ 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("Not in party, quest stage advancement won't be sent"); return BSTEventResult::kOk; + } - spdlog::info("Quest start/stop event: {:X}", apEvent->formId); + spdlog::info("Local OnEvent: quest start/stop event: {:X}", apEvent->formId); if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) { @@ -102,10 +110,13 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const EventDispatcher*) { - if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) + if (!CanAdvanceQuestForParty()) + { + spdlog::warn("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 OnEvent: quest stage event: {: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))) @@ -129,6 +140,8 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev } } + //m_doneStages[apEvent->formId].push_back(apEvent->stageId); + m_world.GetRunner().Queue( [&, formId = apEvent->formId, stageId = apEvent->stageId, type = pQuest->type]() { @@ -150,6 +163,27 @@ 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("TESSceneEvent event: quest stage {}, type {}", apEvent->questStageId, sceneType); + + return BSTEventResult::kOk; +} + +BSTEventResult QuestService::OnEvent(const TESSceneActionEvent* apEvent, const EventDispatcher*) +{ + spdlog::info("TESSceneActionEvent event: quest stage {}, ref alias {:X}", apEvent->questStageId, apEvent->refAliasId); + 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); + return BSTEventResult::kOk; +} + void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { ModSystem& modSystem = World::Get().GetModSystem(); @@ -168,25 +202,32 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii()); } + //const auto& doneStages = m_doneStages[pQuest->formID]; + //if (std::find(doneStages.begin(), doneStages.end(), aUpdate.Stage) != doneStages.end()) + //{ + // We've already completed this stage recently. Return to avoid reflection + //return; + //} + bool bResult = false; switch (aUpdate.Status) { 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 +236,16 @@ 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); } +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)); diff --git a/Code/client/Services/QuestService.h b/Code/client/Services/QuestService.h index b4f1b58fc..6b70c121a 100644 --- a/Code/client/Services/QuestService.h +++ b/Code/client/Services/QuestService.h @@ -10,10 +10,8 @@ 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,10 +28,17 @@ 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; + bool CanAdvanceQuestForParty() const noexcept; + World& m_world; + // (UNUSED) Keep track of done stages to avoid reflecting updates back-and-forth between players + Map> m_doneStages = {}; entt::scoped_connection m_joinedConnection; entt::scoped_connection m_leftConnection; diff --git a/Code/client/TiltedOnlineApp.cpp b/Code/client/TiltedOnlineApp.cpp index 647616e14..94980f232 100644 --- a/Code/client/TiltedOnlineApp.cpp +++ b/Code/client/TiltedOnlineApp.cpp @@ -39,6 +39,7 @@ TiltedOnlineApp::TiltedOnlineApp() console->set_pattern("%^[%H:%M:%S.%e] [%l] [tid %t] %$ %v"); auto logger = std::make_shared("", spdlog::sinks_init_list{console, rotatingLogger}); + logger->set_level(spdlog::level::debug); set_default_logger(logger); } From 5d39964ccacb0ee9b26bfa25b5db9235cc994993 Mon Sep 17 00:00:00 2001 From: Daniil Zakharov Date: Mon, 12 Jan 2026 20:06:21 +0300 Subject: [PATCH 2/3] Cleanup after 46b5bb27 --- Code/client/Games/Skyrim/Actor.cpp | 2 - .../Games/Skyrim/Events/EventDispatcher.h | 6 ++- Code/client/Games/Skyrim/Forms/TESQuest.cpp | 6 +-- .../Services/Generic/CalendarService.cpp | 5 +-- Code/client/Services/Generic/InputService.cpp | 2 - Code/client/Services/Generic/MagicService.cpp | 8 ++-- Code/client/Services/Generic/QuestService.cpp | 44 +++---------------- Code/client/Services/QuestService.h | 7 +-- Code/client/TiltedOnlineApp.cpp | 1 - 9 files changed, 20 insertions(+), 61 deletions(-) diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 3e23b5aa6..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 5a5b621f9..675c2d4cf 100644 --- a/Code/client/Games/Skyrim/Events/EventDispatcher.h +++ b/Code/client/Games/Skyrim/Events/EventDispatcher.h @@ -188,10 +188,12 @@ struct TESResolveNPCTemplatesEvent { }; +// The RE'd fields in TESSceneEvent, TESSceneActionEvent and TESScenePhaseEvent may be incorrect + struct TESSceneEvent { uint32_t sceneFormId; - uint32_t sceneType; // BEGIN or END + uint32_t sceneType; // BEGIN (0) or END (1) uint16_t questStageId; void* callback; }; @@ -210,7 +212,7 @@ struct TESScenePhaseEvent { uint32_t sceneFormId; uint32_t phaseIndex; - uint32_t sceneType; // BEGIN or END + uint32_t sceneType; // BEGIN (0) or END (1) uint16_t questStageId; void* callback; }; diff --git a/Code/client/Games/Skyrim/Forms/TESQuest.cpp b/Code/client/Games/Skyrim/Forms/TESQuest.cpp index 0b83704bc..f5c138a5e 100644 --- a/Code/client/Games/Skyrim/Forms/TESQuest.cpp +++ b/Code/client/Games/Skyrim/Forms/TESQuest.cpp @@ -95,17 +95,17 @@ bool TESQuest::SetStage(uint16_t stageIndex) void TESQuest::ScriptSetStage(uint16_t stageIndex) { - spdlog::info("ScriptSetStage called with a value of {}", stageIndex); + spdlog::debug(__FUNCTION__ ": called with a value of {}", stageIndex); if (currentStage == stageIndex || IsStageDone(stageIndex)) { - spdlog::warn("\tThis stage {} is already done, not calling SetCurrentStageID", 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::info("ScriptSetStage: successfully set to {}", stageIndex); + spdlog::debug(__FUNCTION__ ": stage has been set to {}", stageIndex); } void TESQuest::SetStopped() diff --git a/Code/client/Services/Generic/CalendarService.cpp b/Code/client/Services/Generic/CalendarService.cpp index 092937afd..585d034dc 100644 --- a/Code/client/Services/Generic/CalendarService.cpp +++ b/Code/client/Services/Generic/CalendarService.cpp @@ -21,10 +21,9 @@ bool CalendarService::AllowGameTick() noexcept CalendarService::CalendarService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) : m_world(aWorld), m_transport(aTransport) { - // temporarily commented out time sync for ease of testing - /*m_timeUpdateConnection = aDispatcher.sink().connect<&CalendarService::OnTimeUpdate>(this); + m_timeUpdateConnection = aDispatcher.sink().connect<&CalendarService::OnTimeUpdate>(this); m_updateConnection = aDispatcher.sink().connect<&CalendarService::HandleUpdate>(this); - m_disconnectedConnection = aDispatcher.sink().connect<&CalendarService::OnDisconnected>(this);*/ + m_disconnectedConnection = aDispatcher.sink().connect<&CalendarService::OnDisconnected>(this); } void CalendarService::OnTimeUpdate(const ServerTimeSettings& acMessage) noexcept diff --git a/Code/client/Services/Generic/InputService.cpp b/Code/client/Services/Generic/InputService.cpp index efd5175cd..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 045cb4d62..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. 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); + 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 @@ -424,8 +426,8 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept pActor->GetExtension()->GraphDescriptorHash = AnimationGraphDescriptor_VampireLordBehavior::m_key; // This hack is here because slow time seems to be twice as slow when cast by an npc - //if (pEffect->IsSlowEffect()) - // pActor = PlayerCharacter::Get(); + if (pEffect->IsSlowEffect()) + pActor = PlayerCharacter::Get(); pActor->magicTarget.AddTarget(data, acMessage.ApplyHealPerkBonus, acMessage.ApplyStaminaPerkBonus); spdlog::debug("Applied remote magic effect"); diff --git a/Code/client/Services/Generic/QuestService.cpp b/Code/client/Services/Generic/QuestService.cpp index f36ac9f78..07377d1c6 100644 --- a/Code/client/Services/Generic/QuestService.cpp +++ b/Code/client/Services/Generic/QuestService.cpp @@ -37,10 +37,6 @@ 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 @@ -61,11 +57,11 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons { if (!m_world.Get().GetPartyService().IsInParty()) { - spdlog::debug("Not in party, quest stage advancement won't be sent"); + spdlog::debug("(Local) TESQuestStartStopEvent event: not in party, quest stage advancement won't be sent. Returning."); return BSTEventResult::kOk; } - spdlog::info("Local OnEvent: quest start/stop event: {:X}", apEvent->formId); + spdlog::info("(Local) TESQuestStartStopEvent event: quest start/stop event: {:X}", apEvent->formId); if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) { @@ -112,11 +108,11 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev { if (!CanAdvanceQuestForParty()) { - spdlog::warn("Quest stage advancement won't be sent: either not in party, or a non-leader with disabled controls."); + spdlog::warn("(Local) TESQuestStageEvent event: quest stage advancement won't be sent: either not in party, or a non-leader with disabled controls."); return BSTEventResult::kOk; } - spdlog::info("Local OnEvent: quest stage event: {:X}, stage: {}. Sending to server.", apEvent->formId, apEvent->stageId); + spdlog::info("(Local) TESQuestStageEvent event: {: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))) @@ -140,8 +136,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev } } - //m_doneStages[apEvent->formId].push_back(apEvent->stageId); - m_world.GetRunner().Queue( [&, formId = apEvent->formId, stageId = apEvent->stageId, type = pQuest->type]() { @@ -163,27 +157,6 @@ 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("TESSceneEvent event: quest stage {}, type {}", apEvent->questStageId, sceneType); - - return BSTEventResult::kOk; -} - -BSTEventResult QuestService::OnEvent(const TESSceneActionEvent* apEvent, const EventDispatcher*) -{ - spdlog::info("TESSceneActionEvent event: quest stage {}, ref alias {:X}", apEvent->questStageId, apEvent->refAliasId); - 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); - return BSTEventResult::kOk; -} - void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { ModSystem& modSystem = World::Get().GetModSystem(); @@ -202,13 +175,6 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept aUpdate.ClientQuestType, formId, pQuest->fullName.value.AsAscii()); } - //const auto& doneStages = m_doneStages[pQuest->formID]; - //if (std::find(doneStages.begin(), doneStages.end(), aUpdate.Stage) != doneStages.end()) - //{ - // We've already completed this stage recently. Return to avoid reflection - //return; - //} - bool bResult = false; switch (aUpdate.Status) { @@ -239,7 +205,7 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept bool QuestService::CanAdvanceQuestForParty() const noexcept { const bool isInParty = m_world.Get().GetPartyService().IsInParty(); - // Party leaders can always advance quests. + // 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(); diff --git a/Code/client/Services/QuestService.h b/Code/client/Services/QuestService.h index 6b70c121a..042974fcb 100644 --- a/Code/client/Services/QuestService.h +++ b/Code/client/Services/QuestService.h @@ -11,7 +11,7 @@ struct TESQuest; /** * @brief Handles quest sync */ -class QuestService final : public BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink +class QuestService final : public BSTEventSink, BSTEventSink { public: QuestService(World&, entt::dispatcher&); @@ -28,17 +28,12 @@ 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; bool CanAdvanceQuestForParty() const noexcept; World& m_world; - // (UNUSED) Keep track of done stages to avoid reflecting updates back-and-forth between players - Map> m_doneStages = {}; entt::scoped_connection m_joinedConnection; entt::scoped_connection m_leftConnection; diff --git a/Code/client/TiltedOnlineApp.cpp b/Code/client/TiltedOnlineApp.cpp index 94980f232..647616e14 100644 --- a/Code/client/TiltedOnlineApp.cpp +++ b/Code/client/TiltedOnlineApp.cpp @@ -39,7 +39,6 @@ TiltedOnlineApp::TiltedOnlineApp() console->set_pattern("%^[%H:%M:%S.%e] [%l] [tid %t] %$ %v"); auto logger = std::make_shared("", spdlog::sinks_init_list{console, rotatingLogger}); - logger->set_level(spdlog::level::debug); set_default_logger(logger); } From 51aa20d08c5ae4948206113ae5c7e415f672def6 Mon Sep 17 00:00:00 2001 From: Daniil Zakharov Date: Sun, 18 Jan 2026 21:13:05 +0300 Subject: [PATCH 3/3] NotifyQuestSceneUpdate & RequestQuestSceneUpdate experiments Not really working that well, needs more experimenting --- Code/client/Games/Animation.cpp | 3 + .../Games/Skyrim/Events/EventDispatcher.h | 14 +- Code/client/Games/Skyrim/Forms/TESQuest.cpp | 18 +++ Code/client/Games/Skyrim/Forms/TESQuest.h | 43 +++++- .../Services/Debug/Views/QuestDebugView.cpp | 33 ++++- Code/client/Services/Generic/QuestService.cpp | 122 +++++++++++++++--- Code/client/Services/QuestService.h | 10 +- Code/encoding/Messages/ClientMessageFactory.h | 3 +- .../Messages/NotifyQuestSceneUpdate.cpp | 16 +++ .../Messages/NotifyQuestSceneUpdate.h | 22 ++++ .../Messages/RequestQuestSceneUpdate.cpp | 16 +++ .../Messages/RequestQuestSceneUpdate.h | 22 ++++ Code/encoding/Messages/ServerMessageFactory.h | 3 +- Code/encoding/Opcodes.h | 2 + Code/server/Services/QuestService.cpp | 22 +++- Code/server/Services/QuestService.h | 5 +- 16 files changed, 311 insertions(+), 43 deletions(-) create mode 100644 Code/encoding/Messages/NotifyQuestSceneUpdate.cpp create mode 100644 Code/encoding/Messages/NotifyQuestSceneUpdate.h create mode 100644 Code/encoding/Messages/RequestQuestSceneUpdate.cpp create mode 100644 Code/encoding/Messages/RequestQuestSceneUpdate.h 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/Events/EventDispatcher.h b/Code/client/Games/Skyrim/Events/EventDispatcher.h index 675c2d4cf..e7663c6f5 100644 --- a/Code/client/Games/Skyrim/Events/EventDispatcher.h +++ b/Code/client/Games/Skyrim/Events/EventDispatcher.h @@ -188,24 +188,22 @@ struct TESResolveNPCTemplatesEvent { }; -// The RE'd fields in TESSceneEvent, TESSceneActionEvent and TESScenePhaseEvent may be incorrect - struct TESSceneEvent { + void* ref; uint32_t sceneFormId; uint32_t sceneType; // BEGIN (0) or END (1) - uint16_t questStageId; - void* callback; }; +// RE'd fields in TESSceneActionEvent and TESScenePhaseEvent are probably incorrect + struct TESSceneActionEvent { + void* ref; uint32_t sceneFormId; - uint32_t actionId; + uint32_t actionIndex; uint32_t questFormId; - uint32_t refAliasId; - uint16_t questStageId; - void* callback; + uint32_t actorAliasId; }; struct TESScenePhaseEvent diff --git a/Code/client/Games/Skyrim/Forms/TESQuest.cpp b/Code/client/Games/Skyrim/Forms/TESQuest.cpp index f5c138a5e..dc834b8fc 100644 --- a/Code/client/Games/Skyrim/Forms/TESQuest.cpp +++ b/Code/client/Games/Skyrim/Forms/TESQuest.cpp @@ -114,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 bbad98b76..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 @@ -129,12 +159,13 @@ struct TESQuest : BGSStoryManagerTreeForm 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/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/QuestService.cpp b/Code/client/Services/Generic/QuestService.cpp index 07377d1c6..cbee74a55 100644 --- a/Code/client/Services/Generic/QuestService.cpp +++ b/Code/client/Services/Generic/QuestService.cpp @@ -14,7 +14,9 @@ #include #include +#include #include +#include static TESQuest* FindQuestByNameId(const String& name) { @@ -29,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. @@ -37,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 @@ -57,17 +64,17 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons { if (!m_world.Get().GetPartyService().IsInParty()) { - spdlog::debug("(Local) TESQuestStartStopEvent event: not in party, quest stage advancement won't be sent. Returning."); + spdlog::debug("(Local) TESQuestStartStopEvent: not in party, quest stage advancement won't be sent. Returning."); return BSTEventResult::kOk; } - spdlog::info("(Local) TESQuestStartStopEvent event: 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 @@ -76,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]() { @@ -94,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); } @@ -106,13 +112,18 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const EventDispatcher*) { + if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) + { + spdlog::info("(Local) TESQuestStageEvent: \"{}\" - in cutscene? {}", pQuest->GetName(), pQuest->IsAnyCutscenePlaying()); + } + if (!CanAdvanceQuestForParty()) { - spdlog::warn("(Local) TESQuestStageEvent event: quest stage advancement won't be sent: either not in party, or a non-leader with disabled controls."); + 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("(Local) TESQuestStageEvent event: {:X}, stage: {}. Sending to server.", 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))) @@ -128,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()); } } @@ -157,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(); @@ -170,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; @@ -202,6 +263,26 @@ 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(); @@ -238,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 042974fcb..1fff9432c 100644 --- a/Code/client/Services/QuestService.h +++ b/Code/client/Services/QuestService.h @@ -1,17 +1,18 @@ #pragma once -#include #include #include +#include struct NotifyQuestUpdate; +struct NotifyQuestSceneUpdate; struct TESQuest; /** * @brief Handles quest sync */ -class QuestService final : public BSTEventSink, BSTEventSink +class QuestService final : public BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink, BSTEventSink { public: QuestService(World&, entt::dispatcher&); @@ -28,8 +29,12 @@ 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; @@ -38,4 +43,5 @@ class QuestService final : public BSTEventSink, BSTEvent 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; };