diff --git a/src/Client.hh b/src/Client.hh index 8137745a..2534a204 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -57,6 +57,7 @@ public: HAS_GUILD_CARD_NUMBER = 0x0000000040000000, AT_BANK_COUNTER = 0x0000000080000000, SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, + SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Cheat mode flags SWITCH_ASSIST_ENABLED = 0x0000000100000000, diff --git a/src/Lobby.cc b/src/Lobby.cc index 540b97d5..238181e3 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -485,6 +485,11 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { // received. For this reason, we consume item IDs here only if the client is // NTE or 11/2000. this->assign_inventory_and_bank_item_ids(c, is_pre_v1(c->version())); + // On BB, we send artificial flag state to fix an Episode 2 bug where the + // CCA door lock state is overwritten by quests. + if (c->version() == Version::BB_V4) { + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); + } } // If the lobby is recording a battle record, add the player join event diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 801cf068..3394e764 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -508,6 +508,13 @@ struct QuestFlagsForDifficulty { uint8_t mask = 0x80 >> (flag_index & 7); this->data[byte_index] &= (~mask); } + inline void update_all(bool set) { + if (set) { + this->data.clear(0xFF); + } else { + this->data.clear(0x00); + } + } } __attribute__((packed)); struct QuestFlags { @@ -522,6 +529,14 @@ struct QuestFlags { inline void clear(uint8_t difficulty, uint16_t flag_index) { this->data[difficulty].clear(flag_index); } + inline void update_all(uint8_t difficulty, bool set) { + this->data[difficulty].update_all(set); + } + inline void update_all(bool set) { + for (size_t z = 0; z < 4; z++) { + this->update_all(z, set); + } + } } __attribute__((packed)); struct BattleRules { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 380e50cf..c71588ff 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -345,11 +345,14 @@ static void on_1D(shared_ptr c, uint16_t, uint32_t, string&) { if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) { c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); - auto l = c->require_lobby(); if (!is_ep3(c->version())) { send_game_item_state(c); } } + if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE)) { + c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); + send_game_flag_state(c); + } } static void on_05_XB(shared_ptr c, uint16_t, uint32_t, string&) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index cdd2438a..b829cf32 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1724,19 +1724,26 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla difficulty = cmd.difficulty; } - if (flag_index >= 0x400) { + if ((flag_index >= 0x400) || (difficulty > 3) || (action > 1)) { return; } // TODO: Should we allow overlays here? auto p = c->character(true, false); - // The client explicitly checks for both 0 and 1 - any other value means no - // operation is performed. - if (action == 0) { - p->quest_flags.set(difficulty, flag_index); - } else if (action == 1) { - p->quest_flags.clear(difficulty, flag_index); + auto s = c->require_server_state(); + if (s->quest_flag_persist_mask.get(flag_index)) { + // The client explicitly checks for both 0 and 1 - any other value means no + // operation is performed. + if (action == 0) { + c->log.info("Setting quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index); + p->quest_flags.set(difficulty, flag_index); + } else if (action == 1) { + c->log.info("Clearing quest flag %s:%03hX", name_for_difficulty(difficulty), flag_index); + p->quest_flags.clear(difficulty, flag_index); + } + } else { + c->log.info("Quest flag %s:%03hX cannot be modified", name_for_difficulty(difficulty), flag_index); } forward_subcommand(c, command, flag, data, size); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 90f9bc4f..ae8a2604 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2395,6 +2395,31 @@ void send_game_item_state(shared_ptr c) { } } +void send_game_flag_state(shared_ptr c) { + auto l = c->require_lobby(); + + G_SetQuestFlags_6x6F cmd; + cmd.header.subcommand = 0x6F; + cmd.header.size = sizeof(G_SetQuestFlags_6x6F) >> 2; + cmd.header.unused = 0x0000; + cmd.quest_flags = c->character()->quest_flags; + + for (const auto& lc : l->clients) { + if (!lc) { + continue; + } + if (lc->game_join_command_queue) { + lc->log.info("Client not ready to receive join commands; adding to queue"); + auto& cmd = lc->game_join_command_queue->emplace_back(); + cmd.command = 0x0060; + cmd.flag = 0x00000000; + cmd.data.assign(reinterpret_cast(&cmd), sizeof(cmd)); + } else { + send_command_t(lc, 0x60, 0x00, cmd); + } + } +} + void send_drop_item_to_channel(shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) { uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index b7e29ed0..d34ebe0b 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -298,6 +298,7 @@ void send_ep3_change_music(Channel& ch, uint32_t song); void send_revive_player(std::shared_ptr c); void send_game_item_state(std::shared_ptr c); +void send_game_flag_state(std::shared_ptr c); void send_drop_item_to_channel(std::shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id); void send_drop_item_to_lobby(std::shared_ptr l, const ItemData& item, diff --git a/src/ServerState.cc b/src/ServerState.cc index 243294bd..71420f22 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -676,6 +676,15 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { (this->allowed_drop_modes_v4_battle & (1 << static_cast(Lobby::DropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast(Lobby::DropMode::CLIENT)))) { 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 : json.get_list("PreventPersistQuestFlags")) { + this->quest_flag_persist_mask.clear(flag_id_json->as_int()); + } + } catch (const out_of_range&) { + } + this->persistent_game_idle_timeout_usecs = json.get_int("PersistentGameIdleTimeout", this->persistent_game_idle_timeout_usecs); this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", this->cheat_mode_behavior); this->ep3_send_function_call_enabled = json.get_bool("EnableEpisode3SendFunctionCall", this->ep3_send_function_call_enabled); diff --git a/src/ServerState.hh b/src/ServerState.hh index a31b8322..94ffb191 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -94,6 +94,7 @@ struct ServerState : public std::enable_shared_from_this { Lobby::DropMode default_drop_mode_v4_normal; Lobby::DropMode default_drop_mode_v4_battle; Lobby::DropMode default_drop_mode_v4_challenge; + QuestFlagsForDifficulty quest_flag_persist_mask; uint64_t persistent_game_idle_timeout_usecs; bool ep3_send_function_call_enabled; bool catch_handler_exceptions; diff --git a/system/config.example.json b/system/config.example.json index 8877c47a..3ceb3dbe 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -781,6 +781,12 @@ "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], // 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 c0e5edff..427891e2 100644 --- a/tests/config.json +++ b/tests/config.json @@ -300,4 +300,11 @@ "RewardItem": "031903", }, ], + + "BBMinimumLevels": { + "Episode1": [1, 20, 50, 90], + "Episode2": [1, 30, 60, 100], + "Episode4": [1, 40, 70, 110], + }, + "PreventPersistQuestFlags": [0x0046, 0x0047, 0x0048], }