From 84ed80365cd290320dbbf05dbeb90935dfc397e1 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 23 Dec 2023 23:26:44 -0800 Subject: [PATCH] make BB games deterministic for replays --- TODO.md | 3 --- src/ChatCommands.cc | 2 +- src/Items.cc | 6 ++---- src/Items.hh | 3 ++- src/Lobby.cc | 2 +- src/Map.cc | 7 ++++--- src/Map.hh | 3 ++- src/ReceiveSubcommands.cc | 14 +++++++------- src/SendCommands.cc | 3 ++- src/ServerShell.cc | 2 +- 10 files changed, 22 insertions(+), 23 deletions(-) diff --git a/TODO.md b/TODO.md index b84a944b..66003dac 100644 --- a/TODO.md +++ b/TODO.md @@ -1,19 +1,16 @@ ## General -- Encapsulate BB server-side random state and make replays deterministic - Write a simple status API - Implement per-game logging - Make reloading happen on separate threads so compression doesn't block active clients - Implement decrypt/encrypt actions for VMS files - Make UI strings localizable (e.g. entries in menus, welcome message, etc.) -- Figure out what causes the corruption message on PC proxy sessions and fix it - Add an idle connection timeout for proxy sessions - Look into JP heart symbol bug on Linux ## Episode 3 - Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time -- Add support for recording battles on the proxy server (both in primary and spectator teams) - Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow - Implement ranks (based on total Meseta earned) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index a1a137a1..998ebdbe 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1617,7 +1617,7 @@ static void proxy_command_item(shared_ptr ses, const bool set_drop = (!args.empty() && (args[0] == '!')); ItemData item = s->item_name_index->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args)); - item.id = random_object(); + item.id = random_object() | 0x80000000; if (set_drop) { ses->next_drop_item = item; diff --git a/src/Items.cc b/src/Items.cc index a4c93a7a..a02676f8 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -2,13 +2,11 @@ #include -#include - #include "SendCommands.hh" using namespace std; -void player_use_item(shared_ptr c, size_t item_index) { +void player_use_item(shared_ptr c, size_t item_index, shared_ptr random_crypt) { auto s = c->require_server_state(); // On PC (and presumably DC), the client sends a 6x29 after this to delete the @@ -179,7 +177,7 @@ void player_use_item(shared_ptr c, size_t item_index) { if (sum == 0) { throw runtime_error("no unwrap results available for event"); } - size_t det = random_object() % sum; + size_t det = random_crypt->next() % sum; for (size_t z = 0; z < table.second; z++) { const auto& entry = table.first[z]; if (det > entry.probability) { diff --git a/src/Items.hh b/src/Items.hh index 381a036d..44afa880 100644 --- a/src/Items.hh +++ b/src/Items.hh @@ -6,8 +6,9 @@ #include #include "Client.hh" +#include "PSOEncryption.hh" #include "ServerState.hh" #include "StaticGameData.hh" -void player_use_item(std::shared_ptr c, size_t item_index); +void player_use_item(std::shared_ptr c, size_t item_index, std::shared_ptr random_crypt); void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fed_item_index); diff --git a/src/Lobby.cc b/src/Lobby.cc index 238181e3..7396f1c7 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -254,7 +254,7 @@ void Lobby::create_item_creator() { void Lobby::load_maps() { auto s = this->require_server_state(); - this->map = make_shared(this->lobby_id); + this->map = make_shared(this->lobby_id, this->random_crypt); if (this->quest) { auto leader_c = this->clients.at(this->leader_id); diff --git a/src/Map.cc b/src/Map.cc index 4d10eb55..941323aa 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -131,8 +131,9 @@ string Map::Object::str(shared_ptr name_index) const { } } -Map::Map(uint32_t lobby_id) - : log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level) {} +Map::Map(uint32_t lobby_id, std::shared_ptr random_crypt) + : log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level), + random_crypt(random_crypt) {} void Map::clear() { this->objects.clear(); @@ -167,7 +168,7 @@ bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) { if (default_is_rare) { return true; } - if ((this->rare_enemy_indexes.size() < 0x10) && (random_object() < rare_rate)) { + if ((this->rare_enemy_indexes.size() < 0x10) && (this->random_crypt->next() < rare_rate)) { this->rare_enemy_indexes.emplace_back(this->enemies.size()); return true; } diff --git a/src/Map.hh b/src/Map.hh index 1137550d..9ac77338 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -253,7 +253,7 @@ struct Map { void generate_shuffled_location_table(const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section); }; - explicit Map(uint32_t lobby_id); + Map(uint32_t lobby_id, std::shared_ptr random_crypt); ~Map() = default; void clear(); @@ -309,6 +309,7 @@ struct Map { static std::string disassemble_quest_data(const void* data, size_t size); PrefixedLogger log; + std::shared_ptr random_crypt; std::vector objects; std::vector enemies; std::vector rare_enemy_indexes; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 780b7ae9..413b89c0 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1453,7 +1453,7 @@ static void on_use_item( const auto& item = p->inventory.items[index].data; name = s->describe_item(c->version(), item, false); } - player_use_item(c, index); + player_use_item(c, index, l->random_crypt); if (l->log.should_log(LogLevel::INFO)) { l->log.info("Player %hhu used item %hu:%08" PRIX32 " (%s)", @@ -2803,7 +2803,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, ItemData item = (s->secret_lottery_results.size() == 1) ? s->secret_lottery_results[0] - : s->secret_lottery_results[random_object() % s->secret_lottery_results.size()]; + : s->secret_lottery_results[l->random_crypt->next() % s->secret_lottery_results.size()]; item.enforce_min_stack_size(); item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); @@ -2819,7 +2819,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, out_cmd.unknown_a3.clear(1); } else { for (size_t z = 0; z < out_cmd.unknown_a3.size(); z++) { - out_cmd.unknown_a3[z] = random_object() % s->secret_lottery_results.size(); + out_cmd.unknown_a3[z] = l->random_crypt->next() % s->secret_lottery_results.size(); } } send_command_t(c, 0x24, (slt_index >= 0) ? 0 : 1, out_cmd); @@ -2849,7 +2849,7 @@ static void on_quest_F95E_result_bb(shared_ptr c, uint8_t, uint8_t, void if (results.empty()) { throw runtime_error("invalid result type"); } - ItemData item = (results.size() == 1) ? results[0] : results[random_object() % results.size()]; + ItemData item = (results.size() == 1) ? results[0] : results[l->random_crypt->next() % results.size()]; if (item.data1[0] == 0x04) { // Meseta // TODO: What is the right amount of Meseta to use here? Presumably it // should be random within a certain range, but it's not obvious what @@ -2926,10 +2926,10 @@ static void on_quest_F960_result_bb(shared_ptr c, uint8_t, uint8_t, void size_t tier = cmd.result_tier - num_failures; const auto& results = s->quest_F960_success_results.at(tier); uint64_t probability = results.base_probability + num_failures * results.probability_upgrade; - if (random_object() <= probability) { + if (l->random_crypt->next() <= probability) { c->log.info("Tier %zu yielded a prize", tier); const auto& result_items = results.results.at(weekday); - item = result_items[random_object() % result_items.size()]; + item = result_items[l->random_crypt->next() % result_items.size()]; break; } else { c->log.info("Tier %zu did not yield a prize", tier); @@ -2938,7 +2938,7 @@ static void on_quest_F960_result_bb(shared_ptr c, uint8_t, uint8_t, void if (item.empty()) { c->log.info("Choosing result from failure tier"); const auto& result_items = s->quest_F960_failure_results.results.at(weekday); - item = result_items[random_object() % result_items.size()]; + item = result_items[l->random_crypt->next() % result_items.size()]; } if (item.empty()) { throw runtime_error("no item produced, even from failure tier"); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 5b124c24..bfb3f937 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -501,11 +501,12 @@ void send_pc_console_split_reconnect(shared_ptr c, uint32_t address, } void send_client_init_bb(shared_ptr c, uint32_t error_code) { + auto team = c->team(); S_ClientInit_BB_00E6 cmd; cmd.error_code = error_code; cmd.player_tag = 0x00010000; cmd.guild_card_number = c->license->serial_number; - cmd.team_id = static_cast(random_object()); + cmd.team_id = team ? team->team_id : 0; c->config.serialize_into(cmd.client_config); cmd.can_create_team = 1; cmd.episode_4_unlocked = 1; diff --git a/src/ServerShell.cc b/src/ServerShell.cc index b7d69887..225e4b28 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -811,7 +811,7 @@ Proxy session commands:\n\ auto s = ses->require_server_state(); ItemData item = s->item_name_index->parse_item_description(ses->version(), command_args); - item.id = random_object(); + item.id = random_object() | 0x80000000; if (command_name == "set-next-item") { ses->next_drop_item = item;