diff --git a/src/Episode3/BattleRecord.cc b/src/Episode3/BattleRecord.cc index 44dcf304..ae1e9a2b 100644 --- a/src/Episode3/BattleRecord.cc +++ b/src/Episode3/BattleRecord.cc @@ -12,7 +12,7 @@ namespace Episode3 { void BattleRecord::PlayerEntry::print(FILE* stream) const { // TODO: Format this nicely somehow. Maybe factor out the functions in // QuestScript that format some of these structures - print_data(stream, this, sizeof(this)); + print_data(stream, this, sizeof(*this), 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); } BattleRecord::Event::Event(StringReader& r) { @@ -96,29 +96,30 @@ void BattleRecord::Event::print(FILE* stream) const { for (const auto& player : this->players) { fprintf(stream, " %02" PRIX32, player.lobby_data.client_id.load()); } + fputc('\n', stream); for (const auto& player : this->players) { player.print(stream); } break; case Type::BATTLE_COMMAND: fprintf(stream, "BATTLE_COMMAND\n"); - print_data(stream, this->data); + print_data(stream, this->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); break; case Type::GAME_COMMAND: fprintf(stream, "GAME_COMMAND\n"); - print_data(stream, this->data); + print_data(stream, this->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); break; case Type::EP3_GAME_COMMAND: fprintf(stream, "EP3_GAME_COMMAND\n"); - print_data(stream, this->data); + print_data(stream, this->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); break; case Type::CHAT_MESSAGE: fprintf(stream, "CHAT_MESSAGE %08" PRIX32 "\n", this->guild_card_number); - print_data(stream, this->data); + print_data(stream, this->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); break; case Type::SERVER_DATA_COMMAND: fprintf(stream, "SERVER_DATA_COMMAND\n"); - print_data(stream, this->data); + print_data(stream, this->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); break; default: throw runtime_error("unknown event type in battle record"); @@ -255,6 +256,20 @@ void BattleRecord::add_random_data(const void* data, size_t size) { this->random_stream.append(reinterpret_cast(data), size); } +vector BattleRecord::get_all_server_data_commands() const { + vector ret; + for (const auto& event : this->events) { + if (event.type == Event::Type::SERVER_DATA_COMMAND) { + ret.emplace_back(event.data); + } + } + return ret; +} + +const string& BattleRecord::get_random_stream() const { + return this->random_stream; +} + bool BattleRecord::is_map_definition_event(const Event& ev) { if (ev.type == Event::Type::BATTLE_COMMAND) { auto& header = check_size_t(ev.data, 0xFFFF); @@ -331,7 +346,7 @@ void BattleRecord::set_battle_start_timestamp() { } } for (; it != this->events.end(); it++) { - if (it->type == Event::Type::BATTLE_COMMAND) { + if ((it->type == Event::Type::BATTLE_COMMAND) || (it->type == Event::Type::SERVER_DATA_COMMAND)) { new_events.emplace_back(std::move(*it)); } } @@ -373,7 +388,7 @@ shared_ptr BattleRecordPlayer::get_record() const { return this->record; } -void BattleRecordPlayer::set_lobby(std::shared_ptr l) { +void BattleRecordPlayer::set_lobby(shared_ptr l) { this->lobby = l; } diff --git a/src/Episode3/BattleRecord.hh b/src/Episode3/BattleRecord.hh index c2bc8589..e4ddc806 100644 --- a/src/Episode3/BattleRecord.hh +++ b/src/Episode3/BattleRecord.hh @@ -86,6 +86,9 @@ public: void print(FILE* stream) const; + std::vector get_all_server_data_commands() const; + const std::string& get_random_stream() const; + private: static constexpr uint64_t SIGNATURE_V1 = 0x14C946D56D1DAC50; static constexpr uint64_t SIGNATURE_V2 = 0xD01E5EC12853C377; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index fc1d621c..cc0fe6ac 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -30,7 +30,7 @@ void Server::PresenceEntry::clear() { Server::Server(shared_ptr lobby, Options&& options) : lobby(lobby), - battle_record(lobby->battle_record), + battle_record(lobby ? lobby->battle_record : nullptr), has_lobby(lobby != nullptr), options(std::move(options)), last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr), @@ -260,7 +260,7 @@ void Server::send(const void* data, size_t size, uint8_t command, bool enable_ma } else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) && this->log().info("Generated command")) { - print_data(stderr, data, size); + print_data(stderr, data, size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); } } @@ -1075,7 +1075,13 @@ shared_ptr Server::get_player_state(uint8_t client_id) const } uint32_t Server::get_random_raw() { - le_uint32_t ret = random_from_optional_crypt(this->options.opt_rand_crypt); + le_uint32_t ret; + if (this->options.opt_rand_stream) { + this->options.opt_rand_stream->readx(&ret, sizeof(ret)); + } else { + ret = random_from_optional_crypt(this->options.opt_rand_crypt); + } + if (this->battle_record && this->battle_record->writable()) { this->battle_record->add_random_data(&ret, sizeof(ret)); } diff --git a/src/Episode3/Server.hh b/src/Episode3/Server.hh index 6a2e0a13..ebb81e56 100644 --- a/src/Episode3/Server.hh +++ b/src/Episode3/Server.hh @@ -72,6 +72,7 @@ public: std::shared_ptr card_index; std::shared_ptr map_index; uint32_t behavior_flags; + std::shared_ptr opt_rand_stream; std::shared_ptr opt_rand_crypt; std::shared_ptr tournament; std::array, 5> trap_card_ids; diff --git a/src/Main.cc b/src/Main.cc index 0b26a56c..d3669397 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -765,7 +765,7 @@ Action a_decrypt_dcv2_executable( If --simple is given, uses the simpler encryption method used in some\n\ community modifications of the game. In this case, --seed is not required;\n\ if not given, finds the seed automatically, and prints it to stderr so you\n\ - will be able to use it when re-encrypting.", + will be able to use it when re-encrypting.\n", +[](Arguments& args) { string executable_filename = args.get("executable", true); string executable_data = load_file(executable_filename); @@ -785,14 +785,14 @@ Action a_decrypt_dcv2_executable( }); Action a_encrypt_dcv2_executable( "encrypt-dcv2-executable", "\ - decrypt-dcv2-executable --executable=EXEC --indexes=INDEXES\n\ - decrypt-dcv2-executable --executable=EXEC --simple --seed=SEED\n\ + encrypt-dcv2-executable --executable=EXEC --indexes=INDEXES\n\ + encrypt-dcv2-executable --executable=EXEC --simple --seed=SEED\n\ Encrypt a PSO DC v2 executable file. EXEC should be the path to the\n\ executable (DP_ADDRESS.JPN) and INDEXES should be the path to the index\n\ fixup table (KATSUO.SEA). The output is written to EXEC.enc and\n\ INDEXES.enc.\n\ If --simple is given, uses the simpler encryption method used in some\n\ - community modifications of the game. In this case, --seed is required.", + community modifications of the game. In this case, --seed is required.\n", +[](Arguments& args) { string executable_filename = args.get("executable", true); string executable_data = load_file(executable_filename); @@ -2426,6 +2426,45 @@ Action a_replay_ep3_battle_commands( } }); +Action a_replay_ep3_battle_record( + "replay-ep3-battle-record", nullptr, +[](Arguments& args) { + auto rec = make_shared(read_input_data(args)); + + auto s = make_shared(get_config_filename(args)); + s->load_ep3_cards(false); + s->load_ep3_maps(false); + + bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); + + Episode3::Server::Options options = { + .card_index = s->ep3_card_index, + .map_index = s->ep3_map_index, + .behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS | + Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES | + Episode3::BehaviorFlag::DISABLE_MASKING | + Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING), + .opt_rand_stream = make_shared(rec->get_random_stream()), + .tournament = nullptr, + .trap_card_ids = {}, + }; + if (is_trial) { + options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; + } + options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; + auto server = make_shared(nullptr, std::move(options)); + server->init(); + for (const auto& command : rec->get_all_server_data_commands()) { + log_info("Server data command"); + print_data(stderr, command, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); + server->on_server_data_input(nullptr, command); + } + }); + +Action a_disassemble_ep3_battle_record( + "disassemble-ep3-battle-record", nullptr, +[](Arguments& args) { + Episode3::BattleRecord(read_input_data(args)).print(stdout); + }); + Action a_run_server_replay_log( "", nullptr, +[](Arguments& args) { { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 80a5c261..b9e7a85e 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1588,16 +1588,20 @@ static void on_CA_Ep3(shared_ptr c, uint16_t, uint32_t, string& data) { bool battle_finished_before = l->ep3_server->battle_finished; - try { - l->ep3_server->on_server_data_input(c, data); - } catch (const exception& e) { - c->log.error("Episode 3 engine returned an error: %s", e.what()); - if (l->battle_record) { - string filename = string_printf("system/ep3/battle-records/exc.%" PRIu64 ".mzrd", now()); - save_file(filename, l->battle_record->serialize()); - c->log.error("Saved partial battle record as %s", filename.c_str()); + if (s->catch_handler_exceptions) { + try { + l->ep3_server->on_server_data_input(c, data); + } catch (const exception& e) { + c->log.error("Episode 3 engine returned an error: %s", e.what()); + if (l->battle_record) { + string filename = string_printf("system/ep3/battle-records/exc.%" PRIu64 ".mzrd", now()); + save_file(filename, l->battle_record->serialize()); + c->log.error("Saved partial battle record as %s", filename.c_str()); + } + throw; } - throw; + } else { + l->ep3_server->on_server_data_input(c, data); } // If the battle has finished, finalize the recording and link it to all