mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
fix(missions): prerequisites read as the client reads them
The client checks Missions.prereqMissionID in LWOMissionComponent::getMissionState (0x00cd7400): text without any of !&^;,|(): is one mission ID (met when 0 or ever completed); anything else goes through its expression reader (sub_00a32980), left to right with no precedence, & , ; and, | or, ^ xor, ! ~ not, ( ) groups, and/or skipping the rest once the result is known. Each operand (sub_00c90a80): "id" is met once the mission was ever completed; "id:state" when the client's state mask of that mission equals state. DLU read the text right to left ignoring groups, so "(509|229)&(812|813|814|815)" (mission 781) was met by 509 alone; a plain ID needed the mission's state to be 8, so a repeatable being repeated blocked the missions after it (1870 after 1869); and id:state compared the mission's own state. GetClientMissionState moves to MissionPrerequisites so operands can ask for the mask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,24 +76,7 @@ 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;
|
||||
return MissionPrerequisites::GetClientMissionState(missionId, m_Missions);
|
||||
}
|
||||
|
||||
bool MissionComponent::IsCooldownRunning(const int64_t completedAt, const int64_t cooldownMinutes, const int64_t now) {
|
||||
|
||||
@@ -1,134 +1,172 @@
|
||||
#include "MissionPrerequisites.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
|
||||
#include "CDClientManager.h"
|
||||
#include "Logger.h"
|
||||
#include "CDMissionsTable.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "eMissionState.h"
|
||||
|
||||
namespace {
|
||||
// The token classes of the client's expression reader (sub_00a32720)
|
||||
enum class eToken { End, Open, Close, And, Or, Xor, Not, Word, Space, None };
|
||||
|
||||
PrerequisiteExpression::PrerequisiteExpression(const std::string& str) {
|
||||
std::stringstream a;
|
||||
std::stringstream b;
|
||||
std::stringstream s;
|
||||
|
||||
auto bor = false;
|
||||
|
||||
auto sub = false;
|
||||
|
||||
auto done = false;
|
||||
|
||||
for (auto i = 0u; i < str.size(); ++i) {
|
||||
if (done) {
|
||||
break;
|
||||
eToken Classify(const std::string_view text, const size_t pos) {
|
||||
if (pos >= text.size()) return eToken::End;
|
||||
switch (text[pos]) {
|
||||
case '\0': return eToken::End;
|
||||
case '\t': case '\n': case '\r': case ' ': return eToken::Space;
|
||||
case '!': case '~': return eToken::Not;
|
||||
case '&': case ',': case ';': return eToken::And;
|
||||
case '(': return eToken::Open;
|
||||
case ')': return eToken::Close;
|
||||
case '^': return eToken::Xor;
|
||||
case '|': return eToken::Or;
|
||||
default: return eToken::Word;
|
||||
}
|
||||
}
|
||||
|
||||
const auto character = str[i];
|
||||
// The next token from pos (sub_00a328d0), skipping spaces; a word is read whole
|
||||
eToken NextToken(const std::string_view text, size_t& pos, std::string_view& word) {
|
||||
auto token = eToken::Space;
|
||||
while (token == eToken::Space) token = Classify(text, pos++);
|
||||
if (token != eToken::Word) return token;
|
||||
|
||||
switch (character) {
|
||||
case '|':
|
||||
bor = true;
|
||||
b << str.substr(i + 1);
|
||||
done = true;
|
||||
break;
|
||||
case ' ':
|
||||
case ')':
|
||||
break;
|
||||
case ',':
|
||||
case '&':
|
||||
case '(':
|
||||
b << str.substr(i + 1);
|
||||
done = true;
|
||||
break;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
if (sub) {
|
||||
s << character;
|
||||
} else {
|
||||
a << character;
|
||||
const auto start = pos - 1;
|
||||
while (Classify(text, pos) == eToken::Word) pos++;
|
||||
word = text.substr(start, pos - start);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Moves past the ')' closing the group just opened (sub_00a32840); false when the text ends first
|
||||
bool SkipGroup(const std::string_view text, size_t& pos) {
|
||||
int32_t depth = 1;
|
||||
while (depth > 0) {
|
||||
const auto token = Classify(text, pos++);
|
||||
if (token == eToken::End) return false;
|
||||
if (token == eToken::Open) depth++;
|
||||
else if (token == eToken::Close) depth--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// sub_00a32980: one level, up to its ')' or the end of the text
|
||||
bool Evaluate(const std::string_view text, size_t& pos, const std::function<bool(std::string_view)>& isMet) {
|
||||
bool result = false;
|
||||
auto op = eToken::Space; // the first operand sets the result
|
||||
bool positive = true;
|
||||
bool seenOperand = false;
|
||||
|
||||
const auto apply = [&](const std::function<bool()>& value) {
|
||||
switch (op) {
|
||||
case eToken::Or: if (!result) result = value(); break;
|
||||
case eToken::And: if (result) result = value(); break;
|
||||
case eToken::Xor: result = result ^ value(); break;
|
||||
case eToken::Space: result = value(); break;
|
||||
default: break; // no operator: ignored
|
||||
}
|
||||
break;
|
||||
case ':':
|
||||
sub = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this->m_or = bor;
|
||||
|
||||
const auto aString = a.str();
|
||||
|
||||
if (!aString.empty()) {
|
||||
this->a = std::stoul(a.str());
|
||||
} else {
|
||||
this->a = 0;
|
||||
}
|
||||
|
||||
const auto subString = s.str();
|
||||
|
||||
if (!subString.empty()) {
|
||||
this->sub = std::stoul(s.str());
|
||||
} else {
|
||||
this->sub = 0;
|
||||
}
|
||||
|
||||
const auto bString = b.str();
|
||||
|
||||
if (!bString.empty()) {
|
||||
this->b = new PrerequisiteExpression(bString);
|
||||
} else {
|
||||
this->b = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool PrerequisiteExpression::Execute(const std::unordered_map<uint32_t, Mission*>& missions) const {
|
||||
auto a = this->a == 0;
|
||||
|
||||
auto b = this->b == nullptr;
|
||||
|
||||
if (!a) {
|
||||
const auto index = missions.find(this->a);
|
||||
|
||||
if (index != missions.end()) {
|
||||
const auto* mission = index->second;
|
||||
|
||||
if (this->sub != 0) {
|
||||
// Special case for one Wisp Lee repeatable mission.
|
||||
a = mission->GetMissionState() == static_cast<eMissionState>(this->sub);
|
||||
} else if (mission->IsComplete()) {
|
||||
a = true;
|
||||
while (true) {
|
||||
std::string_view word;
|
||||
const auto token = NextToken(text, pos, word);
|
||||
switch (token) {
|
||||
case eToken::End:
|
||||
case eToken::Close:
|
||||
return result;
|
||||
case eToken::Open: {
|
||||
auto inner = pos;
|
||||
apply([&] { return Evaluate(text, inner, isMet) == positive; });
|
||||
seenOperand = true;
|
||||
positive = true;
|
||||
op = eToken::None;
|
||||
if (!SkipGroup(text, pos)) return result;
|
||||
break;
|
||||
}
|
||||
case eToken::And:
|
||||
case eToken::Or:
|
||||
case eToken::Xor:
|
||||
if (seenOperand) op = token;
|
||||
break;
|
||||
case eToken::Not:
|
||||
positive = !positive;
|
||||
break;
|
||||
case eToken::Word:
|
||||
apply([&] { return isMet(word) == positive; });
|
||||
seenOperand = true;
|
||||
positive = true;
|
||||
op = eToken::None;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!b) {
|
||||
b = this->b->Execute(missions);
|
||||
// wcstol as the client reads an ID: leading digits, 0 when there are none
|
||||
int32_t ReadInt(const std::string_view text) {
|
||||
const std::string copy(text);
|
||||
const auto value = std::strtol(copy.c_str(), nullptr, 10);
|
||||
if (value > std::numeric_limits<int32_t>::max()) return std::numeric_limits<int32_t>::max();
|
||||
if (value < std::numeric_limits<int32_t>::min()) return std::numeric_limits<int32_t>::min();
|
||||
return static_cast<int32_t>(value);
|
||||
}
|
||||
|
||||
if (this->m_or) {
|
||||
return a || b;
|
||||
// LWOMissionComponent::IsMissionDone: the mission is in the completed list
|
||||
bool EverCompleted(const int32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
if (missionId <= 0) return false;
|
||||
const auto it = missions.find(static_cast<uint32_t>(missionId));
|
||||
return it != missions.end() && it->second && (it->second->GetCompletions() > 0 || it->second->IsComplete());
|
||||
}
|
||||
}
|
||||
|
||||
bool MissionPrerequisites::EvaluateExpression(const std::string_view expression, const std::function<bool(std::string_view)>& isMet) {
|
||||
size_t pos = 0;
|
||||
return Evaluate(expression, pos, isMet);
|
||||
}
|
||||
|
||||
bool MissionPrerequisites::IsOperandMet(const std::string_view operand, const uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
constexpr auto COMPLETE = static_cast<int32_t>(eMissionState::COMPLETE);
|
||||
|
||||
const auto colon = operand.find(':');
|
||||
const auto id = ReadInt(operand.substr(0, colon));
|
||||
const auto wanted = colon == std::string_view::npos ? COMPLETE : ReadInt(operand.substr(colon + 1));
|
||||
|
||||
int32_t state = 0;
|
||||
if (wanted == COMPLETE) {
|
||||
state = EverCompleted(id, missions) ? COMPLETE : 0;
|
||||
} else if (id == static_cast<int64_t>(missionId)) {
|
||||
state = static_cast<int32_t>(eMissionState::REWARDING); // the client logs a mission naming itself
|
||||
} else if (id > 0) {
|
||||
state = GetClientMissionState(static_cast<uint32_t>(id), missions);
|
||||
}
|
||||
return state == wanted;
|
||||
}
|
||||
|
||||
int32_t MissionPrerequisites::GetClientMissionState(const uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
// 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 it = missions.find(missionId);
|
||||
const auto* const mission = it != missions.end() ? it->second : nullptr;
|
||||
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) && CheckPrerequisites(missionId, missions)) {
|
||||
state |= static_cast<int32_t>(eMissionState::AVAILABLE);
|
||||
}
|
||||
|
||||
return a && b;
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
PrerequisiteExpression::~PrerequisiteExpression() {
|
||||
delete b;
|
||||
}
|
||||
|
||||
|
||||
bool MissionPrerequisites::CanAccept(const uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
const auto& missionIndex = missions.find(missionId);
|
||||
|
||||
@@ -155,24 +193,25 @@ bool MissionPrerequisites::CanAccept(const uint32_t missionId, const std::unorde
|
||||
return CheckPrerequisites(missionId, missions);
|
||||
}
|
||||
|
||||
bool MissionPrerequisites::CheckPrerequisites(uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
const auto& index = expressions.find(missionId);
|
||||
if (index != expressions.end()) {
|
||||
return index->second->Execute(missions);
|
||||
bool MissionPrerequisites::CheckPrerequisites(const uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions) {
|
||||
auto index = expressions.find(missionId);
|
||||
if (index == expressions.end()) {
|
||||
CDMissions info{};
|
||||
if (!Mission::IsValidMission(missionId, info)) return false;
|
||||
index = expressions.insert_or_assign(missionId, info.prereqMissionID).first;
|
||||
}
|
||||
|
||||
auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();
|
||||
const auto missionEntries = missionsTable->Query([=](const CDMissions& entry) {
|
||||
return entry.id == static_cast<int>(missionId);
|
||||
});
|
||||
const std::string_view text = index->second;
|
||||
if (text.empty()) return true;
|
||||
|
||||
if (missionEntries.empty())
|
||||
return false;
|
||||
// Without any of the client's operator characters (L"!&^;,|():") the text is one mission ID (wcstol), met when 0
|
||||
// or completed
|
||||
if (text.find_first_of("!&^;,|():") == std::string_view::npos) {
|
||||
const auto id = ReadInt(text);
|
||||
return id == 0 || EverCompleted(id, missions);
|
||||
}
|
||||
|
||||
auto* expression = new PrerequisiteExpression(missionEntries[0].prereqMissionID);
|
||||
expressions.insert_or_assign(missionId, expression);
|
||||
|
||||
return expression->Execute(missions);
|
||||
return EvaluateExpression(text, [&](const std::string_view operand) { return IsOperandMet(operand, missionId, missions); });
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, PrerequisiteExpression*> MissionPrerequisites::expressions = {};
|
||||
std::unordered_map<uint32_t, std::string> MissionPrerequisites::expressions = {};
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "Mission.h"
|
||||
|
||||
/**
|
||||
* An expression that checks if a mission may be accepted or not
|
||||
*/
|
||||
class PrerequisiteExpression final
|
||||
{
|
||||
bool m_or;
|
||||
uint32_t a;
|
||||
uint32_t sub;
|
||||
PrerequisiteExpression* b;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Executes the prerequisite, checking its contents and returning whether or not the mission may be accepted
|
||||
* @param missions the list of missions to check the prerequisites against (f.e. whether they're completed)
|
||||
* @return whether or not all the prerequisites are met
|
||||
*/
|
||||
bool Execute(const std::unordered_map<uint32_t, Mission*>& missions) const;
|
||||
|
||||
explicit PrerequisiteExpression(const std::string& str);
|
||||
~PrerequisiteExpression();
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility class for checking whether or not a mission can be accepted using its prerequisites
|
||||
*/
|
||||
@@ -43,17 +23,38 @@ public:
|
||||
static bool CanAccept(uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions);
|
||||
|
||||
/**
|
||||
* Checks the prerequisites for a mission
|
||||
* Checks the prerequisites (Missions.prereqMissionID) of a mission, as the client
|
||||
* (LWOMissionComponent::getMissionState 0x00cd7400): an expression is evaluated by EvaluateExpression with
|
||||
* IsOperandMet for each operand; text without any of !&^;,|(): is one mission ID, met when 0 or once ever completed.
|
||||
* @param missionId the mission ID to check prerequisites for
|
||||
* @param missions the mission inventory to check the prerequisites against
|
||||
* @return whether or not the prerequisites of the mission identified by the specified ID are met
|
||||
*/
|
||||
static bool CheckPrerequisites(uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions);
|
||||
private:
|
||||
|
||||
/**
|
||||
* Cache of all the executed prerequisites
|
||||
* The client's state bit mask for a mission (LWOMissionComponent::getMissionState 0x00cd7400): 8 once ever
|
||||
* completed; in progress, its state (with 8 if completed before); else 1 when never completed (or repeatable) and
|
||||
* the prerequisites are met; 0 for no mission.
|
||||
*/
|
||||
static std::unordered_map<uint32_t, PrerequisiteExpression*> expressions;
|
||||
static int32_t GetClientMissionState(uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions);
|
||||
|
||||
/**
|
||||
* One operand of a prerequisite expression (sub_00c90a80): "id" is met once the mission was ever completed;
|
||||
* "id:state" when the client's state mask of that mission is state (a mission naming itself counts as state 0).
|
||||
*/
|
||||
static bool IsOperandMet(std::string_view operand, uint32_t missionId, const std::unordered_map<uint32_t, Mission*>& missions);
|
||||
|
||||
/**
|
||||
* The client's boolean expression evaluator (sub_00a32980): read left to right, no precedence; & , ; and, | or,
|
||||
* ^ xor, ! ~ negate the next operand, ( ) group. And/or skip their right side once the result is known; an operand
|
||||
* or group with no operator before it (other than the first) is ignored.
|
||||
*/
|
||||
static bool EvaluateExpression(std::string_view expression, const std::function<bool(std::string_view)>& isMet);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Cache of the prerequisite text of each mission
|
||||
*/
|
||||
static std::unordered_map<uint32_t, std::string> expressions;
|
||||
};
|
||||
|
||||
@@ -10,17 +10,28 @@
|
||||
#include "Mission.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "MissionMessages.h"
|
||||
#include "MissionPrerequisites.h"
|
||||
#include "eMissionState.h"
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
// Mission IDs only these tests use (the prerequisite cache is static)
|
||||
constexpr uint32_t VIEWED = 992001;
|
||||
constexpr uint32_t NOT_VIEWED = 992002;
|
||||
constexpr uint32_t FINISHED = 992003;
|
||||
constexpr uint32_t REPEAT = 992004; // repeatable, completed once, repeated now
|
||||
constexpr uint32_t AFTER_REPEAT = 992005; // needs REPEAT
|
||||
constexpr uint32_t DAILY = 992006; // repeatable, completed, no cooldown
|
||||
constexpr uint32_t AFTER_DAILY = 992007; // needs DAILY:9 (completed and available again)
|
||||
constexpr uint32_t AFTER_ACTIVE = 992008; // needs NOT_VIEWED:2 (in progress)
|
||||
constexpr uint32_t GROUPED = 992009; // needs (FINISHED|VIEWED)&(992010|992011)
|
||||
constexpr uint32_t ZERO = 992012; // prerequisite "0"
|
||||
|
||||
CDMissions Row(const uint32_t id, const bool repeatable = false, const std::string& prereq = "", const int64_t cooldown = -1) {
|
||||
CDMissions row{};
|
||||
@@ -40,7 +51,14 @@ protected:
|
||||
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 : { VIEWED, NOT_VIEWED, FINISHED }) missions.push_back(Row(id));
|
||||
for (const auto id : { VIEWED, NOT_VIEWED, FINISHED, 992010u, 992011u }) missions.push_back(Row(id));
|
||||
missions.push_back(Row(REPEAT, true));
|
||||
missions.push_back(Row(AFTER_REPEAT, false, std::to_string(REPEAT)));
|
||||
missions.push_back(Row(DAILY, true, "", 0));
|
||||
missions.push_back(Row(AFTER_DAILY, false, std::to_string(DAILY) + ":9"));
|
||||
missions.push_back(Row(AFTER_ACTIVE, false, std::to_string(NOT_VIEWED) + ":2"));
|
||||
missions.push_back(Row(GROUPED, false, "(992003|992001)&(992010|992011)"));
|
||||
missions.push_back(Row(ZERO, false, "0"));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
@@ -110,3 +128,64 @@ TEST_F(MissionClientRulesTests, SetMissionViewedWire) {
|
||||
EXPECT_TRUE(in.viewed);
|
||||
EXPECT_EQ(stream.GetNumberOfBitsUsed(), 33);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool Eval(const std::string& expression, const std::set<std::string>& met, std::vector<std::string>* asked = nullptr) {
|
||||
return MissionPrerequisites::EvaluateExpression(expression, [&](const std::string_view operand) {
|
||||
if (asked) asked->emplace_back(operand);
|
||||
return met.contains(std::string(operand));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MissionClientRulesTests, ExpressionsReadLeftToRightAsTheClient) {
|
||||
// Groups are their own value (DLU read "(509|229)&(812|...)" as 509 | (229 & (812|...)))
|
||||
EXPECT_FALSE(Eval("(509|229)&(812|813|814|815)", { "509" }));
|
||||
EXPECT_TRUE(Eval("(509|229)&(812|813|814|815)", { "229", "814" }));
|
||||
EXPECT_TRUE(Eval("1203, (1685|1321|1448|1292)", { "1203", "1448" }));
|
||||
EXPECT_FALSE(Eval("1203, (1685|1321|1448|1292)", { "1448" }));
|
||||
// No precedence: a | b & c is (a | b) & c
|
||||
EXPECT_FALSE(Eval("1|2&3", { "1" }));
|
||||
EXPECT_TRUE(Eval("1|2&3", { "1", "3" }));
|
||||
EXPECT_TRUE(Eval("1;2", { "1", "2" }));
|
||||
EXPECT_TRUE(Eval("1^2", { "2" }));
|
||||
EXPECT_FALSE(Eval("1^2", { "1", "2" }));
|
||||
EXPECT_TRUE(Eval("!1", {}));
|
||||
EXPECT_TRUE(Eval("~1 & 2", { "2" }));
|
||||
EXPECT_TRUE(Eval("!(1|2)", {}));
|
||||
EXPECT_TRUE(Eval(" 1882:10 | 1882:2", { "1882:2" }));
|
||||
// An operand with no operator before it is ignored
|
||||
EXPECT_TRUE(Eval("1 2", { "1" }));
|
||||
EXPECT_FALSE(Eval("", { "1" }));
|
||||
// And/or stop once the result is known
|
||||
std::vector<std::string> asked;
|
||||
EXPECT_TRUE(Eval("1|2|3", { "1" }, &asked));
|
||||
EXPECT_EQ(asked, (std::vector<std::string>{ "1" }));
|
||||
asked.clear();
|
||||
EXPECT_FALSE(Eval("1&(2|3)", {}, &asked));
|
||||
EXPECT_EQ(asked, (std::vector<std::string>{ "1" }));
|
||||
}
|
||||
|
||||
TEST_F(MissionClientRulesTests, PrerequisitesAsTheClient) {
|
||||
Entity player(44, info);
|
||||
auto* const missions = player.AddComponent<MissionComponent>(-1);
|
||||
Load(*missions,
|
||||
R"(<m id="992003" state="8" cct="1" cts="1000"/><m id="992004" state="10" cct="1" cts="1000"/><m id="992006" state="8" cct="1" cts="1000"/>)",
|
||||
R"(<m id="992004" state="10"/><m id="992002" state="2"/>)");
|
||||
|
||||
// A plain ID is met once the mission was ever completed, also while it is being repeated (DLU wanted state 8)
|
||||
EXPECT_TRUE(missions->CanAccept(AFTER_REPEAT));
|
||||
// id:state is the client's state mask: 9 = completed and available again (DLU compared the mission's own state)
|
||||
EXPECT_EQ(missions->GetClientMissionState(DAILY), 9);
|
||||
EXPECT_TRUE(missions->CanAccept(AFTER_DAILY));
|
||||
EXPECT_TRUE(missions->CanAccept(AFTER_ACTIVE));
|
||||
EXPECT_FALSE(missions->CanAccept(GROUPED)); // FINISHED done, but neither of the second group
|
||||
EXPECT_TRUE(missions->CanAccept(ZERO));
|
||||
|
||||
Entity other(45, info);
|
||||
auto* const fresh = other.AddComponent<MissionComponent>(-1);
|
||||
EXPECT_FALSE(fresh->CanAccept(AFTER_REPEAT));
|
||||
EXPECT_FALSE(fresh->CanAccept(AFTER_DAILY));
|
||||
EXPECT_FALSE(fresh->CanAccept(AFTER_ACTIVE));
|
||||
EXPECT_EQ(fresh->GetClientMissionState(AFTER_DAILY), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user