From 81af488e26311601d4d56056b32c627e49dbd436 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 16 Dec 2023 11:20:56 -0800 Subject: [PATCH] add ability to hide quests based on game player count --- src/Client.cc | 20 +- src/Client.hh | 4 +- src/Lobby.cc | 7 +- src/QuestAvailabilityExpression.cc | 297 ++++++++++++++++++++--------- src/QuestAvailabilityExpression.hh | 85 ++++++--- src/ReceiveCommands.cc | 4 +- system/quests/battle/b88001.json | 11 +- system/quests/download/q072.json | 3 + system/quests/events/q073.json | 3 + system/quests/events/q220.json | 3 + system/quests/events/q240.json | 3 + system/quests/solo-extra/q143.json | 3 + system/quests/solo-story/q026.json | 2 +- system/quests/solo-story/q027.json | 3 + system/quests/vr/q222.json | 3 + system/quests/vr/q230.json | 3 + 16 files changed, 320 insertions(+), 134 deletions(-) create mode 100644 system/quests/download/q072.json create mode 100644 system/quests/events/q073.json create mode 100644 system/quests/events/q220.json create mode 100644 system/quests/events/q240.json create mode 100644 system/quests/solo-extra/q143.json create mode 100644 system/quests/solo-story/q027.json create mode 100644 system/quests/vr/q222.json create mode 100644 system/quests/vr/q230.json diff --git a/src/Client.cc b/src/Client.cc index c731302c..c2e1bd89 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -295,7 +295,7 @@ shared_ptr Client::team() const { return team; } -bool Client::can_see_quest(shared_ptr q, uint8_t difficulty) const { +bool Client::can_see_quest(shared_ptr q, uint8_t difficulty, size_t num_players) const { if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) { return true; } @@ -303,12 +303,17 @@ bool Client::can_see_quest(shared_ptr q, uint8_t difficulty) const return true; } string expr = q->available_expression->str(); - bool ret = q->available_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team()); - this->log.info("Evaluating quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE"); + QuestAvailabilityExpression::Env env = { + .flags = &this->character()->quest_flags.data.at(difficulty), + .team = this->team(), + .num_players = num_players, + }; + int64_t ret = q->available_expression->evaluate(env); + this->log.info("Evaluated quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE"); return ret; } -bool Client::can_play_quest(shared_ptr q, uint8_t difficulty) const { +bool Client::can_play_quest(shared_ptr q, uint8_t difficulty, size_t num_players) const { if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) { return true; } @@ -316,7 +321,12 @@ bool Client::can_play_quest(shared_ptr q, uint8_t difficulty) const return true; } string expr = q->enabled_expression->str(); - bool ret = q->enabled_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team()); + QuestAvailabilityExpression::Env env = { + .flags = &this->character()->quest_flags.data.at(difficulty), + .team = this->team(), + .num_players = num_players, + }; + bool ret = q->enabled_expression->evaluate(env); this->log.info("Evaluating quest enabled expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE"); return ret; } diff --git a/src/Client.hh b/src/Client.hh index 582f8f09..70053f30 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -261,8 +261,8 @@ public: std::shared_ptr team() const; - bool can_see_quest(std::shared_ptr q, uint8_t difficulty) const; - bool can_play_quest(std::shared_ptr q, uint8_t difficulty) const; + bool can_see_quest(std::shared_ptr q, uint8_t difficulty, size_t num_players) const; + bool can_play_quest(std::shared_ptr q, uint8_t difficulty, size_t num_players) const; static void dispatch_save_game_data(evutil_socket_t, short, void* ctx); void save_game_data(); diff --git a/src/Lobby.cc b/src/Lobby.cc index 71e06137..739bf106 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -736,13 +736,14 @@ unordered_map> Lobby::clients_by_serial_number() co } QuestIndex::IncludeCondition Lobby::quest_include_condition() const { - return [this](shared_ptr q) -> QuestIndex::IncludeState { + size_t num_players = this->count_clients(); + return [this, num_players](shared_ptr q) -> QuestIndex::IncludeState { bool is_enabled = true; for (const auto& lc : this->clients) { - if (lc && !lc->can_see_quest(q, this->difficulty)) { + if (lc && !lc->can_see_quest(q, this->difficulty, num_players)) { return QuestIndex::IncludeState::HIDDEN; } - if (lc && !lc->can_play_quest(q, this->difficulty)) { + if (lc && !lc->can_play_quest(q, this->difficulty, num_players)) { is_enabled = false; } } diff --git a/src/QuestAvailabilityExpression.cc b/src/QuestAvailabilityExpression.cc index 1d6305fb..43dd2cb0 100644 --- a/src/QuestAvailabilityExpression.cc +++ b/src/QuestAvailabilityExpression.cc @@ -24,69 +24,144 @@ using namespace std; QuestAvailabilityExpression::QuestAvailabilityExpression(const string& text) : root(this->parse_expr(text)) {} -QuestAvailabilityExpression::OrNode::OrNode(unique_ptr&& left, unique_ptr&& right) - : left(std::move(left)), +QuestAvailabilityExpression::BinaryOperatorNode::BinaryOperatorNode( + Type type, unique_ptr&& left, unique_ptr&& right) + : type(type), + left(std::move(left)), right(std::move(right)) {} -bool QuestAvailabilityExpression::OrNode::operator==(const Node& other) const { +bool QuestAvailabilityExpression::BinaryOperatorNode::operator==(const Node& other) const { try { - const OrNode& other_or = dynamic_cast(other); - return *other_or.left == *this->left && *other_or.right == *this->right; + const BinaryOperatorNode& other_bin = dynamic_cast(other); + return other_bin.type == this->type && *other_bin.left == *this->left && *other_bin.right == *this->right; } catch (const bad_cast&) { return false; } } -bool QuestAvailabilityExpression::OrNode::evaluate( - const QuestFlagsForDifficulty& flags, shared_ptr team) const { - return this->left->evaluate(flags, team) || this->right->evaluate(flags, team); +int64_t QuestAvailabilityExpression::BinaryOperatorNode::evaluate(const Env& env) const { + switch (this->type) { + case Type::LOGICAL_OR: + return this->left->evaluate(env) || this->right->evaluate(env); + case Type::LOGICAL_AND: + return this->left->evaluate(env) && this->right->evaluate(env); + case Type::BITWISE_OR: + return this->left->evaluate(env) | this->right->evaluate(env); + case Type::BITWISE_AND: + return this->left->evaluate(env) & this->right->evaluate(env); + case Type::BITWISE_XOR: + return this->left->evaluate(env) ^ this->right->evaluate(env); + case Type::LEFT_SHIFT: + return this->left->evaluate(env) << this->right->evaluate(env); + case Type::RIGHT_SHIFT: + return this->left->evaluate(env) >> this->right->evaluate(env); + case Type::LESS_THAN: + return this->left->evaluate(env) < this->right->evaluate(env); + case Type::GREATER_THAN: + return this->left->evaluate(env) > this->right->evaluate(env); + case Type::LESS_OR_EQUAL: + return this->left->evaluate(env) <= this->right->evaluate(env); + case Type::GREATER_OR_EQUAL: + return this->left->evaluate(env) >= this->right->evaluate(env); + case Type::EQUAL: + return this->left->evaluate(env) == this->right->evaluate(env); + case Type::NOT_EQUAL: + return this->left->evaluate(env) != this->right->evaluate(env); + case Type::ADD: + return this->left->evaluate(env) + this->right->evaluate(env); + case Type::SUBTRACT: + return this->left->evaluate(env) - this->right->evaluate(env); + case Type::MULTIPLY: + return this->left->evaluate(env) * this->right->evaluate(env); + case Type::DIVIDE: + return this->left->evaluate(env) / this->right->evaluate(env); + case Type::MODULUS: + return this->left->evaluate(env) % this->right->evaluate(env); + default: + throw logic_error("invalid binary operator type"); + } } -string QuestAvailabilityExpression::OrNode::str() const { - return "(" + this->left->str() + ") || (" + this->right->str() + ")"; +string QuestAvailabilityExpression::BinaryOperatorNode::str() const { + switch (this->type) { + case Type::LOGICAL_OR: + return "(" + this->left->str() + ") || (" + this->right->str() + ")"; + case Type::LOGICAL_AND: + return "(" + this->left->str() + ") && (" + this->right->str() + ")"; + case Type::BITWISE_OR: + return "(" + this->left->str() + ") | (" + this->right->str() + ")"; + case Type::BITWISE_AND: + return "(" + this->left->str() + ") & (" + this->right->str() + ")"; + case Type::BITWISE_XOR: + return "(" + this->left->str() + ") ^ (" + this->right->str() + ")"; + case Type::LEFT_SHIFT: + return "(" + this->left->str() + ") << (" + this->right->str() + ")"; + case Type::RIGHT_SHIFT: + return "(" + this->left->str() + ") >> (" + this->right->str() + ")"; + case Type::LESS_THAN: + return "(" + this->left->str() + ") < (" + this->right->str() + ")"; + case Type::GREATER_THAN: + return "(" + this->left->str() + ") > (" + this->right->str() + ")"; + case Type::LESS_OR_EQUAL: + return "(" + this->left->str() + ") <= (" + this->right->str() + ")"; + case Type::GREATER_OR_EQUAL: + return "(" + this->left->str() + ") >= (" + this->right->str() + ")"; + case Type::EQUAL: + return "(" + this->left->str() + ") == (" + this->right->str() + ")"; + case Type::NOT_EQUAL: + return "(" + this->left->str() + ") != (" + this->right->str() + ")"; + case Type::ADD: + return "(" + this->left->str() + ") + (" + this->right->str() + ")"; + case Type::SUBTRACT: + return "(" + this->left->str() + ") - (" + this->right->str() + ")"; + case Type::MULTIPLY: + return "(" + this->left->str() + ") * (" + this->right->str() + ")"; + case Type::DIVIDE: + return "(" + this->left->str() + ") / (" + this->right->str() + ")"; + case Type::MODULUS: + return "(" + this->left->str() + ") % (" + this->right->str() + ")"; + default: + throw logic_error("invalid binary operator type"); + } } -QuestAvailabilityExpression::AndNode::AndNode(unique_ptr&& left, unique_ptr&& right) - : left(std::move(left)), - right(std::move(right)) {} +QuestAvailabilityExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr&& sub) + : type(type), + sub(std::move(sub)) {} -bool QuestAvailabilityExpression::AndNode::operator==(const Node& other) const { +bool QuestAvailabilityExpression::UnaryOperatorNode::operator==(const Node& other) const { try { - const AndNode& other_and = dynamic_cast(other); - return *other_and.left == *this->left && *other_and.right == *this->right; + const UnaryOperatorNode& other_un = dynamic_cast(other); + return other_un.type == this->type && *other_un.sub == *this->sub; } catch (const bad_cast&) { return false; } } -bool QuestAvailabilityExpression::AndNode::evaluate( - const QuestFlagsForDifficulty& flags, shared_ptr team) const { - return this->left->evaluate(flags, team) && this->right->evaluate(flags, team); -} - -string QuestAvailabilityExpression::AndNode::str() const { - return "(" + this->left->str() + ") && (" + this->right->str() + ")"; -} - -QuestAvailabilityExpression::NotNode::NotNode(unique_ptr&& sub) - : sub(std::move(sub)) {} - -bool QuestAvailabilityExpression::NotNode::operator==(const Node& other) const { - try { - const NotNode& other_not = dynamic_cast(other); - return *other_not.sub == *this->sub; - } catch (const bad_cast&) { - return false; +int64_t QuestAvailabilityExpression::UnaryOperatorNode::evaluate(const Env& env) const { + switch (this->type) { + case Type::LOGICAL_NOT: + return !this->sub->evaluate(env); + case Type::BITWISE_NOT: + return ~this->sub->evaluate(env); + case Type::NEGATIVE: + return -this->sub->evaluate(env); + default: + throw logic_error("invalid unary operator type"); } } -bool QuestAvailabilityExpression::NotNode::evaluate( - const QuestFlagsForDifficulty& flags, shared_ptr team) const { - return !this->sub->evaluate(flags, team); -} - -string QuestAvailabilityExpression::NotNode::str() const { - return "!(" + this->sub->str() + ")"; +string QuestAvailabilityExpression::UnaryOperatorNode::str() const { + switch (this->type) { + case Type::LOGICAL_NOT: + return "!(" + this->sub->str() + ")"; + case Type::BITWISE_NOT: + return "~(" + this->sub->str() + ")"; + case Type::NEGATIVE: + return "-(" + this->sub->str() + ")"; + default: + throw logic_error("invalid unary operator type"); + } } QuestAvailabilityExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) @@ -101,9 +176,8 @@ bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) } } -bool QuestAvailabilityExpression::FlagLookupNode::evaluate( - const QuestFlagsForDifficulty& flags, shared_ptr) const { - return flags.get(this->flag_index); +int64_t QuestAvailabilityExpression::FlagLookupNode::evaluate(const Env& env) const { + return env.flags->get(this->flag_index) ? 1 : 0; } string QuestAvailabilityExpression::FlagLookupNode::str() const { @@ -122,15 +196,28 @@ bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& o } } -bool QuestAvailabilityExpression::TeamRewardLookupNode::evaluate( - const QuestFlagsForDifficulty&, shared_ptr team) const { - return team && team->has_reward(this->reward_name); +int64_t QuestAvailabilityExpression::TeamRewardLookupNode::evaluate(const Env& env) const { + return (env.team && env.team->has_reward(this->reward_name)) ? 1 : 0; } string QuestAvailabilityExpression::TeamRewardLookupNode::str() const { return "T_" + this->reward_name; } +QuestAvailabilityExpression::NumPlayersLookupNode::NumPlayersLookupNode() {} + +bool QuestAvailabilityExpression::NumPlayersLookupNode::operator==(const Node& other) const { + return dynamic_cast(&other) != nullptr; +} + +int64_t QuestAvailabilityExpression::NumPlayersLookupNode::evaluate(const Env& env) const { + return env.num_players; +} + +string QuestAvailabilityExpression::NumPlayersLookupNode::str() const { + return "V_NumPlayers"; +} + QuestAvailabilityExpression::ConstantNode::ConstantNode(bool value) : value(value) {} @@ -143,13 +230,12 @@ bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) co } } -bool QuestAvailabilityExpression::ConstantNode::evaluate( - const QuestFlagsForDifficulty&, shared_ptr) const { +int64_t QuestAvailabilityExpression::ConstantNode::evaluate(const Env&) const { return this->value; } string QuestAvailabilityExpression::ConstantNode::str() const { - return this->value ? "true" : "false"; + return string_printf("%" PRId64, this->value); } unique_ptr QuestAvailabilityExpression::parse_expr(string_view text) { @@ -186,48 +272,66 @@ unique_ptr QuestAvailabilityExpression: break; } } + if (text.empty()) { + throw runtime_error("invalid expression"); + } + + // Check for unary operators + if (text[0] == '!') { + return make_unique(UnaryOperatorNode::Type::LOGICAL_NOT, + QuestAvailabilityExpression::parse_expr(text.substr(1))); + } else if (text[0] == '~') { + return make_unique(UnaryOperatorNode::Type::BITWISE_NOT, + QuestAvailabilityExpression::parse_expr(text.substr(1))); + } else if (text[0] == '-') { + return make_unique(UnaryOperatorNode::Type::NEGATIVE, + QuestAvailabilityExpression::parse_expr(text.substr(1))); + } // Check for binary operators at the root level - size_t paren_level = 0; - size_t and_pos = 0; - size_t or_pos = 0; - for (size_t z = 0; z < text.size() - 1; z++) { - if (text[z] == '(') { - paren_level++; - } else if (text[z] == ')') { - paren_level--; - } else if ((text[z] == '&') && (text[z + 1] == '&') && !paren_level) { - and_pos = z; - } else if ((text[z] == '|') && (text[z + 1] == '|') && !paren_level) { - or_pos = z; + using BinType = BinaryOperatorNode::Type; + static const vector>> binary_operator_levels = { + {{make_pair("*", BinType::MULTIPLY)}, {make_pair("/", BinType::DIVIDE)}, {make_pair("%", BinType::MODULUS)}}, + {{make_pair("+", BinType::ADD)}, {make_pair("-", BinType::SUBTRACT)}}, + {{make_pair("<<", BinType::LEFT_SHIFT)}, {make_pair(">>", BinType::RIGHT_SHIFT)}}, + {{make_pair("<=", BinType::LESS_OR_EQUAL)}, {make_pair(">=", BinType::GREATER_OR_EQUAL)}, {make_pair("<", BinType::LESS_THAN)}, {make_pair(">", BinType::GREATER_THAN)}}, + {{make_pair("==", BinType::EQUAL)}, {make_pair("!=", BinType::NOT_EQUAL)}}, + {{make_pair("&", BinType::BITWISE_AND)}}, + {{make_pair("^", BinType::BITWISE_XOR)}}, + {{make_pair("|", BinType::BITWISE_OR)}}, + {{make_pair("&&", BinType::LOGICAL_AND)}}, + {{make_pair("||", BinType::LOGICAL_OR)}}, + }; + for (const auto& operators : binary_operator_levels) { + size_t paren_level = 0; + for (size_t z = 0; z < text.size() - 1; z++) { + if (text[z] == '(') { + paren_level++; + continue; + } else if (text[z] == ')') { + paren_level--; + continue; + } + if (!paren_level) { + for (const auto& oper : operators) { + // Awful hack (because I'm too lazy to add a tokenization step): if + // the operator is followed or preceded by another copy of itself, + // don't match it (this prevents us from matching & when the token is + // actually &&) + if ((text.size() > z + oper.first.size()) && + ((z < oper.first.size()) || (text.compare(z - oper.first.size(), oper.first.size(), oper.first) != 0)) && + (text.compare(z, oper.first.size(), oper.first) == 0) && + (text.compare(z + oper.first.size(), oper.first.size(), oper.first) != 0)) { + auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, z)); + auto right = QuestAvailabilityExpression::parse_expr(text.substr(z + oper.first.size())); + return make_unique(oper.second, std::move(left), std::move(right)); + } + } + } } } - if ((or_pos && (!and_pos || (and_pos > or_pos)))) { - auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, or_pos)); - auto right = QuestAvailabilityExpression::parse_expr(text.substr(or_pos + 2)); - return make_unique(std::move(left), std::move(right)); - } - if ((and_pos && (!or_pos || (or_pos > and_pos)))) { - auto left = QuestAvailabilityExpression::parse_expr(text.substr(0, and_pos)); - auto right = QuestAvailabilityExpression::parse_expr(text.substr(and_pos + 2)); - return make_unique(std::move(left), std::move(right)); - } - // Check for not operator - if (text.at(0) == '!') { - auto sub = QuestAvailabilityExpression::parse_expr(text.substr(1)); - return make_unique(std::move(sub)); - } - - // Check for constants - if (text == "true") { - return make_unique(true); - } - if (text == "false") { - return make_unique(false); - } - - // Check for flag lookups + // Check for env lookups if (text.starts_with("F_")) { char* endptr = nullptr; uint64_t flag = strtoul(text.data() + 2, &endptr, 16); @@ -239,10 +343,27 @@ unique_ptr QuestAvailabilityExpression: } return make_unique(flag); } - if (text.starts_with("T_")) { return make_unique(string(text.substr(2))); } + if (text == "V_NumPlayers") { + return make_unique(); + } + // Check for constants + if (text == "true") { + return make_unique(1); + } + if (text == "false") { + return make_unique(0); + } + try { + size_t endpos; + int64_t v = stoll(string(text), &endpos, 0); + if (endpos == text.size()) { + return make_unique(v); + } + } catch (const exception&) { + } throw runtime_error("unparseable expression"); } diff --git a/src/QuestAvailabilityExpression.hh b/src/QuestAvailabilityExpression.hh index 7f604532..796381dc 100644 --- a/src/QuestAvailabilityExpression.hh +++ b/src/QuestAvailabilityExpression.hh @@ -15,6 +15,12 @@ class QuestAvailabilityExpression { public: + struct Env { + const QuestFlagsForDifficulty* flags; + std::shared_ptr team; + size_t num_players; + }; + QuestAvailabilityExpression(const std::string& text); ~QuestAvailabilityExpression() = default; inline bool operator==(const QuestAvailabilityExpression& other) const { @@ -23,8 +29,8 @@ public: inline bool operator!=(const QuestAvailabilityExpression& other) const { return !this->operator==(other); } - inline bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const { - return this->root->evaluate(flags, team); + inline int64_t evaluate(const Env& env) const { + return this->root->evaluate(env); } inline std::string str() const { return this->root->str(); @@ -38,48 +44,62 @@ protected: inline bool operator!=(const Node& other) const { return !this->operator==(other); } - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const = 0; + virtual int64_t evaluate(const Env& env) const = 0; virtual std::string str() const = 0; protected: Node() = default; }; - class OrNode : public Node { + class BinaryOperatorNode : public Node { public: - OrNode(std::unique_ptr&& left, std::unique_ptr&& right); - virtual ~OrNode() = default; + enum class Type { + LOGICAL_OR = 0, + LOGICAL_AND, + BITWISE_OR, + BITWISE_AND, + BITWISE_XOR, + LEFT_SHIFT, + RIGHT_SHIFT, + LESS_THAN, + GREATER_THAN, + LESS_OR_EQUAL, + GREATER_OR_EQUAL, + EQUAL, + NOT_EQUAL, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + MODULUS, + }; + BinaryOperatorNode(Type type, std::unique_ptr&& left, std::unique_ptr&& right); + virtual ~BinaryOperatorNode() = default; virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; + virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: + Type type; std::unique_ptr left; std::unique_ptr right; }; - class AndNode : public Node { + class UnaryOperatorNode : public Node { public: - AndNode(std::unique_ptr&& left, std::unique_ptr&& right); - virtual ~AndNode() = default; + enum class Type { + LOGICAL_NOT = 0, + BITWISE_NOT, + NEGATIVE, + }; + UnaryOperatorNode(Type type, std::unique_ptr&& sub); + virtual ~UnaryOperatorNode() = default; virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; - virtual std::string str() const; - - protected: - std::unique_ptr left; - std::unique_ptr right; - }; - - class NotNode : public Node { - public: - NotNode(std::unique_ptr&& sub); - virtual ~NotNode() = default; - virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; + virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: + Type type; std::unique_ptr sub; }; @@ -88,7 +108,7 @@ protected: FlagLookupNode(uint16_t flag_index); virtual ~FlagLookupNode() = default; virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; + virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: @@ -100,23 +120,32 @@ protected: TeamRewardLookupNode(const std::string& reward_name); virtual ~TeamRewardLookupNode() = default; virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; + virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: std::string reward_name; }; + class NumPlayersLookupNode : public Node { + public: + NumPlayersLookupNode(); + virtual ~NumPlayersLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + }; + class ConstantNode : public Node { public: ConstantNode(bool value); virtual ~ConstantNode() = default; virtual bool operator==(const Node& other) const; - virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr team) const; + virtual int64_t evaluate(const Env& env) const; virtual std::string str() const; protected: - bool value; + int64_t value; }; std::unique_ptr parse_expr(std::string_view text); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 328811c0..87cdca01 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2281,8 +2281,8 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { send_lobby_message_box(c, "$C6Your level is too\nhigh to join this\ngame."); break; } - if (game->quest && !c->can_play_quest(game->quest, game->difficulty)) { - send_lobby_message_box(c, "$C6You don't have access\nto the quest in progress\nin this game."); + if (game->quest && !c->can_play_quest(game->quest, game->difficulty, game->count_clients() + 1)) { + send_lobby_message_box(c, "$C6You don't have access\nto the quest in progress\nin this game, or there\nis no space for another\nplayer in the quest."); break; } } diff --git a/system/quests/battle/b88001.json b/system/quests/battle/b88001.json index 89913b08..7811fb9a 100644 --- a/system/quests/battle/b88001.json +++ b/system/quests/battle/b88001.json @@ -37,14 +37,15 @@ // Quests may be set to be unavailable until a preceding quest has been // cleared or a team reward has been purchased. To enable this feature, set a // value for AvailableIf in the quest's JSON file. This field's value should - // be a boolean expression that tests one or more flags or team rewards. An - // example with random values is shown below. This field is ignored if the - // player has the DISABLE_QUEST_REQUIREMENTS flag in their license. - // "AvailableIf": "(F_016D || F_0171 || T_EpicCustomQuest) && !F_0173", + // be an integral expression that tests one or more flags or team rewards, or + // the number of players. An example with random values is shown below. This + // field is ignored if the player has the DISABLE_QUEST_REQUIREMENTS flag in + // their license. + // "AvailableIf": "(F_016D || T_EpicCustomQuest || (V_NumPlayers <= 2)) && !F_0173", // On BB, quests may be disabled but still visible to the player. This // expression controls when that should be the case. If AvailableIf evaluates - // to false, this is ignored. This field is also ignored if the player has + // to false, this is ignored. This field is also ignored if the player has // the DISABLE_QUEST_REQUIREMENTS flag in their license. // "EnabledIf": "!F_0169", } diff --git a/system/quests/download/q072.json b/system/quests/download/q072.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/download/q072.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/events/q073.json b/system/quests/events/q073.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/events/q073.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/events/q220.json b/system/quests/events/q220.json new file mode 100644 index 00000000..3b255ff0 --- /dev/null +++ b/system/quests/events/q220.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers <= 2", +} diff --git a/system/quests/events/q240.json b/system/quests/events/q240.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/events/q240.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/solo-extra/q143.json b/system/quests/solo-extra/q143.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/solo-extra/q143.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/solo-story/q026.json b/system/quests/solo-story/q026.json index 17390bd3..45683a0e 100644 --- a/system/quests/solo-story/q026.json +++ b/system/quests/solo-story/q026.json @@ -1,3 +1,3 @@ { - "AvailableIf": "F_0073", + "AvailableIf": "F_0073 && (V_NumPlayers == 1)", } diff --git a/system/quests/solo-story/q027.json b/system/quests/solo-story/q027.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/solo-story/q027.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/vr/q222.json b/system/quests/vr/q222.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/vr/q222.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +} diff --git a/system/quests/vr/q230.json b/system/quests/vr/q230.json new file mode 100644 index 00000000..ae1c6c16 --- /dev/null +++ b/system/quests/vr/q230.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "V_NumPlayers == 1", +}