From 072ebe81bf277976c1838813c5ecb9da5b5f2d30 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 6 Dec 2023 23:54:53 -0800 Subject: [PATCH] add $savechar and $loadchar commands --- README.md | 8 +++- src/ChatCommands.cc | 90 ++++++++++++++++++++++++++++-------------- src/Client.cc | 1 - src/Client.hh | 8 +++- src/Player.cc | 21 ++++++++++ src/Player.hh | 3 ++ src/ReceiveCommands.cc | 27 ++++++++----- 7 files changed, 114 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 1373a3f1..6e0a50c4 100644 --- a/README.md +++ b/README.md @@ -274,9 +274,13 @@ Some commands only work on the game server and not on the proxy server. The chat * `$exit`: If you're in a lobby, sends you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress. * `$patch `: Run a patch on your client. `` must exactly match the name of a patch on the server. -* Blue Burst player commands (game server only) - * `$bbchar <1-4>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot. Any character already in that slot is overwritten. Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data. +* Character data commands + * `$savechar `: Saves your current character data on the server in the specified slot (each serial number has 4 slots, numbered 1-4). These slots are separate from BB character slots; using this command does not affect BB characters. + * `$loadchar ` (v1 and v2 only): Loads your character data from the specified slot. + * `$bbchar `: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot (1-4). Any character already in that slot is overwritten. (This command is similar to `$savechar`, except it overwrites a BB character slot, and can transfer characters across accounts.) Note that the character's chat data, quick menu config, and bank contents are not copied, since there is no way for the server to request those types of data. * `$edit `: Modifies your character data. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing. + +* Blue Burst player commands (game server only) * `$bank [number]`: Switches your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switches back to your current character's bank. * `$save`: Saves your character, system, and Guild Card data immediately. (By default, your character is saved every 60 seconds while online, and your account and Guild Card data are saved whenever they change.) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 6c99e123..5747da78 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -52,12 +52,6 @@ static void check_version(shared_ptr c, Version version) { } } -static void check_not_version(shared_ptr c, Version version) { - if (c->version() == version) { - throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO."); - } -} - static void check_is_game(shared_ptr l, bool is_game) { if (l->is_game() != is_game) { throw precondition_failed(is_game ? "$C6This command cannot\nbe used in lobbies." : "$C6This command cannot\nbe used in games."); @@ -1024,38 +1018,74 @@ static void server_command_change_bank(shared_ptr c, const std::string& send_text_message_printf(c, "%" PRIu32 " items\n%" PRIu32 " Meseta", bank.num_items.load(), bank.meseta.load()); } -// TODO: This can be implemented on the proxy server too. -static void server_command_convert_char_to_bb(shared_ptr c, const std::string& args) { +static void server_command_bbchar_savechar(shared_ptr c, const std::string& args, bool is_bb_conversion) { auto s = c->require_server_state(); auto l = c->require_lobby(); check_is_game(l, false); - check_not_version(c, Version::BB_V4); - vector tokens = split(args, ' '); - if (tokens.size() != 3) { - send_text_message(c, "$C6Incorrect argument count"); - return; - } - - // username/password are tokens[0] and [1] - c->pending_bb_save_character_index = stoul(tokens[2]) - 1; - if (c->pending_bb_save_character_index > 3) { - send_text_message(c, "$C6Player index must be 1-4"); - return; - } - - try { - c->pending_bb_save_license = s->license_index->verify_bb(tokens[0].c_str(), tokens[1].c_str()); - } catch (const exception& e) { - send_text_message_printf(c, "$C6Login failed: %s", e.what()); - return; + auto pending_export = make_unique(); + pending_export->is_bb_conversion = is_bb_conversion; + + if (is_bb_conversion) { + vector tokens = split(args, ' '); + if (tokens.size() != 3) { + send_text_message(c, "$C6Incorrect argument count"); + return; + } + + // username/password are tokens[0] and [1] + pending_export->character_index = stoll(tokens[2]) - 1; + if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) { + send_text_message(c, "$C6Player index must\nbe in range 1-4"); + return; + } + + try { + c->pending_character_export->license = s->license_index->verify_bb(tokens[0].c_str(), tokens[1].c_str()); + } catch (const exception& e) { + send_text_message_printf(c, "$C6Login failed: %s", e.what()); + return; + } + + } else { + pending_export->character_index = stoll(args) - 1; + if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) { + send_text_message(c, "$C6Player index must\nbe in range 1-4"); + return; + } + pending_export->license = c->license; } + c->pending_character_export = std::move(pending_export); // Request the player data. The client will respond with a 61, and the handler // for that command will execute the conversion send_get_player_info(c); } +static void server_command_bbchar(shared_ptr c, const std::string& args) { + server_command_bbchar_savechar(c, args, true); +} + +static void server_command_savechar(shared_ptr c, const std::string& args) { + server_command_bbchar_savechar(c, args, false); +} + +static void server_command_loadchar(shared_ptr c, const std::string& args) { + if (!is_v1_or_v2(c->version())) { + send_text_message(c, "$C7This command can only\nbe used on v1 or v2"); + return; + } + auto l = c->require_lobby(); + check_is_game(l, false); + + size_t index = stoull(args, nullptr, 0); + c->game_data.load_backup_character(c->license->serial_number, index); + + auto s = c->require_server_state(); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); +} + static void server_command_save(shared_ptr c, const std::string&) { check_version(c, Version::BB_V4); try { @@ -1702,12 +1732,13 @@ static const unordered_map chat_commands({ {"$ax", {server_command_ax, nullptr}}, {"$ban", {server_command_ban, nullptr}}, {"$bank", {server_command_change_bank, nullptr}}, - {"$bbchar", {server_command_convert_char_to_bb, nullptr}}, + {"$bbchar", {server_command_bbchar, nullptr}}, {"$cheat", {server_command_cheat, nullptr}}, {"$debug", {server_command_debug, nullptr}}, {"$defrange", {server_command_ep3_set_def_dice_range, nullptr}}, {"$drop", {server_command_drop, nullptr}}, {"$edit", {server_command_edit, nullptr}}, + {"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}}, {"$event", {server_command_lobby_event, proxy_command_lobby_event}}, {"$exit", {server_command_exit, proxy_command_exit}}, {"$gc", {server_command_get_self_card, proxy_command_get_player_card}}, @@ -1720,7 +1751,7 @@ static const unordered_map chat_commands({ {"$kick", {server_command_kick, nullptr}}, {"$li", {server_command_lobby_info, proxy_command_lobby_info}}, {"$ln", {server_command_lobby_type, proxy_command_lobby_type}}, - {"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}}, + {"$loadchar", {server_command_loadchar, nullptr}}, {"$matcount", {server_command_show_material_counts, nullptr}}, {"$maxlevel", {server_command_max_level, nullptr}}, {"$meseta", {server_command_meseta, nullptr}}, @@ -1739,6 +1770,7 @@ static const unordered_map chat_commands({ {"$quest", {server_command_quest, nullptr}}, {"$rand", {server_command_rand, proxy_command_rand}}, {"$save", {server_command_save, nullptr}}, + {"$savechar", {server_command_savechar, nullptr}}, {"$saverec", {server_command_saverec, nullptr}}, {"$sc", {server_command_send_client, proxy_command_send_client}}, {"$secid", {server_command_secid, proxy_command_secid}}, diff --git a/src/Client.cc b/src/Client.cc index cc53ef77..d42423cf 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -174,7 +174,6 @@ Client::Client( card_battle_table_seat_state(0), next_exp_value(0), can_chat(true), - pending_bb_save_character_index(0), dol_base_addr(0) { this->config.set_flags_for_version(version, -1); diff --git a/src/Client.hh b/src/Client.hh index 595cd2f0..cbf77932 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -202,8 +202,12 @@ struct Client : public std::enable_shared_from_this { uint32_t next_exp_value; // next EXP value to give G_SwitchStateChanged_6x05 last_switch_enabled_command; bool can_chat; - std::shared_ptr pending_bb_save_license; - uint8_t pending_bb_save_character_index; + struct PendingCharacterExport { + std::shared_ptr license; + ssize_t character_index = -1; + bool is_bb_conversion = false; + }; + std::unique_ptr pending_character_export; std::deque> function_call_response_queue; // File loading state diff --git a/src/Player.cc b/src/Player.cc index b8f00644..8aa0a307 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -312,6 +312,10 @@ string ClientGameData::character_filename(const std::string& bb_username, int8_t return string_printf("system/players/player_%s_%hhd.psochar", bb_username.c_str(), index); } +string ClientGameData::backup_character_filename(uint32_t serial_number, size_t index) { + return string_printf("system/players/backup_player_%" PRIu32 "_%zu.psochar", serial_number, index); +} + string ClientGameData::character_filename(int8_t index) const { if (this->bb_username.empty()) { throw logic_error("non-BB players do not have character data"); @@ -613,6 +617,23 @@ void ClientGameData::save_guild_card_file() const { player_data_log.info("Saved Guild Card file %s", filename.c_str()); } +void ClientGameData::load_backup_character(uint32_t serial_number, size_t index) { + string filename = this->backup_character_filename(serial_number, index); + auto f = fopen_unique(filename, "rb"); + auto header = freadx(f.get()); + if (header.size != 0x399C) { + throw runtime_error("incorrect size in character file header"); + } + if (header.command != 0x00E7) { + throw runtime_error("incorrect command in character file header"); + } + if (header.flag != 0x00000000) { + throw runtime_error("incorrect flag in character file header"); + } + this->character_data = make_shared(freadx(f.get())); + this->last_reported_disp_v1_v2.reset(); +} + PlayerBank& ClientGameData::current_bank() { if (this->external_bank) { return *this->external_bank; diff --git a/src/Player.hh b/src/Player.hh index 7528aeff..438280d1 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -114,6 +114,7 @@ public: std::string system_filename() const; static std::string character_filename(const std::string& bb_username, int8_t index); + static std::string backup_character_filename(uint32_t serial_number, size_t index); std::string character_filename(int8_t index = -1) const; std::string guild_card_filename() const; std::string shared_bank_filename() const; @@ -131,6 +132,8 @@ public: void save_character_file(); void save_guild_card_file() const; + void load_backup_character(uint32_t serial_number, size_t index); + PlayerBank& current_bank(); std::shared_ptr current_bank_character(); bool use_shared_bank(); // Returns true if the bank exists; false if it was created diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 8081236e..50d7db4c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2981,17 +2981,27 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri s->remove_client_from_lobby(c); } else if (command == 0x61) { - if (c->pending_bb_save_license) { - shared_ptr dest_license = c->pending_bb_save_license; - c->pending_bb_save_license.reset(); + if (c->pending_character_export) { + unique_ptr pending_export = std::move(c->pending_character_export); + c->pending_character_export.reset(); + + string filename; + if (pending_export->is_bb_conversion) { + filename = ClientGameData::character_filename( + pending_export->license->bb_username, + pending_export->character_index); + } else { + filename = ClientGameData::backup_character_filename( + pending_export->license->serial_number, + pending_export->character_index); + } - string filename = ClientGameData::character_filename(dest_license->bb_username, c->pending_bb_save_character_index); if (s->player_files_manager->get_character(filename)) { send_text_message(c, "$C6The target player\nis currently loaded.\nSign off in Blue\nBurst and try again."); } else { auto bb_player = PSOBBCharacterFile::create_from_config( - dest_license->serial_number, + pending_export->license->serial_number, c->language(), player->disp.visual, player->disp.name.decode(c->language()), @@ -3016,12 +3026,9 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri bb_player->choice_search_config = player->choice_search_config; try { ClientGameData::save_character_file(filename, c->game_data.system(), bb_player); - send_text_message_printf(c, - "$C6BB player data saved\nas player %hhu for user\n%s", - static_cast(c->pending_bb_save_character_index + 1), - dest_license->bb_username.c_str()); + send_text_message(c, "$C6Character data saved"); } catch (const exception& e) { - send_text_message_printf(c, "$C6PSOBB player data could\nnot be saved:\n%s", e.what()); + send_text_message_printf(c, "$C6Character data could\nnot be saved:\n%s", e.what()); } } }