From fe4a1e5e989441f03550b3a427993a690876c461 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 12:59:29 -0600 Subject: [PATCH] fix: check the mission state before completing it from a dialogue MissionDialogueOK accepted or completed the mission for whichever object the message named as the responder. The responder now has to be the player that sent the message, the object the dialogue belongs to has to give or take the mission (on its own or through a random mission's pool), a mission is only accepted when the player can accept it and only completed when it is ready to complete. The object's script runs after these checks. Co-Authored-By: Claude Fable 5.2 --- dGame/dComponents/MissionOfferComponent.cpp | 15 +++++++++ dGame/dComponents/MissionOfferComponent.h | 6 ++++ dGame/dGameMessages/MissionMessages.cpp | 37 +++++++++++++++++---- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/dGame/dComponents/MissionOfferComponent.cpp b/dGame/dComponents/MissionOfferComponent.cpp index 45fb32dcd..f754e52fb 100644 --- a/dGame/dComponents/MissionOfferComponent.cpp +++ b/dGame/dComponents/MissionOfferComponent.cpp @@ -136,6 +136,21 @@ std::vector MissionOfferComponent::ParseRandomPool(const std::string& return pool; } +bool MissionOfferComponent::HasMission(const uint32_t missionId) const { + for (const auto& offeredMission : this->offeredMissions) { + if (offeredMission.GetMissionId() == missionId) return true; + + // A random mission stands for the missions in its pool + CDMissions info{}; + if (!Mission::IsValidMission(offeredMission.GetMissionId(), info) || !info.isRandom) continue; + + const auto randomMissionPool = ParseRandomPool(info.randomPool); + if (std::ranges::find(randomMissionPool, missionId) != randomMissionPool.end()) return true; + } + + return false; +} + const OfferedMission* MissionOfferComponent::GetOfferedMission(const uint32_t missionID) const { const auto offered = std::ranges::find_if(offeredMissions, [missionID](const OfferedMission& other) { return other.GetMissionId() == missionID; }); return offered == offeredMissions.end() ? nullptr : &*offered; diff --git a/dGame/dComponents/MissionOfferComponent.h b/dGame/dComponents/MissionOfferComponent.h index b33ba47f8..fc6a76c50 100644 --- a/dGame/dComponents/MissionOfferComponent.h +++ b/dGame/dComponents/MissionOfferComponent.h @@ -96,6 +96,12 @@ public: */ void OfferMissions(Entity* entity, uint32_t specifiedMissionId = 0); + /** + * Returns whether this entity offers or accepts a mission, on its own or as one of a random mission's pool + * @param missionId the mission to look for + */ + bool HasMission(uint32_t missionId) const; + // RequestLinkedMission after a mission window: offers the next mission of this giver, if there is a new one void OfferLinkedMission(Entity* entity, uint32_t missionID); diff --git a/dGame/dGameMessages/MissionMessages.cpp b/dGame/dGameMessages/MissionMessages.cpp index 23d047929..26aeeb4e0 100644 --- a/dGame/dGameMessages/MissionMessages.cpp +++ b/dGame/dGameMessages/MissionMessages.cpp @@ -143,13 +143,12 @@ namespace GameMessages { void MissionDialogueOK::Handle(Entity& entityRef, const SystemAddress& sysAddr) { Entity* entity = &entityRef; Entity* player = Game::entityManager->GetEntity(responder); - if (!player) { - LOG("MissionDialogueOK for mission %i from unknown responder %llu", missionID, responder); + // A player answers a mission dialogue for themselves + if (!player || !player->IsPlayer() || player->GetSystemAddress() != sysAddr) { + LOG("Responder %llu of MissionDialogueOK for mission %i is not the player that sent it", responder, missionID); return; } - entity->GetScript()->OnMissionDialogueOK(entity, player, missionID, iMissionState); - // Get the player's mission component MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); if (!missionComponent) { @@ -157,9 +156,35 @@ namespace GameMessages { return; } - if (iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE) { + // The mission has to be one the object the dialogue belongs to gives or takes + const auto* const missionOfferComponent = entity->GetComponent(); + if (!missionOfferComponent || !missionOfferComponent->HasMission(missionID)) { + LOG("Object %llu (LOT %i) has no mission %i for player %llu to answer", entity->GetObjectID(), entity->GetLOT(), missionID, player->GetObjectID()); + return; + } + + const bool isAccepting = iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE; + const bool isCompleting = iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE; + + if (isAccepting && !missionComponent->CanAccept(missionID)) { + LOG("Player %llu tried to accept mission %i, which they can not accept", player->GetObjectID(), missionID); + return; + } + + if (isCompleting) { + // Only a mission with all of its tasks done can be handed in + const auto* const readyMission = missionComponent->GetMission(missionID); + if (!readyMission || !readyMission->IsReadyToComplete()) { + LOG("Player %llu tried to hand in mission %i, which is not ready to complete", player->GetObjectID(), missionID); + return; + } + } + + entity->GetScript()->OnMissionDialogueOK(entity, player, missionID, iMissionState); + + if (isAccepting) { missionComponent->AcceptMission(missionID); - } else if (iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE) { + } else if (isCompleting) { // LWOMissionComponent::MissionWindowOK (0x00d501f0) only turns a mission in when every reward fits // (CanReceiveAllRewards, with the reward the player picked) const auto* const mission = missionComponent->GetMission(missionID);