From 18ddfa4ef42d1e14b88472566fc377e78a6da566 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 13 Nov 2023 13:00:22 -0800 Subject: [PATCH] use .psochar format for BB characters --- src/ChatCommands.cc | 30 +- src/Client.cc | 9 +- src/Client.hh | 2 +- src/CommandFormats.hh | 79 ++--- src/Episode3/Tournament.cc | 2 +- src/Items.cc | 8 +- src/Lobby.cc | 6 +- src/Player.cc | 604 ++++++++++++++++--------------------- src/Player.hh | 125 +++----- src/PlayerSubordinates.cc | 113 +++++-- src/PlayerSubordinates.hh | 73 ++++- src/ReceiveCommands.cc | 256 ++++++++-------- src/ReceiveSubcommands.cc | 103 ++++--- src/SaveFileFormats.cc | 285 +++++++++++++++++ src/SaveFileFormats.hh | 162 ++++++++-- src/SendCommands.cc | 109 +++---- src/SendCommands.hh | 3 +- src/Server.cc | 4 +- src/ServerState.cc | 6 - src/ServerState.hh | 1 - 20 files changed, 1138 insertions(+), 842 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index e2527063..799e1f9c 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -270,20 +270,20 @@ static void server_command_quest(shared_ptr c, const std::string& args) } static void server_command_show_material_counts(shared_ptr c, const std::string&) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); if ((c->version() == GameVersion::DC) || (c->version() == GameVersion::PC)) { send_text_message_printf(c, "%hhu HP, %hhu TP", - p->get_material_usage(SavedPlayerDataBB::MaterialType::HP), - p->get_material_usage(SavedPlayerDataBB::MaterialType::TP)); + p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); } else { send_text_message_printf(c, "%hhu HP, %hhu TP, %hhu POW\n%hhu MIND, %hhu EVADE\n%hhu DEF, %hhu LUCK", - p->get_material_usage(SavedPlayerDataBB::MaterialType::HP), - p->get_material_usage(SavedPlayerDataBB::MaterialType::TP), - p->get_material_usage(SavedPlayerDataBB::MaterialType::POWER), - p->get_material_usage(SavedPlayerDataBB::MaterialType::MIND), - p->get_material_usage(SavedPlayerDataBB::MaterialType::EVADE), - p->get_material_usage(SavedPlayerDataBB::MaterialType::DEF), - p->get_material_usage(SavedPlayerDataBB::MaterialType::LUCK)); + p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::TP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER), + p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND), + p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE), + p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF), + p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); } } @@ -832,7 +832,7 @@ static void server_command_edit(shared_ptr c, const std::string& args) { vector tokens = split(encoded_args, ' '); try { - auto p = c->game_data.player(); + auto p = c->game_data.character(); if (tokens.at(0) == "atp") { p->disp.stats.char_stats.atp = stoul(tokens.at(1)); } else if (tokens.at(0) == "mst") { @@ -939,8 +939,8 @@ static void server_command_convert_char_to_bb(shared_ptr c, const std::s } // username/password are tokens[0] and [1] - c->pending_bb_save_player_index = stoul(tokens[2]) - 1; - if (c->pending_bb_save_player_index > 3) { + 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; } @@ -963,7 +963,7 @@ static void server_command_convert_char_to_bb(shared_ptr c, const std::s // Administration commands static string name_for_client(shared_ptr c) { - auto player = c->game_data.player(false); + auto player = c->game_data.character(false); if (player.get()) { return player->disp.name.decode(player->inventory.language); } @@ -1486,7 +1486,7 @@ static void server_command_surrender(shared_ptr c, const std::string&) { send_text_message(c, "$C6Battle has not\nyet started"); return; } - const string& name = c->game_data.player()->disp.name.decode(c->language()); + const string& name = c->game_data.character()->disp.name.decode(c->language()); send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str()); for (const auto& watcher_l : l->watcher_lobbies) { send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str()); diff --git a/src/Client.cc b/src/Client.cc index f439b315..d5a54f99 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -170,7 +170,7 @@ Client::Client( card_battle_table_seat_state(0), next_exp_value(0), can_chat(true), - pending_bb_save_player_index(0), + pending_bb_save_character_index(0), dol_base_addr(0) { this->config.set_flags_for_version(version, -1); @@ -267,11 +267,8 @@ void Client::save_game_data() { if (this->version() != GameVersion::BB) { throw logic_error("save_game_data called for non-BB client"); } - if (this->game_data.account(false)) { - this->game_data.save_account_data(); - } - if (this->game_data.player(false)) { - this->game_data.save_player_data(); + if (this->game_data.character(false)) { + this->game_data.save_character_file(); } } diff --git a/src/Client.hh b/src/Client.hh index 44d662c6..ef7cd206 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -197,7 +197,7 @@ struct Client : public std::enable_shared_from_this { G_SwitchStateChanged_6x05 last_switch_enabled_command; bool can_chat; std::string pending_bb_save_username; - uint8_t pending_bb_save_player_index; + uint8_t pending_bb_save_character_index; std::deque> function_call_response_queue; // File loading state diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index e6ec1b5a..b360b48c 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1671,7 +1671,7 @@ struct C_Login_BB_93 { le_uint32_t guild_card_number = 0; le_uint32_t sub_version = 0; uint8_t language; - uint8_t character_slot; + int8_t character_slot; // Values for connection_phase: // 00 - initial connection (client will request system file, characters, etc.) // 01 - choose character @@ -2829,16 +2829,16 @@ struct S_GameInformation_GC_Ep3_E1 { /* 0298 */ } __packed__; -// E1 (S->C): Create system file (BB) +// E1 (S->C): System file created (BB) // This seems to take the place of 00E2 in certain cases. Perhaps it was used // when a client hadn't logged in before and didn't have a system file, so the // client should use appropriate defaults. -struct S_TeamAndKeyConfigMissing_00E1_BB { +struct S_SystemFileCreated_00E1_BB { // If success is not equal to 1, the client shows a message saying "Forced // server disconnect (907)" and disconnects. Otherwise, the client proceeeds // as if it had received an 00E2 command, and sends its first 00E3. - le_uint32_t success = 0; + le_uint32_t success = 1; } __packed__; // E2 (C->S): Tournament control (Episode 3) @@ -2954,7 +2954,7 @@ struct S_TournamentGameDetails_GC_Ep3_E3 { // E3 (C->S): Player preview request (BB) struct C_PlayerPreviewRequest_BB_E3 { - le_uint32_t player_index = 0; + le_int32_t character_index = 0; le_uint32_t unused = 0; } __packed__; @@ -2990,12 +2990,12 @@ struct S_CardBattleTableState_GC_Ep3_E4 { // E4 (S->C): Player choice or no player present (BB) struct S_ApprovePlayerChoice_BB_00E4 { - le_uint32_t player_index = 0; + le_int32_t character_index = 0; le_uint32_t result = 0; // 1 = approved } __packed__; struct S_PlayerPreview_NoPlayer_BB_00E4 { - le_uint32_t player_index = 0; + le_int32_t character_index = 0; le_uint32_t error = 0; // 2 = no player present } __packed__; @@ -3011,7 +3011,7 @@ struct S_CardBattleTableConfirmation_GC_Ep3_E5 { // E5 (C->S): Create character (BB) struct SC_PlayerPreview_CreateCharacter_BB_00E5 { - le_uint32_t player_index = 0; + le_int32_t character_index = 0; PlayerDispDataBBPreview preview; } __packed__; @@ -3063,34 +3063,13 @@ struct C_CreateSpectatorTeam_GC_Ep3_E7 { // E7 (S->C): Tournament entry list for spectating (Episode 3) // Same format as E2 command. -// E7: Save or load full character data (BB) -// TODO: Verify full breakdown from send_E7 in BB disassembly. +// E7: Sync save files (BB) -struct SC_SyncCharacterSaveFile_BB_00E7 { - /* 0000 */ PlayerInventory inventory; // From player data - /* 034C */ PlayerDispDataBB disp; // From player data - /* 04DC */ le_uint32_t unknown_a1 = 0; - /* 04E0 */ le_uint32_t creation_timestamp = 0; - /* 04E4 */ le_uint32_t signature = 0xA205B064; - /* 04E8 */ le_uint32_t play_time_seconds = 0; - /* 04EC */ le_uint32_t option_flags = 0; // account - /* 04F0 */ parray quest_data1; // player - /* 06F8 */ PlayerBank bank; // player - /* 19C0 */ GuildCardBB guild_card; - /* 1AC8 */ le_uint32_t unknown_a3 = 0; - /* 1ACC */ parray symbol_chats; // account - /* 1FAC */ parray shortcuts; // account - /* 29EC */ pstring auto_reply; // player - /* 2B44 */ pstring info_board; // player - /* 2C9C */ PlayerRecords_Battle battle_records; - /* 2CB4 */ parray unknown_a4; - /* 2CB8 */ PlayerRecordsBB_Challenge challenge_records; - /* 2DF8 */ parray tech_menu_config; // player - /* 2E20 */ parray unknown_a6; - /* 2E4C */ parray quest_data2; // player - /* 2EA4 */ PSOBBSystemFile system_file; // account +struct SC_SyncSaveFiles_BB_E7 { + /* 0000 */ PSOBBCharacterFile char_file; + /* 2EA4 */ PSOBBSystemFile system_file; /* 3994 */ -} __attribute__((packed)); +} __packed__; // E8 (S->C): Join spectator team (Episode 3) // header.flag = player count (including spectators) @@ -3471,7 +3450,7 @@ struct C_UpdateOptionFlags_BB_01ED { le_uint32_t option_flags = 0; } __packed__; struct C_UpdateSymbolChats_BB_02ED { - parray symbol_chats; + parray symbol_chats; } __packed__; struct C_UpdateChatShortcuts_BB_03ED { parray chat_shortcuts; @@ -3483,7 +3462,7 @@ struct C_UpdatePadConfig_BB_05ED { parray pad_config; } __packed__; struct C_UpdateTechMenu_BB_06ED { - parray tech_menu; + parray tech_menu; } __packed__; struct C_UpdateCustomizeMenu_BB_07ED { parray customize; @@ -3734,28 +3713,6 @@ struct G_SendGuildCard_BB_6x06 { // 6x07: Symbol chat -struct SymbolChat { - // Bits: ----------------------DMSSSCCCFF - // S = sound, C = face color, F = face shape, D = capture, M = mute sound - /* 00 */ le_uint32_t spec = 0; - - // Corner objects are specified in reading order ([0] is the top-left one). - // Bits (each entry): ---VHCCCZZZZZZZZ - // V = reverse vertical, H = reverse horizontal, C = color, Z = object - // If Z is all 1 bits (0xFF), no corner object is rendered. - /* 04 */ parray corner_objects; - - struct FacePart { - uint8_t type = 0; // FF = no part in this slot - uint8_t x = 0; - uint8_t y = 0; - // Bits: ------VH (V = reverse vertical, H = reverse horizontal) - uint8_t flags = 0; - } __packed__; - /* 0C */ parray face_parts; - /* 3C */ -} __packed__; - struct G_SymbolChat_6x07 { G_UnusedHeader header; le_uint32_t client_id = 0; @@ -5580,12 +5537,12 @@ struct G_ChallengeModeGrave_BB_6xD1 { le_uint32_t unknown_a5 = 0; } __packed__; -// 6xD2: Set quest data 2 (BB) +// 6xD2: Set quest global flag (BB) // Writes 4 bytes to the 32-bit field specified by index. -struct G_SetQuestData2_BB_6xD2 { +struct G_SetQuestGlobalFlag_BB_6xD2 { G_ClientIDHeader header; - le_uint32_t index = 0; + le_uint32_t index = 0; // There are 0x10 of them (0x00-0x0F) le_uint32_t value = 0; } __packed__; diff --git a/src/Episode3/Tournament.cc b/src/Episode3/Tournament.cc index 2598f142..ad3daa15 100644 --- a/src/Episode3/Tournament.cc +++ b/src/Episode3/Tournament.cc @@ -16,7 +16,7 @@ Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number, const string& playe Tournament::PlayerEntry::PlayerEntry(shared_ptr c) : serial_number(c->license->serial_number), client(c), - player_name(c->game_data.player()->disp.name.decode(c->language())) {} + player_name(c->game_data.character()->disp.name.decode(c->language())) {} Tournament::PlayerEntry::PlayerEntry( shared_ptr com_deck) diff --git a/src/Items.cc b/src/Items.cc index 41a1024e..5507d362 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -16,7 +16,7 @@ void player_use_item(shared_ptr c, size_t item_index) { // delete the item here. bool should_delete_item = (c->version() != GameVersion::DC) && (c->version() != GameVersion::PC); - auto player = c->game_data.player(); + auto player = c->game_data.character(); auto& item = player->inventory.items[item_index]; uint32_t item_identifier = item.data.primary_identifier(); @@ -53,10 +53,10 @@ void player_use_item(shared_ptr c, size_t item_index) { weapon.data.data1[3] += (item.data.data1[2] + 1); } else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material - auto p = c->game_data.player(); + auto p = c->game_data.character(); bool track_non_hp_tp_materials = (c->version() != GameVersion::DC) && (c->version() != GameVersion::PC); - using Type = SavedPlayerDataBB::MaterialType; + using Type = PSOBBCharacterFile::MaterialType; Type type; switch (item.data.data1[2]) { case 0: // Power Material @@ -276,7 +276,7 @@ void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fe }); auto s = c->require_server_state(); - auto player = c->game_data.player(); + auto player = c->game_data.character(); auto& fed_item = player->inventory.items[fed_item_index]; auto& mag_item = player->inventory.items[mag_item_index]; diff --git a/src/Lobby.cc b/src/Lobby.cc index 740438a5..aa43b3ae 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -193,7 +193,7 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { // item IDs if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { auto s = this->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto& inv = p->inventory; size_t count = min(inv.num_items, 30); for (size_t x = 0; x < count; x++) { @@ -205,7 +205,7 @@ void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { // If the lobby is recording a battle record, add the player join event if (this->battle_record) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); PlayerLobbyDataDCGC lobby_data; lobby_data.player_tag = 0x00010000; lobby_data.guild_card_number = c->license->serial_number; @@ -304,7 +304,7 @@ shared_ptr Lobby::find_client(const string* identifier, uint64_t serial_ (lc->license->serial_number == serial_number)) { return lc; } - if (identifier && (lc->game_data.player()->disp.name.eq(*identifier, lc->language()))) { + if (identifier && (lc->game_data.character()->disp.name.eq(*identifier, lc->language()))) { return lc; } } diff --git a/src/Player.cc b/src/Player.cc index 30abc7ee..bedf2bea 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -12,58 +12,51 @@ #include "ItemData.hh" #include "Loggers.hh" #include "PSOEncryption.hh" +#include "PSOProtocol.hh" #include "StaticGameData.hh" #include "Text.hh" #include "Version.hh" using namespace std; -// Originally there was going to be a language-based header, but then I decided -// against it. This string was already in use for that parser, so I didn't -// bother changing it. -static const string ACCOUNT_FILE_SIGNATURE = - "newserv account file format; 7 sections present; sequential;"; - ClientGameData::ClientGameData() - : last_play_time_update(0), - guild_card_number(0), + : guild_card_number(0), should_update_play_time(false), - bb_player_index(0), - should_save(true) {} + bb_character_index(-1), + last_play_time_update(0) { + for (size_t z = 0; z < this->blocked_senders.size(); z++) { + this->blocked_senders[z] = 0; + } +} ClientGameData::~ClientGameData() { - if (!this->bb_username.empty()) { - if (this->account_data.get()) { - this->save_account_data(); - } - if (this->player_data.get()) { - this->save_player_data(); - } + if (!this->bb_username.empty() && this->character_data.get()) { + this->save_character_file(); } } void ClientGameData::create_battle_overlay(shared_ptr rules, shared_ptr level_table) { - this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false))); + this->overlay_character_data.reset(new PSOBBCharacterFile(*this->character(true, false))); if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) { - this->overlay_player_data->inventory.remove_all_items_of_type(0); - this->overlay_player_data->inventory.remove_all_items_of_type(1); + this->overlay_character_data->inventory.remove_all_items_of_type(0); + this->overlay_character_data->inventory.remove_all_items_of_type(1); } if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) { - this->overlay_player_data->inventory.remove_all_items_of_type(2); + this->overlay_character_data->inventory.remove_all_items_of_type(2); } if (rules->tool_mode != BattleRules::ToolMode::ALLOW) { - this->overlay_player_data->inventory.remove_all_items_of_type(3); + this->overlay_character_data->inventory.remove_all_items_of_type(3); } if (rules->replace_char) { // TODO: Shouldn't we clear other material usage here? It looks like the // original code doesn't, but that seems wrong. - this->overlay_player_data->inventory.hp_from_materials = 0; - this->overlay_player_data->inventory.tp_from_materials = 0; + this->overlay_character_data->inventory.hp_from_materials = 0; + this->overlay_character_data->inventory.tp_from_materials = 0; uint32_t target_level = clamp(rules->char_level, 0, 199); - uint8_t char_class = this->overlay_player_data->disp.visual.char_class; - auto& stats = this->overlay_player_data->disp.stats; + uint8_t char_class = this->overlay_character_data->disp.visual.char_class; + auto& stats = this->overlay_character_data->disp.stats; stats.reset_to_base(char_class, level_table); stats.advance_to_level(char_class, target_level, level_table); @@ -74,30 +67,30 @@ void ClientGameData::create_battle_overlay(shared_ptr rules, if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) { // TODO: Verify this is what the game actually does. for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { - uint8_t existing_level = this->overlay_player_data->get_technique_level(tech_num); + uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num); if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) { - this->overlay_player_data->set_technique_level(tech_num, rules->max_tech_level); + this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level); } } } else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) { for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { - this->overlay_player_data->set_technique_level(tech_num, 0xFF); + this->overlay_character_data->set_technique_level(tech_num, 0xFF); } } if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) { - this->overlay_player_data->disp.stats.meseta = 0; + this->overlay_character_data->disp.stats.meseta = 0; } if (rules->forbid_scape_dolls) { - this->overlay_player_data->inventory.remove_all_items_of_type(3, 9); + this->overlay_character_data->inventory.remove_all_items_of_type(3, 9); } } void ClientGameData::create_challenge_overlay(GameVersion version, size_t template_index, shared_ptr level_table) { - const auto& tpl = get_challenge_template_definition( - version, this->player(true, false)->disp.visual.class_flags, template_index); + auto p = this->character(true, false); + const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index); - this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false))); - auto overlay = this->overlay_player_data; + this->overlay_character_data.reset(new PSOBBCharacterFile(*p)); + auto overlay = this->overlay_character_data; for (size_t z = 0; z < overlay->inventory.items.size(); z++) { auto& i = overlay->inventory.items[z]; @@ -137,360 +130,287 @@ void ClientGameData::create_challenge_overlay(GameVersion version, size_t templa } } -shared_ptr ClientGameData::account(bool allow_load) { - if (!this->account_data.get() && allow_load) { - if (this->bb_username.empty()) { - this->account_data.reset(new SavedAccountDataBB()); - this->account_data->signature.encode(ACCOUNT_FILE_SIGNATURE); - } else { - this->load_account_data(); - } +shared_ptr ClientGameData::system(bool allow_load) { + if (!this->system_data && allow_load) { + this->load_all_files(); } - return this->account_data; + return this->system_data; } -shared_ptr ClientGameData::player(bool allow_load, bool allow_overlay) { - if (this->overlay_player_data && allow_overlay) { - return this->overlay_player_data; +shared_ptr ClientGameData::system(bool allow_load) const { + if (!this->system_data.get() && allow_load) { + throw runtime_error("system data is not loaded"); } - if (!this->player_data.get() && allow_load) { - if (this->bb_username.empty()) { - this->player_data.reset(new SavedPlayerDataBB()); - } else { - this->load_player_data(); - } - } - return this->player_data; + return this->system_data; } -shared_ptr ClientGameData::account(bool allow_load) const { - if (!this->account_data.get() && allow_load) { +shared_ptr ClientGameData::character(bool allow_load, bool allow_overlay) { + if (this->overlay_character_data && allow_overlay) { + return this->overlay_character_data; + } + if (!this->character_data && allow_load) { + if (this->bb_character_index < 0) { + throw runtime_error("character index not specified"); + } + this->load_all_files(); + } + return this->character_data; +} + +shared_ptr ClientGameData::character(bool allow_load, bool allow_overlay) const { + if (allow_overlay && this->overlay_character_data) { + return this->overlay_character_data; + } + if (!this->character_data && allow_load) { + throw runtime_error("character data is not loaded"); + } + return this->character_data; +} + +shared_ptr ClientGameData::guild_cards(bool allow_load) { + if (!this->guild_card_data && allow_load) { + this->load_all_files(); + } + return this->guild_card_data; +} + +shared_ptr ClientGameData::guild_cards(bool allow_load) const { + if (!this->guild_card_data && allow_load) { throw runtime_error("account data is not loaded"); } - return this->account_data; + return this->guild_card_data; } -shared_ptr ClientGameData::player(bool allow_load, bool allow_overlay) const { - if (allow_overlay && this->overlay_player_data) { - return this->overlay_player_data; - } - if (!this->player_data.get() && allow_load) { - throw runtime_error("player data is not loaded"); - } - return this->player_data; -} - -string ClientGameData::account_data_filename() const { +string ClientGameData::system_filename() const { if (this->bb_username.empty()) { - throw logic_error("non-BB players do not have account data"); + throw logic_error("non-BB players do not have system data"); } - return string_printf("system/players/account_%s.nsa", - this->bb_username.c_str()); + return string_printf("system/players/system_%s.psosys", this->bb_username.c_str()); } -string ClientGameData::player_data_filename() const { +string ClientGameData::character_filename() const { if (this->bb_username.empty()) { - throw logic_error("non-BB players do not have account data"); + throw logic_error("non-BB players do not have character data"); } - return string_printf("system/players/player_%s_%zu.nsc", - this->bb_username.c_str(), this->bb_player_index + 1); + if (this->bb_character_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); } -string ClientGameData::player_template_filename(uint8_t char_class) { - return string_printf("system/players/default_player_%hhu.nsc", char_class); +string ClientGameData::guild_card_filename() const { + if (this->bb_username.empty()) { + throw logic_error("non-BB players do not have Guild Card files"); + } + return string_printf("system/players/guild_cards_%s.psocard", this->bb_username.c_str()); } -void ClientGameData::create_player( +string ClientGameData::legacy_account_filename() const { + if (this->bb_username.empty()) { + throw logic_error("non-BB players do not have legacy account data"); + } + return string_printf("system/players/account_%s.nsa", this->bb_username.c_str()); +} + +string ClientGameData::legacy_player_filename() const { + if (this->bb_username.empty()) { + throw logic_error("non-BB players do not have legacy player data"); + } + if (this->bb_character_index < 0) { + throw logic_error("character index is not set"); + } + return string_printf( + "system/players/player_%s_%hhd.nsc", + this->bb_username.c_str(), + static_cast(this->bb_character_index + 1)); +} + +void ClientGameData::create_character_file( + uint32_t guild_card_number, + uint8_t language, const PlayerDispDataBBPreview& preview, shared_ptr level_table) { - shared_ptr data(new SavedPlayerDataBB( - load_object_file(player_template_filename(preview.visual.char_class)))); - data->update_to_latest_version(); - - try { - data->disp.apply_preview(preview); - data->disp.stats.char_stats = level_table->base_stats_for_class(data->disp.visual.char_class); - } catch (const exception& e) { - throw runtime_error(string_printf("template application failed: %s", e.what())); - } - - this->player_data = data; - - this->save_player_data(); + this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table); + this->save_character_file(); } -void ClientGameData::load_account_data() { - string filename = this->account_data_filename(); +void ClientGameData::load_all_files() { + if (this->bb_username.empty()) { + this->system_data.reset(new PSOBBSystemFile()); + this->character_data.reset(new PSOBBCharacterFile()); + this->guild_card_data.reset(new PSOBBGuildCardFile()); + return; + } - shared_ptr data; - try { - data.reset(new SavedAccountDataBB( - player_files_cache.get_obj_or_load(filename).obj)); - if (!data->signature.eq(ACCOUNT_FILE_SIGNATURE)) { - throw runtime_error("account data header is incorrect"); + this->system_data.reset(); + this->character_data.reset(); + this->guild_card_data.reset(); + + string sys_filename = this->system_filename(); + if (isfile(sys_filename)) { + this->system_data.reset(new PSOBBSystemFile(load_object_file(sys_filename))); + player_data_log.info("Loaded system data from %s", sys_filename.c_str()); + } + + if (this->bb_character_index >= 0) { + string char_filename = this->character_filename(); + if (isfile(char_filename)) { + auto f = fopen_unique(char_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.reset(new PSOBBCharacterFile(freadx(f.get()))); + 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 + // file instead + if (!this->system_data) { + this->system_data.reset(new PSOBBSystemFile(freadx(f.get()))); + player_data_log.info("Loaded system data from %s", char_filename.c_str()); + } } - player_data_log.info("Loaded account data file %s", filename.c_str()); + } - } catch (const exception& e) { - 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( - player_files_cache.get_obj_or_load( - "system/players/default.nsa") - .obj)); - if (!data->signature.eq(ACCOUNT_FILE_SIGNATURE)) { - throw runtime_error("default account data header is incorrect"); + string card_filename = this->guild_card_filename(); + if (isfile(card_filename)) { + this->guild_card_data.reset(new PSOBBGuildCardFile(load_object_file(card_filename))); + player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str()); + } + + // If any of the above files were missing, try to load from .nsa/.nsc files instead + if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) { + string nsa_filename = this->legacy_account_filename(); + shared_ptr nsa_data; + if (isfile(nsa_filename)) { + nsa_data.reset(new LegacySavedAccountDataBB(load_object_file(nsa_filename))); + if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) { + throw runtime_error("account data header is incorrect"); + } + if (!this->system_data) { + this->system_data.reset(new PSOBBSystemFile(nsa_data->system_file)); + player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str()); + } + if (!this->guild_card_data) { + this->guild_card_data.reset(new PSOBBGuildCardFile(nsa_data->guild_card_file)); + player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str()); + } + } + + if (!this->system_data) { + this->system_data.reset(new PSOBBSystemFile()); + player_data_log.info("Created new system data"); + } + if (!this->guild_card_data) { + this->guild_card_data.reset(new PSOBBGuildCardFile()); + player_data_log.info("Created new Guild Card data"); + } + + if (!this->character_data && (this->bb_character_index >= 0)) { + string nsc_filename = this->legacy_player_filename(); + auto nsc_data = load_object_file(nsc_filename); + if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) { + nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0; + nsc_data.unused.clear(); + nsc_data.battle_records.place_counts.clear(0); + nsc_data.battle_records.disconnect_count = 0; + nsc_data.battle_records.unknown_a1.clear(0); + } else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) { + throw runtime_error("legacy player data has incorrect signature"); + } + + this->character_data.reset(new PSOBBCharacterFile()); + this->character_data->inventory = nsc_data.inventory; + this->character_data->disp = nsc_data.disp; + this->character_data->play_time_seconds = nsc_data.disp.play_time; + this->character_data->unknown_a2 = nsc_data.unknown_a2; + this->character_data->quest_flags = nsc_data.quest_flags; + this->character_data->death_count = nsc_data.death_count; + this->character_data->bank = nsc_data.bank; + this->character_data->guild_card.guild_card_number = this->guild_card_number; + this->character_data->guild_card.name = nsc_data.disp.name; + this->character_data->guild_card.team_name = this->system_data->team_name; + this->character_data->guild_card.description = nsc_data.guild_card_description; + this->character_data->guild_card.present = 1; + this->character_data->guild_card.language = nsc_data.inventory.language; + this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id; + this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class; + this->character_data->auto_reply = nsc_data.auto_reply; + this->character_data->info_board = nsc_data.info_board; + this->character_data->battle_records = nsc_data.battle_records; + this->character_data->challenge_records = nsc_data.challenge_records; + this->character_data->tech_menu_config = nsc_data.tech_menu_config; + this->character_data->quest_global_flags = nsc_data.quest_global_flags; + if (nsa_data) { + this->character_data->option_flags = nsa_data->option_flags; + this->character_data->symbol_chats = nsa_data->symbol_chats; + this->character_data->shortcuts = nsa_data->shortcuts; + player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str()); + } else { + player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str()); + } } - player_data_log.info("Loaded default account data file"); } - this->account_data = data; -} - -void ClientGameData::save_account_data() const { - if (!this->account_data.get()) { - throw logic_error("save_account_data called when no account data loaded"); + this->blocked_senders.fill(0); + for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) { + if (this->guild_card_data->blocked[z].present) { + this->blocked_senders[z] = this->guild_card_data->blocked[z].guild_card_number; + } } - string filename = this->account_data_filename(); - 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()); + + if (this->character_data) { + this->last_play_time_update = now(); } } -void ClientGameData::load_player_data() { - this->last_play_time_update = now(); - string filename = this->player_data_filename(); - this->player_data.reset(new SavedPlayerDataBB( - player_files_cache.get_obj_or_load(filename).obj)); - try { - this->player_data->update_to_latest_version(); - } catch (const exception&) { - this->player_data.reset(); - player_files_cache.delete_key(filename); - throw; +void ClientGameData::save_system_file() const { + if (!this->system_data) { + throw logic_error("no system file loaded"); } - player_data_log.info("Loaded player data file %s", filename.c_str()); + string filename = this->system_filename(); + save_object_file(filename, *this->system_data); + player_data_log.info("Saved system file %s", filename.c_str()); } -void ClientGameData::save_player_data() { - if (!this->player_data.get()) { - throw logic_error("save_player_data called when no player data loaded"); +void ClientGameData::save_character_file() { + if (!this->system_data.get()) { + throw logic_error("no system file loaded"); + } + if (!this->character_data.get()) { + throw logic_error("no character file loaded"); } if (this->should_update_play_time) { // This is slightly inaccurate, since fractions of a second are truncated // off each time we save. I'm lazy, so insert shrug emoji here. uint64_t t = now(); uint64_t seconds = (t - this->last_play_time_update) / 1000000; - this->player_data->disp.play_time += seconds; + this->character_data->disp.play_time += seconds; + this->character_data->play_time_seconds = this->character_data->disp.play_time; player_data_log.info("Added %" PRIu64 " seconds to play time", seconds); this->last_play_time_update = t; } - string filename = this->player_data_filename(); - 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()); - } + + string filename = this->character_filename(); + auto f = fopen_unique(filename, "wb"); + PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBSystemFile), 0x00E7, 0x00000000}; + fwritex(f.get(), header); + fwritex(f.get(), *this->character_data); + fwritex(f.get(), *this->system_data); + player_data_log.info("Saved character file %s", filename.c_str()); } -void SavedPlayerDataBB::update_to_latest_version() { - if (this->signature == PLAYER_FILE_SIGNATURE_V0) { - this->signature = PLAYER_FILE_SIGNATURE_V1; - this->unused.clear(); - this->battle_records.place_counts.clear(0); - this->battle_records.disconnect_count = 0; - this->battle_records.unknown_a1.clear(0); - } else if (this->signature != PLAYER_FILE_SIGNATURE_V1) { - throw runtime_error("player data has incorrect signature"); - } -} - -// TODO: Eliminate duplication between this function and the parallel function -// in PlayerBank -void SavedPlayerDataBB::add_item(const ItemData& item) { - uint32_t pid = item.primary_identifier(); - - // Annoyingly, meseta is in the disp data, not in the inventory struct. If the - // item is meseta, we have to modify disp instead. - if (pid == MESETA_IDENTIFIER) { - this->add_meseta(item.data2d); - return; - } - - // Handle combinable items - size_t combine_max = item.max_stack_size(); - if (combine_max > 1) { - // Get the item index if there's already a stack of the same item in the - // player's inventory - size_t y; - for (y = 0; y < this->inventory.num_items; y++) { - if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) { - break; - } - } - - // If we found an existing stack, add it to the total and return - if (y < this->inventory.num_items) { - size_t new_stack_size = this->inventory.items[y].data.data1[5] + item.data1[5]; - if (new_stack_size > combine_max) { - throw out_of_range("stack is too large"); - } - this->inventory.items[y].data.data1[5] = new_stack_size; - return; - } - } - - // If we get here, then it's not meseta and not a combine item, so it needs to - // go into an empty inventory slot - if (this->inventory.num_items >= 30) { - throw out_of_range("inventory is full"); - } - auto& inv_item = this->inventory.items[this->inventory.num_items]; - inv_item.present = 1; - inv_item.unknown_a1 = 0; - inv_item.flags = 0; - inv_item.data = item; - this->inventory.num_items++; -} - -// TODO: Eliminate code duplication between this function and the parallel -// function in PlayerBank -ItemData SavedPlayerDataBB::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) { - ItemData ret; - - // If we're removing meseta (signaled by an invalid item ID), then create a - // meseta item. - if (item_id == 0xFFFFFFFF) { - this->remove_meseta(amount, allow_meseta_overdraft); - ret.data1[0] = 0x04; - ret.data2d = amount; - return ret; - } - - size_t index = this->inventory.find_item(item_id); - auto& inventory_item = this->inventory.items[index]; - - // If the item is a combine item and are we removing less than we have of it, - // then create a new item and reduce the amount of the existing stack. Note - // that passing amount == 0 means to remove the entire stack, so this only - // applies if amount is nonzero. - if (amount && (inventory_item.data.stack_size() > 1) && - (amount < inventory_item.data.data1[5])) { - ret = inventory_item.data; - ret.data1[5] = amount; - ret.id = 0xFFFFFFFF; - inventory_item.data.data1[5] -= amount; - return ret; - } - - // If we get here, then it's not meseta, and either it's not a combine item or - // we're removing the entire stack. Delete the item from the inventory slot - // and return the deleted item. - ret = inventory_item.data; - this->inventory.num_items--; - for (size_t x = index; x < this->inventory.num_items; x++) { - this->inventory.items[x] = this->inventory.items[x + 1]; - } - auto& last_item = this->inventory.items[this->inventory.num_items]; - last_item.present = 0; - last_item.unknown_a1 = 0; - last_item.flags = 0; - last_item.data.clear(); - return ret; -} - -void SavedPlayerDataBB::add_meseta(uint32_t amount) { - this->disp.stats.meseta = min(static_cast(this->disp.stats.meseta) + amount, 999999); -} - -void SavedPlayerDataBB::remove_meseta(uint32_t amount, bool allow_overdraft) { - if (amount <= this->disp.stats.meseta) { - this->disp.stats.meseta -= amount; - } else if (allow_overdraft) { - this->disp.stats.meseta = 0; - } else { - throw out_of_range("player does not have enough meseta"); - } -} - -uint8_t SavedPlayerDataBB::get_technique_level(uint8_t which) const { - return (this->disp.technique_levels_v1[which] == 0xFF) - ? 0xFF - : (this->disp.technique_levels_v1[which] + this->inventory.items[which].extension_data1); -} - -void SavedPlayerDataBB::set_technique_level(uint8_t which, uint8_t level) { - if (level == 0xFF) { - this->disp.technique_levels_v1[which] = 0xFF; - this->inventory.items[which].extension_data1 = 0x00; - } else if (level <= 0x0E) { - this->disp.technique_levels_v1[which] = level; - this->inventory.items[which].extension_data1 = 0x00; - } else { - this->disp.technique_levels_v1[which] = 0x0E; - this->inventory.items[which].extension_data1 = level - 0x0E; - } -} - -uint8_t SavedPlayerDataBB::get_material_usage(MaterialType which) const { - switch (which) { - case MaterialType::HP: - return this->inventory.hp_from_materials >> 1; - case MaterialType::TP: - return this->inventory.tp_from_materials >> 1; - case MaterialType::POWER: - case MaterialType::MIND: - case MaterialType::EVADE: - case MaterialType::DEF: - case MaterialType::LUCK: - return this->inventory.items[8 + static_cast(which)].extension_data2; - default: - throw logic_error("invalid material type"); - } -} - -void SavedPlayerDataBB::set_material_usage(MaterialType which, uint8_t usage) { - switch (which) { - case MaterialType::HP: - this->inventory.hp_from_materials = usage << 1; - break; - case MaterialType::TP: - this->inventory.tp_from_materials = usage << 1; - break; - case MaterialType::POWER: - case MaterialType::MIND: - case MaterialType::EVADE: - case MaterialType::DEF: - case MaterialType::LUCK: - this->inventory.items[8 + static_cast(which)].extension_data2 = usage; - break; - default: - throw logic_error("invalid material type"); - } -} - -void SavedPlayerDataBB::clear_all_material_usage() { - this->inventory.hp_from_materials = 0; - this->inventory.tp_from_materials = 0; - for (size_t z = 0; z < 5; z++) { - this->inventory.items[z + 8].extension_data2 = 0; - } -} - -void SavedPlayerDataBB::print_inventory(FILE* stream, GameVersion version, shared_ptr name_index) const { - fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load()); - fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items); - for (size_t x = 0; x < this->inventory.num_items; x++) { - const auto& item = this->inventory.items[x]; - auto name = name_index->describe_item(version, item.data); - auto hex = item.data.hex(); - fprintf(stream, "[PlayerInventory] %2zu: %s (%s)\n", x, hex.c_str(), name.c_str()); +void ClientGameData::save_guild_card_file() const { + if (!this->guild_card_data.get()) { + throw logic_error("no Guild Card file loaded"); } + string filename = this->guild_card_filename(); + save_object_file(filename, *this->guild_card_data); + player_data_log.info("Saved Guild Card file %s", filename.c_str()); } diff --git a/src/Player.hh b/src/Player.hh index d7ac73b3..fe9e20b8 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -30,85 +30,15 @@ struct PendingCardTrade { std::vector> card_to_count; }; -constexpr uint64_t PLAYER_FILE_SIGNATURE_V0 = 0x6E65777365727620; -constexpr uint64_t PLAYER_FILE_SIGNATURE_V1 = 0xA904332D5CEF0296; - -struct SavedPlayerDataBB { // .nsc file format - /* 0000 */ be_uint64_t signature = PLAYER_FILE_SIGNATURE_V1; - /* 0008 */ parray unused; - /* 0028 */ PlayerRecords_Battle battle_records; - /* 0040 */ PlayerDispDataBBPreview preview; - /* 00BC */ pstring auto_reply; - /* 0214 */ PlayerBank bank; - /* 14DC */ PlayerRecordsBB_Challenge challenge_records; - /* 161C */ PlayerDispDataBB disp; - /* 17AC */ pstring guild_card_description; - /* 185C */ pstring info_board; - /* 19B4 */ PlayerInventory inventory; - /* 1D00 */ parray quest_data1; - /* 1F08 */ parray quest_data2; - /* 1F60 */ parray tech_menu_config; - /* 1F88 */ - - void update_to_latest_version(); - - void add_item(const ItemData& item); - ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft); - void add_meseta(uint32_t amount); - void remove_meseta(uint32_t amount, bool allow_overdraft); - - uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D - void set_technique_level(uint8_t which, uint8_t level); - - enum class MaterialType : int8_t { - HP = -2, - TP = -1, - POWER = 0, - MIND = 1, - EVADE = 2, - DEF = 3, - LUCK = 4, - }; - - uint8_t get_material_usage(MaterialType which) const; - void set_material_usage(MaterialType which, uint8_t usage); - void clear_all_material_usage(); - - void print_inventory(FILE* stream, GameVersion version, std::shared_ptr name_index) const; -} __attribute__((packed)); - -enum AccountFlag { - IN_DRESSING_ROOM = 0x00000001, -}; - -struct SavedAccountDataBB { // .nsa file format - pstring signature; - parray blocked_senders; - PSOBBGuildCardFile guild_card_file; - PSOBBSystemFile system_file; - le_uint32_t unused; - le_uint32_t option_flags; - parray shortcuts; - parray symbol_chats; - pstring team_name; -} __attribute__((packed)); - class ClientGameData { -private: - std::shared_ptr account_data; - // The overlay player data is used in battle and challenge modes, when player - // data is temporarily replaced in-game. In other play modes and in lobbies, - // overlay_player_data is null. - std::shared_ptr overlay_player_data; - std::shared_ptr player_data; - uint64_t last_play_time_update; - public: uint32_t guild_card_number; bool should_update_play_time; // The following fields are not saved, and are only used in certain situations + std::array blocked_senders; + // Null unless the client is within the trade sequence (D0-D4 or EE commands) std::unique_ptr pending_item_trade; std::unique_ptr pending_card_trade; @@ -118,10 +48,9 @@ public: // These are only used if the client is BB std::string bb_username; - size_t bb_player_index; + int8_t bb_character_index; ItemData identify_result; std::array, 3> shop_contents; - bool should_save; ClientGameData(); ~ClientGameData(); @@ -129,28 +58,48 @@ public: void create_battle_overlay(std::shared_ptr rules, std::shared_ptr level_table); void create_challenge_overlay(GameVersion version, size_t template_index, std::shared_ptr level_table); inline void delete_overlay() { - this->overlay_player_data.reset(); + this->overlay_character_data.reset(); } inline bool has_overlay() const { - return this->overlay_player_data.get() != nullptr; + return this->overlay_character_data.get() != nullptr; } - std::shared_ptr account(bool allow_load = true); - std::shared_ptr player(bool allow_load = true, bool allow_overlay = true); - std::shared_ptr account(bool allow_load = true) const; - std::shared_ptr player(bool allow_load = true, bool allow_overlay = true) const; + std::shared_ptr system(bool allow_load = true); + std::shared_ptr system(bool allow_load = true) const; - std::string account_data_filename() const; - std::string player_data_filename() const; - static std::string player_template_filename(uint8_t char_class); + std::shared_ptr character(bool allow_load = true, bool allow_overlay = true); + std::shared_ptr character(bool allow_load = true, bool allow_overlay = true) const; - void create_player( + std::shared_ptr guild_cards(bool allow_load = true); + std::shared_ptr guild_cards(bool allow_load = true) const; + + void create_character_file( + uint32_t guild_card_number, + uint8_t language, const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); - void load_account_data(); - void save_account_data() const; - void load_player_data(); + void save_system_file() const; // Note: This function is not const because it updates the player's play time. - void save_player_data(); + void save_character_file(); + void save_guild_card_file() const; + +private: + // The overlay character data is used in battle and challenge modes, when + // character data is temporarily replaced in-game. In other play modes and in + // lobbies, overlay_character_data is null. + std::shared_ptr system_data; + std::shared_ptr overlay_character_data; + std::shared_ptr character_data; + std::shared_ptr guild_card_data; + uint64_t last_play_time_update; + + void load_all_files(); + + std::string system_filename() const; + std::string character_filename() const; + std::string guild_card_filename() const; + + std::string legacy_player_filename() const; + std::string legacy_account_filename() const; }; diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 5cc377b2..d89b9423 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -19,8 +19,6 @@ using namespace std; -FileContentsCache player_files_cache(300 * 1000 * 1000); - uint32_t PlayerVisualConfig::compute_name_color_checksum(uint32_t name_color) { uint8_t x = (random_object() % 0xFF) + 1; uint8_t y = (random_object() % 0xFF) + 1; @@ -222,20 +220,6 @@ void GuildCardBB::clear() { this->char_class = 0; } -void PlayerBank::load(const string& 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, bool save_to_filesystem) const { - player_files_cache.replace(filename, this, sizeof(*this)); - if (save_to_filesystem) { - save_file(filename, this, sizeof(*this)); - } -} - void PlayerLobbyDataPC::clear() { this->player_tag = 0; this->guild_card_number = 0; @@ -288,10 +272,17 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Chall times_ep1_online(rec.times_ep1_online), times_ep2_online(0), times_ep1_offline(0), - unknown_g3(rec.unknown_g3), + grave_is_ep2(0), + grave_stage_num(rec.grave_stage_num), + grave_floor(rec.grave_floor), + unknown_g0(0), grave_deaths(rec.grave_deaths), unknown_u4(0), - grave_coords_time(rec.grave_coords_time), + grave_time(rec.grave_time), + unknown_g1(rec.unknown_g1), + grave_x(rec.grave_x), + grave_y(rec.grave_y), + grave_z(rec.grave_z), grave_team(rec.grave_team.decode(), 1), grave_message(rec.grave_message.decode(), 1), unknown_m5(0), @@ -305,10 +296,17 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Chall times_ep1_online(rec.times_ep1_online), times_ep2_online(0), times_ep1_offline(0), - unknown_g3(rec.unknown_g3), + grave_is_ep2(0), + grave_stage_num(rec.grave_stage_num), + grave_floor(rec.grave_floor), + unknown_g0(0), grave_deaths(rec.grave_deaths), unknown_u4(0), - grave_coords_time(rec.grave_coords_time), + grave_time(rec.grave_time), + unknown_g1(rec.unknown_g1), + grave_x(rec.grave_x), + grave_y(rec.grave_y), + grave_z(rec.grave_z), grave_team(rec.grave_team.decode(), 1), grave_message(rec.grave_message.decode(), 1), unknown_m5(0), @@ -322,14 +320,24 @@ PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Chall times_ep1_online(rec.stats.times_ep1_online), times_ep2_online(rec.stats.times_ep2_online), times_ep1_offline(rec.stats.times_ep1_offline), - unknown_g3(rec.stats.unknown_g3), + grave_is_ep2(rec.stats.grave_is_ep2), + grave_stage_num(rec.stats.grave_stage_num), + grave_floor(rec.stats.grave_floor), + unknown_g0(rec.stats.unknown_g0), grave_deaths(rec.stats.grave_deaths), unknown_u4(rec.stats.unknown_u4), - grave_coords_time(rec.stats.grave_coords_time), + grave_time(rec.stats.grave_time), + unknown_g1(rec.stats.unknown_g1), + grave_x(rec.stats.grave_x), + grave_y(rec.stats.grave_y), + grave_z(rec.stats.grave_z), grave_team(rec.stats.grave_team.decode(), 1), grave_message(rec.stats.grave_message.decode(), 1), unknown_m5(rec.stats.unknown_m5), unknown_t6(rec.stats.unknown_t6), + ep1_online_award_state(rec.stats.ep1_online_award_state), + ep2_online_award_state(rec.stats.ep2_online_award_state), + ep1_offline_award_state(rec.stats.ep1_offline_award_state), rank_title(rec.rank_title.decode(), 1), unknown_l7(rec.unknown_l7) {} @@ -339,9 +347,23 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const { ret.unknown_u0 = this->unknown_u0; ret.rank_title.encode(this->rank_title.decode()); ret.times_ep1_online = this->times_ep1_online; - ret.unknown_g3 = 0; + if (this->grave_is_ep2) { + ret.grave_stage_num = 0; + ret.grave_floor = 0; + ret.unknown_g1 = 0; + ret.grave_x = 0; + ret.grave_y = 0; + ret.grave_z = 0; + } else { + ret.grave_stage_num = this->grave_stage_num; + ret.grave_floor = this->grave_floor; + ret.unknown_g1 = this->unknown_g1; + ret.grave_x = this->grave_x; + ret.grave_y = this->grave_y; + ret.grave_z = this->grave_z; + } + ret.grave_time = this->grave_time; ret.grave_deaths = this->grave_deaths; - ret.grave_coords_time = this->grave_coords_time; ret.grave_team.encode(this->grave_team.decode()); ret.grave_message.encode(this->grave_message.decode()); ret.times_ep1_offline = this->times_ep1_offline; @@ -355,9 +377,23 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const { ret.unknown_u0 = this->unknown_u0; ret.rank_title = this->rank_title; ret.times_ep1_online = this->times_ep1_online; - ret.unknown_g3 = 0; + if (this->grave_is_ep2) { + ret.grave_stage_num = 0; + ret.grave_floor = 0; + ret.unknown_g1 = 0; + ret.grave_x = 0; + ret.grave_y = 0; + ret.grave_z = 0; + } else { + ret.grave_stage_num = this->grave_stage_num; + ret.grave_floor = this->grave_floor; + ret.unknown_g1 = this->unknown_g1; + ret.grave_x = this->grave_x; + ret.grave_y = this->grave_y; + ret.grave_z = this->grave_z; + } + ret.grave_time = this->grave_time; ret.grave_deaths = this->grave_deaths; - ret.grave_coords_time = this->grave_coords_time; ret.grave_team.encode(this->grave_team.decode()); ret.grave_message.encode(this->grave_message.decode()); ret.times_ep1_offline = this->times_ep1_offline; @@ -372,15 +408,25 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge() const { ret.stats.times_ep1_online = this->times_ep1_online; ret.stats.times_ep2_online = this->times_ep2_online; ret.stats.times_ep1_offline = this->times_ep1_offline; - ret.stats.unknown_g3 = this->unknown_g3; + ret.stats.grave_is_ep2 = this->grave_is_ep2; + ret.stats.grave_stage_num = this->grave_stage_num; + ret.stats.grave_floor = this->grave_floor; + ret.stats.unknown_g0 = this->unknown_g0; ret.stats.grave_deaths = this->grave_deaths; ret.stats.unknown_u4 = this->unknown_u4; - ret.stats.grave_coords_time = this->grave_coords_time; - ret.stats.grave_team.encode(this->grave_team.decode()); - ret.stats.grave_message.encode(this->grave_message.decode()); + ret.stats.grave_time = this->grave_time; + ret.stats.unknown_g1 = this->unknown_g1; + ret.stats.grave_x = this->grave_x; + ret.stats.grave_y = this->grave_y; + ret.stats.grave_z = this->grave_z; + ret.stats.grave_team.encode(this->grave_team.decode(), 1); + ret.stats.grave_message.encode(this->grave_message.decode(), 1); ret.stats.unknown_m5 = this->unknown_m5; ret.stats.unknown_t6 = this->unknown_t6; - ret.rank_title.encode(this->rank_title.decode()); + ret.stats.ep1_online_award_state = this->ep1_online_award_state; + ret.stats.ep2_online_award_state = this->ep2_online_award_state; + ret.stats.ep1_offline_award_state = this->ep1_offline_award_state; + ret.rank_title.encode(this->rank_title.decode(), 1); ret.unknown_l7 = this->unknown_l7; return ret; } @@ -956,3 +1002,8 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(GameVersion throw runtime_error("invalid class flags on original player"); } } + +SymbolChat::SymbolChat() + : spec(0), + corner_objects(0x00FF), + face_parts() {} diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 76e22867..bd326295 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -18,8 +18,6 @@ class ItemParameterTable; -extern FileContentsCache player_files_cache; - // PSO V2 stored some extra data in the character structs in a format that I'm // sure Sega thought was very clever for backward compatibility, but for us is // just plain annoying. Specifically, they used the third and fourth bytes of @@ -89,12 +87,6 @@ struct PlayerBank { /* 0008 */ parray items; /* 12C8 */ - void load(const std::string& filename); - 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); - void add_item(const ItemData& item); ItemData remove_item(uint32_t item_id, uint32_t amount); size_t find_item(uint32_t item_id); @@ -325,9 +317,21 @@ struct PlayerRecordsDCPC_Challenge { /* 02 */ parray unknown_u0; /* 04 */ pstring rank_title; /* 10 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times - /* 34 */ le_uint16_t unknown_g3 = 0; + /* 34 */ uint8_t grave_stage_num = 0; + /* 35 */ uint8_t grave_floor = 0; /* 36 */ le_uint16_t grave_deaths = 0; - /* 38 */ parray grave_coords_time; + // grave_time is encoded with the following bit fields: + // YYYYMMMM DDDDDDDD HHHHHHHH mmmmmmmm + // Y = year after 2000 (clamped to [0, 15]) + // M = month + // D = day + // H = hour + // m = minute + /* 38 */ le_uint32_t grave_time = 0; + /* 3C */ le_uint32_t unknown_g1 = 0; + /* 40 */ le_float grave_x = 0.0f; + /* 44 */ le_float grave_y = 0.0f; + /* 48 */ le_float grave_z = 0.0f; /* 4C */ pstring grave_team; /* 60 */ pstring grave_message; /* 78 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times @@ -345,6 +349,7 @@ template struct PlayerRecordsV3_Challenge { using U16T = typename std::conditional::type; using U32T = typename std::conditional::type; + using FloatT = typename std::conditional::type; // Offsets are (1) relative to start of C5 entry, and (2) relative to start // of save file structure @@ -354,10 +359,17 @@ struct PlayerRecordsV3_Challenge { /* 04:20 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time /* 28:44 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time /* 3C:58 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time - /* 60:7C */ parray unknown_g3; + /* 60:7C */ uint8_t grave_is_ep2 = 0; + /* 61:7D */ uint8_t grave_stage_num = 0; + /* 62:7E */ uint8_t grave_floor = 0; + /* 63:7F */ uint8_t unknown_g0 = 0; /* 64:80 */ U16T grave_deaths = 0; /* 66:82 */ parray unknown_u4; - /* 68:84 */ parray grave_coords_time; + /* 68:84 */ U32T grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge + /* 6C:88 */ U32T unknown_g1 = 0; + /* 70:8C */ FloatT grave_x = 0.0f; + /* 74:90 */ FloatT grave_y = 0.0f; + /* 78:94 */ FloatT grave_z = 0.0f; /* 7C:98 */ pstring grave_team; /* 90:AC */ pstring grave_message; /* B0:CC */ parray unknown_m5; @@ -386,10 +398,17 @@ struct PlayerRecordsBB_Challenge { /* 0004 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time /* 0028 */ parray times_ep2_online; // Encrypted; see decrypt_challenge_time /* 003C */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time - /* 0060 */ parray unknown_g3; + /* 0060 */ uint8_t grave_is_ep2 = 0; + /* 0061 */ uint8_t grave_stage_num = 0; + /* 0062 */ uint8_t grave_floor = 0; + /* 0063 */ uint8_t unknown_g0 = 0; /* 0064 */ le_uint16_t grave_deaths = 0; /* 0066 */ parray unknown_u4; - /* 0068 */ parray grave_coords_time; + /* 0068 */ le_uint32_t grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge + /* 006C */ le_uint32_t unknown_g1 = 0; + /* 0070 */ le_float grave_x = 0.0f; + /* 0074 */ le_float grave_y = 0.0f; + /* 0078 */ le_float grave_z = 0.0f; /* 007C */ pstring grave_team; /* 00A4 */ pstring grave_message; /* 00E4 */ parray unknown_m5; @@ -428,7 +447,7 @@ struct PlayerRecords_Battle { template struct ChoiceSearchConfig { // 0 = enabled, 1 = disabled. Unused for command C3 - le_uint32_t choice_search_disabled = 0; + le_uint32_t disabled = 1; struct Entry { ItemIDT parent_category_id = 0; ItemIDT category_id = 0; @@ -592,3 +611,27 @@ struct ChallengeTemplateDefinition { }; const ChallengeTemplateDefinition& get_challenge_template_definition(GameVersion version, uint32_t class_flags, size_t index); + +struct SymbolChat { + // Bits: ----------------------DMSSSCCCFF + // S = sound, C = face color, F = face shape, D = capture, M = mute sound + /* 00 */ le_uint32_t spec = 0; + + // Corner objects are specified in reading order ([0] is the top-left one). + // Bits (each entry): ---VHCCCZZZZZZZZ + // V = reverse vertical, H = reverse horizontal, C = color, Z = object + // If Z is all 1 bits (0xFF), no corner object is rendered. + /* 04 */ parray corner_objects; + + struct FacePart { + uint8_t type = 0xFF; // FF = no part in this slot + uint8_t x = 0; + uint8_t y = 0; + // Bits: ------VH (V = reverse vertical, H = reverse horizontal) + uint8_t flags = 0; + } __attribute__((packed)); + /* 0C */ parray face_parts; + /* 3C */ + + SymbolChat(); +} __attribute__((packed)); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 5196cd85..bf791ae9 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1005,9 +1005,7 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { string version_string = is_old_format ? cmd.var.old_client_config.as_string() : cmd.var.new_clients.client_config.as_string(); - print_data(stderr, version_string); strip_trailing_zeroes(version_string); - // Note: Tethealla PSOBB is actually Japanese PSOBB, but with most of the // files replaced with English text/graphics/etc. For this reason, it still // reports its language as Japanese, so we have to account for that @@ -1019,7 +1017,7 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } c->channel.language = c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? 1 : cmd.language; c->bb_connection_phase = cmd.connection_phase; - c->game_data.bb_player_index = cmd.character_slot; + c->game_data.bb_character_index = cmd.character_slot; if (cmd.menu_id == MenuID::LOBBY) { c->preferred_lobby_id = cmd.preferred_lobby_id; @@ -1492,7 +1490,7 @@ static void on_CA_Ep3(shared_ptr c, uint16_t, uint32_t, string& data) { l->battle_record.reset(new Episode3::BattleRecord(s->ep3_behavior_flags)); for (auto existing_c : l->clients) { if (existing_c) { - auto existing_p = existing_c->game_data.player(); + auto existing_p = existing_c->game_data.character(); PlayerLobbyDataDCGC lobby_data; lobby_data.name.encode(existing_p->disp.name.decode(existing_c->language()), c->language()); lobby_data.player_tag = 0x00010000; @@ -1677,7 +1675,7 @@ static void on_09(shared_ptr c, uint16_t, uint32_t, string& data) { for (size_t x = 0; x < game->max_clients; x++) { const auto& game_c = game->clients[x]; if (game_c.get()) { - auto player = game_c->game_data.player(); + auto player = game_c->game_data.character(); string name = player->disp.name.decode(game_c->language()); if (game->is_ep3()) { info += string_printf("%zu: $C6%s$C7 L%" PRIu32 "\n", @@ -1860,12 +1858,12 @@ void set_lobby_quest(shared_ptr l, shared_ptr q) { // If an overlay was created, item IDs need to be assigned if (lc->game_data.has_overlay()) { - auto overlay = lc->game_data.player(); + auto overlay = lc->game_data.character(); for (size_t z = 0; z < overlay->inventory.num_items; z++) { overlay->inventory.items[z].data.id = l->generate_item_id(client_id); } lc->log.info("Assigned overlay item IDs"); - lc->game_data.player()->print_inventory(stderr, lc->version(), s->item_name_index); + overlay->print_inventory(stderr, lc->version(), s->item_name_index); } string bin_filename = vq->bin_filename(); @@ -1955,7 +1953,7 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { } case MainMenuItemID::PROXY_DESTINATIONS: - if (!c->game_data.player(false, false)) { + if (!c->game_data.character(false, false)) { send_get_player_info(c); } send_proxy_destinations_menu(c); @@ -2207,7 +2205,7 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { send_lobby_message_box(c, "$C6Incorrect password."); break; } - auto p = c->game_data.player(); + auto p = c->game_data.character(); if (p->disp.stats.level < game->min_level) { send_lobby_message_box(c, "$C6Your level is too\nlow to join this\ngame."); break; @@ -2361,7 +2359,7 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { break; } if (team_name.empty()) { - team_name = c->game_data.player()->disp.name.decode(c->language()); + team_name = c->game_data.character()->disp.name.decode(c->language()); team_name += string_printf("/%" PRIX32, c->license->serial_number); } auto s = c->require_server_state(); @@ -2726,35 +2724,36 @@ static void on_13_A7_V3_BB(shared_ptr c, uint16_t command, uint32_t flag static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, string& data) { auto s = c->require_server_state(); - auto player = c->game_data.player(); - auto account = c->game_data.account(); + auto player = c->game_data.character(); switch (c->version()) { case GameVersion::DC: { if (c->config.check_flag(Client::Flag::IS_DC_V1)) { - const auto& pd = check_size_t(data); - player->inventory = pd.inventory; - player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language); + const auto& cmd = check_size_t(data); + player->inventory = cmd.inventory; + player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language); } else { - const auto& pd = check_size_t(data, 0xFFFF); - player->inventory = pd.inventory; - player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language); - player->battle_records = pd.records.battle; - player->challenge_records = pd.records.challenge; - // TODO: Parse choice search config + const auto& cmd = check_size_t(data, 0xFFFF); + player->inventory = cmd.inventory; + player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language); + player->battle_records = cmd.records.battle; + player->challenge_records = cmd.records.challenge; + player->choice_search_config = cmd.choice_search_config; } break; } case GameVersion::PC: { - const auto& pd = check_size_t(data, 0xFFFF); - player->inventory = pd.inventory; - player->disp = pd.disp.to_bb(player->inventory.language, player->inventory.language); - player->battle_records = pd.records.battle; - player->challenge_records = pd.records.challenge; - // TODO: Parse choice search config - account->blocked_senders = pd.blocked_senders; - if (pd.auto_reply_enabled) { - string auto_reply = data.substr(sizeof(pd)); + const auto& cmd = check_size_t(data, 0xFFFF); + player->inventory = cmd.inventory; + player->disp = cmd.disp.to_bb(player->inventory.language, player->inventory.language); + player->battle_records = cmd.records.battle; + player->challenge_records = cmd.records.challenge; + player->choice_search_config = cmd.choice_search_config; + for (size_t z = 0; z < cmd.blocked_senders.size(); z++) { + c->game_data.blocked_senders.at(z) = cmd.blocked_senders[z]; + } + if (cmd.auto_reply_enabled) { + string auto_reply = data.substr(sizeof(cmd)); strip_trailing_zeroes(auto_reply); if (auto_reply.size() & 1) { auto_reply.push_back(0); @@ -2772,9 +2771,9 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri if (!c->config.check_flag(Client::Flag::IS_EPISODE_3)) { throw runtime_error("non-Episode 3 client sent Episode 3 player data"); } - const auto* pd3 = &check_size_t(data); - c->game_data.ep3_config.reset(new Episode3::PlayerConfig(pd3->ep3_config)); - cmd = reinterpret_cast(pd3); + const auto* cmd3 = &check_size_t(data); + c->game_data.ep3_config.reset(new Episode3::PlayerConfig(cmd3->ep3_config)); + cmd = reinterpret_cast(cmd3); } else { if (c->config.check_flag(Client::Flag::IS_EPISODE_3)) { c->config.set_flag(Client::Flag::IS_EP3_TRIAL_EDITION); @@ -2817,7 +2816,9 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri player->challenge_records = cmd->records.challenge; // TODO: Parse choice search config player->info_board.encode(cmd->info_board.decode(player->inventory.language), player->inventory.language); - account->blocked_senders = cmd->blocked_senders; + for (size_t z = 0; z < cmd->blocked_senders.size(); z++) { + c->game_data.blocked_senders.at(z) = cmd->blocked_senders[z]; + } if (cmd->auto_reply_enabled) { string auto_reply = data.substr(sizeof(cmd), 0xAC); strip_trailing_zeroes(auto_reply); @@ -2836,7 +2837,9 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri player->challenge_records = cmd.records.challenge; // TODO: Parse choice search config player->info_board = cmd.info_board; - account->blocked_senders = cmd.blocked_senders; + for (size_t z = 0; z < cmd.blocked_senders.size(); z++) { + c->game_data.blocked_senders.at(z) = cmd.blocked_senders[z]; + } if (cmd.auto_reply_enabled) { string auto_reply = data.substr(sizeof(cmd), 0xAC); strip_trailing_zeroes(auto_reply); @@ -2872,14 +2875,14 @@ 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; - size_t prev_bb_player_index = c->game_data.bb_player_index; + 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.bb_player_index = c->pending_bb_save_player_index; + c->game_data.bb_character_index = c->pending_bb_save_character_index; bool failure = false; try { - c->game_data.save_player_data(); + c->game_data.save_character_file(); } catch (const exception& e) { send_text_message_printf(c, "$C6PSOBB player data could\nnot be saved:\n%s", e.what()); failure = true; @@ -2888,12 +2891,12 @@ static void on_61_98(shared_ptr c, uint16_t command, uint32_t flag, stri if (!failure) { send_text_message_printf(c, "$C6BB player data saved\nas player %hhu for user\n%s", - static_cast(c->pending_bb_save_player_index + 1), + static_cast(c->pending_bb_save_character_index + 1), c->pending_bb_save_username.c_str()); } c->game_data.bb_username = prev_bb_username; - c->game_data.bb_player_index = prev_bb_player_index; + c->game_data.bb_character_index = prev_bb_character_index; c->pending_bb_save_username.clear(); } @@ -2951,7 +2954,7 @@ static void on_06(shared_ptr c, uint16_t, uint32_t, string& data) { return; } - auto p = c->game_data.player(); + auto p = c->game_data.character(); string from_name = p->disp.name.decode(c->language()); if (from_name.size() >= 2 && from_name[0] == '\t' && (from_name[1] == 'E' || from_name[1] == 'J')) { from_name = from_name.substr(2); @@ -2990,6 +2993,8 @@ static void on_E0_BB(shared_ptr c, uint16_t, uint32_t, string& data) { static void on_E3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { const auto& cmd = check_size_t(data); + c->game_data.bb_character_index = cmd.character_index; + if (c->bb_connection_phase != 0x00) { send_approve_player_choice_bb(c); @@ -3002,15 +3007,16 @@ static void on_E3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { ClientGameData temp_gd; temp_gd.guild_card_number = c->license->serial_number; temp_gd.bb_username = c->license->bb_username; - temp_gd.bb_player_index = cmd.player_index; + temp_gd.bb_character_index = cmd.character_index; try { - auto preview = temp_gd.player()->disp.to_preview(); - send_player_preview_bb(c, cmd.player_index, &preview); + auto preview = temp_gd.character()->disp.to_preview(); + send_player_preview_bb(c, cmd.character_index, &preview); } catch (const exception& e) { // Player doesn't exist - send_player_preview_bb(c, cmd.player_index, nullptr); + c->log.warning("Can\'t load character data: %s", e.what()); + send_player_preview_bb(c, cmd.character_index, nullptr); } } } @@ -3018,10 +3024,12 @@ static void on_E3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& data) { constexpr size_t max_count = sizeof(PSOBBGuildCardFile::entries) / sizeof(PSOBBGuildCardFile::Entry); constexpr size_t max_blocked = sizeof(PSOBBGuildCardFile::blocked) / sizeof(GuildCardBB); + auto gcf = c->game_data.guild_cards(); + bool should_save = false; switch (command) { case 0x01E8: { // Check guild card file checksum const auto& cmd = check_size_t(data); - uint32_t checksum = c->game_data.account()->guild_card_file.checksum(); + uint32_t checksum = gcf->checksum(); c->log.info("(Guild card file) Server checksum = %08" PRIX32 ", client checksum = %08" PRIX32, checksum, cmd.checksum.load()); S_GuildCardChecksumResponse_BB_02E8 response = { @@ -3035,13 +3043,12 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d break; case 0x04E8: { // Add guild card auto& new_gc = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_count; z++) { - if (!gcf.entries[z].data.present) { - gcf.entries[z].data = new_gc; - gcf.entries[z].unknown_a1.clear(0); - c->log.info("Added guild card %" PRIu32 " at position %zu", - new_gc.guild_card_number.load(), z); + if (!gcf->entries[z].data.present) { + gcf->entries[z].data = new_gc; + gcf->entries[z].unknown_a1.clear(0); + c->log.info("Added guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); + should_save = true; break; } } @@ -3049,15 +3056,14 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d } case 0x05E8: { // Delete guild card auto& cmd = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_count; z++) { - if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { - c->log.info("Deleted guild card %" PRIu32 " at position %zu", - cmd.guild_card_number.load(), z); + if (gcf->entries[z].data.guild_card_number == cmd.guild_card_number) { + c->log.info("Deleted guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); for (z = 0; z < max_count - 1; z++) { - gcf.entries[z] = gcf.entries[z + 1]; + gcf->entries[z] = gcf->entries[z + 1]; } - gcf.entries[max_count - 1].clear(); + gcf->entries[max_count - 1].clear(); + should_save = true; break; } } @@ -3065,26 +3071,24 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d } case 0x06E8: { // Update guild card auto& new_gc = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_count; z++) { - if (gcf.entries[z].data.guild_card_number == new_gc.guild_card_number) { - gcf.entries[z].data = new_gc; - c->log.info("Updated guild card %" PRIu32 " at position %zu", - new_gc.guild_card_number.load(), z); + if (gcf->entries[z].data.guild_card_number == new_gc.guild_card_number) { + gcf->entries[z].data = new_gc; + c->log.info("Updated guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); + should_save = true; } } break; } case 0x07E8: { // Add blocked user auto& new_gc = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_blocked; z++) { - if (!gcf.blocked[z].present) { - gcf.blocked[z] = new_gc; - c->log.info("Added blocked guild card %" PRIu32 " at position %zu", - new_gc.guild_card_number.load(), z); + if (!gcf->blocked[z].present) { + gcf->blocked[z] = new_gc; + c->log.info("Added blocked guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); // Note: The client also sends a C6 command, so we don't have to // manually sync the actual blocked senders list here + should_save = true; break; } } @@ -3092,17 +3096,17 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d } case 0x08E8: { // Delete blocked user auto& cmd = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_blocked; z++) { - if (gcf.blocked[z].guild_card_number == cmd.guild_card_number) { + if (gcf->blocked[z].guild_card_number == cmd.guild_card_number) { c->log.info("Deleted blocked guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); for (z = 0; z < max_blocked - 1; z++) { - gcf.blocked[z] = gcf.blocked[z + 1]; + gcf->blocked[z] = gcf->blocked[z + 1]; } - gcf.blocked[max_blocked - 1].clear(); + gcf->blocked[max_blocked - 1].clear(); // Note: The client also sends a C6 command, so we don't have to // manually sync the actual blocked senders list here + should_save = true; break; } } @@ -3110,12 +3114,11 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d } case 0x09E8: { // Write comment auto& cmd = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; for (size_t z = 0; z < max_count; z++) { - if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { - gcf.entries[z].comment = cmd.comment; - c->log.info("Updated comment on guild card %" PRIu32 " at position %zu", - cmd.guild_card_number.load(), z); + if (gcf->entries[z].data.guild_card_number == cmd.guild_card_number) { + gcf->entries[z].comment = cmd.comment; + c->log.info("Updated comment on guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); + should_save = true; break; } } @@ -3123,34 +3126,36 @@ static void on_E8_BB(shared_ptr c, uint16_t command, uint32_t, string& d } case 0x0AE8: { // Move guild card in list auto& cmd = check_size_t(data); - auto& gcf = c->game_data.account()->guild_card_file; if (cmd.position >= max_count) { throw invalid_argument("invalid new position"); } size_t index; for (index = 0; index < max_count; index++) { - if (gcf.entries[index].data.guild_card_number == cmd.guild_card_number) { + if (gcf->entries[index].data.guild_card_number == cmd.guild_card_number) { break; } } if (index >= max_count) { throw invalid_argument("player does not have requested guild card"); } - auto moved_gc = gcf.entries[index]; + auto moved_gc = gcf->entries[index]; for (; index < cmd.position; index++) { - gcf.entries[index] = gcf.entries[index + 1]; + gcf->entries[index] = gcf->entries[index + 1]; } for (; index > cmd.position; index--) { - gcf.entries[index] = gcf.entries[index - 1]; + gcf->entries[index] = gcf->entries[index - 1]; } - gcf.entries[index] = moved_gc; - c->log.info("Moved guild card %" PRIu32 " to position %zu", - cmd.guild_card_number.load(), index); + gcf->entries[index] = moved_gc; + c->log.info("Moved guild card %" PRIu32 " to position %zu", cmd.guild_card_number.load(), index); + should_save = true; break; } default: throw invalid_argument("invalid command"); } + if (should_save) { + c->game_data.save_guild_card_file(); + } } static void on_DC_BB(shared_ptr c, uint16_t, uint32_t, string& data) { @@ -3184,15 +3189,17 @@ static void on_E5_BB(shared_ptr c, uint16_t, uint32_t, string& data) { return; } - if (c->game_data.player(false).get()) { + if (c->game_data.character(false).get()) { throw runtime_error("player already exists"); } - c->game_data.bb_player_index = cmd.player_index; + c->game_data.bb_character_index = -1; + c->game_data.system(); // Ensure system file is loaded + c->game_data.bb_character_index = cmd.character_index; if (c->bb_connection_phase == 0x03) { // Dressing room try { - c->game_data.player()->disp.apply_dressing_room(cmd.preview); + c->game_data.character()->disp.apply_dressing_room(cmd.preview); } catch (const exception& e) { send_message_box(c, string_printf("$C6Character could not be modified:\n%s", e.what())); return; @@ -3200,7 +3207,7 @@ static void on_E5_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } else { try { auto s = c->require_server_state(); - c->game_data.create_player(cmd.preview, s->level_table); + c->game_data.create_character_file(c->license->serial_number, c->language(), cmd.preview, s->level_table); } catch (const exception& e) { send_message_box(c, string_printf("$C6New character could not be created:\n%s", e.what())); return; @@ -3212,45 +3219,49 @@ static void on_E5_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } static void on_ED_BB(shared_ptr c, uint16_t command, uint32_t, string& data) { + auto p = c->game_data.character(); + auto sys = c->game_data.system(); switch (command) { case 0x01ED: { const auto& cmd = check_size_t(data); - c->game_data.account()->option_flags = cmd.option_flags; + p->option_flags = cmd.option_flags; break; } case 0x02ED: { const auto& cmd = check_size_t(data); - c->game_data.account()->symbol_chats = cmd.symbol_chats; + p->symbol_chats = cmd.symbol_chats; break; } case 0x03ED: { const auto& cmd = check_size_t(data); - c->game_data.account()->shortcuts = cmd.chat_shortcuts; + p->shortcuts = cmd.chat_shortcuts; break; } case 0x04ED: { const auto& cmd = check_size_t(data); - c->game_data.account()->system_file.key_config = cmd.key_config; + sys->key_config = cmd.key_config; + c->game_data.save_system_file(); break; } case 0x05ED: { const auto& cmd = check_size_t(data); - c->game_data.account()->system_file.joystick_config = cmd.pad_config; + sys->joystick_config = cmd.pad_config; + c->game_data.save_system_file(); break; } case 0x06ED: { const auto& cmd = check_size_t(data); - c->game_data.player()->tech_menu_config = cmd.tech_menu; + p->tech_menu_config = cmd.tech_menu; break; } case 0x07ED: { const auto& cmd = check_size_t(data); - c->game_data.player()->disp.config = cmd.customize; + p->disp.config = cmd.customize; break; } case 0x08ED: { const auto& cmd = check_size_t(data); - c->game_data.player()->challenge_records = cmd.records; + p->challenge_records = cmd.records; break; } default: @@ -3259,22 +3270,26 @@ static void on_ED_BB(shared_ptr c, uint16_t command, uint32_t, string& d } static void on_E7_BB(shared_ptr c, uint16_t, uint32_t, string& data) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); - // We only trust the player's quest data and challenge data. - // TODO: In the future, we shouldn't even need to trust these fields. We - // should instead verify our copy of the player against what the client sent, - // and alert on anything that's out of sync. - // TODO: In the future, we should save battle records here too. - auto p = c->game_data.player(); - p->challenge_records = cmd.challenge_records; - p->quest_data1 = cmd.quest_data1; - p->quest_data2 = cmd.quest_data2; + // TODO: In the future, we shouldn't need to trust any of the client's data + // here. We should instead verify our copy of the player against what the + // client sent, and alert on anything that's out of sync. + auto p = c->game_data.character(); + p->challenge_records = cmd.char_file.challenge_records; + p->battle_records = cmd.char_file.battle_records; + p->death_count = cmd.char_file.death_count; + *c->game_data.system() = cmd.system_file; } static void on_E2_BB(shared_ptr c, uint16_t, uint32_t, string& data) { auto& cmd = check_size_t(data); - c->game_data.account()->system_file = cmd; + auto sys = c->game_data.system(); + *sys = cmd; + c->game_data.save_system_file(); + + S_SystemFileCreated_00E1_BB out_cmd = {1}; + send_command_t(c, 0x00E1, 0x00000000, out_cmd); } static void on_DF_BB(shared_ptr c, uint16_t command, uint32_t, string& data) { @@ -3349,7 +3364,7 @@ static void on_DF_BB(shared_ptr c, uint16_t command, uint32_t, string& d case 0x07DF: { const auto& cmd = check_size_t(data); - auto p = c->game_data.player(true, false); + auto p = c->game_data.character(true, false); auto& award_state = (l->episode == Episode::EP2) ? p->challenge_records.ep2_online_award_state : p->challenge_records.ep1_online_award_state; @@ -3434,14 +3449,14 @@ static void on_81(shared_ptr c, uint16_t, uint32_t, string& data) { } else { // If the sender is blocked, don't forward the mail for (size_t y = 0; y < 30; y++) { - if (target->game_data.account()->blocked_senders.data()[y] == c->license->serial_number) { + if (target->game_data.blocked_senders.data()[y] == c->license->serial_number) { return; } } // If the target has auto-reply enabled, send the autoreply. Note that we also // forward the message in this case. - auto target_p = target->game_data.player(); + auto target_p = target->game_data.character(); if (!target_p->auto_reply.empty()) { send_simple_mail( c, @@ -3454,7 +3469,7 @@ static void on_81(shared_ptr c, uint16_t, uint32_t, string& data) { send_simple_mail( target, c->license->serial_number, - c->game_data.player()->disp.name.decode(c->language()), + c->game_data.character()->disp.name.decode(c->language()), message); } } @@ -3470,7 +3485,7 @@ void on_D9(shared_ptr c, uint16_t, uint32_t, string& data) { if (is_w && (data.size() & 1)) { data.push_back(0); } - c->game_data.player(true, false)->info_board.encode(tt_decode_marked(data, c->language(), is_w), c->language()); + c->game_data.character(true, false)->info_board.encode(tt_decode_marked(data, c->language(), is_w), c->language()); } void on_C7(shared_ptr c, uint16_t, uint32_t, string& data) { @@ -3479,21 +3494,26 @@ void on_C7(shared_ptr c, uint16_t, uint32_t, string& data) { if (is_w && (data.size() & 1)) { data.push_back(0); } - c->game_data.player(true, false)->auto_reply.encode(tt_decode_marked(data, c->language(), is_w), c->language()); + c->game_data.character(true, false)->auto_reply.encode(tt_decode_marked(data, c->language(), is_w), c->language()); } static void on_C8(shared_ptr c, uint16_t, uint32_t, string& data) { check_size_v(data.size(), 0); - c->game_data.player(true, false)->auto_reply.clear(); + c->game_data.character(true, false)->auto_reply.clear(); } static void on_C6(shared_ptr c, uint16_t, uint32_t, string& data) { + c->game_data.blocked_senders.fill(0); if (c->version() == GameVersion::BB) { const auto& cmd = check_size_t(data); - c->game_data.account()->blocked_senders = cmd.blocked_senders; + for (size_t z = 0; z < cmd.blocked_senders.size(); z++) { + c->game_data.blocked_senders[z] = cmd.blocked_senders[z]; + } } else { const auto& cmd = check_size_t(data); - c->game_data.account()->blocked_senders = cmd.blocked_senders; + for (size_t z = 0; z < cmd.blocked_senders.size(); z++) { + c->game_data.blocked_senders[z] = cmd.blocked_senders[z]; + } } } @@ -3553,7 +3573,7 @@ shared_ptr create_game_generic( throw runtime_error("invalid episode"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); if (!(c->license->flags & License::Flag::FREE_JOIN_GAMES) && (min_level > p->disp.stats.level)) { // Note: We don't throw here because this is a situation players might diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 46194fbb..b10a99b5 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -430,22 +430,22 @@ static void on_send_guild_card(shared_ptr c, uint8_t command, uint8_t fl switch (c->version()) { case GameVersion::DC: { const auto& cmd = check_size_t(data, size); - c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language()); + c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language()); break; } case GameVersion::PC: { const auto& cmd = check_size_t(data, size); - c->game_data.player(true, false)->guild_card_description = cmd.guild_card.description; + c->game_data.character(true, false)->guild_card.description = cmd.guild_card.description; break; } case GameVersion::GC: { const auto& cmd = check_size_t(data, size); - c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language()); + c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language()); break; } case GameVersion::XB: { const auto& cmd = check_size_t(data, size); - c->game_data.player(true, false)->guild_card_description.encode(cmd.guild_card.description.decode(c->language()), c->language()); + c->game_data.character(true, false)->guild_card.description.encode(cmd.guild_card.description.decode(c->language()), c->language()); break; } case GameVersion::BB: @@ -522,7 +522,7 @@ static void on_word_select_t(shared_ptr c, uint8_t command, uint8_t, con } } catch (const exception& e) { - string name = c->game_data.player()->disp.name.decode(c->language()); + string name = c->game_data.character()->disp.name.decode(c->language()); lc->log.warning("Untranslatable Word Select message: %s", e.what()); send_text_message_printf(lc, "$C4Untranslatable Word\nSelect message from\n%s", name.c_str()); } @@ -576,7 +576,7 @@ static void on_player_died(shared_ptr c, uint8_t command, uint8_t flag, if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { try { - auto& inventory = c->game_data.player()->inventory; + auto& inventory = c->game_data.character()->inventory; size_t mag_index = inventory.find_equipped_mag(); auto& data = inventory.items[mag_index].data; data.data2[0] = max(static_cast(data.data2[0] - 5), 0); @@ -709,7 +709,7 @@ static void on_player_drop_item(shared_ptr c, uint8_t command, uint8_t f auto l = c->require_lobby(); if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = p->remove_item(cmd.item_id, 0, c->version() != GameVersion::BB); l->add_item(item, cmd.area, cmd.x, cmd.z); @@ -768,7 +768,7 @@ static void on_create_inventory_item_t(shared_ptr c, uint8_t command, ui auto l = c->require_lobby(); if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); ItemData item = cmd.item_data; item.decode_for_version(c->version()); l->on_item_id_generated_externally(item.id); @@ -827,7 +827,7 @@ static void on_drop_partial_stack_t(shared_ptr c, uint8_t command, uint8 string name = s->describe_item(c->version(), item, true); send_text_message_printf(c, "$C5SPLIT %08" PRIX32 "\n%s", item.id.load(), name.c_str()); } - c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index); + c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index); } forward_subcommand_with_item_transcode_t(c, command, flag, cmd); @@ -856,7 +856,7 @@ static void on_drop_partial_stack_bb(shared_ptr c, uint8_t command, uint throw logic_error("item tracking not enabled in BB game"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB); // If a stack was split, the original item still exists, so the dropped item @@ -905,7 +905,7 @@ static void on_buy_shop_item(shared_ptr c, uint8_t command, uint8_t flag } if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); ItemData item = cmd.item_data; item.data2d = 0; // Clear the price field item.decode_for_version(c->version()); @@ -1004,7 +1004,7 @@ static void on_pick_up_item(shared_ptr c, uint8_t command, uint8_t flag, if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { auto s = c->require_server_state(); - auto effective_p = effective_c->game_data.player(); + auto effective_p = effective_c->game_data.character(); // It seems the client just plays it fast and loose with these commands. // There can be multiple 6x5A (request to pick up item) commands in flight, @@ -1072,7 +1072,7 @@ static void on_pick_up_item_request(shared_ptr c, uint8_t command, uint8 throw logic_error("item tracking not enabled in BB game"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = l->remove_item(cmd.item_id); p->add_item(item); @@ -1107,7 +1107,7 @@ static void on_equip_unequip_item(shared_ptr c, uint8_t command, uint8_t auto l = c->require_lobby(); if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t index = p->inventory.find_item(cmd.item_id); if (cmd.header.subcommand == 0x25) { // Equip p->inventory.items[index].flags |= 0x00000008; @@ -1135,7 +1135,7 @@ static void on_use_item( auto l = c->require_lobby(); if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { auto s = c->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t index = p->inventory.find_item(cmd.item_id); string name, colored_name; { @@ -1173,7 +1173,7 @@ static void on_feed_mag( auto l = c->require_lobby(); if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { auto s = c->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t mag_index = p->inventory.find_item(cmd.mag_item_id); size_t fed_index = p->inventory.find_item(cmd.fed_item_id); @@ -1228,7 +1228,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr c, uint8_t com } auto s = c->require_server_state(); - size_t level = c->game_data.player()->disp.stats.level + 1; + size_t level = c->game_data.character()->disp.stats.level + 1; switch (cmd.shop_type) { case 0: c->game_data.shop_contents[0] = l->item_creator->generate_tool_shop_contents(level); @@ -1275,7 +1275,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint throw logic_error("item tracking not enabled in BB game"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); if (cmd.action == 0) { // Deposit if (cmd.item_id == 0xFFFFFFFF) { // Deposit Meseta if (cmd.meseta_amount > p->disp.stats.meseta) { @@ -1299,7 +1299,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint string name = s->item_name_index->describe_item(GameVersion::BB, item); l->log.info("Player %hu deposited item %08" PRIX32 " (x%hhu) (%s) in the bank", c->lobby_client_id, cmd.item_id.load(), cmd.item_amount, name.c_str()); - c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index); + c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index); } } else if (cmd.action == 1) { // Take @@ -1326,7 +1326,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint string name = s->item_name_index->describe_item(GameVersion::BB, item); l->log.info("Player %hu withdrew item %08" PRIX32 " (x%hhu) (%s) from the bank", c->lobby_client_id, cmd.item_id.load(), cmd.item_amount, name.c_str()); - c->game_data.player()->print_inventory(stderr, c->version(), s->item_name_index); + c->game_data.character()->print_inventory(stderr, c->version(), s->item_name_index); } } @@ -1344,7 +1344,7 @@ static void on_sort_inventory_bb(shared_ptr c, uint8_t, uint8_t, const v throw logic_error("item tracking not enabled in BB game"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); // Make sure the set of item IDs passed in by the client exactly matches the // set of item IDs present in the inventory @@ -1495,17 +1495,16 @@ static void on_set_quest_flag(shared_ptr c, uint8_t command, uint8_t fla } // TODO: Should we allow overlays here? - auto p = c->game_data.player(true, false); + auto p = c->game_data.character(true, false); // The client explicitly checks for both 0 and 1 - any other value means no // operation is performed. - size_t bit_index = (difficulty << 10) + flag_index; - size_t byte_index = bit_index >> 3; - uint8_t mask = 0x80 >> (bit_index & 7); + size_t byte_index = flag_index >> 3; + uint8_t mask = 0x80 >> (flag_index & 7); if (action == 0) { - p->quest_data1[byte_index] |= mask; + p->quest_flags[difficulty][byte_index] |= mask; } else if (action == 1) { - p->quest_data1[byte_index] &= (~mask); + p->quest_flags[difficulty][byte_index] &= (~mask); } forward_subcommand(c, command, flag, data, size); @@ -1655,7 +1654,7 @@ static void on_charge_attack_bb(shared_ptr c, uint8_t command, uint8_t f forward_subcommand(c, command, flag, data, size); const auto& cmd = check_size_t(data, size); - auto& disp = c->game_data.player()->disp; + auto& disp = c->game_data.character()->disp; if (cmd.meseta_amount > disp.stats.meseta) { disp.stats.meseta = 0; } else { @@ -1671,7 +1670,7 @@ static void on_level_up(shared_ptr c, uint8_t command, uint8_t flag, con return; } - auto p = c->game_data.player(); + auto p = c->game_data.character(); p->disp.stats.char_stats.atp = cmd.atp; p->disp.stats.char_stats.mst = cmd.mst; p->disp.stats.char_stats.evp = cmd.evp; @@ -1685,7 +1684,7 @@ static void on_level_up(shared_ptr c, uint8_t command, uint8_t flag, con static void add_player_exp(shared_ptr c, uint32_t exp) { auto s = c->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); p->disp.stats.experience += exp; send_give_experience(c, exp); @@ -1720,7 +1719,7 @@ static void on_steal_exp_bb(shared_ptr c, uint8_t, uint8_t, const void* const auto& cmd = check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); const auto& enemy = l->map->enemies.at(cmd.enemy_index); const auto& inventory = p->inventory; const auto& weapon = inventory.items[inventory.find_equipped_weapon()]; @@ -1821,14 +1820,14 @@ static void on_enemy_killed_bb(shared_ptr c, uint8_t command, uint8_t fl send_text_message_printf(c, "$C5+%" PRIu32 " E-%hX %s", player_exp, cmd.enemy_index.load(), name_for_enum(e.type)); } - if (other_c->game_data.player()->disp.stats.level < 199) { + if (other_c->game_data.character()->disp.stats.level < 199) { add_player_exp(other_c, player_exp); } } } // Update kill counts on unsealable items - auto& inventory = c->game_data.player()->inventory; + auto& inventory = c->game_data.character()->inventory; for (size_t z = 0; z < inventory.num_items; z++) { auto& item = inventory.items[z]; if ((item.flags & 0x08) && @@ -1841,7 +1840,7 @@ static void on_enemy_killed_bb(shared_ptr c, uint8_t command, uint8_t fl void on_meseta_reward_request_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { const auto& cmd = check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); if (cmd.amount < 0) { if (-cmd.amount > static_cast(p->disp.stats.meseta.load())) { p->disp.stats.meseta = 0; @@ -1867,7 +1866,7 @@ void on_item_reward_request_bb(shared_ptr c, uint8_t, uint8_t, const voi ItemData item; item = cmd.item_data; item.id = l->generate_item_id(0xFF); - c->game_data.player()->add_item(item); + c->game_data.character()->add_item(item); send_create_inventory_item(c, item); } @@ -1884,7 +1883,7 @@ static void on_destroy_inventory_item(shared_ptr c, uint8_t command, uin if (l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) { auto s = c->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB); auto name = s->describe_item(c->version(), item, false); l->log.info("Player %hhu destroyed inventory item %hu:%08" PRIX32 " (%s)", @@ -1936,7 +1935,7 @@ static void on_identify_item_bb(shared_ptr c, uint8_t command, uint8_t f throw logic_error("received item identify subcommand without item creator present"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t x = p->inventory.find_item(cmd.item_id); if (p->inventory.items[x].data.data1[0] != 0) { return; // Only weapons can be identified @@ -1968,7 +1967,7 @@ static void on_accept_identify_item_bb(shared_ptr c, uint8_t command, ui if (c->game_data.identify_result.id != cmd.item_id) { throw runtime_error("accepted item ID does not match previous identify request"); } - c->game_data.player()->add_item(c->game_data.identify_result); + c->game_data.character()->add_item(c->game_data.identify_result); send_create_inventory_item(c, c->game_data.identify_result); c->game_data.identify_result.clear(); @@ -1988,7 +1987,7 @@ static void on_sell_item_at_shop_bb(shared_ptr c, uint8_t command, uint8 } auto s = c->require_server_state(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = p->remove_item(cmd.item_id, cmd.amount, c->version() != GameVersion::BB); size_t price = (s->item_parameter_table_for_version(c->version())->price_for_item(item) >> 3) * cmd.amount; p->add_meseta(price); @@ -2025,7 +2024,7 @@ static void on_buy_shop_item_bb(shared_ptr c, uint8_t, uint8_t, const vo size_t price = item.data2d * cmd.amount; item.data2d = 0; - auto p = c->game_data.player(); + auto p = c->game_data.character(); p->remove_meseta(price, false); item.id = cmd.inventory_item_id; @@ -2048,7 +2047,7 @@ static void on_buy_shop_item_bb(shared_ptr c, uint8_t, uint8_t, const vo static void on_medical_center_bb(shared_ptr c, uint8_t, uint8_t, const void*, size_t) { auto l = c->require_lobby(); if (l->is_game() && (l->base_version == GameVersion::BB)) { - c->game_data.player()->remove_meseta(10, false); + c->game_data.character()->remove_meseta(10, false); } } @@ -2089,7 +2088,7 @@ static void on_battle_level_up_bb(shared_ptr c, uint8_t, uint8_t, const auto lc = l->clients[cmd.header.client_id]; if (lc) { auto s = c->require_server_state(); - auto lp = lc->game_data.player(); + auto lp = lc->game_data.character(); uint32_t target_level = lp->disp.stats.level + cmd.num_levels; uint32_t before_exp = lp->disp.stats.experience; lp->disp.stats.advance_to_level(lp->disp.visual.char_class, target_level, s->level_table); @@ -2107,7 +2106,7 @@ static void on_quest_exchange_item_bb(shared_ptr c, uint8_t, uint8_t, co const auto& cmd = check_size_t(data, size); try { - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false); @@ -2134,7 +2133,7 @@ static void on_wrap_item_bb(shared_ptr c, uint8_t, uint8_t, const void* if (l->is_game() && (l->base_version == GameVersion::BB)) { const auto& cmd = check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); auto item = p->remove_item(cmd.item.id, 1, false); send_destroy_item(c, item.id, 1); item.wrap(); @@ -2149,7 +2148,7 @@ static void on_photon_drop_exchange_bb(shared_ptr c, uint8_t, uint8_t, c const auto& cmd = check_size_t(data, size); try { - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000); auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, false); @@ -2175,7 +2174,7 @@ static void on_photon_crystal_exchange_bb(shared_ptr c, uint8_t, uint8_t auto l = c->require_lobby(); if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); size_t index = p->inventory.find_item_by_primary_identifier(0x031002); auto item = p->remove_item(p->inventory.items[index].data.id, 1, false); send_destroy_item(c, item.id, 1); @@ -2187,7 +2186,7 @@ static void on_momoka_item_exchange_bb(shared_ptr c, uint8_t, uint8_t, c auto l = c->require_lobby(); if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); try { size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false); @@ -2216,7 +2215,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ auto l = c->require_lobby(); if (l->is_game() && (l->base_version == GameVersion::BB) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); - auto p = c->game_data.player(); + auto p = c->game_data.character(); try { size_t item_index = p->inventory.find_item(cmd.item_id); auto& item = p->inventory.items[item_index].data; @@ -2266,9 +2265,9 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ } } -static void on_write_quest_data2_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { - const auto& cmd = check_size_t(data, size); - c->game_data.player()->quest_data2[cmd.index] = cmd.value; +static void on_write_quest_global_flag_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + const auto& cmd = check_size_t(data, size); + c->game_data.character()->quest_global_flags[cmd.index] = cmd.value; } //////////////////////////////////////////////////////////////////////////////// @@ -2486,7 +2485,7 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 6xCF */ on_battle_restart_bb, /* 6xD0 */ on_battle_level_up_bb, /* 6xD1 */ nullptr, - /* 6xD2 */ on_write_quest_data2_bb, + /* 6xD2 */ on_write_quest_global_flag_bb, /* 6xD3 */ nullptr, /* 6xD4 */ nullptr, /* 6xD5 */ on_quest_exchange_item_bb, diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index cc103eb6..9eda55ad 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -3,8 +3,15 @@ #include #include +#include "PSOProtocol.hh" + using namespace std; +// Originally there was going to be a language-based header, but then I decided +// against it. This string was already in use for that parser, so I didn't +// bother changing it. +const char* LegacySavedAccountDataBB::SIGNATURE = "newserv account file format; 7 sections present; sequential;"; + ShuffleTables::ShuffleTables(PSOV2Encryption& crypt) { for (size_t x = 0; x < 0x100; x++) { this->forward_table[x] = x; @@ -189,3 +196,281 @@ void PSOBBGuildCardFile::Entry::clear() { uint32_t PSOBBGuildCardFile::checksum() const { return crc32(this, sizeof(*this)); } + +PSOBBSystemFile::PSOBBSystemFile(uint32_t guild_card_number) + : PSOBBSystemFile() { + // This field is based on 1/1/2000, not 1/1/1970, so adjust appropriately + this->base.creation_timestamp = (now() - 946684800000000ULL) / 1000000; + for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_KEY_CONFIG.size(); z++) { + this->key_config[z] = PSOBBSystemFile::DEFAULT_KEY_CONFIG[z]; + } + for (size_t z = 0; z < PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG.size(); z++) { + this->joystick_config[z] = PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG[z]; + } + this->guild_card_number = guild_card_number; +} + +shared_ptr PSOBBCharacterFile::create_from_preview( + uint32_t guild_card_number, + uint8_t language, + const PlayerDispDataBBPreview& preview, + shared_ptr level_table) { + shared_ptr ret(new PSOBBCharacterFile()); + ret->disp.apply_preview(preview); + ret->disp.stats.reset_to_base(ret->disp.visual.char_class, level_table); + ret->inventory.language = language; + ret->guild_card.guild_card_number = guild_card_number; + ret->guild_card.name = ret->disp.name; + ret->guild_card.present = 1; + ret->guild_card.language = ret->inventory.language; + ret->guild_card.section_id = ret->disp.visual.section_id; + ret->guild_card.char_class = ret->disp.visual.char_class; + for (size_t z = 0; z < PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS.size(); z++) { + ret->symbol_chats[z] = PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS[z].to_entry(); + } + for (size_t z = 0; z < PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG.size(); z++) { + ret->tech_menu_config[z] = PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG[z]; + } + return ret; +} + +PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry() const { + SymbolChatEntry ret; + ret.present = 1; + ret.name.encode(this->name, 1); + ret.data.spec = this->spec; + for (size_t z = 0; z < 4; z++) { + ret.data.corner_objects[z] = this->corner_objects[z]; + } + for (size_t z = 0; z < 12; z++) { + ret.data.face_parts[z] = this->face_parts[z]; + } + return ret; +} + +// TODO: Eliminate duplication between this function and the parallel function +// in PlayerBank +void PSOBBCharacterFile::add_item(const ItemData& item) { + uint32_t pid = item.primary_identifier(); + + // Annoyingly, meseta is in the disp data, not in the inventory struct. If the + // item is meseta, we have to modify disp instead. + if (pid == MESETA_IDENTIFIER) { + this->add_meseta(item.data2d); + return; + } + + // Handle combinable items + size_t combine_max = item.max_stack_size(); + if (combine_max > 1) { + // Get the item index if there's already a stack of the same item in the + // player's inventory + size_t y; + for (y = 0; y < this->inventory.num_items; y++) { + if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) { + break; + } + } + + // If we found an existing stack, add it to the total and return + if (y < this->inventory.num_items) { + size_t new_stack_size = this->inventory.items[y].data.data1[5] + item.data1[5]; + if (new_stack_size > combine_max) { + throw out_of_range("stack is too large"); + } + this->inventory.items[y].data.data1[5] = new_stack_size; + return; + } + } + + // If we get here, then it's not meseta and not a combine item, so it needs to + // go into an empty inventory slot + if (this->inventory.num_items >= 30) { + throw out_of_range("inventory is full"); + } + auto& inv_item = this->inventory.items[this->inventory.num_items]; + inv_item.present = 1; + inv_item.unknown_a1 = 0; + inv_item.flags = 0; + inv_item.data = item; + this->inventory.num_items++; +} + +// TODO: Eliminate code duplication between this function and the parallel +// function in PlayerBank +ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) { + ItemData ret; + + // If we're removing meseta (signaled by an invalid item ID), then create a + // meseta item. + if (item_id == 0xFFFFFFFF) { + this->remove_meseta(amount, allow_meseta_overdraft); + ret.data1[0] = 0x04; + ret.data2d = amount; + return ret; + } + + size_t index = this->inventory.find_item(item_id); + auto& inventory_item = this->inventory.items[index]; + + // If the item is a combine item and are we removing less than we have of it, + // then create a new item and reduce the amount of the existing stack. Note + // that passing amount == 0 means to remove the entire stack, so this only + // applies if amount is nonzero. + if (amount && (inventory_item.data.stack_size() > 1) && + (amount < inventory_item.data.data1[5])) { + ret = inventory_item.data; + ret.data1[5] = amount; + ret.id = 0xFFFFFFFF; + inventory_item.data.data1[5] -= amount; + return ret; + } + + // If we get here, then it's not meseta, and either it's not a combine item or + // we're removing the entire stack. Delete the item from the inventory slot + // and return the deleted item. + ret = inventory_item.data; + this->inventory.num_items--; + for (size_t x = index; x < this->inventory.num_items; x++) { + this->inventory.items[x] = this->inventory.items[x + 1]; + } + auto& last_item = this->inventory.items[this->inventory.num_items]; + last_item.present = 0; + last_item.unknown_a1 = 0; + last_item.flags = 0; + last_item.data.clear(); + return ret; +} + +void PSOBBCharacterFile::add_meseta(uint32_t amount) { + this->disp.stats.meseta = min(static_cast(this->disp.stats.meseta) + amount, 999999); +} + +void PSOBBCharacterFile::remove_meseta(uint32_t amount, bool allow_overdraft) { + if (amount <= this->disp.stats.meseta) { + this->disp.stats.meseta -= amount; + } else if (allow_overdraft) { + this->disp.stats.meseta = 0; + } else { + throw out_of_range("player does not have enough meseta"); + } +} + +uint8_t PSOBBCharacterFile::get_technique_level(uint8_t which) const { + return (this->disp.technique_levels_v1[which] == 0xFF) + ? 0xFF + : (this->disp.technique_levels_v1[which] + this->inventory.items[which].extension_data1); +} + +void PSOBBCharacterFile::set_technique_level(uint8_t which, uint8_t level) { + if (level == 0xFF) { + this->disp.technique_levels_v1[which] = 0xFF; + this->inventory.items[which].extension_data1 = 0x00; + } else if (level <= 0x0E) { + this->disp.technique_levels_v1[which] = level; + this->inventory.items[which].extension_data1 = 0x00; + } else { + this->disp.technique_levels_v1[which] = 0x0E; + this->inventory.items[which].extension_data1 = level - 0x0E; + } +} + +uint8_t PSOBBCharacterFile::get_material_usage(MaterialType which) const { + switch (which) { + case MaterialType::HP: + return this->inventory.hp_from_materials >> 1; + case MaterialType::TP: + return this->inventory.tp_from_materials >> 1; + case MaterialType::POWER: + case MaterialType::MIND: + case MaterialType::EVADE: + case MaterialType::DEF: + case MaterialType::LUCK: + return this->inventory.items[8 + static_cast(which)].extension_data2; + default: + throw logic_error("invalid material type"); + } +} + +void PSOBBCharacterFile::set_material_usage(MaterialType which, uint8_t usage) { + switch (which) { + case MaterialType::HP: + this->inventory.hp_from_materials = usage << 1; + break; + case MaterialType::TP: + this->inventory.tp_from_materials = usage << 1; + break; + case MaterialType::POWER: + case MaterialType::MIND: + case MaterialType::EVADE: + case MaterialType::DEF: + case MaterialType::LUCK: + this->inventory.items[8 + static_cast(which)].extension_data2 = usage; + break; + default: + throw logic_error("invalid material type"); + } +} + +void PSOBBCharacterFile::clear_all_material_usage() { + this->inventory.hp_from_materials = 0; + this->inventory.tp_from_materials = 0; + for (size_t z = 0; z < 5; z++) { + this->inventory.items[z + 8].extension_data2 = 0; + } +} + +void PSOBBCharacterFile::print_inventory(FILE* stream, GameVersion version, shared_ptr name_index) const { + fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", this->disp.stats.meseta.load()); + fprintf(stream, "[PlayerInventory] %hhu items\n", this->inventory.num_items); + for (size_t x = 0; x < this->inventory.num_items; x++) { + const auto& item = this->inventory.items[x]; + auto name = name_index->describe_item(version, item.data); + auto hex = item.data.hex(); + fprintf(stream, "[PlayerInventory] %2zu: %s (%s)\n", x, hex.c_str(), name.c_str()); + } +} + +const std::array PSOBBCharacterFile::DEFAULT_SYMBOL_CHATS = { + DefaultSymbolChatEntry{"\tEHello", 0x28, {0xFFFF, 0x000D, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x05, 0x18, 0x1D, 0x00}, {0x05, 0x28, 0x1D, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, + DefaultSymbolChatEntry{"\tEGood-bye", 0x74, {0x0476, 0x000C, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x36, 0x20, 0x2A, 0x00}, {0x3C, 0x00, 0x32, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, + DefaultSymbolChatEntry{"\tEHurrah!", 0x28, {0x0362, 0x0362, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x09, 0x16, 0x1B, 0x00}, {0x09, 0x2B, 0x1B, 0x01}, {0x37, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, + DefaultSymbolChatEntry{"\tECrying", 0x74, {0x074F, 0xFFFF, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x06, 0x15, 0x14, 0x00}, {0x06, 0x2B, 0x14, 0x01}, {0x05, 0x18, 0x1F, 0x00}, {0x05, 0x28, 0x1F, 0x01}, {0x21, 0x20, 0x2E, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, + DefaultSymbolChatEntry{"\tEI\'m angry!", 0x5C, {0x0116, 0x0001, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x0B, 0x18, 0x1B, 0x01}, {0x0B, 0x28, 0x1B, 0x00}, {0x33, 0x20, 0x2A, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, + DefaultSymbolChatEntry{"\tEHelp me!", 0xEC, {0x065E, 0x0138, 0xFFFF, 0xFFFF}, {SymbolChat::FacePart{0x02, 0x17, 0x1B, 0x01}, {0x02, 0x2A, 0x1B, 0x00}, {0x31, 0x20, 0x2C, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}, {0xFF, 0x00, 0x00, 0x02}}}, +}; + +const std::array PSOBBCharacterFile::DEFAULT_TECH_MENU_CONFIG = { + 0x0000, 0x0006, 0x0003, 0x0001, 0x0007, 0x0004, 0x0002, 0x0008, 0x0005, 0x0009, + 0x0012, 0x000F, 0x0010, 0x0011, 0x000D, 0x000A, 0x000B, 0x000C, 0x000E, 0x0000}; + +const std::array PSOBBSystemFile::DEFAULT_KEY_CONFIG = { + 0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; + +const std::array PSOBBSystemFile::DEFAULT_JOYSTICK_CONFIG = { + 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}; diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 93af7ce5..8f13e8fa 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -11,6 +11,7 @@ #include #include "Episode3/DataIndexes.hh" +#include "ItemNameIndex.hh" #include "PSOEncryption.hh" #include "PlayerSubordinates.hh" #include "Text.hh" @@ -120,7 +121,7 @@ struct PSOGCEp3SystemFile { struct PSOBBSystemFileBase { /* 0000 */ be_uint32_t checksum = 0; - /* 0004 */ be_int16_t music_volume = -50; + /* 0004 */ be_int16_t music_volume = 0; /* 0006 */ int8_t sound_volume = 0; /* 0007 */ uint8_t language = 0; /* 0008 */ be_uint32_t server_time_delta_frames = 1728000; @@ -144,6 +145,92 @@ struct PSOBBSystemFile { /* 02EC */ parray team_flag; /* 0AEC */ le_uint32_t team_rewards = 0; /* 0AF0 */ + + static const std::array DEFAULT_KEY_CONFIG; + static const std::array DEFAULT_JOYSTICK_CONFIG; + + PSOBBSystemFile() = default; + explicit PSOBBSystemFile(uint32_t guild_card_number); +} __attribute__((packed)); + +struct PSOBBCharacterFile { + struct SymbolChatEntry { + /* 00 */ le_uint32_t present = 0; + /* 04 */ pstring name; + /* 2C */ SymbolChat data; + /* 68 */ + } __attribute__((packed)); + + struct DefaultSymbolChatEntry { + const char* name; + uint32_t spec; + std::array corner_objects; + std::array face_parts; + + SymbolChatEntry to_entry() const; + }; + + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataBB disp; + /* 04DC */ le_uint32_t flags = 0; + /* 04E0 */ le_uint32_t creation_timestamp = 0; + /* 04E4 */ le_uint32_t signature = 0xA205B064; + /* 04E8 */ le_uint32_t play_time_seconds = 0; + /* 04EC */ le_uint32_t option_flags = 0; + /* 04F0 */ parray unknown_a2; + /* 04F4 */ parray, 4> quest_flags; + /* 06F4 */ le_uint32_t death_count = 0; + /* 06F8 */ PlayerBank bank; + /* 19C0 */ GuildCardBB guild_card; + /* 1AC8 */ le_uint32_t unknown_a3 = 0; + /* 1ACC */ parray symbol_chats; + /* 1FAC */ parray shortcuts; + /* 29EC */ pstring auto_reply; + /* 2B44 */ pstring info_board; + /* 2C9C */ PlayerRecords_Battle battle_records; + /* 2CB4 */ parray unknown_a4; + /* 2CB8 */ PlayerRecordsBB_Challenge challenge_records; + /* 2DF8 */ parray tech_menu_config; + /* 2E20 */ ChoiceSearchConfig choice_search_config; + /* 2E38 */ parray unknown_a6; + /* 2E48 */ parray quest_global_flags; + /* 2E88 */ parray unknown_a7; + /* 2EA4 */ + + static const std::array DEFAULT_SYMBOL_CHATS; + static const std::array DEFAULT_TECH_MENU_CONFIG; + + PSOBBCharacterFile() = default; + + static std::shared_ptr create_from_preview( + uint32_t guild_card_number, + uint8_t language, + const PlayerDispDataBBPreview& preview, + std::shared_ptr level_table); + + void add_item(const ItemData& item); + ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft); + void add_meseta(uint32_t amount); + void remove_meseta(uint32_t amount, bool allow_overdraft); + + uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D + void set_technique_level(uint8_t which, uint8_t level); + + enum class MaterialType : int8_t { + HP = -2, + TP = -1, + POWER = 0, + MIND = 1, + EVADE = 2, + DEF = 3, + LUCK = 4, + }; + + uint8_t get_material_usage(MaterialType which) const; + void set_material_usage(MaterialType which, uint8_t usage); + void clear_all_material_usage(); + + void print_inventory(FILE* stream, GameVersion version, std::shared_ptr name_index) const; } __attribute__((packed)); struct PSOBBGuildCardFile { @@ -156,10 +243,13 @@ struct PSOBBGuildCardFile { void clear(); } __attribute__((packed)); - PSOBBSystemFileBase system_file; - parray blocked; - parray unknown_a2; - parray entries; + /* 0000 */ PSOBBSystemFileBase system_file; + /* 0114 */ parray blocked; + /* 1DF4 */ parray unknown_a2; + /* 1F74 */ parray entries; + /* D590 */ + + PSOBBGuildCardFile() = default; uint32_t checksum() const; } __attribute__((packed)); @@ -167,9 +257,7 @@ struct PSOBBGuildCardFile { struct PSOGCSaveFileSymbolChatEntry { /* 00 */ be_uint32_t present; /* 04 */ pstring name; - /* 1C */ be_uint16_t unused; - /* 1E */ uint8_t flags; - /* 1F */ uint8_t face_spec; + /* 1C */ be_uint32_t spec; struct CornerObject { uint8_t type; uint8_t flags_color; @@ -188,21 +276,7 @@ struct PSOGCSaveFileSymbolChatEntry { struct PSOPCSaveFileSymbolChatEntry { /* 00 */ le_uint32_t present; /* 04 */ pstring name; - /* 34 */ uint8_t face_spec; - /* 35 */ uint8_t flags; - /* 36 */ be_uint16_t unused; - struct CornerObject { - uint8_t type; - uint8_t flags_color; - } __attribute__((packed)); - /* 38 */ parray corner_objects; - struct FacePart { - uint8_t type; - uint8_t x; - uint8_t y; - uint8_t flags; - } __attribute__((packed)); - /* 40 */ parray face_parts; + /* 34 */ SymbolChat data; /* 70 */ } __attribute__((packed)); @@ -651,3 +725,45 @@ struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD /* 00440 */ parray entries; /* ECE40 */ } __attribute__((packed)); + +// This format is specific to newserv and is no longer used, but remains here +// for backward compatibility. +struct LegacySavedPlayerDataBB { // .nsc file format + static constexpr uint64_t SIGNATURE_V0 = 0x6E65777365727620; + static constexpr uint64_t SIGNATURE_V1 = 0xA904332D5CEF0296; + + /* 0000 */ be_uint64_t signature = SIGNATURE_V1; + /* 0008 */ parray unused; + /* 0028 */ PlayerRecords_Battle battle_records; + /* 0040 */ PlayerDispDataBBPreview preview; + /* 00BC */ pstring auto_reply; + /* 0214 */ PlayerBank bank; + /* 14DC */ PlayerRecordsBB_Challenge challenge_records; + /* 161C */ PlayerDispDataBB disp; + /* 17AC */ pstring guild_card_description; + /* 185C */ pstring info_board; + /* 19B4 */ PlayerInventory inventory; + /* 1D00 */ parray unknown_a2; + /* 1D04 */ parray, 4> quest_flags; + /* 1F04 */ le_uint32_t death_count; + /* 1F08 */ parray quest_global_flags; + /* 1F60 */ parray tech_menu_config; + /* 1F88 */ +} __attribute__((packed)); + +// This format is specific to newserv and is no longer used, but remains here +// for backward compatibility. +struct LegacySavedAccountDataBB { // .nsa file format + static const char* SIGNATURE; + + /* 0000 */ pstring signature; + /* 0040 */ parray blocked_senders; + /* 00B8 */ PSOBBGuildCardFile guild_card_file; + /* D648 */ PSOBBSystemFile system_file; + /* E138 */ le_uint32_t unused; + /* E13C */ le_uint32_t option_flags; + /* E140 */ parray shortcuts; + /* EB80 */ parray symbol_chats; + /* F060 */ pstring team_name; + /* F080 */ +} __attribute__((packed)); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 8e707e74..2467f392 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -470,25 +470,23 @@ void send_client_init_bb(shared_ptr c, uint32_t error_code) { } void send_system_file_bb(shared_ptr c) { - send_command_t(c, 0x00E2, 0x00000000, c->game_data.account()->system_file); + send_command_t(c, 0x00E2, 0x00000000, *c->game_data.system()); } -void send_player_preview_bb(shared_ptr c, uint8_t player_index, - const PlayerDispDataBBPreview* preview) { - +void send_player_preview_bb(shared_ptr c, int8_t character_index, const PlayerDispDataBBPreview* preview) { if (!preview) { // no player exists - S_PlayerPreview_NoPlayer_BB_00E4 cmd = {player_index, 0x00000002}; + S_PlayerPreview_NoPlayer_BB_00E4 cmd = {character_index, 0x00000002}; send_command_t(c, 0x00E4, 0x00000000, cmd); } else { - SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {player_index, *preview}; + SC_PlayerPreview_CreateCharacter_BB_00E5 cmd = {character_index, *preview}; send_command_t(c, 0x00E5, 0x00000000, cmd); } } void send_guild_card_header_bb(shared_ptr c) { - uint32_t checksum = c->game_data.account()->guild_card_file.checksum(); + uint32_t checksum = c->game_data.guild_cards()->checksum(); S_GuildCardHeader_BB_01DC cmd = {1, sizeof(PSOBBGuildCardFile), checksum}; send_command_t(c, 0x01DC, 0x00000000, cmd); } @@ -505,7 +503,7 @@ void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { cmd.unknown = 0; cmd.chunk_index = chunk_index; cmd.data.assign_range( - reinterpret_cast(&c->game_data.account()->guild_card_file) + chunk_offset, + reinterpret_cast(c->game_data.guild_cards().get()) + chunk_offset, data_size, 0); send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size); @@ -591,50 +589,19 @@ void send_stream_file_chunk_bb(shared_ptr c, uint32_t chunk_index) { } void send_approve_player_choice_bb(shared_ptr c) { - S_ApprovePlayerChoice_BB_00E4 cmd = {c->game_data.bb_player_index, 1}; + S_ApprovePlayerChoice_BB_00E4 cmd = {c->game_data.bb_character_index, 1}; send_command_t(c, 0x00E4, 0x00000000, cmd); } void send_complete_player_bb(shared_ptr c) { - auto account = c->game_data.account(); - auto player = c->game_data.player(true, false); + auto p = c->game_data.character(true, false); + auto sys = c->game_data.system(true); if (c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB)) { - player->inventory.language = 1; + p->inventory.language = 1; + p->guild_card.language = 1; + sys->base.language = 1; } - - SC_SyncCharacterSaveFile_BB_00E7 cmd; - cmd.inventory = player->inventory; - cmd.disp = player->disp; - cmd.disp.visual.compute_name_color_checksum(); - cmd.disp.play_time = 0; - cmd.unknown_a1 = 0; - cmd.creation_timestamp = 0; - cmd.signature = 0xA205B064; - cmd.play_time_seconds = player->disp.play_time; - cmd.option_flags = account->option_flags; - cmd.quest_data1 = player->quest_data1; - cmd.bank = player->bank; - cmd.guild_card.guild_card_number = c->game_data.guild_card_number; - cmd.guild_card.name = player->disp.name; - cmd.guild_card.team_name = account->team_name; - cmd.guild_card.description = player->guild_card_description; - cmd.guild_card.present = 1; - cmd.guild_card.language = cmd.inventory.language; - cmd.guild_card.section_id = player->disp.visual.section_id; - cmd.guild_card.char_class = player->disp.visual.char_class; - cmd.unknown_a3 = 0; - cmd.symbol_chats = account->symbol_chats; - cmd.shortcuts = account->shortcuts; - cmd.auto_reply = player->auto_reply; - cmd.info_board = player->info_board; - cmd.battle_records = player->battle_records; - cmd.unknown_a4.clear(0); - cmd.challenge_records = player->challenge_records; - cmd.tech_menu_config = player->tech_menu_config; - cmd.unknown_a6.clear(0); - cmd.quest_data2 = player->quest_data2; - cmd.system_file = account->system_file; - + SC_SyncSaveFiles_BB_E7 cmd = {*p, *sys}; send_command_t(c, 0x00E7, 0x00000000, cmd); } @@ -948,7 +915,7 @@ void send_info_board_t(shared_ptr c) { if (!other_c.get()) { continue; } - auto other_p = other_c->game_data.player(true, false); + auto other_p = other_c->game_data.character(true, false); auto& e = entries.emplace_back(); e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language()); e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language()); @@ -998,7 +965,7 @@ void send_card_search_result_t( cmd.location_string.encode(location_string, c->language()); cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY; cmd.extension.lobby_refs[0].item_id = result_lobby->lobby_id; - auto rp = result->game_data.player(true, false); + auto rp = result->game_data.character(true, false); cmd.extension.player_name.encode(rp->disp.name.decode(rp->inventory.language), c->language()); send_command_t(c, 0x41, 0x00, cmd); @@ -1135,14 +1102,14 @@ void send_guild_card(shared_ptr c, shared_ptr source) { throw runtime_error("source player does not have a license"); } - auto source_p = source->game_data.player(true, false); + auto source_p = source->game_data.character(true, false); uint32_t guild_card_number = source->license->serial_number; uint64_t xb_user_id = source->license->xb_user_id ? source->license->xb_user_id : (0xAE00000000000000 | guild_card_number); uint8_t language = source_p->inventory.language; string name = source_p->disp.name.decode(language); - string description = source_p->guild_card_description.decode(language); + string description = source_p->guild_card.description.decode(language); uint8_t section_id = source_p->disp.visual.section_id; uint8_t char_class = source_p->disp.visual.char_class; @@ -1301,7 +1268,9 @@ void send_game_menu_t( e.flags |= 0x20; break; case GameMode::SOLO: - e.flags |= 0x34; + // These should only be visible to other BB clients + e.flags |= 0x04; // Grayed (but not disabled apparently) + e.episode = 0x10 | episode_num; break; default: throw logic_error("invalid game mode"); @@ -1445,7 +1414,7 @@ template void send_player_records_t(shared_ptr c, shared_ptr l, shared_ptr joining_client) { vector entries; auto add_client = [&](shared_ptr lc) -> void { - auto lp = lc->game_data.player(true, false); + auto lp = lc->game_data.character(true, false); auto& e = entries.emplace_back(); e.client_id = lc->lobby_client_id; e.challenge = lp->challenge_records; @@ -1497,7 +1466,7 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr l) if (!wc) { continue; } - auto wc_p = wc->game_data.player(); + auto wc_p = wc->game_data.character(); auto& p = cmd.players[z]; p.lobby_data.player_tag = 0x00010000; p.lobby_data.guild_card_number = wc->license->serial_number; @@ -1564,7 +1533,7 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr l) for (size_t z = 4; z < 12; z++) { if (l->clients[z]) { auto other_c = l->clients[z]; - auto other_p = other_c->game_data.player(); + auto other_p = other_c->game_data.character(); auto& cmd_p = cmd.spectator_players[z - 4]; auto& cmd_e = cmd.entries[z]; cmd_p.lobby_data.player_tag = 0x00010000; @@ -1606,7 +1575,7 @@ void send_join_game(shared_ptr c, shared_ptr l) { cmd.lobby_data[x].player_tag = 0x00010000; cmd.lobby_data[x].guild_card_number = lc->license->serial_number; cmd.lobby_data[x].client_id = lc->lobby_client_id; - cmd.lobby_data[x].name.encode(lc->game_data.player()->disp.name.decode(lc->language()), c->language()); + cmd.lobby_data[x].name.encode(lc->game_data.character()->disp.name.decode(lc->language()), c->language()); player_count++; } else { cmd.lobby_data[x].clear(); @@ -1677,7 +1646,7 @@ void send_join_game(shared_ptr c, shared_ptr l) { auto s = c->require_server_state(); for (size_t x = 0; x < 4; x++) { if (l->clients[x]) { - auto other_p = l->clients[x]->game_data.player(); + auto other_p = l->clients[x]->game_data.character(); cmd.players_ep3[x].inventory = other_p->inventory; cmd.players_ep3[x].inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version())); cmd.players_ep3[x].disp = convert_player_disp_data(other_p->disp, c->language(), other_p->inventory.language); @@ -1794,7 +1763,7 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, shared_ptrgame_data.player(); + auto lp = lc->game_data.character(); auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card_number = lc->license->serial_number; @@ -1866,7 +1835,7 @@ void send_join_lobby_xb(shared_ptr c, shared_ptr l, shared_ptrgame_data.player(); + auto lp = lc->game_data.character(); auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card_number = lc->license->serial_number; @@ -1919,7 +1888,7 @@ void send_join_lobby_dc_nte(shared_ptr c, shared_ptr l, size_t used_entries = 0; for (const auto& lc : lobby_clients) { - auto lp = lc->game_data.player(); + auto lp = lc->game_data.character(); auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card_number = lc->license->serial_number; @@ -2242,7 +2211,7 @@ void send_bank(shared_ptr c) { throw logic_error("6xBC can only be sent to BB clients"); } - auto p = c->game_data.player(); + auto p = c->game_data.character(); const auto* items_it = p->bank.items.data(); vector items(items_it, items_it + p->bank.num_items); @@ -2278,7 +2247,7 @@ void send_shop(shared_ptr c, uint8_t shop_type) { void send_level_up(shared_ptr c) { auto l = c->require_lobby(); - auto p = c->game_data.player(); + auto p = c->game_data.character(); CharacterStats stats = p->disp.stats.char_stats; const ItemData* mag = nullptr; @@ -2547,12 +2516,12 @@ string ep3_description_for_client(shared_ptr c) { if (!c->config.check_flag(Client::Flag::IS_EPISODE_3)) { throw runtime_error("client is not Episode 3"); } - auto player = c->game_data.player(); + auto p = c->game_data.character(); return string_printf( "%s CLv%" PRIu32 " %c", - name_for_char_class(player->disp.visual.char_class), - player->disp.stats.level + 1, - char_for_language_code(player->inventory.language)); + name_for_char_class(p->disp.visual.char_class), + p->disp.stats.level + 1, + char_for_language_code(p->inventory.language)); } void send_ep3_game_details(shared_ptr c, shared_ptr l) { @@ -2595,7 +2564,7 @@ void send_ep3_game_details(shared_ptr c, shared_ptr l) { if (player.is_human()) { try { auto other_c = serial_number_to_client.at(player.serial_number); - entry.name.encode(other_c->game_data.player()->disp.name.decode(other_c->language()), c->language()); + entry.name.encode(other_c->game_data.character()->disp.name.decode(other_c->language()), c->language()); entry.description.encode(ep3_description_for_client(other_c), c->language()); } catch (const out_of_range&) { entry.name.encode(player.player_name, c->language()); @@ -2616,7 +2585,7 @@ void send_ep3_game_details(shared_ptr c, shared_ptr l) { for (auto spec_c : l->clients) { if (spec_c) { auto& entry = cmd.spectator_entries[cmd.num_spectators++]; - entry.name.encode(spec_c->game_data.player()->disp.name.decode(spec_c->language()), c->language()); + entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language()); entry.description.encode(ep3_description_for_client(spec_c), c->language()); } } @@ -2633,7 +2602,7 @@ void send_ep3_game_details(shared_ptr c, shared_ptr l) { size_t num_players = 0; for (const auto& opp_c : primary_lobby->clients) { if (opp_c) { - cmd.player_entries[num_players].name.encode(opp_c->game_data.player()->disp.name.decode(opp_c->language()), c->language()); + cmd.player_entries[num_players].name.encode(opp_c->game_data.character()->disp.name.decode(opp_c->language()), c->language()); cmd.player_entries[num_players].description.encode(ep3_description_for_client(opp_c), c->language()); num_players++; } @@ -2646,7 +2615,7 @@ void send_ep3_game_details(shared_ptr c, shared_ptr l) { for (auto spec_c : l->clients) { if (spec_c) { auto& entry = cmd.spectator_entries[num_spectators++]; - entry.name.encode(spec_c->game_data.player()->disp.name.decode(spec_c->language()), c->language()); + entry.name.encode(spec_c->game_data.character()->disp.name.decode(spec_c->language()), c->language()); entry.description.encode(ep3_description_for_client(spec_c), c->language()); } } @@ -2757,7 +2726,7 @@ void send_ep3_tournament_match_result(shared_ptr l, uint32_t meseta_rewar if (player.is_human()) { try { auto pc = serial_number_to_client.at(player.serial_number); - entry.player_names[z].encode(pc->game_data.player()->disp.name.decode(pc->language()), lc->language()); + entry.player_names[z].encode(pc->game_data.character()->disp.name.decode(pc->language()), lc->language()); } catch (const out_of_range&) { entry.player_names[z].encode(player.player_name, lc->language()); } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 34f5638b..8d54242d 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -161,8 +161,7 @@ void send_pc_console_split_reconnect( void send_client_init_bb(std::shared_ptr c, uint32_t error); void send_system_file_bb(std::shared_ptr c); -void send_player_preview_bb(std::shared_ptr c, uint8_t player_index, - const PlayerDispDataBBPreview* preview); +void send_player_preview_bb(std::shared_ptr c, int8_t character_index, const PlayerDispDataBBPreview* preview); void send_accept_client_checksum_bb(std::shared_ptr c); void send_guild_card_header_bb(std::shared_ptr c); void send_guild_card_chunk_bb(std::shared_ptr c, size_t chunk_index); diff --git a/src/Server.cc b/src/Server.cc index f0b78aa0..9d881817 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -112,7 +112,6 @@ void Server::on_listen_accept( BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); shared_ptr c(new Client( this->shared_from_this(), 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; @@ -133,7 +132,6 @@ 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(this->shared_from_this(), 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; @@ -318,7 +316,7 @@ vector> Server::get_clients_by_identifier(const string& ident continue; } - auto p = c->game_data.player(false, false); + auto p = c->game_data.character(false, false); if (p && p->disp.name.eq(ident, p->inventory.language)) { results.emplace_back(std::move(c)); continue; diff --git a/src/ServerState.cc b/src/ServerState.cc index 322c1bad..f87e0973 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -23,7 +23,6 @@ ServerState::ServerState(const char* config_filename, bool is_replay) dns_server_port(0), ip_stack_debug(false), allow_unregistered_users(false), - allow_saving(true), allow_dc_pc_games(false), allow_gc_xb_games(true), item_tracking_enabled(true), @@ -108,11 +107,6 @@ void ServerState::init() { this->load_quest_index(); this->compile_functions(); this->load_dol_files(); - - if (this->is_replay) { - this->allow_saving = false; - config_log.info("Saving disabled because this is a replay session"); - } } void ServerState::add_client_to_available_lobby(shared_ptr c) { diff --git a/src/ServerState.hh b/src/ServerState.hh index c560367a..ec3fabea 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -66,7 +66,6 @@ struct ServerState : public std::enable_shared_from_this { std::vector ip_stack_addresses; bool ip_stack_debug; bool allow_unregistered_users; - bool allow_saving; bool allow_dc_pc_games; bool allow_gc_xb_games; bool item_tracking_enabled;