mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
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>
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
* Copyright 2019
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include "MissionOfferComponent.h"
|
||||
#include "ObjectMessages.h"
|
||||
@@ -15,6 +18,7 @@
|
||||
#include "MissionComponent.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "MissionPrerequisites.h"
|
||||
#include "eMissionState.h"
|
||||
|
||||
@@ -62,6 +66,12 @@ void MissionOfferComponent::LoadDataFromTemplate() {
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -85,8 +95,99 @@ bool MissionOfferComponent::OnRequestUse(GameMessages::RequestUseEvent& event) {
|
||||
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) {
|
||||
// First, get the entity's MissionComponent. If there is not one, then we cannot offer missions to this entity.
|
||||
if (!entity) return;
|
||||
auto* missionComponent = entity->GetComponent<MissionComponent>();
|
||||
|
||||
if (!missionComponent) {
|
||||
@@ -94,111 +195,51 @@ void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifi
|
||||
return;
|
||||
}
|
||||
|
||||
CDMissions info{};
|
||||
const auto entries = GetMissionsForPlayer(*missionComponent, false);
|
||||
|
||||
if (specifiedMissionId > 0 && !Mission::IsValidMission(specifiedMissionId, info)) {
|
||||
if (specifiedMissionId == 0) {
|
||||
if (!entries.empty()) OfferMissionToPlayer(*entity, entries.front().missionID, m_Parent->GetObjectID());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto offeredMission : this->offeredMissions) {
|
||||
if (specifiedMissionId > 0) {
|
||||
if (offeredMission.GetMissionId() != specifiedMissionId && !info.isRandom) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::ranges::any_of(entries, [specifiedMissionId](const MissionOfferEntry& entry) { return entry.missionID == specifiedMissionId; })) {
|
||||
OfferMissionToPlayer(*entity, specifiedMissionId, m_Parent->GetObjectID());
|
||||
return;
|
||||
}
|
||||
|
||||
// First, check if we already have the mission
|
||||
const auto missionId = offeredMission.GetMissionId();
|
||||
|
||||
auto* mission = missionComponent->GetMission(missionId);
|
||||
|
||||
if (mission != nullptr) {
|
||||
if (specifiedMissionId <= 0) {
|
||||
// Handles the odd case where the offer object should not display the mission again
|
||||
if (!mission->IsComplete() && mission->GetClientInfo().offer_objectID == m_Parent->GetLOT() && mission->GetClientInfo().target_objectID != m_Parent->GetLOT() && mission->IsFetchMission()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We have the mission, if it is not complete, offer it
|
||||
if (mission->IsActive() || mission->IsReadyToComplete()) {
|
||||
OfferMissionToPlayer(*entity, missionId, m_Parent->GetObjectID());
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const auto canAccept = MissionPrerequisites::CanAccept(missionId, missionComponent->GetMissions());
|
||||
|
||||
// Mission has not yet been accepted - check the prereqs
|
||||
if (!canAccept || !Mission::IsValidMission(missionId, info)) continue;
|
||||
|
||||
const auto& randomPool = info.randomPool;
|
||||
const auto isRandom = info.isRandom;
|
||||
|
||||
// This means the mission is part of a random pool of missions.
|
||||
if (isRandom && randomPool.empty()) continue;
|
||||
|
||||
if (isRandom && !randomPool.empty()) {
|
||||
std::istringstream stream(randomPool);
|
||||
std::string token;
|
||||
|
||||
std::vector<uint32_t> randomMissionPool;
|
||||
|
||||
while (std::getline(stream, token, ',')) {
|
||||
try {
|
||||
const auto value = std::stoul(token);
|
||||
|
||||
randomMissionPool.push_back(value);
|
||||
} catch (std::invalid_argument& exception) {
|
||||
LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what());
|
||||
}
|
||||
}
|
||||
|
||||
if (specifiedMissionId > 0) {
|
||||
const auto& iter = std::find(randomMissionPool.begin(), randomMissionPool.end(), specifiedMissionId);
|
||||
|
||||
if (iter != randomMissionPool.end() && MissionPrerequisites::CanAccept(specifiedMissionId, missionComponent->GetMissions())) {
|
||||
OfferMissionToPlayer(*entity, specifiedMissionId, m_Parent->GetObjectID());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint32_t> canAcceptPool;
|
||||
|
||||
for (const auto sample : randomMissionPool) {
|
||||
const auto state = missionComponent->GetMissionState(sample);
|
||||
|
||||
if (state == eMissionState::ACTIVE ||
|
||||
state == eMissionState::COMPLETE_ACTIVE ||
|
||||
state == eMissionState::READY_TO_COMPLETE ||
|
||||
state == eMissionState::COMPLETE_READY_TO_COMPLETE ||
|
||||
sample == specifiedMissionId) {
|
||||
mission = missionComponent->GetMission(sample);
|
||||
|
||||
if (mission == nullptr || mission->IsAchievement()) continue;
|
||||
|
||||
OfferMissionToPlayer(*entity, sample, m_Parent->GetObjectID());
|
||||
|
||||
canAcceptPool.clear();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (MissionPrerequisites::CanAccept(sample, missionComponent->GetMissions())) {
|
||||
canAcceptPool.push_back(sample);
|
||||
}
|
||||
}
|
||||
|
||||
// If the mission is already active or we already completed one of them today
|
||||
if (canAcceptPool.empty()) continue;
|
||||
|
||||
const auto selected = canAcceptPool[GeneralUtils::GenerateRandomNumber<int>(0, canAcceptPool.size() - 1)];
|
||||
|
||||
OfferMissionToPlayer(*entity, selected, m_Parent->GetObjectID());
|
||||
} else if (offeredMission.GetOffersMission()) {
|
||||
OfferMissionToPlayer(*entity, missionId, m_Parent->GetObjectID());
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,26 @@
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "Component.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <stdint.h>
|
||||
#include "eReplicaComponentType.h"
|
||||
|
||||
class Entity;
|
||||
class MissionComponent;
|
||||
|
||||
// One mission of a mission giver for a player, as the client lists them (LWOMissionOfferComponent 0x00c605f0): the
|
||||
// lowest priority comes first. 0 a mission to turn in here, 5 one in progress, 10 a new one, 15 a repeat, 20 a
|
||||
// completed one.
|
||||
struct MissionOfferEntry {
|
||||
uint8_t priority{};
|
||||
uint32_t missionID{};
|
||||
int32_t state{};
|
||||
bool isTurnIn{};
|
||||
|
||||
bool operator==(const MissionOfferEntry&) const = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Light wrapper around missions that may be offered by an entity
|
||||
@@ -81,7 +96,26 @@ public:
|
||||
*/
|
||||
void OfferMissions(Entity* entity, uint32_t specifiedMissionId = 0);
|
||||
|
||||
// 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);
|
||||
|
||||
// The client's entry for one mission (0x00bebf30), from its state bit mask (MissionComponent::GetClientMissionState)
|
||||
static std::optional<MissionOfferEntry> GetOfferEntry(uint32_t missionID, int32_t state, bool cooldownRunning, bool offers, bool accepts, bool alsoCompleted);
|
||||
|
||||
// Inserts after the entries of the same priority, as the client's multimap does
|
||||
static void InsertByPriority(std::vector<MissionOfferEntry>& entries, const MissionOfferEntry& entry);
|
||||
|
||||
// The missions of this giver for a player, in the client's order (0x00c605f0)
|
||||
std::vector<MissionOfferEntry> GetMissionsForPlayer(const MissionComponent& missionComponent, bool alsoCompleted) const;
|
||||
|
||||
// The mission a RequestLinkedMission offers: the head of the list without the mission the window was for, when new
|
||||
static std::optional<uint32_t> GetLinkedMission(std::vector<MissionOfferEntry> entries, uint32_t missionID);
|
||||
|
||||
static std::vector<uint32_t> ParseRandomPool(const std::string& randomPool);
|
||||
|
||||
private:
|
||||
const OfferedMission* GetOfferedMission(uint32_t missionID) const;
|
||||
static std::optional<MissionOfferEntry> GetEntryFor(const MissionComponent& missionComponent, const OfferedMission& offered, bool alsoCompleted);
|
||||
|
||||
/**
|
||||
* The missions this entity has to offer
|
||||
|
||||
@@ -14,6 +14,7 @@ set(DCOMPONENTS_TESTS
|
||||
"SavingTests.cpp"
|
||||
"CharacterSaveFieldsTests.cpp"
|
||||
"CollectibleComponentTests.cpp"
|
||||
"MissionOfferTests.cpp"
|
||||
"ReplicaComponentOrderTests.cpp"
|
||||
"QuickBuildCompleteTests.cpp"
|
||||
"RocketLaunchTests.cpp"
|
||||
|
||||
220
tests/dGameTests/dComponentsTests/MissionOfferTests.cpp
Normal file
220
tests/dGameTests/dComponentsTests/MissionOfferTests.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
// Which missions a mission giver lists for a player and offers, as the 1.10.64 client works it out
|
||||
// (LWOMissionOfferComponent 0x00c605f0 / 0x00bebf30, LWOMissionComponent::getMissionState 0x00cd7400).
|
||||
#include "GameDependencies.h"
|
||||
#include "PacketTestUtils.h"
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "CDMissionNPCComponentTable.h"
|
||||
#include "CDMissionsTable.h"
|
||||
#include "Entity.h"
|
||||
#include "GameMessages.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "MissionOfferComponent.h"
|
||||
#include "eMissionState.h"
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
// Mission IDs only these tests use (the prerequisite cache is static)
|
||||
constexpr uint32_t NEW = 991001; // no prerequisites
|
||||
constexpr uint32_t ACTIVE = 991002;
|
||||
constexpr uint32_t TURN_IN = 991003; // ready to complete, accepted here
|
||||
constexpr uint32_t ELSEWHERE = 991004; // ready to complete, only offered here
|
||||
constexpr uint32_t DONE = 991005; // completed, not repeatable
|
||||
constexpr uint32_t DAILY = 991006; // completed, repeatable, 60 minute cooldown
|
||||
constexpr uint32_t LOCKED = 991007; // needs DONE_LATER
|
||||
constexpr uint32_t DONE_LATER = 991008;
|
||||
constexpr uint32_t NEXT = 991009; // needs DONE
|
||||
constexpr int32_t GIVER = 99100;
|
||||
|
||||
CDMissions Row(const uint32_t id, const bool repeatable = false, const std::string& prereq = "", const int64_t cooldown = -1) {
|
||||
CDMissions row{};
|
||||
row.id = id;
|
||||
row.isMission = true;
|
||||
row.repeatable = repeatable;
|
||||
row.prereqMissionID = prereq;
|
||||
row.cooldownTime = cooldown;
|
||||
return row;
|
||||
}
|
||||
|
||||
int32_t State(const eMissionState state) { return static_cast<int32_t>(state); }
|
||||
}
|
||||
|
||||
class MissionOfferTests : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);");
|
||||
auto& missions = CDClientManager::GetEntriesMutable<CDMissionsTable>();
|
||||
for (const auto id : { NEW, ACTIVE, TURN_IN, ELSEWHERE, DONE, DONE_LATER }) missions.push_back(Row(id));
|
||||
missions.push_back(Row(DAILY, true, "", 60));
|
||||
missions.push_back(Row(LOCKED, false, std::to_string(DONE_LATER)));
|
||||
missions.push_back(Row(NEXT, false, std::to_string(DONE)));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
CDClientManager::GetEntriesMutable<CDMissionsTable>().clear();
|
||||
CDClientManager::GetEntriesMutable<CDMissionNPCComponentTable>().clear();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
static void Load(MissionComponent& missions, const std::string& done, const std::string& cur) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto xml = R"(<obj v="1"><mis><done>)" + done + "</done><cur>" + cur + "</cur></mis></obj>";
|
||||
ASSERT_EQ(doc.Parse(xml.c_str()), tinyxml2::XML_SUCCESS);
|
||||
missions.LoadFromSaveData(doc);
|
||||
}
|
||||
|
||||
static std::string Done(const uint32_t id, const uint32_t state, const int64_t at) {
|
||||
return "<m id=\"" + std::to_string(id) + "\" state=\"" + std::to_string(state) + "\" cct=\"1\" cts=\"" + std::to_string(at) + "\"/>";
|
||||
}
|
||||
|
||||
static std::string Cur(const uint32_t id, const uint32_t state) {
|
||||
return "<m id=\"" + std::to_string(id) + "\" state=\"" + std::to_string(state) + "\"/>";
|
||||
}
|
||||
|
||||
static void Giver(const std::vector<CDMissionNPCComponent>& rows) {
|
||||
auto& table = CDClientManager::GetEntriesMutable<CDMissionNPCComponentTable>();
|
||||
table = rows;
|
||||
}
|
||||
|
||||
static std::vector<uint32_t> OfferedIDs(const std::vector<CapturedPacket>& packets) {
|
||||
std::vector<uint32_t> ids;
|
||||
for (const auto& packet : packets) {
|
||||
// 8 byte header, the target, the message ID, then the mission ID
|
||||
if (packet.bytes.size() < 22) continue;
|
||||
const uint16_t messageID = packet.bytes[16] | (packet.bytes[17] << 8);
|
||||
if (messageID != 248) continue;
|
||||
ids.push_back(packet.bytes[18] | (packet.bytes[19] << 8) | (packet.bytes[20] << 16) | (static_cast<uint32_t>(packet.bytes[21]) << 24));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MissionOfferTests, EntryPrioritiesAsTheClient) {
|
||||
using C = MissionOfferComponent;
|
||||
const auto ready = State(eMissionState::READY_TO_COMPLETE);
|
||||
// Ready to turn in: 5 where offered and accepted, 0 (a turn in) where only accepted, nothing where only offered
|
||||
EXPECT_EQ(C::GetOfferEntry(1, ready, false, true, true, false), (MissionOfferEntry{ 5, 1, ready, false }));
|
||||
EXPECT_EQ(C::GetOfferEntry(1, ready, false, false, true, false), (MissionOfferEntry{ 0, 1, ready, true }));
|
||||
EXPECT_FALSE(C::GetOfferEntry(1, ready, false, true, false, false));
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::COMPLETE_READY_TO_COMPLETE), false, false, true, false)->priority, 0);
|
||||
// In progress: every giver that has it
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::ACTIVE), false, false, false, false)->priority, 5);
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::COMPLETE_ACTIVE), false, false, false, false)->priority, 5);
|
||||
// Available: only where offered and off cooldown; a repeat after a new one
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::AVAILABLE), false, true, false, false)->priority, 10);
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::COMPLETE_AVAILABLE), false, true, false, false)->priority, 15);
|
||||
EXPECT_FALSE(C::GetOfferEntry(1, State(eMissionState::AVAILABLE), false, false, true, false));
|
||||
EXPECT_FALSE(C::GetOfferEntry(1, State(eMissionState::COMPLETE_AVAILABLE), true, true, false, false));
|
||||
// Completed: only when asked for
|
||||
EXPECT_FALSE(C::GetOfferEntry(1, State(eMissionState::COMPLETE), false, true, true, false));
|
||||
EXPECT_EQ(C::GetOfferEntry(1, State(eMissionState::COMPLETE), false, false, true, true), (MissionOfferEntry{ 20, 1, 8, true }));
|
||||
EXPECT_FALSE(C::GetOfferEntry(1, 0, false, true, true, true));
|
||||
}
|
||||
|
||||
TEST_F(MissionOfferTests, SamePriorityKeepsInsertionOrder) {
|
||||
std::vector<MissionOfferEntry> entries;
|
||||
MissionOfferComponent::InsertByPriority(entries, { 10, 1 });
|
||||
MissionOfferComponent::InsertByPriority(entries, { 5, 2 });
|
||||
MissionOfferComponent::InsertByPriority(entries, { 10, 3 });
|
||||
MissionOfferComponent::InsertByPriority(entries, { 0, 4 });
|
||||
MissionOfferComponent::InsertByPriority(entries, { 5, 5 });
|
||||
std::vector<uint32_t> ids;
|
||||
for (const auto& entry : entries) ids.push_back(entry.missionID);
|
||||
EXPECT_EQ(ids, (std::vector<uint32_t>{ 4, 2, 5, 1, 3 }));
|
||||
}
|
||||
|
||||
TEST_F(MissionOfferTests, CooldownInWholeMinutes) {
|
||||
constexpr int64_t at = 1000000;
|
||||
EXPECT_TRUE(MissionComponent::IsCooldownRunning(at, 60, at + 59 * 60 + 59));
|
||||
EXPECT_FALSE(MissionComponent::IsCooldownRunning(at, 60, at + 60 * 60));
|
||||
EXPECT_FALSE(MissionComponent::IsCooldownRunning(at, 0, at)); // no cooldown
|
||||
EXPECT_FALSE(MissionComponent::IsCooldownRunning(at, -1, at));
|
||||
EXPECT_TRUE(MissionComponent::IsCooldownRunning(at, 1, at - 120)); // completed in the future
|
||||
}
|
||||
|
||||
TEST_F(MissionOfferTests, ClientMissionStateIsABitMask) {
|
||||
Entity player(42, info);
|
||||
auto* const missions = player.AddComponent<MissionComponent>(-1);
|
||||
const auto now = static_cast<int64_t>(std::time(nullptr));
|
||||
Load(*missions, Done(DONE, 8, now) + Done(DAILY, 8, now), Cur(ACTIVE, 2) + Cur(TURN_IN, 4));
|
||||
|
||||
EXPECT_EQ(missions->GetClientMissionState(NEW), State(eMissionState::AVAILABLE));
|
||||
EXPECT_EQ(missions->GetClientMissionState(ACTIVE), State(eMissionState::ACTIVE));
|
||||
EXPECT_EQ(missions->GetClientMissionState(TURN_IN), State(eMissionState::READY_TO_COMPLETE));
|
||||
EXPECT_EQ(missions->GetClientMissionState(DONE), State(eMissionState::COMPLETE));
|
||||
EXPECT_EQ(missions->GetClientMissionState(DAILY), State(eMissionState::COMPLETE_AVAILABLE)); // even on cooldown
|
||||
EXPECT_EQ(missions->GetClientMissionState(LOCKED), 0); // prerequisite not met
|
||||
EXPECT_EQ(missions->GetClientMissionState(NEXT), State(eMissionState::AVAILABLE));
|
||||
EXPECT_EQ(missions->GetClientMissionState(66666666), 0); // no such mission
|
||||
EXPECT_EQ(missions->GetClientMissionState(0), 0);
|
||||
EXPECT_TRUE(missions->IsCooldownRunning(DAILY));
|
||||
EXPECT_FALSE(missions->IsCooldownRunning(DONE)); // no cooldown
|
||||
EXPECT_FALSE(missions->IsCooldownRunning(NEW)); // never completed
|
||||
|
||||
// The message answers with the same mask (DLU answered -1 for a mission that cannot be taken)
|
||||
GameMessages::GetMissionState locked;
|
||||
locked.missionID = LOCKED;
|
||||
player.HandleMsg(locked);
|
||||
EXPECT_EQ(locked.missionState, static_cast<eMissionState>(0));
|
||||
|
||||
GameMessages::GetMissionState daily;
|
||||
daily.missionID = DAILY;
|
||||
daily.cooldownInfoRequested = true;
|
||||
player.HandleMsg(daily);
|
||||
EXPECT_EQ(daily.missionState, eMissionState::COMPLETE_AVAILABLE);
|
||||
EXPECT_TRUE(daily.cooldownFinished); // the client only checks the cooldown of a repeat in progress
|
||||
}
|
||||
|
||||
TEST_F(MissionOfferTests, GiverListsMissionsInTheClientsOrder) {
|
||||
Giver({
|
||||
{ GIVER, NEW, true, false, "" },
|
||||
{ GIVER, ACTIVE, true, false, "" },
|
||||
{ GIVER, TURN_IN, false, true, "" },
|
||||
{ GIVER, ELSEWHERE, true, false, "" },
|
||||
{ GIVER, DONE, true, true, "" },
|
||||
{ GIVER, DAILY, true, true, "" },
|
||||
{ GIVER, LOCKED, true, false, "" },
|
||||
});
|
||||
Entity npc(43, info);
|
||||
auto* const giver = npc.AddComponent<MissionOfferComponent>(GIVER);
|
||||
giver->LoadDataFromTemplate();
|
||||
|
||||
Entity player(44, info);
|
||||
auto* const missions = player.AddComponent<MissionComponent>(-1);
|
||||
const auto now = static_cast<int64_t>(std::time(nullptr));
|
||||
Load(*missions, Done(DONE, 8, now) + Done(DAILY, 8, now), Cur(ACTIVE, 2) + Cur(TURN_IN, 4) + Cur(ELSEWHERE, 4));
|
||||
|
||||
std::vector<std::pair<uint32_t, uint8_t>> listed;
|
||||
for (const auto& entry : giver->GetMissionsForPlayer(*missions, false)) listed.emplace_back(entry.missionID, entry.priority);
|
||||
// The turn in first, then the mission in progress, then the new one; not the one ready to turn in elsewhere, not
|
||||
// the daily on cooldown, not the done or locked ones
|
||||
EXPECT_EQ(listed, (std::vector<std::pair<uint32_t, uint8_t>>{ { TURN_IN, 0 }, { ACTIVE, 5 }, { NEW, 10 } }));
|
||||
|
||||
// A use offers only the first one (live sent one OfferMission pair per use)
|
||||
const auto sent = PacketTestUtils::Capture([&] { giver->OfferMissions(&player); });
|
||||
EXPECT_EQ(OfferedIDs(sent), (std::vector<uint32_t>{ TURN_IN, TURN_IN }));
|
||||
|
||||
// A pick from the multi interaction menu offers that one, if it is listed
|
||||
EXPECT_EQ(OfferedIDs(PacketTestUtils::Capture([&] { giver->OfferMissions(&player, NEW); })), (std::vector<uint32_t>{ NEW, NEW }));
|
||||
EXPECT_TRUE(OfferedIDs(PacketTestUtils::Capture([&] { giver->OfferMissions(&player, LOCKED); })).empty());
|
||||
}
|
||||
|
||||
TEST_F(MissionOfferTests, LinkedMissionOnlyAfterATurnIn) {
|
||||
const auto active = State(eMissionState::ACTIVE);
|
||||
const auto available = State(eMissionState::AVAILABLE);
|
||||
// Just accepted: the list starts with that mission; nothing else new
|
||||
EXPECT_FALSE(MissionOfferComponent::GetLinkedMission({ { 5, 697, active } }, 697));
|
||||
// Just turned in: the next one
|
||||
EXPECT_EQ(MissionOfferComponent::GetLinkedMission({ { 10, 698, available } }, 697), 698u);
|
||||
// Not a repeat, and not a mission in progress
|
||||
EXPECT_FALSE(MissionOfferComponent::GetLinkedMission({ { 15, 698, State(eMissionState::COMPLETE_AVAILABLE) } }, 697));
|
||||
EXPECT_FALSE(MissionOfferComponent::GetLinkedMission({ { 5, 700, active }, { 10, 698, available } }, 697));
|
||||
EXPECT_FALSE(MissionOfferComponent::GetLinkedMission({}, 697));
|
||||
}
|
||||
Reference in New Issue
Block a user