add $savechar and $loadchar commands

This commit is contained in:
Martin Michelsen
2023-12-06 23:54:53 -08:00
parent 7db761f181
commit 072ebe81bf
7 changed files with 114 additions and 44 deletions
+6 -2
View File
@@ -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 <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
* Blue Burst player commands (game server only)
* `$bbchar <username> <password> <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 <slot>`: 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 <slot>` (v1 and v2 only): Loads your character data from the specified slot.
* `$bbchar <username> <password> <slot>`: 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 <stat> <value>`: 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.)
+61 -29
View File
@@ -52,12 +52,6 @@ static void check_version(shared_ptr<Client> c, Version version) {
}
}
static void check_not_version(shared_ptr<Client> 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<Lobby> 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<Client> 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<Client> c, const std::string& args) {
static void server_command_bbchar_savechar(shared_ptr<Client> 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<string> 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<Client::PendingCharacterExport>();
pending_export->is_bb_conversion = is_bb_conversion;
if (is_bb_conversion) {
vector<string> 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<Client> c, const std::string& args) {
server_command_bbchar_savechar(c, args, true);
}
static void server_command_savechar(shared_ptr<Client> c, const std::string& args) {
server_command_bbchar_savechar(c, args, false);
}
static void server_command_loadchar(shared_ptr<Client> 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<Client> c, const std::string&) {
check_version(c, Version::BB_V4);
try {
@@ -1702,12 +1732,13 @@ static const unordered_map<string, ChatCommandDefinition> 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<string, ChatCommandDefinition> 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<string, ChatCommandDefinition> 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}},
-1
View File
@@ -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);
+6 -2
View File
@@ -202,8 +202,12 @@ struct Client : public std::enable_shared_from_this<Client> {
uint32_t next_exp_value; // next EXP value to give
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
std::shared_ptr<License> pending_bb_save_license;
uint8_t pending_bb_save_character_index;
struct PendingCharacterExport {
std::shared_ptr<const License> license;
ssize_t character_index = -1;
bool is_bb_conversion = false;
};
std::unique_ptr<PendingCharacterExport> pending_character_export;
std::deque<std::function<void(uint32_t, uint32_t)>> function_call_response_queue;
// File loading state
+21
View File
@@ -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<PSOCommandHeaderBB>(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<PSOBBCharacterFile>(freadx<PSOBBCharacterFile>(f.get()));
this->last_reported_disp_v1_v2.reset();
}
PlayerBank& ClientGameData::current_bank() {
if (this->external_bank) {
return *this->external_bank;
+3
View File
@@ -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<PSOBBCharacterFile> current_bank_character();
bool use_shared_bank(); // Returns true if the bank exists; false if it was created
+17 -10
View File
@@ -2981,17 +2981,27 @@ static void on_61_98(shared_ptr<Client> 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<License> dest_license = c->pending_bb_save_license;
c->pending_bb_save_license.reset();
if (c->pending_character_export) {
unique_ptr<Client::PendingCharacterExport> 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<Client> 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<uint8_t>(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());
}
}
}