From 4a3b0118a89730c3e74a0e70281810bc44fbaed4 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 30 Mar 2024 22:35:34 -0700 Subject: [PATCH] replace UnlockAllAreas and PreventPersistQuestFlags with generalized rewrite map --- CMakeLists.txt | 2 +- src/Client.cc | 6 +- src/Client.hh | 2 +- ...ityExpression.cc => IntegralExpression.cc} | 94 ++++++++++--------- ...ityExpression.hh => IntegralExpression.hh} | 10 +- src/Quest.cc | 12 +-- src/Quest.hh | 14 +-- src/ReceiveCommands.cc | 54 +++++------ src/ReceiveSubcommands.cc | 14 +-- src/ServerState.cc | 27 ++++-- src/ServerState.hh | 5 +- system/config.example.json | 63 ++++++++++--- tests/config.json | 36 ++++++- 13 files changed, 209 insertions(+), 130 deletions(-) rename src/{QuestAvailabilityExpression.cc => IntegralExpression.cc} (76%) rename src/{QuestAvailabilityExpression.hh => IntegralExpression.hh} (90%) diff --git a/CMakeLists.txt b/CMakeLists.txt index dfb9d30d..61c811f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,7 @@ set(SOURCES src/PSOGCObjectGraph.cc src/PSOProtocol.cc src/Quest.cc - src/QuestAvailabilityExpression.cc + src/IntegralExpression.cc src/QuestScript.cc src/RareItemSet.cc src/ReceiveCommands.cc diff --git a/src/Client.cc b/src/Client.cc index bb57534f..927458cf 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -350,7 +350,7 @@ shared_ptr Client::team() const { } bool Client::evaluate_quest_availability_expression( - shared_ptr expr, + shared_ptr expr, shared_ptr game, uint8_t event, uint8_t difficulty, @@ -366,8 +366,8 @@ bool Client::evaluate_quest_availability_expression( throw logic_error("quest flags are missing from game"); } auto p = this->character(); - QuestAvailabilityExpression::Env env = { - .flags = (game && !game->quest_flags_known) ? &game->quest_flag_values->data.at(difficulty) : &p->quest_flags.data.at(difficulty), + IntegralExpression::Env env = { + .flags = &p->quest_flags.data.at(difficulty), .challenge_records = &p->challenge_records, .team = this->team(), .num_players = num_players, diff --git a/src/Client.hh b/src/Client.hh index 48168e9a..ac1f24d5 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -294,7 +294,7 @@ public: std::shared_ptr team() const; bool evaluate_quest_availability_expression( - std::shared_ptr expr, + std::shared_ptr expr, std::shared_ptr game, uint8_t event, uint8_t difficulty, diff --git a/src/QuestAvailabilityExpression.cc b/src/IntegralExpression.cc similarity index 76% rename from src/QuestAvailabilityExpression.cc rename to src/IntegralExpression.cc index f2a72e2a..bd12bc67 100644 --- a/src/QuestAvailabilityExpression.cc +++ b/src/IntegralExpression.cc @@ -1,4 +1,4 @@ -#include "QuestAvailabilityExpression.hh" +#include "IntegralExpression.hh" #include #include @@ -21,16 +21,16 @@ using namespace std; -QuestAvailabilityExpression::QuestAvailabilityExpression(const string& text) +IntegralExpression::IntegralExpression(const string& text) : root(this->parse_expr(text)) {} -QuestAvailabilityExpression::BinaryOperatorNode::BinaryOperatorNode( +IntegralExpression::BinaryOperatorNode::BinaryOperatorNode( Type type, unique_ptr&& left, unique_ptr&& right) : type(type), left(std::move(left)), right(std::move(right)) {} -bool QuestAvailabilityExpression::BinaryOperatorNode::operator==(const Node& other) const { +bool IntegralExpression::BinaryOperatorNode::operator==(const Node& other) const { try { const BinaryOperatorNode& other_bin = dynamic_cast(other); return other_bin.type == this->type && *other_bin.left == *this->left && *other_bin.right == *this->right; @@ -39,7 +39,7 @@ bool QuestAvailabilityExpression::BinaryOperatorNode::operator==(const Node& oth } } -int64_t QuestAvailabilityExpression::BinaryOperatorNode::evaluate(const Env& env) const { +int64_t IntegralExpression::BinaryOperatorNode::evaluate(const Env& env) const { switch (this->type) { case Type::LOGICAL_OR: return this->left->evaluate(env) || this->right->evaluate(env); @@ -82,7 +82,7 @@ int64_t QuestAvailabilityExpression::BinaryOperatorNode::evaluate(const Env& env } } -string QuestAvailabilityExpression::BinaryOperatorNode::str() const { +string IntegralExpression::BinaryOperatorNode::str() const { switch (this->type) { case Type::LOGICAL_OR: return "(" + this->left->str() + ") || (" + this->right->str() + ")"; @@ -125,11 +125,11 @@ string QuestAvailabilityExpression::BinaryOperatorNode::str() const { } } -QuestAvailabilityExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr&& sub) +IntegralExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr&& sub) : type(type), sub(std::move(sub)) {} -bool QuestAvailabilityExpression::UnaryOperatorNode::operator==(const Node& other) const { +bool IntegralExpression::UnaryOperatorNode::operator==(const Node& other) const { try { const UnaryOperatorNode& other_un = dynamic_cast(other); return other_un.type == this->type && *other_un.sub == *this->sub; @@ -138,7 +138,7 @@ bool QuestAvailabilityExpression::UnaryOperatorNode::operator==(const Node& othe } } -int64_t QuestAvailabilityExpression::UnaryOperatorNode::evaluate(const Env& env) const { +int64_t IntegralExpression::UnaryOperatorNode::evaluate(const Env& env) const { switch (this->type) { case Type::LOGICAL_NOT: return !this->sub->evaluate(env); @@ -151,7 +151,7 @@ int64_t QuestAvailabilityExpression::UnaryOperatorNode::evaluate(const Env& env) } } -string QuestAvailabilityExpression::UnaryOperatorNode::str() const { +string IntegralExpression::UnaryOperatorNode::str() const { switch (this->type) { case Type::LOGICAL_NOT: return "!(" + this->sub->str() + ")"; @@ -164,10 +164,10 @@ string QuestAvailabilityExpression::UnaryOperatorNode::str() const { } } -QuestAvailabilityExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) +IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) : flag_index(flag_index) {} -bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) const { +bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const { try { const FlagLookupNode& other_flag = dynamic_cast(other); return other_flag.flag_index == this->flag_index; @@ -176,20 +176,23 @@ bool QuestAvailabilityExpression::FlagLookupNode::operator==(const Node& other) } } -int64_t QuestAvailabilityExpression::FlagLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const { + if (!env.flags) { + throw runtime_error("quest flags not available"); + } return env.flags->get(this->flag_index) ? 1 : 0; } -string QuestAvailabilityExpression::FlagLookupNode::str() const { +string IntegralExpression::FlagLookupNode::str() const { return string_printf("F_%04hX", this->flag_index); } -QuestAvailabilityExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( +IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( Episode episode, uint8_t stage_index) : episode(episode), stage_index(stage_index) {} -bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { +bool IntegralExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { try { const ChallengeCompletionLookupNode& other_cc = dynamic_cast(other); return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index; @@ -198,7 +201,10 @@ bool QuestAvailabilityExpression::ChallengeCompletionLookupNode::operator==(cons } } -int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const { + if (!env.challenge_records) { + throw runtime_error("challenge records not available"); + } if (this->episode == Episode::EP1) { return env.challenge_records->times_ep1_online.at(this->stage_index).has_value(); } else if (this->episode == Episode::EP2) { @@ -207,14 +213,14 @@ int64_t QuestAvailabilityExpression::ChallengeCompletionLookupNode::evaluate(con return false; } -string QuestAvailabilityExpression::ChallengeCompletionLookupNode::str() const { +string IntegralExpression::ChallengeCompletionLookupNode::str() const { return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast(this->stage_index + 1)); } -QuestAvailabilityExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) +IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) : reward_name(reward_name) {} -bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& other) const { +bool IntegralExpression::TeamRewardLookupNode::operator==(const Node& other) const { try { const TeamRewardLookupNode& other_team_reward = dynamic_cast(other); return other_team_reward.reward_name == this->reward_name; @@ -223,60 +229,60 @@ bool QuestAvailabilityExpression::TeamRewardLookupNode::operator==(const Node& o } } -int64_t QuestAvailabilityExpression::TeamRewardLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::TeamRewardLookupNode::evaluate(const Env& env) const { return (env.team && env.team->has_reward(this->reward_name)) ? 1 : 0; } -string QuestAvailabilityExpression::TeamRewardLookupNode::str() const { +string IntegralExpression::TeamRewardLookupNode::str() const { return "T_" + this->reward_name; } -QuestAvailabilityExpression::NumPlayersLookupNode::NumPlayersLookupNode() {} +IntegralExpression::NumPlayersLookupNode::NumPlayersLookupNode() {} -bool QuestAvailabilityExpression::NumPlayersLookupNode::operator==(const Node& other) const { +bool IntegralExpression::NumPlayersLookupNode::operator==(const Node& other) const { return dynamic_cast(&other) != nullptr; } -int64_t QuestAvailabilityExpression::NumPlayersLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::NumPlayersLookupNode::evaluate(const Env& env) const { return env.num_players; } -string QuestAvailabilityExpression::NumPlayersLookupNode::str() const { +string IntegralExpression::NumPlayersLookupNode::str() const { return "V_NumPlayers"; } -QuestAvailabilityExpression::EventLookupNode::EventLookupNode() {} +IntegralExpression::EventLookupNode::EventLookupNode() {} -bool QuestAvailabilityExpression::EventLookupNode::operator==(const Node& other) const { +bool IntegralExpression::EventLookupNode::operator==(const Node& other) const { return dynamic_cast(&other) != nullptr; } -int64_t QuestAvailabilityExpression::EventLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::EventLookupNode::evaluate(const Env& env) const { return env.event; } -string QuestAvailabilityExpression::EventLookupNode::str() const { +string IntegralExpression::EventLookupNode::str() const { return "V_Event"; } -QuestAvailabilityExpression::V1PresenceLookupNode::V1PresenceLookupNode() {} +IntegralExpression::V1PresenceLookupNode::V1PresenceLookupNode() {} -bool QuestAvailabilityExpression::V1PresenceLookupNode::operator==(const Node& other) const { +bool IntegralExpression::V1PresenceLookupNode::operator==(const Node& other) const { return dynamic_cast(&other) != nullptr; } -int64_t QuestAvailabilityExpression::V1PresenceLookupNode::evaluate(const Env& env) const { +int64_t IntegralExpression::V1PresenceLookupNode::evaluate(const Env& env) const { return env.v1_present ? 1 : 0; } -string QuestAvailabilityExpression::V1PresenceLookupNode::str() const { +string IntegralExpression::V1PresenceLookupNode::str() const { return "V_V1Present"; } -QuestAvailabilityExpression::ConstantNode::ConstantNode(int64_t value) +IntegralExpression::ConstantNode::ConstantNode(int64_t value) : value(value) {} -bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) const { +bool IntegralExpression::ConstantNode::operator==(const Node& other) const { try { const ConstantNode& other_const = dynamic_cast(other); return other_const.value == this->value; @@ -285,15 +291,15 @@ bool QuestAvailabilityExpression::ConstantNode::operator==(const Node& other) co } } -int64_t QuestAvailabilityExpression::ConstantNode::evaluate(const Env&) const { +int64_t IntegralExpression::ConstantNode::evaluate(const Env&) const { return this->value; } -string QuestAvailabilityExpression::ConstantNode::str() const { +string IntegralExpression::ConstantNode::str() const { return string_printf("%" PRId64, this->value); } -unique_ptr QuestAvailabilityExpression::parse_expr(string_view text) { +unique_ptr IntegralExpression::parse_expr(string_view text) { // Strip off spaces and fully-enclosing parentheses for (;;) { size_t starting_size = text.size(); @@ -334,13 +340,13 @@ unique_ptr QuestAvailabilityExpression: // Check for unary operators if (text[0] == '!') { return make_unique(UnaryOperatorNode::Type::LOGICAL_NOT, - QuestAvailabilityExpression::parse_expr(text.substr(1))); + IntegralExpression::parse_expr(text.substr(1))); } else if (text[0] == '~') { return make_unique(UnaryOperatorNode::Type::BITWISE_NOT, - QuestAvailabilityExpression::parse_expr(text.substr(1))); + IntegralExpression::parse_expr(text.substr(1))); } else if (text[0] == '-') { return make_unique(UnaryOperatorNode::Type::NEGATIVE, - QuestAvailabilityExpression::parse_expr(text.substr(1))); + IntegralExpression::parse_expr(text.substr(1))); } // Check for binary operators at the root level @@ -377,8 +383,8 @@ unique_ptr QuestAvailabilityExpression: ((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())); + auto left = IntegralExpression::parse_expr(text.substr(0, z)); + auto right = IntegralExpression::parse_expr(text.substr(z + oper.first.size())); return make_unique(oper.second, std::move(left), std::move(right)); } } diff --git a/src/QuestAvailabilityExpression.hh b/src/IntegralExpression.hh similarity index 90% rename from src/QuestAvailabilityExpression.hh rename to src/IntegralExpression.hh index 37946d95..4c0fcaf5 100644 --- a/src/QuestAvailabilityExpression.hh +++ b/src/IntegralExpression.hh @@ -13,7 +13,7 @@ #include "StaticGameData.hh" #include "TeamIndex.hh" -class QuestAvailabilityExpression { +class IntegralExpression { public: struct Env { const QuestFlagsForDifficulty* flags; @@ -24,12 +24,12 @@ public: bool v1_present; }; - QuestAvailabilityExpression(const std::string& text); - ~QuestAvailabilityExpression() = default; - inline bool operator==(const QuestAvailabilityExpression& other) const { + IntegralExpression(const std::string& text); + ~IntegralExpression() = default; + inline bool operator==(const IntegralExpression& other) const { return this->root->operator==(*other.root); } - inline bool operator!=(const QuestAvailabilityExpression& other) const { + inline bool operator!=(const IntegralExpression& other) const { return !this->operator==(other); } inline int64_t evaluate(const Env& env) const { diff --git a/src/Quest.cc b/src/Quest.cc index 7c38f234..163b5934 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -205,8 +205,8 @@ VersionedQuest::VersionedQuest( std::shared_ptr pvr_contents, std::shared_ptr battle_rules, ssize_t challenge_template_index, - std::shared_ptr available_expression, - std::shared_ptr enabled_expression) + std::shared_ptr available_expression, + std::shared_ptr enabled_expression) : quest_number(quest_number), category_id(category_id), episode(Episode::NONE), @@ -673,8 +673,8 @@ QuestIndex::QuestIndex( const FileData* json_filedata = nullptr; shared_ptr battle_rules; ssize_t challenge_template_index = -1; - shared_ptr available_expression; - shared_ptr enabled_expression; + shared_ptr available_expression; + shared_ptr enabled_expression; try { json_filedata = &json_files.at(basename); } catch (const out_of_range&) { @@ -698,11 +698,11 @@ QuestIndex::QuestIndex( } catch (const out_of_range&) { } try { - available_expression = make_shared(metadata_json.get_string("AvailableIf")); + available_expression = make_shared(metadata_json.get_string("AvailableIf")); } catch (const out_of_range&) { } try { - enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); + enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); } catch (const out_of_range&) { } } diff --git a/src/Quest.hh b/src/Quest.hh index fb486bb1..42df7f24 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -8,8 +8,8 @@ #include #include +#include "IntegralExpression.hh" #include "PlayerSubordinates.hh" -#include "QuestAvailabilityExpression.hh" #include "QuestScript.hh" #include "StaticGameData.hh" #include "TeamIndex.hh" @@ -76,8 +76,8 @@ struct VersionedQuest { std::shared_ptr pvr_contents; std::shared_ptr battle_rules; ssize_t challenge_template_index; - std::shared_ptr available_expression; - std::shared_ptr enabled_expression; + std::shared_ptr available_expression; + std::shared_ptr enabled_expression; VersionedQuest( uint32_t quest_number, @@ -89,8 +89,8 @@ struct VersionedQuest { std::shared_ptr pvr_contents, std::shared_ptr battle_rules = nullptr, ssize_t challenge_template_index = -1, - std::shared_ptr available_expression = nullptr, - std::shared_ptr enabled_expression = nullptr); + std::shared_ptr available_expression = nullptr, + std::shared_ptr enabled_expression = nullptr); std::string bin_filename() const; std::string dat_filename() const; @@ -124,8 +124,8 @@ public: std::string name; std::shared_ptr battle_rules; ssize_t challenge_template_index; - std::shared_ptr available_expression; - std::shared_ptr enabled_expression; + std::shared_ptr available_expression; + std::shared_ptr enabled_expression; std::map> versions; }; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index f20fd62a..7870415e 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -4219,6 +4219,7 @@ shared_ptr create_game_generic( game->base_exp_multiplier = s->bb_global_exp_multiplier; } + const unordered_map* quest_flag_rewrites; switch (game->base_version) { case Version::DC_NTE: case Version::DC_V1_11_2000_PROTOTYPE: @@ -4226,6 +4227,7 @@ shared_ptr create_game_generic( case Version::DC_V2: case Version::PC_NTE: case Version::PC_V2: + quest_flag_rewrites = &s->quest_flag_rewrites_v1_v2; if (game->mode == GameMode::BATTLE) { game->set_drop_mode(s->default_drop_mode_v1_v2_battle); game->allowed_drop_modes = s->allowed_drop_modes_v1_v2_battle; @@ -4240,6 +4242,7 @@ shared_ptr create_game_generic( case Version::GC_NTE: case Version::GC_V3: case Version::XB_V3: + quest_flag_rewrites = &s->quest_flag_rewrites_v3; if (game->mode == GameMode::BATTLE) { game->set_drop_mode(s->default_drop_mode_v3_battle); game->allowed_drop_modes = s->allowed_drop_modes_v3_battle; @@ -4253,10 +4256,12 @@ shared_ptr create_game_generic( break; case Version::GC_EP3_NTE: case Version::GC_EP3: + quest_flag_rewrites = nullptr; game->set_drop_mode(Lobby::DropMode::DISABLED); game->allowed_drop_modes = (1 << static_cast(game->drop_mode)); break; case Version::BB_V4: + quest_flag_rewrites = &s->quest_flag_rewrites_v4; if (game->mode == GameMode::BATTLE) { game->set_drop_mode(s->default_drop_mode_v4_battle); game->allowed_drop_modes = s->allowed_drop_modes_v4_battle; @@ -4323,37 +4328,28 @@ shared_ptr create_game_generic( game->quest_flags_known = make_unique(); } - if (s->unlock_all_areas) { - static const vector flags_ep1_v123 = {0x0017, 0x0020, 0x002A}; - static const vector flags_ep1_v4 = {0x01F9, 0x0201, 0x0207}; - static const vector flags_ep2_v123 = {0x004C, 0x004F, 0x0052}; - static const vector flags_ep2_v4 = {0x021B, 0x0225, 0x022F}; - static const vector flags_ep4_v4 = {0x02BD, 0x02BE, 0x02BF, 0x02C0, 0x02C1}; - - const vector* flags_to_enable; - switch (game->episode) { - case Episode::EP1: - flags_to_enable = is_v4(game->base_version) ? &flags_ep1_v4 : &flags_ep1_v123; - break; - case Episode::EP2: - flags_to_enable = is_v4(game->base_version) ? &flags_ep2_v4 : &flags_ep2_v123; - break; - case Episode::EP4: - flags_to_enable = &flags_ep1_v4; - break; - default: - flags_to_enable = nullptr; - } - - if (flags_to_enable) { - for (uint16_t flag_num : *flags_to_enable) { - game->quest_flag_values->set(game->difficulty, flag_num); - if (game->quest_flags_known) { - game->quest_flags_known->set(game->difficulty, flag_num); - } + if (quest_flag_rewrites && !quest_flag_rewrites->empty()) { + IntegralExpression::Env env = { + .flags = &p->quest_flags.data.at(difficulty), + .challenge_records = &p->challenge_records, + .team = c->team(), + .num_players = 1, + .event = game->event, + .v1_present = is_v1(game->base_version), + }; + for (const auto& it : *quest_flag_rewrites) { + bool should_set = it.second.evaluate(env); + game->log.info("Overriding quest flag %04hX = %s", it.first, should_set ? "true" : "false"); + if (should_set) { + game->quest_flag_values->set(game->difficulty, it.first); + } else { + game->quest_flag_values->clear(game->difficulty, it.first); + } + if (game->quest_flags_known) { + game->quest_flags_known->set(game->difficulty, it.first); } - c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); } + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); } game->switch_flags = make_unique(); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index c390bacb..4dafdff7 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2717,16 +2717,12 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla auto s = c->require_server_state(); // TODO: Should we allow overlays here? auto p = c->character(true, false); - if (s->quest_flag_persist_mask.get(flag_num)) { - if (should_set) { - c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); - p->quest_flags.set(difficulty, flag_num); - } else { - c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); - p->quest_flags.clear(difficulty, flag_num); - } + if (should_set) { + c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); + p->quest_flags.set(difficulty, flag_num); } else { - c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_num); + c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_num); + p->quest_flags.clear(difficulty, flag_num); } } diff --git a/src/ServerState.cc b/src/ServerState.cc index 83a8bfc9..65706332 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -718,13 +718,27 @@ void ServerState::load_config_early() { throw runtime_error("CLIENT drop mode cannot be allowed in V4"); } - this->quest_flag_persist_mask.update_all(true); - try { - for (const auto& flag_id_json : this->config_json->get_list("PreventPersistQuestFlags")) { - this->quest_flag_persist_mask.clear(flag_id_json->as_int()); + auto parse_quest_flag_rewrites = [&json = this->config_json](const char* key) -> std::unordered_map { + std::unordered_map ret; + try { + for (const auto& it : json->get_dict(key)) { + if (!it.first.starts_with("F_")) { + throw runtime_error("invalid flag reference: " + it.first); + } + uint16_t flag = stoul(it.first.substr(2), nullptr, 16); + if (it.second->is_bool()) { + ret.emplace(flag, it.second->as_bool() ? "true" : "false"); + } else { + ret.emplace(flag, it.second->as_string()); + } + } + } catch (const out_of_range&) { } - } catch (const out_of_range&) { - } + return ret; + }; + this->quest_flag_rewrites_v1_v2 = parse_quest_flag_rewrites("QuestFlagRewritesV1V2"); + this->quest_flag_rewrites_v3 = parse_quest_flag_rewrites("QuestFlagRewritesV3"); + this->quest_flag_rewrites_v4 = parse_quest_flag_rewrites("QuestFlagRewritesV4"); this->persistent_game_idle_timeout_usecs = this->config_json->get_int("PersistentGameIdleTimeout", 0); this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT); @@ -890,7 +904,6 @@ void ServerState::load_config_early() { this->allow_dc_pc_games = this->config_json->get_bool("AllowDCPCGames", true); this->allow_gc_xb_games = this->config_json->get_bool("AllowGCXBGames", true); this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true); - this->unlock_all_areas = this->config_json->get_bool("UnlockAllAreas", false); this->version_name_colors.reset(); try { diff --git a/src/ServerState.hh b/src/ServerState.hh index b207982d..0a154c97 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -91,7 +91,6 @@ struct ServerState : public std::enable_shared_from_this { bool allow_dc_pc_games = true; bool allow_gc_xb_games = true; bool enable_chat_commands = true; - bool unlock_all_areas = false; std::unique_ptr> version_name_colors; uint8_t allowed_drop_modes_v1_v2_normal = 0x1F; uint8_t allowed_drop_modes_v1_v2_battle = 0x07; @@ -111,7 +110,9 @@ struct ServerState : public std::enable_shared_from_this { Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED; Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED; Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED; - QuestFlagsForDifficulty quest_flag_persist_mask; + std::unordered_map quest_flag_rewrites_v1_v2; + std::unordered_map quest_flag_rewrites_v3; + std::unordered_map quest_flag_rewrites_v4; uint64_t persistent_game_idle_timeout_usecs = 0; bool ep3_send_function_call_enabled = false; bool enable_v3_v4_protected_subcommands = false; diff --git a/system/config.example.json b/system/config.example.json index e6ec4945..b1ccd09d 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -958,14 +958,6 @@ // available on the proxy server. "CheatModeBehavior": "OnByDefault", - // Whether to unlock all areas by default in Ep1/2/4 games. If this is on, - // the Ragol warp in Pioneer 2 will allow access to all base areas (Forest 1, - // Cave 1, Mine 1, and Ruins 1 in Episode 1, for example) even if the player - // who created the game does not yet have access to those areas. - // Note that some late PSOBB client versions (for example, the Tethealla - // client) open all areas by default, so this setting has no effect for them. - "UnlockAllAreas": false, - // Whether to enable rare drop notifications by default. Players can toggle // this behavior for themselves with the $itemnotifs command. "RareNotificationsEnabledByDefaultV1V2": false, @@ -1107,12 +1099,55 @@ "Episode2": [1, 30, 60, 100], "Episode4": [1, 40, 70, 110], }, - // Some quest flags should not be written to the character file when they're - // updated during a quest. Specify the flag IDs here to prevent those flags - // from being updated in saved BB character data. The default value here - // prevents the door locks in CCA from being deactivated in free-roam by - // loading a quest. - "PreventPersistQuestFlags": [0x0046, 0x0047, 0x0048], + + // Some quest flags should be changed when a game is started in order to fix + // certain in-game issues (noted in the comments in the default values below). + // These can be true, false, or expressions to make values conditional on + // other flags' values. For non-BB versions, you should generally only use + // true and false here, since the server doesn't have direct access to the + // client's quest flags from their save file. + // If you use an expression, the format is the same as the AvailableIf and + // EnabledIf fields in quest JSON files (see system/quests/battle/b88001.json + // for details). Note that the expression is only evaluated at the time the + // game is created, and the player-specific tokens like C_EpX_YY refer to the + // player who created the game. + // The UnlockAllAreas option is now gone; if you want the same behavior as if + // it were enabled, uncomment all the "area unlocks" lines below. Note that + // some late PSOBB client versions (for example, the Tethealla client) open + // all areas by default, so the area unlock flags have no effect for them. + "QuestFlagRewritesV1V2": { + // "F_0017": true, // Ep1 area unlocks + // "F_0020": true, // Ep1 area unlocks + // "F_002A": true, // Ep1 area unlocks + }, + "QuestFlagRewritesV3": { + // "F_0017": true, // Ep1 area unlocks + // "F_0020": true, // Ep1 area unlocks + // "F_002A": true, // Ep1 area unlocks + // "F_004C": true, // Ep2 area unlocks + // "F_004F": true, // Ep2 area unlocks + // "F_0052": true, // Ep2 area unlocks + }, + "QuestFlagRewritesV4": { + // "F_01F9": true, // Ep1 area unlocks + // "F_0201": true, // Ep1 area unlocks + // "F_0207": true, // Ep1 area unlocks + // "F_021B": true, // Ep2 area unlocks + // "F_0225": true, // Ep2 area unlocks + // "F_022F": true, // Ep2 area unlocks + // "F_02BD": true, // Ep4 area unlocks + // "F_02BE": true, // Ep4 area unlocks + // "F_02BF": true, // Ep4 area unlocks + // "F_02C0": true, // Ep4 area unlocks + // "F_02C1": true, // Ep4 area unlocks + "F_0046": false, // Ep2 CCA door lock fix + "F_0047": false, // Ep2 CCA door lock fix + "F_0048": false, // Ep2 CCA door lock fix + "F_002C": "F_01F7", // Ep1 Forest monument state = 1-2 cleared + "F_002D": "F_01FD", // Ep1 Cave monument state = 2-2 cleared + "F_002E": "F_0209", // Ep1 Mine monument state = 4-1 cleared + "F_002F": "F_01F7 && F_01FD && F_0209", // All monuments state + }, // Whether to enable certain exception handling. Disabling this causes // newserv to abort when any client causes an exception, which is generally diff --git a/tests/config.json b/tests/config.json index 1fe0542a..fd2e5b38 100644 --- a/tests/config.json +++ b/tests/config.json @@ -30,7 +30,6 @@ "DefaultDropModeV4Battle": "SERVER_SHARED", "DefaultDropModeV4Challenge": "SERVER_SHARED", "CheatModeBehavior": "OnByDefault", - "UnlockAllAreas": false, "RareNotificationsEnabledByDefault": false, "NotifyGameForItemPrimaryIdentifiers": [], "NotifyServerForItemPrimaryIdentifiers": [], @@ -407,5 +406,38 @@ "Episode2": [1, 30, 60, 100], "Episode4": [1, 40, 70, 110], }, - "PreventPersistQuestFlags": [0x0046, 0x0047, 0x0048], + + "QuestFlagRewritesV1V2": { + // "F_0017": true, // Ep1 area unlocks + // "F_0020": true, // Ep1 area unlocks + // "F_002A": true, // Ep1 area unlocks + }, + "QuestFlagRewritesV3": { + // "F_0017": true, // Ep1 area unlocks + // "F_0020": true, // Ep1 area unlocks + // "F_002A": true, // Ep1 area unlocks + // "F_004C": true, // Ep2 area unlocks + // "F_004F": true, // Ep2 area unlocks + // "F_0052": true, // Ep2 area unlocks + }, + "QuestFlagRewritesV4": { + // "F_01F9": true, // Ep1 area unlocks + // "F_0201": true, // Ep1 area unlocks + // "F_0207": true, // Ep1 area unlocks + // "F_021B": true, // Ep2 area unlocks + // "F_0225": true, // Ep2 area unlocks + // "F_022F": true, // Ep2 area unlocks + // "F_02BD": true, // Ep4 area unlocks + // "F_02BE": true, // Ep4 area unlocks + // "F_02BF": true, // Ep4 area unlocks + // "F_02C0": true, // Ep4 area unlocks + // "F_02C1": true, // Ep4 area unlocks + "F_0046": false, // Ep2 CCA door lock fix + "F_0047": false, // Ep2 CCA door lock fix + "F_0048": false, // Ep2 CCA door lock fix + "F_002C": "F_01F7", // Ep1 Forest monument state = 1-2 cleared + "F_002D": "F_01FD", // Ep1 Cave monument state = 2-2 cleared + "F_002E": "F_0209", // Ep1 Mine monument state = 4-1 cleared + "F_002F": "F_01F7 && F_01FD && F_0209", // All monuments state + }, }