Files
DarkflameServer/dGame/dComponents/MissionOfferComponent.cpp
Aaron Kimbrell 04e0c13678 fix(mission offer): offer the first mission of the giver's list, as the client lists them
LWOMissionOfferComponent builds a player's missions for a giver
(0x00c605f0, one entry 0x00bebf30) in priority order: 0 a mission to turn
in here, 5 one in progress, 10 a new one, 15 a repeat. A ready mission is
only listed where MissionNPCComponent.acceptsMission is set, a new one
only where offersMission is set and its cooldown has run out, a mission in
progress by every giver that has it. A random mission (0x00c5ffd0) lists
one of its pool that the giver has: one to turn in first, then one in
progress, else a random one. Missions are kept by mission ID; equal
priorities keep that order.

DLU sent an OfferMission for every mission that qualified, including one
ready to turn in at a giver that does not accept it. In the live captures
every use got one mission's OfferMission pair. A use now offers the head
of the list, and a pick from the multi interaction menu the picked one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:43:08 -05:00

246 lines
10 KiB
C++

/*
* Darkflame Universe
* Copyright 2019
*/
#include <algorithm>
#include <cctype>
#include <map>
#include <sstream>
#include "MissionOfferComponent.h"
#include "ObjectMessages.h"
#include "CDClientManager.h"
#include "CDMissionsTable.h"
#include "CDMissionNPCComponentTable.h"
#include "GameMessages.h"
#include "MissionMessages.h"
#include "Entity.h"
#include "MissionComponent.h"
#include "Logger.h"
#include "Game.h"
#include "GeneralUtils.h"
#include "MissionPrerequisites.h"
#include "eMissionState.h"
#include "CDComponentsRegistryTable.h"
OfferedMission::OfferedMission(const uint32_t missionId, const bool offersMission, const bool acceptsMission) {
this->missionId = missionId;
this->offersMission = offersMission;
this->acceptsMission = acceptsMission;
}
uint32_t OfferedMission::GetMissionId() const {
return this->missionId;
}
bool OfferedMission::GetOffersMission() const {
return this->offersMission;
}
bool OfferedMission::GetAcceptsMission() const {
return this->acceptsMission;
}
//------------------------ MissionOfferComponent below ------------------------
MissionOfferComponent::MissionOfferComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
RegisterMsg(&MissionOfferComponent::OnRequestUse);
}
// The MissionNPCComponent rows: the missions this offers and accepts
void MissionOfferComponent::LoadDataFromTemplate() {
if (m_ComponentID != -1) {
// Now lookup the missions in the MissionNPCComponent table
auto* missionNpcComponentTable = CDClientManager::GetTable<CDMissionNPCComponentTable>();
auto missions = missionNpcComponentTable->Query([this](const CDMissionNPCComponent& entry) {
return entry.id == static_cast<unsigned>(m_ComponentID);
});
for (auto& mission : missions) {
this->offeredMissions.emplace_back(mission.missionID, mission.offersMission, mission.acceptsMission);
}
}
}
namespace {
std::string_view Trimmed(std::string_view text) {
while (!text.empty() && std::isspace(static_cast<unsigned char>(text.front()))) text.remove_prefix(1);
while (!text.empty() && std::isspace(static_cast<unsigned char>(text.back()))) text.remove_suffix(1);
return text;
}
// "Why are we sending it twice, once to a non-player object?"
// The first one (targeting the offerer) makes the client zoom into the object.
// The second (targeting the player) actually makes the UI pop up so the mission can be offered.
// Why is it like this? Because LU isn't just a clown, it's the entire circus.
void OfferMissionToPlayer(const Entity& player, const int32_t missionID, const LWOOBJID offerer) {
GameMessages::OfferMission offerMission;
offerMission.missionID = missionID;
offerMission.offerer = offerer;
offerMission.target = offerer;
offerMission.SendToClient(player.GetSystemAddress());
offerMission.target = player.GetObjectID();
offerMission.SendToClient(player.GetSystemAddress());
}
}
bool MissionOfferComponent::OnRequestUse(GameMessages::RequestUseEvent& event) {
if (event.msg.IsMissionChoice()) OfferMissions(event.user, event.msg.multiInteractID);
else OfferMissions(event.user);
return true;
}
std::optional<MissionOfferEntry> MissionOfferComponent::GetOfferEntry(const uint32_t missionID, const int32_t state, const bool cooldownRunning, const bool offers, const bool accepts, const bool alsoCompleted) {
constexpr auto AVAILABLE = static_cast<int32_t>(eMissionState::AVAILABLE);
constexpr auto ACTIVE = static_cast<int32_t>(eMissionState::ACTIVE);
constexpr auto READY_TO_COMPLETE = static_cast<int32_t>(eMissionState::READY_TO_COMPLETE);
constexpr auto COMPLETE = static_cast<int32_t>(eMissionState::COMPLETE);
// Ready to turn in: only where it is accepted
if (state & READY_TO_COMPLETE) {
if (offers && accepts) return MissionOfferEntry{ 5, missionID, state, false };
if (accepts) return MissionOfferEntry{ 0, missionID, state, true };
return std::nullopt;
}
// In progress: shown by every giver that has it
if (state & ACTIVE) return MissionOfferEntry{ 5, missionID, state, false };
// Available: only where it is offered, and not while its cooldown runs
if (state & AVAILABLE) {
if (!offers || cooldownRunning) return std::nullopt;
return MissionOfferEntry{ static_cast<uint8_t>((state & COMPLETE) ? 15 : 10), missionID, state, false };
}
if (alsoCompleted && (state & COMPLETE) && !cooldownRunning) return MissionOfferEntry{ 20, missionID, state, !offers && accepts };
return std::nullopt;
}
void MissionOfferComponent::InsertByPriority(std::vector<MissionOfferEntry>& entries, const MissionOfferEntry& entry) {
const auto after = std::ranges::find_if(entries, [&entry](const MissionOfferEntry& other) { return other.priority > entry.priority; });
entries.insert(after, entry);
}
std::vector<uint32_t> MissionOfferComponent::ParseRandomPool(const std::string& randomPool) {
std::vector<uint32_t> pool;
for (const auto& token : GeneralUtils::SplitString(randomPool, ',')) {
const auto sample = GeneralUtils::TryParse<uint32_t>(Trimmed(token));
if (sample) pool.push_back(*sample);
}
return pool;
}
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;
}
std::optional<MissionOfferEntry> MissionOfferComponent::GetEntryFor(const MissionComponent& missionComponent, const OfferedMission& offered, const bool alsoCompleted) {
const auto missionID = offered.GetMissionId();
const auto state = missionComponent.GetClientMissionState(missionID);
return GetOfferEntry(missionID, state, missionComponent.IsCooldownRunning(missionID), offered.GetOffersMission(), offered.GetAcceptsMission(), alsoCompleted);
}
std::vector<MissionOfferEntry> MissionOfferComponent::GetMissionsForPlayer(const MissionComponent& missionComponent, const bool alsoCompleted) const {
// The client keeps the giver's missions by mission ID
std::map<uint32_t, const OfferedMission*> byID;
for (const auto& offered : offeredMissions) byID.emplace(offered.GetMissionId(), &offered);
std::vector<MissionOfferEntry> entries;
for (const auto& [missionID, offered] : byID) {
CDMissions info{};
if (!Mission::IsValidMission(missionID, info)) continue;
if (!info.isRandom) {
if (const auto entry = GetEntryFor(missionComponent, *offered, alsoCompleted)) InsertByPriority(entries, *entry);
continue;
}
// A random mission (0x00c5ffd0): of its pool, the missions this giver has. One to turn in here first, then one
// in progress, else a random one
std::vector<MissionOfferEntry> pool;
for (const auto sample : ParseRandomPool(info.randomPool)) {
const auto sampleOffered = byID.find(sample);
if (sampleOffered == byID.end()) continue;
if (const auto entry = GetEntryFor(missionComponent, *sampleOffered->second, alsoCompleted)) InsertByPriority(pool, *entry);
}
if (pool.empty()) continue;
const auto withPriority = [&pool](const uint8_t priority) {
return std::ranges::find_if(pool, [priority](const MissionOfferEntry& entry) { return entry.priority == priority; });
};
auto chosen = withPriority(0);
if (chosen == pool.end()) chosen = withPriority(5);
if (chosen == pool.end()) chosen = pool.begin() + GeneralUtils::GenerateRandomNumber<size_t>(0, pool.size() - 1);
InsertByPriority(entries, *chosen);
}
return entries;
}
// The server offers the first mission of the giver's list (live: one OfferMission per use), or the one picked from the
// client's multi interaction menu
void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifiedMissionId) {
if (!entity) return;
auto* missionComponent = entity->GetComponent<MissionComponent>();
if (!missionComponent) {
LOG("Unable to get mission component for Entity %llu", entity->GetObjectID());
return;
}
const auto entries = GetMissionsForPlayer(*missionComponent, false);
if (specifiedMissionId == 0) {
if (!entries.empty()) OfferMissionToPlayer(*entity, entries.front().missionID, m_Parent->GetObjectID());
return;
}
if (std::ranges::any_of(entries, [specifiedMissionId](const MissionOfferEntry& entry) { return entry.missionID == specifiedMissionId; })) {
OfferMissionToPlayer(*entity, specifiedMissionId, m_Parent->GetObjectID());
return;
}
// The client rolls its own mission of a random pool; offer that one if it is in a pool of this giver and listable
const auto* const sampleOffered = GetOfferedMission(specifiedMissionId);
if (!sampleOffered || !GetEntryFor(*missionComponent, *sampleOffered, false)) return;
for (const auto& offered : offeredMissions) {
CDMissions info{};
if (!Mission::IsValidMission(offered.GetMissionId(), info) || !info.isRandom) continue;
const auto pool = ParseRandomPool(info.randomPool);
if (std::ranges::find(pool, specifiedMissionId) != pool.end()) {
OfferMissionToPlayer(*entity, specifiedMissionId, m_Parent->GetObjectID());
return;
}
}
}
// LWOMissionOfferComponent's RequestLinkedMission handler (0x00cd6eb0): the giver's list without the mission the window
// was for; a new mission at its head is offered. Live offered nothing after an accept, and the next mission after a
// turn in, once per RequestLinkedMission.
void MissionOfferComponent::OfferLinkedMission(Entity* entity, const uint32_t missionID) {
if (!entity) return;
const auto* const missionComponent = entity->GetComponent<MissionComponent>();
if (!missionComponent) return;
const auto next = GetLinkedMission(GetMissionsForPlayer(*missionComponent, false), missionID);
if (next) OfferMissionToPlayer(*entity, *next, m_Parent->GetObjectID());
}
std::optional<uint32_t> MissionOfferComponent::GetLinkedMission(std::vector<MissionOfferEntry> entries, const uint32_t missionID) {
if (!entries.empty() && entries.front().missionID == missionID) entries.erase(entries.begin());
if (entries.empty()) return std::nullopt;
const auto state = entries.front().state;
if ((state & static_cast<int32_t>(eMissionState::AVAILABLE)) && !(state & static_cast<int32_t>(eMissionState::COMPLETE))) {
return entries.front().missionID;
}
return std::nullopt;
}