From 8375c61236420793cca7f4ee881ff2249fc94793 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 28 Feb 2024 21:08:04 -0800 Subject: [PATCH] add some tools for ep3 replay --- src/Episode3/DataIndexes.hh | 1 + src/Episode3/Server.cc | 31 ++++++++++++----------- src/Main.cc | 49 ++++++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index cfaef5ec..1645b1f8 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -37,6 +37,7 @@ enum BehaviorFlag : uint32_t { DISABLE_INTERFERENCE = 0x00000100, ALLOW_NON_COM_INTERFERENCE = 0x00000200, IS_TRIAL_EDITION = 0x00000400, + LOG_COMMANDS_IF_LOBBY_MISSING = 0x00000800, }; enum class StatSwapType : uint8_t { diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index a6bcf83b..c2dc0c33 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -256,7 +256,8 @@ void Server::send(const void* data, size_t size, uint8_t command, bool enable_ma l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size); } - } else if (this->log().info("Generated command")) { + } else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) && + this->log().info("Generated command")) { print_data(stderr, data, size); } } @@ -1824,8 +1825,10 @@ const unordered_map Server::subcommand_handlers({ void Server::on_server_data_input(shared_ptr sender_c, const string& data) { auto header = check_size_t(data, 0xFFFF); - if (header.size * 4 < data.size()) { - throw runtime_error("command is incomplete"); + size_t expected_size = header.size * 4; + if (expected_size < data.size()) { + print_data(stderr, data); + throw runtime_error(string_printf("command is incomplete: expected %zX bytes, received %zX bytes", expected_size, data.size())); } if (header.subcommand != 0xB3) { throw runtime_error("server data command is not 6xB3"); @@ -2163,33 +2166,29 @@ void Server::handle_CAx13_update_map_during_setup_t(shared_ptr c, const (this->map_and_rules->num_players == 0) && (this->registration_phase != RegistrationPhase::REGISTERED) && (this->registration_phase != RegistrationPhase::BATTLE_STARTED)) { - if (!this->last_chosen_map) { - throw runtime_error("CAx13 sent with no map chosen"); - } - *this->map_and_rules = in_cmd.map_and_rules_state; // The client will likely send incorrect values for the extended rules (or // in the case of NTE, no values at all, since the Rules structure is // smaller). So, use the values from the last chosen map if applicable, or // the values from the $dicerange command if available. - const auto& map_rules = this->last_chosen_map->version(c->language())->map->default_rules; + const Rules* map_rules = this->last_chosen_map ? &this->last_chosen_map->version(c->language())->map->default_rules : nullptr; auto& server_rules = this->map_and_rules->rules; // NTE can specify the DEF dice value range in its Rules struct, so we use // that unless the map or $dicerange overrides it. - server_rules.def_dice_value_range = (map_rules.def_dice_value_range != 0xFF) - ? map_rules.def_dice_value_range + server_rules.def_dice_value_range = (map_rules && (map_rules->def_dice_value_range != 0xFF)) + ? map_rules->def_dice_value_range : (this->def_dice_value_range_override != 0xFF) ? this->def_dice_value_range_override : this->options.is_nte() ? server_rules.def_dice_value_range : 0; - server_rules.atk_dice_value_range_2v1 = (map_rules.atk_dice_value_range_2v1 != 0xFF) - ? map_rules.atk_dice_value_range_2v1 + server_rules.atk_dice_value_range_2v1 = (map_rules && (map_rules->atk_dice_value_range_2v1 != 0xFF)) + ? map_rules->atk_dice_value_range_2v1 : (this->atk_dice_value_range_2v1_override != 0xFF) ? this->atk_dice_value_range_2v1_override : 0; - server_rules.def_dice_value_range_2v1 = (map_rules.def_dice_value_range_2v1 != 0xFF) - ? map_rules.def_dice_value_range_2v1 + server_rules.def_dice_value_range_2v1 = (map_rules && (map_rules->def_dice_value_range_2v1 != 0xFF)) + ? map_rules->def_dice_value_range_2v1 : (this->def_dice_value_range_2v1_override != 0xFF) ? this->def_dice_value_range_2v1_override : 0; @@ -2585,6 +2584,10 @@ void Server::handle_CAx40_map_list_request(shared_ptr sender_c, const st } void Server::send_6xB6x41_to_all_clients() const { + if (!this->last_chosen_map) { + throw logic_error("cannot send 6xB4x41 without a map chosen"); + } + auto l = this->lobby.lock(); if (l) { vector map_commands_by_language; diff --git a/src/Main.cc b/src/Main.cc index 22e25091..c93246b8 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2306,26 +2306,47 @@ Action a_replay_ep3_battle_commands( s->load_ep3_cards(false); s->load_ep3_maps(false); - auto opt_rand_crypt = make_shared(args.get("seed", 0, Arguments::IntFormat::HEX)); - Episode3::Server::Options options = { - .card_index = s->ep3_card_index, - .map_index = s->ep3_map_index, - .behavior_flags = 0x0092, - .opt_rand_crypt = opt_rand_crypt, - .tournament = nullptr, - .trap_card_ids = {}, - }; - auto server = make_shared(nullptr, std::move(options)); - server->init(); + int64_t base_seed = args.get("seed", -1); + bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); auto input = read_input_data(args); - auto lines = split(input, '\n'); - for (const auto& line : lines) { + vector commands; + for (const auto& line : split(input, '\n')) { string data = parse_data_string(line); if (!data.empty()) { - server->on_server_data_input(nullptr, data); + commands.emplace_back(std::move(data)); } } + + auto run_replay = [&](int64_t seed, size_t) { + Episode3::Server::Options options = { + .card_index = s->ep3_card_index, + .map_index = s->ep3_map_index, + .behavior_flags = 0x0092, + .opt_rand_crypt = (seed >= 0) ? make_shared(seed) : nullptr, + .tournament = nullptr, + .trap_card_ids = {}, + }; + if (is_trial) { + options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; + } + if (base_seed >= 0) { + options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; + } + auto server = make_shared(nullptr, std::move(options)); + server->init(); + for (const auto& command : commands) { + server->on_server_data_input(nullptr, command); + } + return false; + }; + + if (base_seed >= 0) { + run_replay(base_seed, 0); + } else { + size_t num_threads = args.get("threads", 0); + parallel_range(run_replay, 0, 0x100000000, num_threads); + } }); Action a_run_server_replay_log(