diff --git a/README.md b/README.md index f98fd8da..34b4e8f1 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,7 @@ Some commands only work on the game server and not on the proxy server. The chat * 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. * `$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. + * `$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.) * Game state commands (game server only) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index b48ceef0..80095028 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -980,11 +980,33 @@ static void server_command_edit(shared_ptr c, const std::string& args) { s->send_lobby_join_notifications(l, c); } -// TODO: implement this (and make sure the bank name is filesystem-safe) -/* static void server_command_change_bank(shared_ptr c, const std::string&) { +static void server_command_change_bank(shared_ptr c, const std::string& args) { check_version(c, Version::BB_V4); - ... -} */ + + if (c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) { + throw runtime_error("cannot change banks while at the bank counter"); + } + + ssize_t new_char_index = args.empty() ? (c->game_data.bb_character_index + 1) : stol(args, nullptr, 0); + + if (new_char_index == 0) { + if (c->game_data.use_shared_bank()) { + send_text_message_printf(c, "$C6Using shared bank (0)"); + } else { + send_text_message_printf(c, "$C6Created shared bank (0)"); + } + } else if (new_char_index <= 4) { + c->game_data.use_character_bank(new_char_index - 1); + auto bp = c->game_data.current_bank_character(); + auto name = bp->disp.name.decode(c->language()); + send_text_message_printf(c, "$C6Using %s\'s bank (%zu)", name.c_str(), new_char_index); + } else { + throw runtime_error("invalid bank number"); + } + + const auto& bank = c->game_data.current_bank(); + 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) { @@ -1023,22 +1045,10 @@ static void server_command_convert_char_to_bb(shared_ptr c, const std::s static void server_command_save(shared_ptr c, const std::string&) { check_version(c, Version::BB_V4); try { - c->game_data.save_character_file(); - send_text_message(c, "Character data saved"); + c->game_data.save_all(); + send_text_message(c, "All data saved"); } catch (const exception& e) { - send_text_message_printf(c, "Can\'t save character:\n%s", e.what()); - } - try { - c->game_data.save_system_file(); - send_text_message(c, "System data saved"); - } catch (const exception& e) { - send_text_message_printf(c, "Can\'t save system data:\n%s", e.what()); - } - try { - c->game_data.save_guild_card_file(); - send_text_message(c, "Guild Card data saved"); - } catch (const exception& e) { - send_text_message_printf(c, "Can\'t save Guild Cards:\n%s", e.what()); + send_text_message_printf(c, "Can\'t save data:\n%s", e.what()); } c->reschedule_save_game_data_event(); } @@ -1677,6 +1687,7 @@ static const unordered_map chat_commands({ {"$auction", {server_command_auction, proxy_command_auction}}, {"$ax", {server_command_ax, nullptr}}, {"$ban", {server_command_ban, nullptr}}, + {"$bank", {server_command_change_bank, nullptr}}, {"$bbchar", {server_command_convert_char_to_bb, nullptr}}, {"$cheat", {server_command_cheat, nullptr}}, {"$debug", {server_command_debug, nullptr}}, diff --git a/src/Client.cc b/src/Client.cc index 2d3f9a0e..66abe1f7 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -226,7 +226,7 @@ void Client::set_license(shared_ptr l) { this->license = l; this->game_data.guild_card_number = this->license->serial_number; if (this->version() == Version::BB_V4) { - this->game_data.bb_username = this->license->bb_username; + this->game_data.set_bb_username(this->license->bb_username); } } @@ -296,7 +296,7 @@ void Client::save_game_data() { throw logic_error("save_game_data called for non-BB client"); } if (this->game_data.character(false)) { - this->game_data.save_character_file(); + this->game_data.save_all(); } } diff --git a/src/Player.cc b/src/Player.cc index e28bb252..1e0a7d9a 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -66,19 +66,35 @@ std::shared_ptr PlayerFilesManager::get_guild_card(const std } } +std::shared_ptr PlayerFilesManager::get_bank(const std::string& filename) { + try { + return this->loaded_bank_files.at(filename); + } catch (const out_of_range&) { + return nullptr; + } +} + void PlayerFilesManager::set_system(const std::string& filename, std::shared_ptr file) { if (!this->loaded_system_files.emplace(filename, file).second) { - throw runtime_error("Guild Card file already loaded"); + throw runtime_error("Guild Card file already loaded: " + filename); } } + void PlayerFilesManager::set_character(const std::string& filename, std::shared_ptr file) { if (!this->loaded_character_files.emplace(filename, file).second) { - throw runtime_error("character file already loaded"); + throw runtime_error("character file already loaded: " + filename); } } + void PlayerFilesManager::set_guild_card(const std::string& filename, std::shared_ptr file) { if (!this->loaded_guild_card_files.emplace(filename, file).second) { - throw runtime_error("Guild Card file already loaded"); + throw runtime_error("Guild Card file already loaded: " + filename); + } +} + +void PlayerFilesManager::set_bank(const std::string& filename, std::shared_ptr file) { + if (!this->loaded_bank_files.emplace(filename, file).second) { + throw runtime_error("bank file already loaded: " + filename); } } @@ -96,6 +112,10 @@ void PlayerFilesManager::clear_expired_files(evutil_socket_t, short, void* ctx) if (num_deleted) { player_data_log.info("Cleared %zu expired Guild Card file(s)", num_deleted); } + num_deleted = erase_unused(self->loaded_bank_files); + if (num_deleted) { + player_data_log.info("Cleared %zu expired bank file(s)", num_deleted); + } } ClientGameData::ClientGameData(std::shared_ptr files_manager) @@ -111,10 +131,24 @@ ClientGameData::ClientGameData(std::shared_ptr files_manager ClientGameData::~ClientGameData() { if (!this->bb_username.empty() && this->character_data.get()) { - this->save_character_file(); + this->save_all(); } } +const string& ClientGameData::get_bb_username() const { + return this->bb_username; +} + +void ClientGameData::set_bb_username(const string& bb_username) { + // Make sure bb_username is filename-safe + for (char ch : bb_username) { + if (!isalnum(ch) && (ch != '-') && (ch != '_')) { + throw runtime_error("invalid characters in username"); + } + } + this->bb_username = bb_username; +} + void ClientGameData::create_battle_overlay(shared_ptr rules, shared_ptr level_table) { this->overlay_character_data = make_shared(*this->character(true, false)); @@ -268,14 +302,17 @@ string ClientGameData::system_filename() const { return string_printf("system/players/system_%s.psosys", this->bb_username.c_str()); } -string ClientGameData::character_filename() const { +string ClientGameData::character_filename(int8_t index) const { if (this->bb_username.empty()) { throw logic_error("non-BB players do not have character data"); } - if (this->bb_character_index < 0) { + if (index < 0) { + index = this->bb_character_index; + } + if (index < 0) { throw logic_error("character index is not set"); } - return string_printf("system/players/player_%s_%hhd.psochar", this->bb_username.c_str(), this->bb_character_index); + return string_printf("system/players/player_%s_%hhd.psochar", this->bb_username.c_str(), index); } string ClientGameData::guild_card_filename() const { @@ -285,6 +322,13 @@ string ClientGameData::guild_card_filename() const { return string_printf("system/players/guild_cards_%s.psocard", this->bb_username.c_str()); } +string ClientGameData::shared_bank_filename() const { + if (this->bb_username.empty()) { + throw logic_error("non-BB players do not have shared bank files"); + } + return string_printf("system/players/shared_bank_%s.psobank", this->bb_username.c_str()); +} + string ClientGameData::legacy_account_filename() const { if (this->bb_username.empty()) { throw logic_error("non-BB players do not have legacy account data"); @@ -356,7 +400,7 @@ void ClientGameData::load_all_files() { throw runtime_error("incorrect flag in character file header"); } this->character_data = make_shared(freadx(f.get())); - this->files_manager->set_character(this->character_filename(), this->character_data); + this->files_manager->set_character(char_filename, this->character_data); player_data_log.info("Loaded character data from %s", char_filename.c_str()); // If there was no .psosys file, load the system file from the .psochar @@ -473,6 +517,29 @@ void ClientGameData::load_all_files() { } } +void ClientGameData::save_all() { + if (this->system_data) { + this->save_system_file(); + } + if (this->character_data) { + this->save_character_file(); + } + if (this->guild_card_data) { + this->save_guild_card_file(); + } + if (this->external_bank) { + string filename = this->shared_bank_filename(); + save_object_file(filename, *this->external_bank); + player_data_log.info("Saved shared bank file %s", filename.c_str()); + } + if (this->external_bank_character) { + this->save_character_file( + this->character_filename(this->external_bank_character_index), + this->system_data, + this->external_bank_character); + } +} + void ClientGameData::save_system_file() const { if (!this->system_data) { throw logic_error("no system file loaded"); @@ -482,6 +549,30 @@ void ClientGameData::save_system_file() const { player_data_log.info("Saved system file %s", filename.c_str()); } +void ClientGameData::save_character_file( + const string& filename, + shared_ptr system, + shared_ptr character) { + auto f = fopen_unique(filename, "wb"); + PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000}; + fwritex(f.get(), header); + fwritex(f.get(), *character); + fwritex(f.get(), *system); + // TODO: Technically, we should write the actual team membership struct to the + // file here, but that would cause ClientGameData to depend on License, which + // it currently does not. This data doesn't matter at all for correctness + // within newserv, since it ignores this data entirely and instead generates + // the membership struct from the team ID in the License and the team's state. + // So, writing correct data here would mostly be for compatibility with other + // PSO servers. But if the other server is newserv, then this data would be + // used anyway, and if it's not, then it would presumably have a different set + // of teams with a different set of team IDs anyway, so the membership struct + // here would be useless either way. + static const PSOBBTeamMembership empty_membership; + fwritex(f.get(), empty_membership); + player_data_log.info("Saved character file %s", filename.c_str()); +} + void ClientGameData::save_character_file() { if (!this->system_data.get()) { throw logic_error("no system file loaded"); @@ -500,25 +591,7 @@ void ClientGameData::save_character_file() { this->last_play_time_update = t; } - string filename = this->character_filename(); - auto f = fopen_unique(filename, "wb"); - PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000}; - fwritex(f.get(), header); - fwritex(f.get(), *this->character_data); - fwritex(f.get(), *this->system_data); - // TODO: Technically, we should write the actual team membership struct to the - // file here, but that would cause ClientGameData to depend on License, which - // it currently does not. This data doesn't matter at all for correctness - // within newserv, since it ignores this data entirely and instead generates - // the membership struct from the team ID in the License and the team's state. - // So, writing correct data here would mostly be for compatibility with other - // PSO servers. But if the other server is newserv, then this data would be - // used anyway, and if it's not, then it would presumably have a different set - // of teams with a different set of team IDs anyway, so the membership struct - // here would be useless either way. - static const PSOBBTeamMembership empty_membership; - fwritex(f.get(), empty_membership); - player_data_log.info("Saved character file %s", filename.c_str()); + this->save_character_file(this->character_filename(), this->system_data, this->character_data); } void ClientGameData::save_guild_card_file() const { @@ -529,3 +602,75 @@ void ClientGameData::save_guild_card_file() const { save_object_file(filename, *this->guild_card_data); player_data_log.info("Saved Guild Card file %s", filename.c_str()); } + +PlayerBank& ClientGameData::current_bank() { + if (this->external_bank) { + return *this->external_bank; + } else if (this->external_bank_character) { + return this->external_bank_character->bank; + } + return this->character()->bank; +} + +std::shared_ptr ClientGameData::current_bank_character() { + return this->external_bank_character ? this->external_bank_character : this->character(); +} + +void ClientGameData::use_default_bank() { + if (this->external_bank) { + string filename = this->shared_bank_filename(); + save_object_file(filename, *this->external_bank); + this->external_bank.reset(); + player_data_log.info("Detached shared bank %s", filename.c_str()); + } + if (this->external_bank_character) { + string filename = this->character_filename(this->external_bank_character_index); + this->save_character_file(filename, this->system_data, this->external_bank_character); + this->external_bank_character.reset(); + player_data_log.info("Detached character %s from bank", filename.c_str()); + } +} + +bool ClientGameData::use_shared_bank() { + this->use_default_bank(); + string filename = this->shared_bank_filename(); + if (isfile(filename)) { + this->external_bank = make_shared(load_object_file(filename)); + player_data_log.info("Loaded shared bank %s", filename.c_str()); + return true; + } else { + this->external_bank = make_shared(); + player_data_log.info("Created shared bank for %s", filename.c_str()); + return false; + } +} + +void ClientGameData::use_character_bank(int8_t index) { + this->use_default_bank(); + if (index != this->bb_character_index) { + string filename = this->character_filename(index); + this->external_bank_character = this->files_manager->get_character(filename); + if (this->external_bank_character) { + this->external_bank_character_index = index; + player_data_log.info("Using loaded character file %s for external bank", filename.c_str()); + } else if (isfile(filename)) { + 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->external_bank_character = make_shared(freadx(f.get())); + this->external_bank_character_index = index; + this->files_manager->set_character(filename, this->external_bank_character); + player_data_log.info("Loaded character data from %s for external bank", filename.c_str()); + } else { + throw runtime_error("character does not exist"); + } + } +} diff --git a/src/Player.hh b/src/Player.hh index 4fd84a2e..34e9e90a 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -39,10 +39,12 @@ public: std::shared_ptr get_system(const std::string& filename); std::shared_ptr get_character(const std::string& filename); std::shared_ptr get_guild_card(const std::string& filename); + std::shared_ptr get_bank(const std::string& filename); void set_system(const std::string& filename, std::shared_ptr file); void set_character(const std::string& filename, std::shared_ptr file); void set_guild_card(const std::string& filename, std::shared_ptr file); + void set_bank(const std::string& filename, std::shared_ptr file); private: std::shared_ptr base; @@ -51,6 +53,7 @@ private: std::unordered_map> loaded_system_files; std::unordered_map> loaded_character_files; std::unordered_map> loaded_guild_card_files; + std::unordered_map> loaded_bank_files; static void clear_expired_files(evutil_socket_t fd, short events, void* ctx); }; @@ -75,7 +78,6 @@ public: std::shared_ptr ep3_config; // These are only used if the client is BB - std::string bb_username; int8_t bb_character_index; ItemData identify_result; std::array, 3> shop_contents; @@ -83,6 +85,9 @@ public: explicit ClientGameData(std::shared_ptr files_manager); ~ClientGameData(); + const std::string& get_bb_username() const; + void set_bb_username(const std::string& bb_username); + void create_battle_overlay(std::shared_ptr rules, std::shared_ptr level_table); void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr level_table); inline void delete_overlay() { @@ -107,12 +112,24 @@ public: const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); + void save_all(); void save_system_file() const; + static void save_character_file( + const std::string& filename, + std::shared_ptr sys, + std::shared_ptr character); // Note: This function is not const because it updates the player's play time. void save_character_file(); void save_guild_card_file() const; + PlayerBank& current_bank(); + std::shared_ptr current_bank_character(); + bool use_shared_bank(); // Returns true if the bank exists; false if it was created + void use_character_bank(int8_t bb_character_index); + void use_default_bank(); + private: + std::string bb_username; std::shared_ptr files_manager; // The overlay character data is used in battle and challenge modes, when @@ -122,13 +139,19 @@ private: std::shared_ptr overlay_character_data; std::shared_ptr character_data; std::shared_ptr guild_card_data; + std::shared_ptr external_bank; + std::shared_ptr external_bank_character; + int8_t external_bank_character_index; uint64_t last_play_time_update; + void save_and_clear_external_bank(); + void load_all_files(); std::string system_filename() const; - std::string character_filename() const; + std::string character_filename(int8_t index = -1) const; std::string guild_card_filename() const; + std::string shared_bank_filename() const; std::string legacy_player_filename() const; std::string legacy_account_filename() const; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 9df5c155..8b77759a 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2974,10 +2974,10 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri } else if (command == 0x61) { if (!c->pending_bb_save_username.empty()) { - string prev_bb_username = c->game_data.bb_username; + string prev_bb_username = c->game_data.get_bb_username(); int8_t prev_bb_character_index = c->game_data.bb_character_index; - c->game_data.bb_username = c->pending_bb_save_username; + c->game_data.set_bb_username(c->pending_bb_save_username); c->game_data.bb_character_index = c->pending_bb_save_character_index; // Update a few fields for BB @@ -3005,7 +3005,7 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri c->pending_bb_save_username.c_str()); } - c->game_data.bb_username = prev_bb_username; + c->game_data.set_bb_username(prev_bb_username); c->game_data.bb_character_index = prev_bb_character_index; c->pending_bb_save_username.clear(); @@ -3117,7 +3117,7 @@ static void on_E3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { ClientGameData temp_gd(s->player_files_manager); temp_gd.guild_card_number = c->license->serial_number; - temp_gd.bb_username = c->license->bb_username; + temp_gd.set_bb_username(c->license->bb_username); temp_gd.bb_character_index = cmd.character_index; try { @@ -4789,7 +4789,7 @@ static void on_EA_BB(shared_ptr c, uint16_t command, uint32_t flag, stri } } if (!reward.reward_item.empty()) { - c->game_data.character()->bank.add_item(reward.reward_item); + c->game_data.current_bank().add_item(reward.reward_item); } } break; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 772b2da4..b8166d67 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1453,24 +1453,25 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint } auto p = c->game_data.character(); + auto& bank = c->game_data.current_bank(); if (cmd.action == 0) { // Deposit if (cmd.item_id == 0xFFFFFFFF) { // Deposit Meseta if (cmd.meseta_amount > p->disp.stats.meseta) { l->log.info("Player %hu attempted to deposit %" PRIu32 " Meseta in the bank, but has only %" PRIu32 " Meseta on hand", c->lobby_client_id, cmd.meseta_amount.load(), p->disp.stats.meseta.load()); - } else if ((p->bank.meseta + cmd.meseta_amount) > 999999) { + } else if ((bank.meseta + cmd.meseta_amount) > 999999) { l->log.info("Player %hu attempted to deposit %" PRIu32 " Meseta in the bank, but already has %" PRIu32 " Meseta in the bank", c->lobby_client_id, cmd.meseta_amount.load(), p->disp.stats.meseta.load()); } else { - p->bank.meseta += cmd.meseta_amount; + bank.meseta += cmd.meseta_amount; p->disp.stats.meseta -= cmd.meseta_amount; l->log.info("Player %hu deposited %" PRIu32 " Meseta in the bank (bank now has %" PRIu32 "; inventory now has %" PRIu32 ")", - c->lobby_client_id, cmd.meseta_amount.load(), p->bank.meseta.load(), p->disp.stats.meseta.load()); + c->lobby_client_id, cmd.meseta_amount.load(), bank.meseta.load(), p->disp.stats.meseta.load()); } } else { // Deposit item auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version() != Version::BB_V4); - p->bank.add_item(item); + bank.add_item(item); send_destroy_item(c, cmd.item_id, cmd.item_amount); string name = s->item_name_index->describe_item(Version::BB_V4, item); @@ -1481,21 +1482,21 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint } else if (cmd.action == 1) { // Take if (cmd.item_index == 0xFFFF) { // Take Meseta - if (cmd.meseta_amount > p->bank.meseta) { + if (cmd.meseta_amount > bank.meseta) { l->log.info("Player %hu attempted to withdraw %" PRIu32 " Meseta from the bank, but has only %" PRIu32 " Meseta in the bank", - c->lobby_client_id, cmd.meseta_amount.load(), p->bank.meseta.load()); + c->lobby_client_id, cmd.meseta_amount.load(), bank.meseta.load()); } else if ((p->disp.stats.meseta + cmd.meseta_amount) > 999999) { l->log.info("Player %hu attempted to withdraw %" PRIu32 " Meseta from the bank, but already has %" PRIu32 " Meseta on hand", c->lobby_client_id, cmd.meseta_amount.load(), p->disp.stats.meseta.load()); } else { - p->bank.meseta -= cmd.meseta_amount; + bank.meseta -= cmd.meseta_amount; p->disp.stats.meseta += cmd.meseta_amount; l->log.info("Player %hu withdrew %" PRIu32 " Meseta from the bank (bank now has %" PRIu32 "; inventory now has %" PRIu32 ")", - c->lobby_client_id, cmd.meseta_amount.load(), p->bank.meseta.load(), p->disp.stats.meseta.load()); + c->lobby_client_id, cmd.meseta_amount.load(), bank.meseta.load(), p->disp.stats.meseta.load()); } } else { // Take item - auto item = p->bank.remove_item_by_index(cmd.item_index, cmd.item_amount); + auto item = bank.remove_item_by_index(cmd.item_index, cmd.item_amount); item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item); send_create_inventory_item(c, item); @@ -2160,9 +2161,8 @@ void on_transfer_item_via_mail_message_bb(shared_ptr c, uint8_t command, (target_c->version() == Version::BB_V4) && (target_c->game_data.character(false) != nullptr) && !target_c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) { - auto target_p = target_c->game_data.character(false); try { - target_p->bank.add_item(item); + target_c->game_data.current_bank().add_item(item); item_sent = true; } catch (const runtime_error&) { } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 130326d5..fe4b7ca9 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2357,14 +2357,15 @@ void send_bank(shared_ptr c) { } auto p = c->game_data.character(); - const auto* items_it = p->bank.items.data(); - vector items(items_it, items_it + p->bank.num_items); + const auto& bank = c->game_data.current_bank(); + const auto* items_it = bank.items.data(); + vector items(items_it, items_it + bank.num_items); G_BankContentsHeader_BB_6xBC cmd = { {{0xBC, 0, 0}, sizeof(G_BankContentsHeader_BB_6xBC) + items.size() * sizeof(PlayerBankItem)}, random_object(), - p->bank.num_items, - p->bank.meseta}; + bank.num_items, + bank.meseta}; send_command_t_vt(c, 0x6C, 0x00, cmd, items); } @@ -3459,7 +3460,7 @@ void send_team_reward_list(std::shared_ptr c, bool show_purchased) { } auto s = c->require_server_state(); - bool show_item_rewards = show_purchased || (c->game_data.character()->bank.num_items < 200); + bool show_item_rewards = show_purchased || (c->game_data.current_bank().num_items < 200); vector entries; for (const auto& reward : s->team_index->reward_definitions()) {