fix(mission): GetMissionState answers the client's state bit mask

LWOMissionComponent::getMissionState (0x00cd7400) returns a mask: COMPLETE
once the mission was completed, ORed with the state of the mission in
progress; a mission not in progress gets AVAILABLE when it was never
completed or is repeatable and its prerequisites are met, else 0. DLU's
handler answered UNKNOWN (-1, every bit set) for a mission that cannot be
taken and COMPLETE for a completed repeatable one, and ignored
cooldownInfoRequested. With cooldown info requested (0x00cd7770) the
client only checks the cooldown of a repeat in progress (state 9 or 10):
fewer whole minutes since the last completion than Missions.cooldownTime.

MissionComponent::GetMissionState, used across the server, is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 09:42:25 -05:00
parent 0253fea006
commit 8564e293fa
3 changed files with 70 additions and 8 deletions

View File

@@ -3,6 +3,8 @@
* Copyright 2019
*/
#include <ctime>
#include <limits>
#include <ranges>
#include <sstream>
#include <string>
@@ -73,6 +75,38 @@ eMissionState MissionComponent::GetMissionState(const uint32_t missionId) const
}
int32_t MissionComponent::GetClientMissionState(const uint32_t missionId) const {
// A mission ID below 1 is no mission
if (missionId == 0 || missionId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max())) return 0;
const auto* const mission = GetMission(missionId);
const bool done = mission && (mission->GetCompletions() > 0 || mission->IsComplete());
int32_t state = done ? static_cast<int32_t>(eMissionState::COMPLETE) : 0;
// In progress: its own state
if (mission && !mission->IsComplete()) return state | static_cast<int32_t>(mission->GetMissionState());
CDMissions info{};
if (!Mission::IsValidMission(missionId, info)) return 0;
if ((!done || info.repeatable) && MissionPrerequisites::CheckPrerequisites(missionId, m_Missions)) {
state |= static_cast<int32_t>(eMissionState::AVAILABLE);
}
return state;
}
bool MissionComponent::IsCooldownRunning(const int64_t completedAt, const int64_t cooldownMinutes, const int64_t now) {
return cooldownMinutes > 0 && (now - completedAt) / 60 < cooldownMinutes;
}
bool MissionComponent::IsCooldownRunning(const uint32_t missionId) const {
const auto* const mission = GetMission(missionId);
if (!mission || (mission->GetCompletions() == 0 && !mission->IsComplete())) return false;
return IsCooldownRunning(mission->GetTimestamp(), mission->GetClientInfo().cooldownTime, std::time(nullptr));
}
const std::unordered_map<uint32_t, Mission*>& MissionComponent::GetMissions() const {
return m_Missions;
}
@@ -720,8 +754,19 @@ bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo&
return true;
}
// LWOMissionComponent::msgGetMissionState (0x00d176e0): the client's mission state bit mask; with cooldown info
// requested (0x00cd7770), the cooldown is only checked for a repeat in progress (state 9 or 10) and otherwise finished
bool MissionComponent::OnGetMissionState(GameMessages::GetMissionState& getMissionState) {
getMissionState.missionState = GetMissionState(getMissionState.missionID);
const auto missionID = static_cast<uint32_t>(getMissionState.missionID);
const auto state = getMissionState.missionID < 1 ? 0 : GetClientMissionState(missionID);
getMissionState.missionState = static_cast<eMissionState>(state);
if (getMissionState.cooldownInfoRequested) {
const auto* const mission = GetMission(missionID);
const bool inProgress = mission && !mission->IsComplete();
const bool repeat = state == static_cast<int32_t>(eMissionState::COMPLETE_AVAILABLE) || state == static_cast<int32_t>(eMissionState::COMPLETE_ACTIVE);
getMissionState.cooldownFinished = !(inProgress && repeat && IsCooldownRunning(missionID));
}
return true;
}

View File

@@ -56,6 +56,22 @@ public:
*/
eMissionState GetMissionState(uint32_t missionId) const;
/**
* The mission state as the client works it out (LWOMissionComponent::getMissionState 0x00cd7400): a bit mask,
* COMPLETE (8) once the mission was ever completed, ORed with the state of the mission in progress; a mission not in
* progress gets AVAILABLE (1) when it was never completed or is repeatable and its prerequisites are met. 0 for an
* unknown mission.
*/
int32_t GetClientMissionState(uint32_t missionId) const;
/**
* Whether the cooldown (Missions.cooldownTime, minutes) of a completed mission is still running, as the client
* checks it: only for a mission completed before, a cooldown above 0, and while fewer whole minutes than the
* cooldown have passed since the last completion.
*/
bool IsCooldownRunning(uint32_t missionId) const;
static bool IsCooldownRunning(int64_t completedAt, int64_t cooldownMinutes, int64_t now);
/**
* Checks if the entity has all the requirements for accepting the mission specified by the ID.
* @param missionId the mission ID to check for if the character may accept it