diff --git a/README.md b/README.md index a4f58af6..7367e565 100644 --- a/README.md +++ b/README.md @@ -602,7 +602,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are * `$savechar `: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details. * `$loadchar `: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details. * `$bbchar `: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details. - * `$checkchar `: Tells you basic information about a server-side character previously saved using `$savechar`. + * `$checkchar [slot]`: Tells you basic information about a server-side character previously saved using `$savechar`. If `slot` is not given, tells you which slots are used and which are free. * `$deletechar `: Deletes a server-side character previously saved using `$savechar`. * `$edit `: Modify your character data. See the [using $edit](#using-edit) section for details. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 64f43bd7..13d997ec 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -24,6 +24,41 @@ using namespace std; +//////////////////////////////////////////////////////////////////////////////// +// Tools + +string str_for_flag_ranges(const vector& flags) { + string ret; + auto add_result = [&](size_t start, size_t end) { + if (!ret.empty()) { + ret.push_back(','); + } + if (start == end) { + ret += std::format("{}", start); + } else if (start == end - 1) { + ret += std::format("{},{}", start, end); + } else { + ret += std::format("{}-{}", start, end); + } + }; + + size_t range_start = 0; + bool in_range = false; + for (size_t z = 0; z < flags.size(); z++) { + if (flags[z] && !in_range) { + in_range = true; + range_start = z; + } else if (!flags[z] && in_range) { + in_range = false; + add_result(range_start, z - 1); + } + } + if (in_range) { + add_result(range_start, flags.size() - 1); + } + return ret; +} + //////////////////////////////////////////////////////////////////////////////// // Checks @@ -421,7 +456,7 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool shared_ptr dest_account; shared_ptr dest_bb_license; - ssize_t dest_character_index = 0; + size_t dest_character_index = 0; if (is_bb_conversion) { vector tokens = phosg::split(a.text, ' '); if (tokens.size() != 3) { @@ -430,8 +465,8 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool // username/password are tokens[0] and [1] dest_character_index = stoll(tokens[2]) - 1; - if ((dest_character_index > 3) || (dest_character_index < 0)) { - throw precondition_failed("$C6Player index must\nbe in range 1-4"); + if ((dest_character_index >= 127) || (dest_character_index < 0)) { + throw precondition_failed("$C6Player index must\nbe in range 1-127"); } try { @@ -444,8 +479,8 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool } else { dest_character_index = stoll(a.text) - 1; - if ((dest_character_index > 15) || (dest_character_index < 0)) { - throw precondition_failed("$C6Player index must\nbe in range 1-16"); + if ((dest_character_index >= s->num_backup_character_slots) || (dest_character_index < 0)) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); } dest_account = a.c->login->account; } @@ -574,30 +609,49 @@ ChatCommandDefinition cc_checkchar( throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount"); } - size_t index = stoull(a.text, nullptr, 0) - 1; - if (index > 15) { - throw precondition_failed("$C6Player index must\nbe in range 1-16"); - } + auto s = a.c->require_server_state(); - try { - if (is_ep3(a.c->version())) { - string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true); - auto ch = phosg::load_object_file(filename); - send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}", - index + 1, ch.disp.visual.name.decode(), - name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class), - (ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100, - (ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100); - } else { - string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false); - auto ch = PSOCHARFile::load_shared(filename, false).character_file; - send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}", - index + 1, ch->disp.name.decode(), - name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class), - ch->disp.stats.level + 1); + if (a.text.empty()) { + bool is_ep3 = ::is_ep3(a.c->version()); + + vector flags; + flags.emplace_back(false); + for (size_t z = 0; z < s->num_backup_character_slots; z++) { + string filename = a.c->backup_character_filename(a.c->login->account->account_id, z, is_ep3); + flags.emplace_back(std::filesystem::is_regular_file(filename)); + } + string used_str = str_for_flag_ranges(flags); + flags.flip(); + flags[0] = false; + string free_str = str_for_flag_ranges(flags); + send_text_message_fmt(a.c, "Used: {}\nFree: {}", used_str, free_str); + + } else { + size_t index = stoull(a.text, nullptr, 0) - 1; + if (index >= s->num_backup_character_slots) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); + } + + try { + if (is_ep3(a.c->version())) { + string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, true); + auto ch = phosg::load_object_file(filename); + send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nCLv: on {}.{}, off {}.{}", + index + 1, ch.disp.visual.name.decode(), + name_for_section_id(ch.disp.visual.section_id), name_for_char_class(ch.disp.visual.char_class), + (ch.ep3_config.online_clv_exp / 100) + 1, ch.ep3_config.online_clv_exp % 100, + (ch.ep3_config.offline_clv_exp / 100) + 1, ch.ep3_config.offline_clv_exp % 100); + } else { + string filename = a.c->backup_character_filename(a.c->login->account->account_id, index, false); + auto ch = PSOCHARFile::load_shared(filename, false).character_file; + send_text_message_fmt(a.c, "Slot {}: $C6{}$C7\n{} {}\nLevel {}", + index + 1, ch->disp.name.decode(), + name_for_section_id(ch->disp.visual.section_id), name_for_char_class(ch->disp.visual.char_class), + ch->disp.stats.level + 1); + } + } catch (const phosg::cannot_open_file&) { + send_text_message_fmt(a.c, "No character in\nslot {}", index + 1); } - } catch (const phosg::cannot_open_file&) { - send_text_message_fmt(a.c, "No character in\nslot {}", index + 1); } co_return; @@ -1508,11 +1562,12 @@ ChatCommandDefinition cc_loadchar( throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount"); } + auto s = a.c->require_server_state(); auto l = a.c->require_lobby(); size_t index = stoull(a.text, nullptr, 0) - 1; - if (index > 15) { - throw precondition_failed("$C6Player index must\nbe in range 1-16"); + if (index >= s->num_backup_character_slots) { + throw precondition_failed("$C6Player index must\nbe in range 1-{}", s->num_backup_character_slots); } shared_ptr ep3_char; @@ -1524,7 +1579,6 @@ ChatCommandDefinition cc_loadchar( if (a.c->version() == Version::BB_V4) { // On BB, it suffices to simply send the character file again - auto s = a.c->require_server_state(); send_complete_player_bb(a.c); send_player_leave_notification(l, a.c->lobby_client_id); s->send_lobby_join_notifications(l, a.c); diff --git a/src/ServerState.cc b/src/ServerState.cc index 084ed92f..e79c3865 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1095,6 +1095,7 @@ void ServerState::load_config_early() { } this->enable_chat_commands = this->config_json->get_bool("EnableChatCommands", true); + this->num_backup_character_slots = this->config_json->get_int("BackupCharacterSlots", 16); this->version_name_colors.reset(); this->client_customization_name_color = 0; diff --git a/src/ServerState.hh b/src/ServerState.hh index c66bd440..f193c51c 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -115,6 +115,7 @@ struct ServerState : public std::enable_shared_from_this { bool use_temp_accounts_for_prototypes = true; std::array compatibility_groups = {}; bool enable_chat_commands = true; + size_t num_backup_character_slots = 16; std::unique_ptr> version_name_colors; uint32_t client_customization_name_color = 0x00000000; uint8_t allowed_drop_modes_v1_v2_normal = 0x1F; diff --git a/system/config.example.json b/system/config.example.json index c2ac8b5b..dcc2e4f9 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -279,6 +279,11 @@ // use chat commands. "EnableChatCommands": true, + // Number of backup character slots for each account, accessible with the + // $savechar, $loadchar, and $checkchar commands. This can be any value, but + // it's recommended to use a small number such as 16. + "BackupCharacterSlots": 16, + // Information menu contents. Each entry is a list containing [title, // short description, full contents]. In the short description and full // contents, you can use PSO escape codes with the $ character (for example,