From 673c767a42fd570c7114f09704bfbddf863508fb Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 21 Apr 2024 01:13:34 -0700 Subject: [PATCH] add random stream into Ep3 battle records --- src/Episode3/BattleRecord.cc | 24 +++++++++++++-- src/Episode3/BattleRecord.hh | 6 +++- src/Episode3/CardSpecial.cc | 20 ++++++------ src/Episode3/DeckState.cc | 20 +++++++++--- src/Episode3/DeckState.hh | 14 +++++---- src/Episode3/PlayerState.cc | 2 +- src/Episode3/Server.cc | 59 ++++++++++++++++++------------------ src/Episode3/Server.hh | 3 ++ 8 files changed, 94 insertions(+), 54 deletions(-) diff --git a/src/Episode3/BattleRecord.cc b/src/Episode3/BattleRecord.cc index 86f619a6..44dcf304 100644 --- a/src/Episode3/BattleRecord.cc +++ b/src/Episode3/BattleRecord.cc @@ -121,7 +121,7 @@ void BattleRecord::Event::print(FILE* stream) const { print_data(stream, this->data); break; default: - throw runtime_error("unknown event type in batlte record"); + throw runtime_error("unknown event type in battle record"); } } @@ -137,14 +137,23 @@ BattleRecord::BattleRecord(const string& data) battle_start_timestamp(0), battle_end_timestamp(0) { StringReader r(data); + uint64_t signature = r.get_u64l(); - if (signature != this->SIGNATURE) { + bool has_random_stream; + if (signature == this->SIGNATURE_V1) { + has_random_stream = false; + } else if (signature == this->SIGNATURE_V2) { + has_random_stream = true; + } else { throw runtime_error("incorrect battle record signature"); } this->battle_start_timestamp = r.get_u64l(); this->battle_end_timestamp = r.get_u64l(); this->behavior_flags = r.get_u32l(); + if (has_random_stream) { + this->random_stream = r.read(r.get_u32l()); + } while (!r.eof()) { this->events.emplace_back(r); } @@ -152,10 +161,12 @@ BattleRecord::BattleRecord(const string& data) string BattleRecord::serialize() const { StringWriter w; - w.put_u64l(this->SIGNATURE); + w.put_u64l(this->SIGNATURE_V2); w.put_u64l(this->battle_start_timestamp); w.put_u64l(this->battle_end_timestamp); w.put_u32l(this->behavior_flags); + w.put_u32l(this->random_stream.size()); + w.write(this->random_stream); for (const auto& ev : this->events) { ev.serialize(w); } @@ -240,6 +251,10 @@ void BattleRecord::add_chat_message( ev.data = std::move(data); } +void BattleRecord::add_random_data(const void* data, size_t size) { + this->random_stream.append(reinterpret_cast(data), size); +} + bool BattleRecord::is_map_definition_event(const Event& ev) { if (ev.type == Event::Type::BATTLE_COMMAND) { auto& header = check_size_t(ev.data, 0xFFFF); @@ -321,6 +336,9 @@ void BattleRecord::set_battle_start_timestamp() { } } this->events = std::move(new_events); + + // Clear any existing random data (there shouldn't be any) + this->random_stream.clear(); } void BattleRecord::set_battle_end_timestamp() { diff --git a/src/Episode3/BattleRecord.hh b/src/Episode3/BattleRecord.hh index 562d5aa9..c2bc8589 100644 --- a/src/Episode3/BattleRecord.hh +++ b/src/Episode3/BattleRecord.hh @@ -76,6 +76,7 @@ public: void add_command(Event::Type type, const void* data, size_t size); void add_command(Event::Type type, std::string&& data); void add_chat_message(uint32_t guild_card_number, std::string&& data); + void add_random_data(const void* data, size_t size); // This function collapses all the existing player join/leave events into a // single SET_INITIAL_PLAYERS event, and deletes all events before the latest // BATTLE_COMMAND command that specifies the battle map. This should provide a @@ -86,7 +87,8 @@ public: void print(FILE* stream) const; private: - static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50; + static constexpr uint64_t SIGNATURE_V1 = 0x14C946D56D1DAC50; + static constexpr uint64_t SIGNATURE_V2 = 0xD01E5EC12853C377; static bool is_map_definition_event(const Event& ev); @@ -96,6 +98,7 @@ private: uint64_t battle_start_timestamp; uint64_t battle_end_timestamp; std::deque events; + std::string random_stream; friend class BattleRecordPlayer; }; @@ -120,6 +123,7 @@ private: std::shared_ptr base; std::weak_ptr lobby; std::shared_ptr next_command_ev; + StringReader random_r; }; } // namespace Episode3 diff --git a/src/Episode3/CardSpecial.cc b/src/Episode3/CardSpecial.cc index 26c1017f..381c2edb 100644 --- a/src/Episode3/CardSpecial.cc +++ b/src/Episode3/CardSpecial.cc @@ -3859,8 +3859,10 @@ void CardSpecial::check_for_defense_interference( shared_ptr attacker_card, shared_ptr target_card, int16_t* inout_unknown_p4) { + auto s = this->server(); + // Note: This check is not part of the original implementation. - if (this->server()->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) { + if (s->options.behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) { return; } @@ -3871,13 +3873,13 @@ void CardSpecial::check_for_defense_interference( return; } - uint16_t ally_sc_card_ref = this->server()->ruler_server->get_ally_sc_card_ref( + uint16_t ally_sc_card_ref = s->ruler_server->get_ally_sc_card_ref( target_card->get_card_ref()); if (ally_sc_card_ref == 0xFFFF) { return; } - auto ally_sc = this->server()->card_for_set_card_ref(ally_sc_card_ref); + auto ally_sc = s->card_for_set_card_ref(ally_sc_card_ref); if (!ally_sc || (ally_sc->card_flags & 2)) { return; } @@ -3892,17 +3894,17 @@ void CardSpecial::check_for_defense_interference( return; } - auto ally_hes = this->server()->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id); - if (!ally_hes || (!(this->server()->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) { + auto ally_hes = s->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id); + if (!ally_hes || (!(s->options.behavior_flags & BehaviorFlag::ALLOW_NON_COM_INTERFERENCE) && !ally_hes->is_cpu_player)) { return; } - uint16_t target_card_id = this->server()->card_id_for_card_ref(target_card->get_card_ref()); + uint16_t target_card_id = s->card_id_for_card_ref(target_card->get_card_ref()); if (target_card_id == 0xFFFF) { return; } - uint16_t ally_sc_card_id = this->server()->card_id_for_card_ref(ally_sc_card_ref); + uint16_t ally_sc_card_id = s->card_id_for_card_ref(ally_sc_card_ref); if (ally_sc_card_id == 0xFFFF) { return; } @@ -3916,7 +3918,7 @@ void CardSpecial::check_for_defense_interference( } auto entry = get_interference_probability_entry( target_card_id, ally_sc_card_id, false); - if (!entry || (this->server()->get_random(99) >= entry->defense_probability)) { + if (!entry || (s->get_random(99) >= entry->defense_probability)) { return; } @@ -3928,7 +3930,7 @@ void CardSpecial::check_for_defense_interference( cmd.effect.target_card_ref = target_card->get_card_ref(); cmd.effect.value = 0; cmd.effect.operation = 0x7D; - this->server()->send(cmd); + s->send(cmd); if (inout_unknown_p4) { *inout_unknown_p4 = 0; target_card->action_metadata.set_flags(0x10); diff --git a/src/Episode3/DeckState.cc b/src/Episode3/DeckState.cc index c69834cd..cd59b2dd 100644 --- a/src/Episode3/DeckState.cc +++ b/src/Episode3/DeckState.cc @@ -1,5 +1,7 @@ #include "DeckState.hh" +#include "Server.hh" + using namespace std; namespace Episode3 { @@ -203,12 +205,17 @@ void DeckState::do_mulligan(bool is_nte) { this->card_refs[index + 5] = temp_ref; } + auto s = this->server.lock(); + if (!s) { + throw runtime_error("server is missing"); + } + // Shuffle the deck, except the first 5 cards (which are about to be drawn). size_t max = this->num_drawable_cards() - 5; uint8_t base_index = this->draw_index + 5; for (size_t z = 0; z < this->card_refs.size(); z++) { - uint8_t index1 = random_from_optional_crypt(this->opt_rand_crypt) % max; - uint8_t index2 = random_from_optional_crypt(this->opt_rand_crypt) % max; + uint8_t index1 = s->get_random(max); + uint8_t index2 = s->get_random(max); uint16_t temp_ref = this->card_refs[base_index + index1]; this->card_refs[base_index + index1] = this->card_refs[base_index + index2]; this->card_refs[base_index + index2] = temp_ref; @@ -260,6 +267,11 @@ void DeckState::set_card_discarded(uint16_t card_ref) { void DeckState::shuffle() { if (this->shuffle_enabled) { + auto s = this->server.lock(); + if (!s) { + throw runtime_error("server is missing"); + } + size_t max = this->num_drawable_cards(); for (size_t z = 0; z < this->card_refs.size(); z++) { // Note: This is the way Sega originally implemented shuffling - they just @@ -267,8 +279,8 @@ void DeckState::shuffle() { // instead swap each item with another random item (possibly itself) that // doesn't appear earlier than it in the array, but this is not what Sega // did. - uint8_t index1 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max; - uint8_t index2 = this->draw_index + random_from_optional_crypt(this->opt_rand_crypt) % max; + uint8_t index1 = this->draw_index + s->get_random(max); + uint8_t index2 = this->draw_index + s->get_random(max); uint16_t temp_ref = this->card_refs[index1]; this->card_refs[index1] = this->card_refs[index2]; this->card_refs[index2] = temp_ref; diff --git a/src/Episode3/DeckState.hh b/src/Episode3/DeckState.hh index 7dcc3391..07e323ce 100644 --- a/src/Episode3/DeckState.hh +++ b/src/Episode3/DeckState.hh @@ -10,6 +10,8 @@ namespace Episode3 { +class Server; + struct NameEntry { /* 00 */ pstring name; /* 10 */ uint8_t client_id; @@ -57,13 +59,13 @@ public: DeckState( uint8_t client_id, const parray& card_ids, - std::shared_ptr opt_rand_crypt) - : client_id(client_id), + std::shared_ptr server) + : server(server), + client_id(client_id), draw_index(1), card_ref_base(this->client_id << 8), shuffle_enabled(true), - loop_enabled(true), - opt_rand_crypt(opt_rand_crypt) { + loop_enabled(true) { for (size_t z = 0; z < card_ids.size(); z++) { auto& e = this->entries[z]; e.card_id = card_ids[z]; @@ -99,6 +101,8 @@ public: void print(FILE* stream, std::shared_ptr card_index = nullptr) const; private: + std::weak_ptr server; + struct CardEntry { uint16_t card_id; uint8_t deck_index; @@ -111,8 +115,6 @@ private: bool loop_enabled; parray entries; parray card_refs; - - std::shared_ptr opt_rand_crypt; }; } // namespace Episode3 diff --git a/src/Episode3/PlayerState.cc b/src/Episode3/PlayerState.cc index 7f46f5bb..9b23bf9c 100644 --- a/src/Episode3/PlayerState.cc +++ b/src/Episode3/PlayerState.cc @@ -49,7 +49,7 @@ void PlayerState::init() { throw logic_error("replacing a player state object is not permitted"); } - this->deck_state = make_shared(this->client_id, s->deck_entries[client_id]->card_ids, s->options.opt_rand_crypt); + this->deck_state = make_shared(this->client_id, s->deck_entries[client_id]->card_ids, s); if (s->map_and_rules->rules.disable_deck_shuffle) { this->deck_state->disable_shuffle(); } diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index adf339aa..fc1d621c 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -4,6 +4,7 @@ #include #include "../Loggers.hh" +#include "../Revision.hh" #include "../SendCommands.hh" using namespace std; @@ -29,6 +30,7 @@ void Server::PresenceEntry::clear() { Server::Server(shared_ptr lobby, Options&& options) : lobby(lobby), + battle_record(lobby->battle_record), has_lobby(lobby != nullptr), options(std::move(options)), last_chosen_map(this->options.tournament ? this->options.tournament->get_map() : nullptr), @@ -99,10 +101,10 @@ void Server::init() { this->card_special = make_shared(this->shared_from_this()); // Note: The original implementation calls the default PSOV2Encryption - // constructor for opt_rand_crypt, which just uses 0 as the seed. It then + // constructor for random_crypt, which just uses 0 as the seed. It then // re-seeds the generator later. We instead expect the caller to provide a // seeded generator, and we don't re-seed it at all. - // this->opt_rand_crypt = make_shared(0); + // this->random_crypt = make_shared(0); this->state_flags = make_shared(); @@ -252,8 +254,8 @@ void Server::send(const void* data, size_t size, uint8_t command, bool enable_ma for (auto watcher_l : l->watcher_lobbies) { send_command_if_not_loading(watcher_l, command, 0x00, data, size); } - if (l->battle_record && l->battle_record->writable()) { - l->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size); + if (this->battle_record && this->battle_record->writable()) { + this->battle_record->add_command(BattleRecord::Event::Type::BATTLE_COMMAND, data, size); } } else if ((this->options.behavior_flags & BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING) && @@ -271,19 +273,8 @@ void Server::send_6xB4x46() const { G_ServerVersionStrings_Ep3_6xB4x46 cmd; cmd.version_signature.encode(this->options.is_nte() ? VERSION_SIGNATURE_NTE : VERSION_SIGNATURE, 1); cmd.date_str1.encode(format_time(this->options.card_index->definitions_mtime() * 1000000), 1); - string date_str2; - if (this->options.opt_rand_crypt) { - date_str2 = string_printf( - "Random:%08" PRIX32 "+%08" PRIX32, - this->options.opt_rand_crypt->seed(), - this->options.opt_rand_crypt->absolute_offset()); - } else { - date_str2 = "Random:"; - } - if (this->last_chosen_map) { - date_str2 += string_printf(" Map:%08" PRIX32, this->last_chosen_map->map_number); - } - cmd.date_str2.encode(date_str2, 1); + string build_date = format_time(BUILD_TIMESTAMP); + cmd.date_str2.encode(string_printf("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()), 1); this->send(cmd); } @@ -1083,16 +1074,24 @@ shared_ptr Server::get_player_state(uint8_t client_id) const return this->player_states[client_id]; } +uint32_t Server::get_random_raw() { + le_uint32_t 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)); + } + return ret; +} + uint32_t Server::get_random(uint32_t max) { // The original implementation was essentially: - // return (static_cast(this->opt_rand_crypt->next() >> 16) / 65536.0) * max + // return (static_cast(this->random_source->next() >> 16) / 65536.0) * max // This is unnecessarily complicated and imprecise, so we instead just do: - return random_from_optional_crypt(this->options.opt_rand_crypt) % max; + return this->get_random_raw() % max; } float Server::get_random_float_0_1() { // This lacks some precision, but matches the original implementation. - return (static_cast(random_from_optional_crypt(this->options.opt_rand_crypt) >> 16) / 65536.0); + return (static_cast(this->get_random_raw() >> 16) / 65536.0); } uint32_t Server::get_round_num() const { @@ -1549,8 +1548,8 @@ void Server::setup_and_start_battle() { this->setup_phase = SetupPhase::STARTER_ROLLS; - // Note: This is where original implementation re-seeds opt_rand_crypt (it - // uses time() as the seed value). + // Note: This is where original implementation re-seeds its random generator + // (it uses time() as the seed value). for (size_t z = 0; z < 4; z++) { if (!this->check_presence_entry(z)) { @@ -1840,9 +1839,8 @@ void Server::on_server_data_input(shared_ptr sender_c, const string& dat throw runtime_error("unknown CAx subsubcommand"); } - auto l = this->lobby.lock(); - if (l && l->battle_record && l->battle_record->writable()) { - l->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size()); + if (this->battle_record && this->battle_record->writable()) { + this->battle_record->add_command(BattleRecord::Event::Type::SERVER_DATA_COMMAND, data.data(), data.size()); } if ((sender_c && (sender_c->version() == Version::GC_EP3_NTE)) || !header.mask_key) { @@ -2356,11 +2354,12 @@ void Server::handle_CAx1D_start_battle(shared_ptr, const string& data) { } if (should_start) { + if (this->battle_record && this->battle_record->writable()) { + this->battle_record->set_battle_start_timestamp(); + } + auto l = this->lobby.lock(); if (l) { - if (l->battle_record) { - l->battle_record->set_battle_start_timestamp(); - } // Note: Sega's implementation doesn't set EX results values here; they // did it at game join time instead. We do it here for code simplicity. if ((l->base_version != Version::GC_EP3_NTE) && l->ep3_ex_result_values) { @@ -2617,13 +2616,13 @@ void Server::send_6xB6x41_to_all_clients() const { } } - if (l->battle_record && l->battle_record->writable()) { + if (this->battle_record && this->battle_record->writable()) { // TODO: It's not great that we just pick the first one; ideally we'd put // all of them in the recording and send the appropriate one to the client // in the playback lobby for (string& data : map_commands_by_language) { if (!data.empty()) { - l->battle_record->add_command( + this->battle_record->add_command( BattleRecord::Event::Type::BATTLE_COMMAND, std::move(data)); break; } diff --git a/src/Episode3/Server.hh b/src/Episode3/Server.hh index 5664ba30..6a2e0a13 100644 --- a/src/Episode3/Server.hh +++ b/src/Episode3/Server.hh @@ -9,6 +9,7 @@ #include "../CommandFormats.hh" #include "../Text.hh" #include "AssistServer.hh" +#include "BattleRecord.hh" #include "CardSpecial.hh" #include "MapState.hh" #include "PlayerState.hh" @@ -191,6 +192,7 @@ public: uint8_t get_current_team_turn() const; std::shared_ptr get_player_state(uint8_t client_id); std::shared_ptr get_player_state(uint8_t client_id) const; + uint32_t get_random_raw(); uint32_t get_random(uint32_t max); float get_random_float_0_1(); uint32_t get_round_num() const; @@ -273,6 +275,7 @@ private: public: // These fields are not part of the original implementation std::weak_ptr lobby; + std::shared_ptr battle_record; bool has_lobby; Options options; std::shared_ptr last_chosen_map;