From e5227080b8b04b1c521c5604843aa481114f35b3 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 7 Jul 2022 23:46:50 -0700 Subject: [PATCH] make replays useful on BB --- README.md | 1 + src/FileContentsCache.cc | 79 +++++++++++++++++-------- src/FileContentsCache.hh | 80 +++++++++++++++++++++++--- src/License.cc | 19 ++++-- src/License.hh | 7 ++- src/Main.cc | 10 +++- src/Map.cc | 5 +- src/PSOProtocol.cc | 2 +- src/PSOProtocol.hh | 48 ++++++++++------ src/Player.cc | 51 ++++++++++++----- src/Player.hh | 5 +- src/ReceiveCommands.cc | 10 ++-- src/ReplaySession.cc | 121 +++++++++++++++++++++++++++++++++++++++ src/ReplaySession.hh | 2 + src/SendCommands.cc | 27 ++++++--- src/Server.cc | 2 + src/ServerState.cc | 1 + src/ServerState.hh | 1 + 18 files changed, 382 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index a82a7a22..1ac215ca 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Current known issues / missing features: - Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed. - Implement private and overflow lobbies. - Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.) +- Encapsulate BB server-side random state and make replays deterministic. ## Usage diff --git a/src/FileContentsCache.cc b/src/FileContentsCache.cc index 39180dfc..267d0c81 100644 --- a/src/FileContentsCache.cc +++ b/src/FileContentsCache.cc @@ -8,36 +8,67 @@ using namespace std; + +FileContentsCache::FileContentsCache(uint64_t ttl_usecs) : ttl_usecs(ttl_usecs) { } + FileContentsCache::File::File(const string& name, shared_ptr contents, uint64_t load_time) : name(name), contents(contents), load_time(load_time) { } -shared_ptr FileContentsCache::get(const std::string& name) { - return this->get(name, [name]() -> string { return load_file(name); }); -} - -shared_ptr FileContentsCache::get(const char* name) { - return this->get(string(name)); -} - -shared_ptr FileContentsCache::get(const std::string& name, - std::function generate) { - uint64_t t = now(); - try { - auto& entry = this->name_to_file.at(name); - if (t - entry.load_time < 300000000) { // not 5 minutes old? return it - return entry.contents; - } - } catch (const out_of_range& e) { } - - shared_ptr contents(new string(generate())); - this->name_to_file.erase(name); - this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name), +shared_ptr FileContentsCache::replace( + const string& name, string&& data, uint64_t t) { + if (t == 0) { + t = now(); + } + shared_ptr contents(new string(move(data))); + auto emplace_ret = this->name_to_file.emplace( + piecewise_construct, + forward_as_tuple(name), forward_as_tuple(name, contents, t)); - + if (!emplace_ret.second) { + emplace_ret.first->second.contents = contents; + emplace_ret.first->second.load_time = t; + } return contents; } -shared_ptr FileContentsCache::get(const char* name, - std::function generate) { +shared_ptr FileContentsCache::replace( + const string& name, const void* data, size_t size, uint64_t t) { + string s(reinterpret_cast(data), size); + return this->replace(name, move(s), t); +} + +FileContentsCache::GetResult FileContentsCache::get_or_load(const std::string& name) { + return this->get(name, load_file); +} + +FileContentsCache::GetResult FileContentsCache::get_or_load(const char* name) { + return this->get_or_load(string(name)); +} + +shared_ptr FileContentsCache::get_or_throw(const std::string& name) { + auto throw_fn = +[](const std::string&) -> string { + throw out_of_range("file missing from cache"); + }; + return this->get(name, throw_fn).data; +} + +shared_ptr FileContentsCache::get_or_throw(const char* name) { + return this->get_or_throw(string(name)); +} + +FileContentsCache::GetResult FileContentsCache::get(const std::string& name, + std::function generate) { + uint64_t t = now(); + try { + auto& entry = this->name_to_file.at(name); + if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) { + return {entry.contents, false}; + } + } catch (const out_of_range& e) { } + return {this->replace(name, generate(name)), true}; +} + +FileContentsCache::GetResult FileContentsCache::get(const char* name, + std::function generate) { return this->get(string(name), generate); } diff --git a/src/FileContentsCache.hh b/src/FileContentsCache.hh index 65245c18..d3ea0c72 100644 --- a/src/FileContentsCache.hh +++ b/src/FileContentsCache.hh @@ -5,6 +5,8 @@ #include #include +#include + using namespace std; @@ -26,21 +28,85 @@ private: }; public: - FileContentsCache() = default; + explicit FileContentsCache(uint64_t ttl_usecs); FileContentsCache(const FileContentsCache&) = delete; FileContentsCache(FileContentsCache&&) = delete; FileContentsCache& operator=(const FileContentsCache&) = delete; FileContentsCache& operator=(FileContentsCache&&) = delete; ~FileContentsCache() = default; - std::shared_ptr get(const std::string& name); - std::shared_ptr get(const char* name); + template + bool delete_key(NameT key) { + return this->name_to_file.erase(key); + } - std::shared_ptr get( - const std::string& name, std::function generate); - std::shared_ptr get( - const char* name, std::function generate); + std::shared_ptr replace( + const std::string& name, std::string&& data, uint64_t t = 0); + std::shared_ptr replace( + const std::string& name, const void* data, size_t size, uint64_t t = 0); + + struct GetResult { + std::shared_ptr data; + bool generate_called; + }; + + GetResult get_or_load(const std::string& name); + GetResult get_or_load(const char* name); + std::shared_ptr get_or_throw(const std::string& name); + std::shared_ptr get_or_throw(const char* name); + + GetResult get( + const std::string& name, std::function generate); + GetResult get( + const char* name, std::function generate); + + template + struct GetObjResult { + const T& obj; + std::shared_ptr data; + bool generate_called; + }; + + template + GetObjResult get_obj_or_load(NameT name) { + auto res = this->get_or_load(name); + if (res.data->size() != sizeof(T)) { + throw runtime_error("cached string size is incorrect"); + } + return {*reinterpret_cast(res.data->data()), res.data, res.generate_called}; + } + template + GetObjResult get_obj_or_throw(NameT name) { + auto res = this->get_or_throw(name); + if (res->size() != sizeof(T)) { + throw runtime_error("cached string size is incorrect"); + } + return {*reinterpret_cast(res.data->data()), res.data, res.generate_called}; + } + template + GetObjResult get_obj(NameT name, std::function generate) { + uint64_t t = now(); + try { + auto& entry = this->name_to_file.at(name); + if (entry.contents->size() != sizeof(T)) { + throw runtime_error("cached string size is incorrect"); + } + if (this->ttl_usecs && (t - entry.load_time < this->ttl_usecs)) { + return {*reinterpret_cast(entry.contents->data()), entry.contents, false}; + } + } catch (const out_of_range& e) { } + T value = generate(name); + auto ret = this->replace_obj(name, value); + ret.generate_called = true; + return ret; + } + template + GetObjResult replace_obj(NameT name, const T& value) { + auto cached_value = this->replace(name, &value, sizeof(value)); + return {*reinterpret_cast(cached_value->data()), cached_value, false}; + } private: std::unordered_map name_to_file; + uint64_t ttl_usecs; }; diff --git a/src/License.cc b/src/License.cc index b210ce68..afba5a8e 100644 --- a/src/License.cc +++ b/src/License.cc @@ -41,7 +41,8 @@ string License::str() const { -LicenseManager::LicenseManager(const string& filename) : filename(filename) { +LicenseManager::LicenseManager(const string& filename) + : filename(filename), autosave(true) { try { auto licenses = load_vector_file(this->filename); for (const auto& read_license : licenses) { @@ -73,6 +74,10 @@ void LicenseManager::save() const { } } +void LicenseManager::set_autosave(bool autosave) { + this->autosave = autosave; +} + shared_ptr LicenseManager::verify_pc(uint32_t serial_number, const string& access_key) const { auto& license = this->serial_number_to_license.at(serial_number); @@ -132,7 +137,9 @@ size_t LicenseManager::count() const { void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) { this->serial_number_to_license.at(serial_number)->ban_end_time = end_time; - this->save(); + if (this->autosave) { + this->save(); + } } void LicenseManager::add(shared_ptr l) { @@ -141,7 +148,9 @@ void LicenseManager::add(shared_ptr l) { if (!l->username.empty()) { this->bb_username_to_license.emplace(l->username, l); } - this->save(); + if (this->autosave) { + this->save(); + } } void LicenseManager::remove(uint32_t serial_number) { @@ -150,7 +159,9 @@ void LicenseManager::remove(uint32_t serial_number) { if (!l->username.empty()) { this->bb_username_to_license.erase(l->username); } - this->save(); + if (this->autosave) { + this->save(); + } } vector LicenseManager::snapshot() const { diff --git a/src/License.hh b/src/License.hh index ddc14516..b7b8b08b 100644 --- a/src/License.hh +++ b/src/License.hh @@ -49,6 +49,9 @@ public: LicenseManager(const std::string& filename); ~LicenseManager() = default; + void save() const; + void set_autosave(bool autosave); + std::shared_ptr verify_pc(uint32_t serial_number, const std::string& access_key) const; std::shared_ptr verify_gc(uint32_t serial_number, @@ -75,9 +78,9 @@ public: const std::string& password, bool temporary); protected: - void save() const; - std::string filename; + bool autosave; + std::unordered_map> bb_username_to_license; std::unordered_map> serial_number_to_license; }; diff --git a/src/Main.cc b/src/Main.cc index a6945b86..7f6327ec 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -11,7 +11,6 @@ #include #include "DNSServer.hh" -#include "FileContentsCache.hh" #include "IPStackSimulator.hh" #include "Loggers.hh" #include "NetworkAddresses.hh" @@ -27,7 +26,6 @@ using namespace std; -FileContentsCache file_cache; bool use_terminal_colors = false; @@ -372,8 +370,14 @@ int main(int argc, char** argv) { config_log.info("Creating menus"); state->create_menus(config_json); + if (replay_log_filename) { + state->allow_saving = false; + state->license_manager->set_autosave(false); + config_log.info("Saving disabled because this is a replay session"); + } + shared_ptr dns_server; - if (state->dns_server_port) { + if (state->dns_server_port && !replay_log_filename) { config_log.info("Starting DNS server"); dns_server.reset(new DNSServer(base, state->local_address, state->external_address)); diff --git a/src/Map.cc b/src/Map.cc index 673627cc..58963aae 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -8,8 +8,6 @@ using namespace std; -extern FileContentsCache file_cache; - static void load_battle_param_file(const string& filename, BattleParams* entries) { @@ -437,7 +435,8 @@ static vector parse_map(uint8_t episode, uint8_t difficulty, vector load_map(const std::string& filename, uint8_t episode, uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) { - shared_ptr data = file_cache.get(filename); + static FileContentsCache map_file_cache(300 * 1000 * 1000); + shared_ptr data = map_file_cache.get_or_load(filename).data; const EnemyEntry* entries = reinterpret_cast(data->data()); size_t entry_count = data->size() / sizeof(EnemyEntry); return parse_map(episode, difficulty, battle_params, entries, entry_count, diff --git a/src/PSOProtocol.cc b/src/PSOProtocol.cc index 0833df8a..890428be 100644 --- a/src/PSOProtocol.cc +++ b/src/PSOProtocol.cc @@ -186,7 +186,7 @@ std::string prepend_command_header( case GameVersion::BB: { PSOCommandHeaderBB header; if (encryption_enabled) { - header.size = (sizeof(header) + data.size() + 7) & ~7; + header.size = (sizeof(header) + data.size() + 3) & ~3; } else { header.size = (sizeof(header) + data.size()); } diff --git a/src/PSOProtocol.hh b/src/PSOProtocol.hh index ff639bc0..ed7bbf7e 100644 --- a/src/PSOProtocol.hh +++ b/src/PSOProtocol.hh @@ -55,38 +55,54 @@ union PSOSubcommand { // This function is used in a lot of places to check received command sizes and // cast them to the appropriate type template +const T& check_size_t( + const void* data, + size_t size, + size_t min_size = sizeof(T), + size_t max_size = sizeof(T)) { + if (size < min_size) { + throw std::runtime_error(string_printf( + "command too small (expected at least 0x%zX bytes, received 0x%zX bytes)", + min_size, size)); + } + if (size > max_size) { + throw std::runtime_error(string_printf( + "command too large (expected at most 0x%zX bytes, received 0x%zX bytes)", + max_size, size)); + } + return *reinterpret_cast(data); +} +template const T& check_size_t( const std::string& data, size_t min_size = sizeof(T), size_t max_size = sizeof(T)) { - if (data.size() < min_size) { + return check_size_t(data.data(), data.size(), min_size, max_size); +} +template +T& check_size_t( + void* data, + size_t size, + size_t min_size = sizeof(T), + size_t max_size = sizeof(T)) { + if (size < min_size) { throw std::runtime_error(string_printf( "command too small (expected at least 0x%zX bytes, received 0x%zX bytes)", - min_size, data.size())); + min_size, size)); } - if (data.size() > max_size) { + if (size > max_size) { throw std::runtime_error(string_printf( "command too large (expected at most 0x%zX bytes, received 0x%zX bytes)", - max_size, data.size())); + max_size, size)); } - return *reinterpret_cast(data.data()); + return *reinterpret_cast(data); } template T& check_size_t( std::string& data, size_t min_size = sizeof(T), size_t max_size = sizeof(T)) { - if (data.size() < min_size) { - throw std::runtime_error(string_printf( - "command too small (expected at least 0x%zX bytes, received 0x%zX bytes)", - min_size, data.size())); - } - if (data.size() > max_size) { - throw std::runtime_error(string_printf( - "command too large (expected at most 0x%zX bytes, received 0x%zX bytes)", - max_size, data.size())); - } - return *reinterpret_cast(data.data()); + return check_size_t(data.data(), data.size(), min_size, max_size); } void check_size_v(size_t size, size_t min_size, size_t max_size = 0); diff --git a/src/Player.cc b/src/Player.cc index 15cb7197..9d4bce48 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -7,10 +7,11 @@ #include #include +#include "FileContentsCache.hh" #include "Loggers.hh" +#include "StaticGameData.hh" #include "Text.hh" #include "Version.hh" -#include "StaticGameData.hh" using namespace std; @@ -26,6 +27,10 @@ static const string ACCOUNT_FILE_SIGNATURE = +static FileContentsCache player_files_cache(300 * 1000 * 1000); + + + PlayerStats::PlayerStats() noexcept : atp(0), mst(0), evp(0), hp(0), dfp(0), ata(0), lck(0) { } @@ -271,14 +276,17 @@ GuildCardBB::GuildCardBB() noexcept void PlayerBank::load(const string& filename) { - *this = load_object_file(filename); + *this = player_files_cache.get_obj_or_load(filename).obj; for (uint32_t x = 0; x < this->num_items; x++) { this->items[x].data.id = 0x0F010000 + x; } } -void PlayerBank::save(const string& filename) const { - save_file(filename, this, sizeof(*this)); +void PlayerBank::save(const string& filename, bool save_to_filesystem) const { + player_files_cache.replace(filename, this, sizeof(*this)); + if (save_to_filesystem) { + save_file(filename, this, sizeof(*this)); + } } @@ -381,35 +389,45 @@ void ClientGameData::load_account_data() { shared_ptr data; try { data.reset(new SavedAccountDataBB( - load_object_file(filename))); + player_files_cache.get_obj_or_load(filename).obj)); if (data->signature != ACCOUNT_FILE_SIGNATURE) { throw runtime_error("account data header is incorrect"); } + player_data_log.info("Loaded account data file %s", filename.c_str()); + } catch (const exception& e) { - player_data_log.info("No account data for %s; using default", - this->bb_username.c_str()); + player_data_log.info("Cannot load account data for %s (%s); using default", + this->bb_username.c_str(), e.what()); + player_files_cache.delete_key(filename); data.reset(new SavedAccountDataBB( - load_object_file("system/players/default.nsa"))); + player_files_cache.get_obj_or_load( + "system/players/default.nsa").obj)); if (data->signature != ACCOUNT_FILE_SIGNATURE) { throw runtime_error("default account data header is incorrect"); } + player_data_log.info("Loaded default account data file"); } this->account_data = data; - player_data_log.info("Loaded account data file %s", filename.c_str()); } void ClientGameData::save_account_data() const { string filename = this->account_data_filename(); - save_file(filename, this->account_data.get(), sizeof(SavedAccountDataBB)); - player_data_log.info("Saved account data file %s", filename.c_str()); + player_files_cache.replace(filename, this->account_data.get(), sizeof(SavedAccountDataBB)); + if (this->should_save) { + save_file(filename, this->account_data.get(), sizeof(SavedAccountDataBB)); + player_data_log.info("Saved account data file %s to filesystem", filename.c_str()); + } else { + player_data_log.info("Saved account data file %s to cache only", filename.c_str()); + } } void ClientGameData::load_player_data() { string filename = this->player_data_filename(); shared_ptr data(new SavedPlayerDataBB( - load_object_file(filename))); + player_files_cache.get_obj_or_load(filename).obj)); if (data->signature != PLAYER_FILE_SIGNATURE) { + player_files_cache.delete_key(filename); throw runtime_error("player data header is incorrect"); } this->player_data = data; @@ -418,8 +436,13 @@ void ClientGameData::load_player_data() { void ClientGameData::save_player_data() const { string filename = this->player_data_filename(); - save_file(filename, this->player_data.get(), sizeof(SavedPlayerDataBB)); - player_data_log.info("Saved player data file %s", filename.c_str()); + player_files_cache.replace(filename, this->player_data.get(), sizeof(SavedPlayerDataBB)); + if (this->should_save) { + save_file(filename, this->player_data.get(), sizeof(SavedPlayerDataBB)); + player_data_log.info("Saved player data file %s to filesystem", filename.c_str()); + } else { + player_data_log.info("Saved player data file %s to cache only", filename.c_str()); + } } void ClientGameData::import_player(const PSOPlayerDataPC& pc) { diff --git a/src/Player.hh b/src/Player.hh index 74e76e72..34448e6f 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -71,7 +71,7 @@ struct PlayerBank { PlayerBankItem items[200]; void load(const std::string& filename); - void save(const std::string& filename) const; + void save(const std::string& filename, bool save_to_filesystem) const; bool switch_with_file(const std::string& save_filename, const std::string& load_filename); @@ -407,8 +407,9 @@ public: size_t bb_player_index; PlayerInventoryItem identify_result; std::vector shop_contents; + bool should_save; - ClientGameData() : serial_number(0), bb_player_index(0) { } + ClientGameData() : serial_number(0), bb_player_index(0), should_save(true) { } ~ClientGameData(); std::shared_ptr account(bool should_load = true); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 960810db..5c29e962 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -25,10 +25,6 @@ using namespace std; -extern FileContentsCache file_cache; - - - vector quest_categories_menu({ MenuItem(static_cast(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), MenuItem(static_cast(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), @@ -339,7 +335,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, return; } else { shared_ptr l = LicenseManager::create_license_bb( - fnv1a32(cmd.username), cmd.username, cmd.password, true); + fnv1a32(cmd.username) & 0x7FFFFFFF, cmd.username, cmd.password, true); s->license_manager->add(l); c->set_license(l); } @@ -1346,7 +1342,9 @@ void process_gba_file_request(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // D7 string filename(data); strip_trailing_zeroes(filename); - auto contents = file_cache.get("system/gba/" + filename); + + static FileContentsCache gba_file_cache(300 * 1000 * 1000); + auto contents = gba_file_cache.get_or_load("system/gba/" + filename).data; send_quest_file(c, "", filename, *contents, QuestFileType::GBA_DEMO); } diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 945858ba..bf7f24d3 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -44,6 +44,117 @@ shared_ptr ReplaySession::create_event( return event; } +void ReplaySession::apply_default_mask(shared_ptr ev) { + auto version = this->clients.at(ev->client_id)->version; + + void* cmd_data = ev->mask.data() + ((version == GameVersion::BB) ? 8 : 4); + size_t cmd_size = ev->mask.size() - ((version == GameVersion::BB) ? 8 : 4); + + switch (version) { + case GameVersion::PATCH: { + const auto& header = check_size_t( + ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF); + if (header.command == 0x02) { + auto& cmd_mask = check_size_t(cmd_data, cmd_size); + cmd_mask.server_key = 0; + cmd_mask.client_key = 0; + } + break; + } + case GameVersion::PC: + case GameVersion::GC: { + uint8_t command; + if (version == GameVersion::PC) { + command = check_size_t( + ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF).command; + } else { + command = check_size_t( + ev->data, sizeof(PSOCommandHeaderDCGC), 0xFFFF).command; + } + switch (command) { + case 0x02: + case 0x17: + case 0x91: + case 0x9B: { + auto& cmd_mask = check_size_t( + cmd_data, cmd_size, sizeof(S_ServerInit_DC_PC_GC_02_17_91_9B), 0xFFFF); + cmd_mask.server_key = 0; + cmd_mask.client_key = 0; + break; + } + case 0x0019: { + auto& cmd_mask = check_size_t(cmd_data, cmd_size); + cmd_mask.address = 0; + break; + } + case 0x64: { + if (version == GameVersion::PC) { + auto& cmd_mask = check_size_t(cmd_data, cmd_size, + offsetof(S_JoinGame_GC_64, players_ep3)); + cmd_mask.variations.clear(0); + cmd_mask.rare_seed = 0; + } else { // GC + auto& cmd_mask = check_size_t(cmd_data, cmd_size, + offsetof(S_JoinGame_GC_64, players_ep3)); + cmd_mask.variations.clear(0); + cmd_mask.rare_seed = 0; + } + break; + } + case 0xB1: { + for (size_t x = 8; x < ev->mask.size(); x++) { + ev->mask[x] = 0; + } + break; + } + } + break; + } + case GameVersion::BB: { + uint16_t command = check_size_t( + ev->data, sizeof(PSOCommandHeaderBB), 0xFFFF).command; + switch (command) { + case 0x0003: { + auto& cmd_mask = check_size_t( + cmd_data, cmd_size, sizeof(S_ServerInit_BB_03_9B), 0xFFFF); + cmd_mask.server_key.clear(0); + cmd_mask.client_key.clear(0); + break; + } + case 0x0019: { + auto& cmd_mask = check_size_t(cmd_data, cmd_size); + cmd_mask.address = 0; + break; + } + case 0x0064: { + auto& cmd_mask = check_size_t(cmd_data, cmd_size, + offsetof(S_JoinGame_BB_64, players_ep3), + offsetof(S_JoinGame_BB_64, players_ep3)); + cmd_mask.variations.clear(0); + cmd_mask.rare_seed = 0; + break; + } + case 0x00B1: { + for (size_t x = 8; x < ev->mask.size(); x++) { + ev->mask[x] = 0; + } + break; + } + case 0x00E6: { + auto& cmd_mask = check_size_t(cmd_data, cmd_size); + cmd_mask.team_id = 0; + break; + } + } + break; + } + case GameVersion::DC: + throw logic_error("DC auto-masking is not implemented"); + default: + throw logic_error("invalid game version"); + } +} + ReplaySession::ReplaySession( shared_ptr base, FILE* input_log, @@ -79,6 +190,9 @@ ReplaySession::ReplaySession( parsing_command->mask += mask_bytes; continue; } else { + if (parsing_command->type == Event::Type::RECEIVE) { + this->apply_default_mask(parsing_command); + } parsing_command = nullptr; } } @@ -304,6 +418,13 @@ void ReplaySession::on_command_received( switch (c->version) { case GameVersion::DC: throw runtime_error("DC encryption is not supported during replays"); + case GameVersion::PATCH: + if (command == 0x02) { + auto& cmd = check_size_t(data); + c->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); + c->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); + } + break; case GameVersion::PC: case GameVersion::GC: if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { diff --git a/src/ReplaySession.hh b/src/ReplaySession.hh index 845f2665..8824d8a9 100644 --- a/src/ReplaySession.hh +++ b/src/ReplaySession.hh @@ -78,6 +78,8 @@ private: Event::Type type, std::shared_ptr c); void update_timeout_event(); + void apply_default_mask(std::shared_ptr ev); + static void dispatch_on_timeout(evutil_socket_t fd, short events, void* ctx); static void dispatch_on_command_received( Channel& ch, uint16_t command, uint32_t flag, std::string& data); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a184a2da..c22e7d00 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -344,6 +344,7 @@ static const vector stream_file_entries = { "BattleParamEntry_ep4_on.dat", "PlyLevelTbl.prs", }; +static FileContentsCache bb_stream_files_cache(3600 * 1000 * 1000); void send_stream_file_index_bb(shared_ptr c) { @@ -357,11 +358,22 @@ void send_stream_file_index_bb(shared_ptr c) { vector entries; size_t offset = 0; for (const string& filename : stream_file_entries) { - auto file_data = file_cache.get("system/blueburst/" + filename); + string key = "system/blueburst/" + filename; + auto cache_res = bb_stream_files_cache.get_or_load(key); auto& e = entries.emplace_back(); - e.size = file_data->size(); - // TODO: memoize the checksum somewhere; computing it can be slow - e.checksum = crc32(file_data->data(), e.size); + e.size = cache_res.data->size(); + // Computing the checksum can be slow, so we cache it along with the file + // data. If the cache result was just populated, then it may be different, + // so we always recompute the checksum in that case. + if (cache_res.generate_called) { + e.checksum = crc32(cache_res.data->data(), e.size); + bb_stream_files_cache.replace_obj(key + ".crc32", e.checksum); + } else { + auto compute_checksum = [&](const string&) -> uint32_t { + return crc32(cache_res.data->data(), e.size); + }; + e.checksum = bb_stream_files_cache.get_obj(key + ".crc32", compute_checksum).obj; + } e.offset = offset; e.filename = filename; offset += e.size; @@ -370,19 +382,20 @@ void send_stream_file_index_bb(shared_ptr c) { } void send_stream_file_chunk_bb(shared_ptr c, uint32_t chunk_index) { - auto contents = file_cache.get("", +[]() -> string { + auto cache_result = bb_stream_files_cache.get("", +[](const string&) -> string { size_t bytes = 0; for (const auto& name : stream_file_entries) { - bytes += file_cache.get("system/blueburst/" + name)->size(); + bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).data->size(); } string ret; ret.reserve(bytes); for (const auto& name : stream_file_entries) { - ret += *file_cache.get("system/blueburst/" + name); + ret += *bb_stream_files_cache.get_or_load("system/blueburst/" + name).data; } return ret; }); + auto contents = cache_result.data; S_StreamFileChunk_BB_02EB chunk_cmd; chunk_cmd.chunk_index = chunk_index; diff --git a/src/Server.cc b/src/Server.cc index 5a62826d..85571fb8 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -80,6 +80,7 @@ void Server::on_listen_accept(struct evconnlistener* listener, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); shared_ptr c(new Client( bev, listening_socket->version, listening_socket->behavior)); + c->game_data.should_save = this->state->allow_saving; c->channel.on_command_received = Server::on_client_input; c->channel.on_error = Server::on_client_error; c->channel.context_obj = this; @@ -100,6 +101,7 @@ void Server::connect_client( struct bufferevent* bev, uint32_t address, uint16_t client_port, uint16_t server_port, GameVersion version, ServerBehavior initial_state) { shared_ptr c(new Client(bev, version, initial_state)); + c->game_data.should_save = this->state->allow_saving; c->channel.on_command_received = Server::on_client_input; c->channel.on_error = Server::on_client_error; c->channel.context_obj = this; diff --git a/src/ServerState.cc b/src/ServerState.cc index 4d8eb49c..57313635 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -19,6 +19,7 @@ ServerState::ServerState() : dns_server_port(0), ip_stack_debug(false), allow_unregistered_users(false), + allow_saving(true), item_tracking_enabled(true), run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1), pre_lobby_event(0), diff --git a/src/ServerState.hh b/src/ServerState.hh index 1d8f68e2..c6e216f1 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -46,6 +46,7 @@ struct ServerState { std::vector ip_stack_addresses; bool ip_stack_debug; bool allow_unregistered_users; + bool allow_saving; bool item_tracking_enabled; RunShellBehavior run_shell_behavior; std::vector> bb_private_keys;