implement solo quest progression flags

This commit is contained in:
Martin Michelsen
2023-12-08 17:01:11 -08:00
parent 009a0fc93d
commit b7bc148e09
187 changed files with 663 additions and 108 deletions
+1
View File
@@ -94,6 +94,7 @@ set(SOURCES
src/PSOGCObjectGraph.cc src/PSOGCObjectGraph.cc
src/PSOProtocol.cc src/PSOProtocol.cc
src/Quest.cc src/Quest.cc
src/QuestAvailabilityExpression.cc
src/QuestScript.cc src/QuestScript.cc
src/RareItemSet.cc src/RareItemSet.cc
src/ReceiveCommands.cc src/ReceiveCommands.cc
+19 -10
View File
@@ -295,21 +295,30 @@ shared_ptr<const TeamIndex::Team> Client::team() const {
return team; return team;
} }
bool Client::can_access_quest(shared_ptr<const Quest> q, uint8_t difficulty) const { bool Client::can_see_quest(shared_ptr<const Quest> q, uint8_t difficulty) const {
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) { if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
return true; return true;
} }
if ((q->require_flag >= 0) && if (!q->available_expression) {
!this->character()->quest_flags.get(difficulty, q->require_flag)) { return true;
return false;
} }
if (!q->require_team_reward_key.empty()) { string expr = q->available_expression->str();
auto team = this->team(); bool ret = q->available_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team());
if (!team || !team->has_reward(q->require_team_reward_key)) { this->log.info("Evaluating quest availability expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
return false; return ret;
} }
bool Client::can_play_quest(shared_ptr<const Quest> q, uint8_t difficulty) const {
if (this->license && (this->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
return true;
} }
return true; if (!q->enabled_expression) {
return true;
}
string expr = q->enabled_expression->str();
bool ret = q->enabled_expression->evaluate(this->character()->quest_flags.data.at(difficulty), this->team());
this->log.info("Evaluating quest enabled expression %s => %s", expr.c_str(), ret ? "TRUE" : "FALSE");
return ret;
} }
void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) { void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) {
+2 -1
View File
@@ -260,7 +260,8 @@ public:
std::shared_ptr<const TeamIndex::Team> team() const; std::shared_ptr<const TeamIndex::Team> team() const;
bool can_access_quest(std::shared_ptr<const Quest> q, uint8_t difficulty) const; bool can_see_quest(std::shared_ptr<const Quest> q, uint8_t difficulty) const;
bool can_play_quest(std::shared_ptr<const Quest> q, uint8_t difficulty) const;
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx); static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
void save_game_data(); void save_game_data();
+20 -1
View File
@@ -1996,6 +1996,15 @@ struct C_ChangeShipOrBlock_A0_A1 {
template <TextEncoding Encoding, size_t ShortDescLength> template <TextEncoding Encoding, size_t ShortDescLength>
struct S_QuestMenuEntry { struct S_QuestMenuEntry {
// Note: The game treats menu_id as two 8-bit fields followed by a 16-bit
// field. In most situations, this is opaque to the server, so we treat it as
// a single 32-bit field, but in the case of the quest menu, the second byte
// is used to determine the icon that appears to the left of the quest name.
// Specifically:
// 0 = online quest icon (green diamond)
// 1 = download quest icon (green square with outlined diamond)
// 2 = completed download quest icon (orange square with outlined diamond)
// Anything else = same as 1
le_uint32_t menu_id = 0; le_uint32_t menu_id = 0;
le_uint32_t item_id = 0; le_uint32_t item_id = 0;
pstring<Encoding, 0x20> name; pstring<Encoding, 0x20> name;
@@ -2007,7 +2016,17 @@ struct S_QuestMenuEntry_DC_GC_A2_A4 : S_QuestMenuEntry<TextEncoding::MARKED, 0x7
} __packed__; } __packed__;
struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry<TextEncoding::MARKED, 0x80> { struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry<TextEncoding::MARKED, 0x80> {
} __packed__; } __packed__;
struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry<TextEncoding::UTF16, 0x7A> {
struct S_QuestMenuEntry_BB_A2_A4 {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
pstring<TextEncoding::UTF16, 0x20> name;
pstring<TextEncoding::UTF16, 0x78> short_description;
// If this field is set, a yellow hex icon is displayed instead of the green
// or orange diamond icon, and the quest is grayed out and cannot be selected.
// This field is ignored if the icon type (see S_QuestMenuEntry) isn't 1 or 2.
uint8_t disabled = 0;
parray<uint8_t, 3> unused;
} __packed__; } __packed__;
// A3 (S->C): Quest information // A3 (S->C): Quest information
+8 -4
View File
@@ -410,12 +410,16 @@ unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() co
} }
QuestIndex::IncludeCondition Lobby::quest_include_condition() const { QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
return [this](shared_ptr<const Quest> q) -> bool { return [this](shared_ptr<const Quest> q) -> QuestIndex::IncludeState {
bool is_enabled = true;
for (const auto& lc : this->clients) { for (const auto& lc : this->clients) {
if (lc && !lc->can_access_quest(q, this->difficulty)) { if (lc && !lc->can_see_quest(q, this->difficulty)) {
return false; return QuestIndex::IncludeState::HIDDEN;
}
if (lc && !lc->can_play_quest(q, this->difficulty)) {
is_enabled = false;
} }
} }
return true; return is_enabled ? QuestIndex::IncludeState::AVAILABLE : QuestIndex::IncludeState::DISABLED;
}; };
} }
+2 -2
View File
@@ -17,8 +17,8 @@ constexpr uint32_t MAIN = 0x11000011;
constexpr uint32_t INFORMATION = 0x22000022; constexpr uint32_t INFORMATION = 0x22000022;
constexpr uint32_t LOBBY = 0x33000033; constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044; constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST = 0x55000055; constexpr uint32_t QUEST = 0x55010155;
constexpr uint32_t QUEST_CATEGORIES = 0x66000066; constexpr uint32_t QUEST_CATEGORIES = 0x66010166;
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077; constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088; constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099; constexpr uint32_t PATCHES = 0x99000099;
+35 -23
View File
@@ -205,8 +205,8 @@ VersionedQuest::VersionedQuest(
std::shared_ptr<const std::string> pvr_contents, std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules, std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index, ssize_t challenge_template_index,
int16_t require_flag, std::shared_ptr<const QuestAvailabilityExpression> available_expression,
const string& require_team_reward_key) std::shared_ptr<const QuestAvailabilityExpression> enabled_expression)
: quest_number(quest_number), : quest_number(quest_number),
category_id(category_id), category_id(category_id),
episode(Episode::NONE), episode(Episode::NONE),
@@ -219,8 +219,8 @@ VersionedQuest::VersionedQuest(
pvr_contents(pvr_contents), pvr_contents(pvr_contents),
battle_rules(battle_rules), battle_rules(battle_rules),
challenge_template_index(challenge_template_index), challenge_template_index(challenge_template_index),
require_flag(require_flag), available_expression(available_expression),
require_team_reward_key(require_team_reward_key) { enabled_expression(enabled_expression) {
auto bin_decompressed = prs_decompress(*this->bin_contents); auto bin_decompressed = prs_decompress(*this->bin_contents);
@@ -378,8 +378,8 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
name(initial_version->name), name(initial_version->name),
battle_rules(initial_version->battle_rules), battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index), challenge_template_index(initial_version->challenge_template_index),
require_flag(initial_version->require_flag), available_expression(initial_version->available_expression),
require_team_reward_key(initial_version->require_team_reward_key) { enabled_expression(initial_version->enabled_expression) {
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version); this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
} }
@@ -409,11 +409,17 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->challenge_template_index != vq->challenge_template_index) { if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error("quest version has different challenge template index"); throw runtime_error("quest version has different challenge template index");
} }
if (this->require_flag != vq->require_flag) { if (!this->available_expression != !vq->available_expression) {
throw runtime_error("quest version has different required flag"); throw runtime_error("quest version has available expression but root quest does not, or vice versa");
} }
if (this->require_team_reward_key != vq->require_team_reward_key) { if (this->available_expression && *this->available_expression != *vq->available_expression) {
throw runtime_error("quest version has different required team reward key"); throw runtime_error("quest version has a different available expression");
}
if (!this->enabled_expression != !vq->enabled_expression) {
throw runtime_error("quest version has enabled expression but root quest does not, or vice versa");
}
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
throw runtime_error("quest version has a different enabled expression");
} }
this->versions.emplace(this->versions_key(vq->version, vq->language), vq); this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
@@ -639,8 +645,8 @@ QuestIndex::QuestIndex(
JSON metadata_json = nullptr; JSON metadata_json = nullptr;
shared_ptr<BattleRules> battle_rules; shared_ptr<BattleRules> battle_rules;
ssize_t challenge_template_index = -1; ssize_t challenge_template_index = -1;
int16_t require_flag = -1; shared_ptr<const QuestAvailabilityExpression> available_expression;
string require_team_reward_key; shared_ptr<const QuestAvailabilityExpression> enabled_expression;
try { try {
json_filename = basename; json_filename = basename;
metadata_json = JSON::parse(*json_files.at(json_filename)); metadata_json = JSON::parse(*json_files.at(json_filename));
@@ -665,9 +671,12 @@ QuestIndex::QuestIndex(
challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int(); challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
require_flag = metadata_json.get_int("RequireFlag", -1);
try { try {
require_team_reward_key = metadata_json.get_string("RequireTeamRewardKey"); available_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
enabled_expression = make_shared<QuestAvailabilityExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
} }
@@ -682,8 +691,8 @@ QuestIndex::QuestIndex(
pvr_contents, pvr_contents,
battle_rules, battle_rules,
challenge_template_index, challenge_template_index,
require_flag, available_expression,
require_team_reward_key); enabled_expression);
auto category_name = this->category_index->at(vq->category_id)->name; auto category_name = this->category_index->at(vq->category_id)->name;
string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat"); string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat");
@@ -737,7 +746,7 @@ vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
QuestMenuType menu_type, QuestMenuType menu_type,
Episode episode, Episode episode,
Version version, Version version,
function<bool(std::shared_ptr<const Quest>)> include_condition) const { IncludeCondition include_condition) const {
// The episode filter should apply in normal or solo mode // The episode filter should apply in normal or solo mode
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) { if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE; episode = Episode::NONE;
@@ -752,27 +761,30 @@ vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
return ret; return ret;
} }
vector<shared_ptr<const Quest>> QuestIndex::filter( vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filter(
QuestMenuType menu_type, QuestMenuType menu_type,
Episode episode, Episode episode,
Version version, Version version,
uint32_t category_id, uint32_t category_id,
function<bool(std::shared_ptr<const Quest>)> include_condition, IncludeCondition include_condition,
size_t limit) const { size_t limit) const {
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) { if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE; episode = Episode::NONE;
} }
vector<shared_ptr<const Quest>> ret; vector<pair<IncludeState, shared_ptr<const Quest>>> ret;
auto category_it = this->quests_by_category_id_and_number.find(category_id); auto category_it = this->quests_by_category_id_and_number.find(category_id);
if (category_it == this->quests_by_category_id_and_number.end()) { if (category_it == this->quests_by_category_id_and_number.end()) {
return ret; return ret;
} }
for (auto it : category_it->second) { for (auto it : category_it->second) {
if (((episode == Episode::NONE) || (it.second->episode == episode)) && if (((episode == Episode::NONE) || (it.second->episode == episode)) &&
it.second->has_version_any_language(version) && it.second->has_version_any_language(version)) {
(!include_condition || include_condition(it.second))) { IncludeState state = include_condition ? include_condition(it.second) : IncludeState::AVAILABLE;
ret.emplace_back(it.second); if (state == IncludeState::HIDDEN) {
continue;
}
ret.emplace_back(make_pair(state, it.second));
if (limit && (ret.size() >= limit)) { if (limit && (ret.size() >= limit)) {
break; break;
} }
+14 -8
View File
@@ -9,6 +9,7 @@
#include <vector> #include <vector>
#include "PlayerSubordinates.hh" #include "PlayerSubordinates.hh"
#include "QuestAvailabilityExpression.hh"
#include "QuestScript.hh" #include "QuestScript.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
#include "TeamIndex.hh" #include "TeamIndex.hh"
@@ -70,8 +71,8 @@ struct VersionedQuest {
std::shared_ptr<const std::string> pvr_contents; std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules; std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index; ssize_t challenge_template_index;
int16_t require_flag; // <0 = none std::shared_ptr<const QuestAvailabilityExpression> available_expression;
std::string require_team_reward_key; std::shared_ptr<const QuestAvailabilityExpression> enabled_expression;
VersionedQuest( VersionedQuest(
uint32_t quest_number, uint32_t quest_number,
@@ -83,8 +84,8 @@ struct VersionedQuest {
std::shared_ptr<const std::string> pvr_contents, std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr, std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1, ssize_t challenge_template_index = -1,
int16_t require_flag = -1, std::shared_ptr<const QuestAvailabilityExpression> available_expression = nullptr,
const std::string& require_team_reward_key = ""); std::shared_ptr<const QuestAvailabilityExpression> enabled_expression = nullptr);
std::string bin_filename() const; std::string bin_filename() const;
std::string dat_filename() const; std::string dat_filename() const;
@@ -117,13 +118,18 @@ public:
std::string name; std::string name;
std::shared_ptr<const BattleRules> battle_rules; std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index; ssize_t challenge_template_index;
int16_t require_flag; std::shared_ptr<const QuestAvailabilityExpression> available_expression;
std::string require_team_reward_key; std::shared_ptr<const QuestAvailabilityExpression> enabled_expression;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions; std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
}; };
struct QuestIndex { struct QuestIndex {
using IncludeCondition = std::function<bool(std::shared_ptr<const Quest>)>; enum class IncludeState {
HIDDEN = 0,
AVAILABLE,
DISABLED,
};
using IncludeCondition = std::function<IncludeState(std::shared_ptr<const Quest>)>;
std::string directory; std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index; std::shared_ptr<const QuestCategoryIndex> category_index;
@@ -140,7 +146,7 @@ struct QuestIndex {
Episode episode, Episode episode,
Version version, Version version,
IncludeCondition include_condition = nullptr) const; IncludeCondition include_condition = nullptr) const;
std::vector<std::shared_ptr<const Quest>> filter( std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>> filter(
QuestMenuType menu_type, QuestMenuType menu_type,
Episode episode, Episode episode,
Version version, Version version,
+248
View File
@@ -0,0 +1,248 @@
#include "QuestAvailabilityExpression.hh"
#include <algorithm>
#include <mutex>
#include <phosg/Encoding.hh>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Tools.hh>
#include <string>
#include <unordered_map>
#include "CommandFormats.hh"
#include "Compression.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "QuestScript.hh"
#include "SaveFileFormats.hh"
#include "Text.hh"
using namespace std;
QuestAvailabilityExpression::QuestAvailabilityExpression(const string& text)
: root(this->parse_expr(text)) {}
QuestAvailabilityExpression::OrNode::OrNode(unique_ptr<const Node>&& left, unique_ptr<const Node>&& right)
: left(std::move(left)),
right(std::move(right)) {}
bool QuestAvailabilityExpression::OrNode::operator==(const Node& other) const {
try {
const OrNode& other_or = dynamic_cast<const OrNode&>(other);
return *other_or.left == *this->left && *other_or.right == *this->right;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::OrNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> team) const {
return this->left->evaluate(flags, team) || this->right->evaluate(flags, team);
}
string QuestAvailabilityExpression::OrNode::str() const {
return "(" + this->left->str() + ") || (" + this->right->str() + ")";
}
QuestAvailabilityExpression::AndNode::AndNode(unique_ptr<const Node>&& left, unique_ptr<const Node>&& right)
: left(std::move(left)),
right(std::move(right)) {}
bool QuestAvailabilityExpression::AndNode::operator==(const Node& other) const {
try {
const AndNode& other_and = dynamic_cast<const AndNode&>(other);
return *other_and.left == *this->left && *other_and.right == *this->right;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::AndNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> 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<const Node>&& sub)
: sub(std::move(sub)) {}
bool QuestAvailabilityExpression::NotNode::operator==(const Node& other) const {
try {
const NotNode& other_not = dynamic_cast<const NotNode&>(other);
return *other_not.sub == *this->sub;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::NotNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team> team) const {
return !this->sub->evaluate(flags, team);
}
string QuestAvailabilityExpression::NotNode::str() const {
return "!(" + this->sub->str() + ")";
}
QuestAvailabilityExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index)
: flag_index(flag_index) {}
bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) const {
try {
const FlagLookupNode& other_flag = dynamic_cast<const FlagLookupNode&>(other);
return other_flag.flag_index == this->flag_index;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::FlagLookupNode::evaluate(
const QuestFlagsForDifficulty& flags, shared_ptr<const TeamIndex::Team>) const {
return flags.get(this->flag_index);
}
string QuestAvailabilityExpression::FlagLookupNode::str() const {
return string_printf("F_%04hX", this->flag_index);
}
QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name)
: reward_name(reward_name) {}
bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& other) const {
try {
const TeamRewardLookupNode& other_team_reward = dynamic_cast<const TeamRewardLookupNode&>(other);
return other_team_reward.reward_name == this->reward_name;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::TeamRewardLookupNode::evaluate(
const QuestFlagsForDifficulty&, shared_ptr<const TeamIndex::Team> team) const {
return team && team->has_reward(this->reward_name);
}
string QuestAvailabilityExpression::TeamRewardLookupNode::str() const {
return "T_" + this->reward_name;
}
QuestAvailabilityExpression::ConstantNode::ConstantNode(bool value)
: value(value) {}
bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) const {
try {
const ConstantNode& other_const = dynamic_cast<const ConstantNode&>(other);
return other_const.value == this->value;
} catch (const bad_cast&) {
return false;
}
}
bool QuestAvailabilityExpression::ConstantNode::evaluate(
const QuestFlagsForDifficulty&, shared_ptr<const TeamIndex::Team>) const {
return this->value;
}
string QuestAvailabilityExpression::ConstantNode::str() const {
return this->value ? "true" : "false";
}
unique_ptr<const QuestAvailabilityExpression::Node> QuestAvailabilityExpression::parse_expr(string_view text) {
// Strip off spaces and fully-enclosing parentheses
for (;;) {
size_t starting_size = text.size();
while (text.at(0) == ' ') {
text = text.substr(1);
}
while (text.at(text.size() - 1) == ' ') {
text = text.substr(0, text.size() - 1);
}
if (text.at(0) == '(' && text.at(text.size() - 1) == ')') {
// It doesn't suffice to just check the first ant last characters, since
// text could be like "(a) && (b)". Instead, we ignore the first and last
// characters, and don't strip anything if the internal parentheses are
// unbalanced.
size_t paren_level = 1;
for (size_t z = 1; z < text.size() - 1; z++) {
if (text[z] == '(') {
paren_level++;
} else if (text[z] == ')') {
paren_level--;
if (paren_level == 0) {
break;
}
}
}
if (paren_level > 0) {
text = text.substr(1, text.size() - 2);
}
}
if (text.size() == starting_size) {
break;
}
}
// 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;
}
}
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<OrNode>(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<AndNode>(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<NotNode>(std::move(sub));
}
// Check for constants
if (text == "true") {
return make_unique<ConstantNode>(true);
}
if (text == "false") {
return make_unique<ConstantNode>(false);
}
// Check for flag lookups
if (text.starts_with("F_")) {
char* endptr = nullptr;
uint64_t flag = strtoul(text.data() + 2, &endptr, 16);
if (endptr != text.data() + text.size()) {
throw runtime_error("invalid flag lookup token");
}
if (flag >= 0x400) {
throw runtime_error("invalid flag index");
}
return make_unique<FlagLookupNode>(flag);
}
if (text.starts_with("T_")) {
return make_unique<TeamRewardLookupNode>(string(text.substr(2)));
}
throw runtime_error("unparseable expression");
}
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <stdint.h>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
class QuestAvailabilityExpression {
public:
QuestAvailabilityExpression(const std::string& text);
~QuestAvailabilityExpression() = default;
inline bool operator==(const QuestAvailabilityExpression& other) const {
return this->root->operator==(*other.root);
}
inline bool operator!=(const QuestAvailabilityExpression& other) const {
return !this->operator==(other);
}
inline bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const {
return this->root->evaluate(flags, team);
}
inline std::string str() const {
return this->root->str();
}
protected:
class Node {
public:
virtual ~Node() = default;
virtual bool operator==(const Node& other) const = 0;
inline bool operator!=(const Node& other) const {
return !this->operator==(other);
}
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const = 0;
virtual std::string str() const = 0;
protected:
Node() = default;
};
class OrNode : public Node {
public:
OrNode(std::unique_ptr<const Node>&& left, std::unique_ptr<const Node>&& right);
virtual ~OrNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> left;
std::unique_ptr<const Node> right;
};
class AndNode : public Node {
public:
AndNode(std::unique_ptr<const Node>&& left, std::unique_ptr<const Node>&& right);
virtual ~AndNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> left;
std::unique_ptr<const Node> right;
};
class NotNode : public Node {
public:
NotNode(std::unique_ptr<const Node>&& sub);
virtual ~NotNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::unique_ptr<const Node> sub;
};
class FlagLookupNode : public Node {
public:
FlagLookupNode(uint16_t flag_index);
virtual ~FlagLookupNode() = default;
virtual bool operator==(const Node& other) const;
virtual bool evaluate(const QuestFlagsForDifficulty& flags, std::shared_ptr<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
uint16_t flag_index;
};
class TeamRewardLookupNode : public Node {
public:
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<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
std::string reward_name;
};
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<const TeamIndex::Team> team) const;
virtual std::string str() const;
protected:
bool value;
};
std::unique_ptr<const Node> parse_expr(std::string_view text);
std::unique_ptr<const Node> root;
};
+2 -2
View File
@@ -2287,7 +2287,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
send_lobby_message_box(c, "$C6Your level is too\nhigh to join this\ngame."); send_lobby_message_box(c, "$C6Your level is too\nhigh to join this\ngame.");
break; break;
} }
if (game->quest && !c->can_access_quest(game->quest, game->difficulty)) { 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."); send_lobby_message_box(c, "$C6You don't have access\nto the quest in progress\nin this game.");
break; break;
} }
@@ -2370,7 +2370,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
send_lobby_message_box(c, "$C6A quest is already\nin progress."); send_lobby_message_box(c, "$C6A quest is already\nin progress.");
break; break;
} }
if (!l->quest_include_condition()(q)) { if (l->quest_include_condition()(q) != QuestIndex::IncludeState::AVAILABLE) {
send_lobby_message_box(c, "$C6This quest has not\nbeen unlocked for\nall players in this\ngame."); send_lobby_message_box(c, "$C6This quest has not\nbeen unlocked for\nall players in this\ngame.");
break; break;
} }
+33 -7
View File
@@ -1414,25 +1414,48 @@ template <typename EntryT>
void send_quest_menu_t( void send_quest_menu_t(
shared_ptr<Client> c, shared_ptr<Client> c,
uint32_t menu_id, uint32_t menu_id,
const vector<shared_ptr<const Quest>>& quests, const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) { bool is_download_menu) {
auto v = c->version(); auto v = c->version();
vector<EntryT> entries; vector<EntryT> entries;
for (const auto& quest : quests) { for (const auto& it : quests) {
auto vq = quest->version(v, c->language()); auto vq = it.second->version(v, c->language());
if (!vq) { if (!vq) {
continue; continue;
} }
auto& e = entries.emplace_back(); auto& e = entries.emplace_back();
e.menu_id = menu_id; e.menu_id = menu_id;
e.item_id = quest->quest_number; e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language()); e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language()); e.short_description.encode(add_color(vq->short_description), c->language());
} }
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
} }
void send_quest_menu_bb(
shared_ptr<Client> c,
uint32_t menu_id,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) {
auto v = c->version();
vector<S_QuestMenuEntry_BB_A2_A4> entries;
for (const auto& it : quests) {
auto vq = it.second->version(v, c->language());
if (!vq) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
template <typename EntryT> template <typename EntryT>
void send_quest_categories_menu_t( void send_quest_categories_menu_t(
shared_ptr<Client> c, shared_ptr<Client> c,
@@ -1459,8 +1482,11 @@ void send_quest_categories_menu_t(
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
} }
void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id, void send_quest_menu(
const vector<shared_ptr<const Quest>>& quests, bool is_download_menu) { shared_ptr<Client> c,
uint32_t menu_id,
const vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>>& quests,
bool is_download_menu) {
switch (c->version()) { switch (c->version()) {
case Version::PC_V2: case Version::PC_V2:
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, quests, is_download_menu); send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, quests, is_download_menu);
@@ -1479,7 +1505,7 @@ void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, quests, is_download_menu); send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, quests, is_download_menu);
break; break;
case Version::BB_V4: case Version::BB_V4:
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, quests, is_download_menu); send_quest_menu_bb(c, menu_id, quests, is_download_menu);
break; break;
default: default:
throw logic_error("unimplemented versioned command"); throw logic_error("unimplemented versioned command");
+1 -1
View File
@@ -258,7 +258,7 @@ void send_game_menu(
void send_quest_menu( void send_quest_menu(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
uint32_t menu_id, uint32_t menu_id,
const std::vector<std::shared_ptr<const Quest>>& quests, const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
bool is_download_menu); bool is_download_menu);
void send_quest_categories_menu( void send_quest_categories_menu(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
+2 -1
View File
@@ -501,7 +501,8 @@
[0x02, "battle", "Battle", "$E$C6Battle mode rule\nsets"], [0x02, "battle", "Battle", "$E$C6Battle mode rule\nsets"],
[0x04, "challenge-ep1", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"], [0x04, "challenge-ep1", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"],
[0x84, "challenge-ep2", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"], [0x84, "challenge-ep2", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],
[0x08, "solo", "Solo", "$E$C6Quests that require\na single player"], [0x08, "solo-story", "Story", "$E$C6Quests that follow\nthe Episode 1 story"],
[0x08, "solo-extra", "Solo", "$E$C6Quests that require\na single player"],
[0x10, "government-ep1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"], [0x10, "government-ep1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"],
[0x10, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"], [0x10, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
[0x10, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"], [0x10, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
+11 -9
View File
@@ -35,14 +35,16 @@
// "ChallengeTemplateIndex": 0, // "ChallengeTemplateIndex": 0,
// Quests may be set to be unavailable until a preceding quest has been // Quests may be set to be unavailable until a preceding quest has been
// cleared. To enable this feature, set a value for RequireFlag in the quest's // cleared or a team reward has been purchased. To enable this feature, set a
// JSON file. This field is ignored if the player has the // value for AvailableIf in the quest's JSON file. This field's value should
// DISABLE_QUEST_REQUIREMENTS flag in their license. // be a boolean expression that tests one or more flags or team rewards. An
// "RequireFlag": 0x01F5, // 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",
// Quests on BB may be set to be available only through a team reward. To // On BB, quests may be disabled but still visible to the player. This
// enable this feature, set a value for RequireTeamRewardKey in the quest's // expression controls when that should be the case. If AvailableIf evaluates
// JSON file. This field is ignored if the player has the // to false, this is ignored. This field is also ignored if the player has
// DISABLE_QUEST_REQUIREMENTS flag in their license. // the DISABLE_QUEST_REQUIREMENTS flag in their license.
// "RequireTeamRewardKey": "PointOfDisasterQuest", // "EnabledIf": "!F_0169",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01F5, "AvailableIf": "F_01F5",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01F7, "AvailableIf": "F_01F7",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01F9, "AvailableIf": "F_01F9",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01FB, "AvailableIf": "F_01FB",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01FD, "AvailableIf": "F_01FD",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x01FF, "AvailableIf": "F_01FF",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0201, "AvailableIf": "F_0201",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0203, "AvailableIf": "F_0203",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0205, "AvailableIf": "F_0205",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0207, "AvailableIf": "F_0207",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0209, "AvailableIf": "F_0209",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x020B, "AvailableIf": "F_020B",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x020D, "AvailableIf": "F_020D",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x020F, "AvailableIf": "F_020F",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0213, "AvailableIf": "F_0213",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0215, "AvailableIf": "F_0215",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0217, "AvailableIf": "F_0217",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0219, "AvailableIf": "F_0219",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x021B, "AvailableIf": "F_021B",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x021D, "AvailableIf": "F_021D",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x021F, "AvailableIf": "F_021F",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0221, "AvailableIf": "F_0221",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0223, "AvailableIf": "F_0223",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0225, "AvailableIf": "F_0225",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0227, "AvailableIf": "F_0227",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0229, "AvailableIf": "F_0229",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x022B, "AvailableIf": "F_022B",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x022D, "AvailableIf": "F_022D",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x022F, "AvailableIf": "F_022F",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0231, "AvailableIf": "F_0231",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x0233, "AvailableIf": "F_0233",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02BD, "AvailableIf": "F_02BD",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02BE, "AvailableIf": "F_02BE",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02BF, "AvailableIf": "F_02BF",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02C0, "AvailableIf": "F_02C0",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02C1, "AvailableIf": "F_02C1",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02C2, "AvailableIf": "F_02C2",
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"RequireFlag": 0x02C3, "AvailableIf": "F_02C3",
} }
+3
View File
@@ -0,0 +1,3 @@
{
"EnabledIf": "!F_0065 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+3
View File
@@ -0,0 +1,3 @@
{
"EnabledIf": "!F_0067 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0065 && F_0067 && F_006B && F_01F9",
"EnabledIf": "!F_0069 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+3
View File
@@ -0,0 +1,3 @@
{
"EnabledIf": "!F_006B || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0065 && F_0067 && F_006B",
"EnabledIf": "!F_006D || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0065 && F_0067 && F_006B",
"EnabledIf": "!F_006F || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0065 && F_0067 && F_006B",
"EnabledIf": "!F_0071 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0071",
"EnabledIf": "!F_0073 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}
+4
View File
@@ -0,0 +1,4 @@
{
"AvailableIf": "F_0065 && F_0067 && F_006B",
"EnabledIf": "!F_0075 || (F_0065 && F_0067 && F_0069 && F_006B && F_006D && F_006F && F_0071 && F_0073 && F_0075 && F_0077 && F_0079 && F_007B && F_007D && F_007F && F_0081 && F_0083 && F_0085 && F_0087 && F_0089 && F_008B && F_008D && F_008F && F_0091 && F_0093 && F_0095)",
}

Some files were not shown because too many files have changed in this diff Show More