#pragma once #include #include #include #include #include #include #include #include "Episode3/DataIndexes.hh" #include "ItemData.hh" #include "LevelTable.hh" #include "Text.hh" #include "Version.hh" struct PlayerBankItem; // 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 // the InventoryItem struct to store some things not present in V1. The game // stores arrays of bytes striped across these structures. In newserv, we call // those fields extension_data. They contain: // items[0].extension_data1 through items[19].extension_data1: // Extended technique levels. The values in the v1_technique_levels array // only go up to 14 (tech level 15); if the player has a technique above // level 15, the corresponding extension_data1 field holds the remaining // levels (so a level 20 tech would have 14 in v1_technique_levels and 5 // in the corresponding item's extension_data1 field). // items[0].extension_data2 through items[3].extension_data2: // The value known as unknown_a1 in the PSOGCCharacterFile::Character // struct. See SaveFileFormats.hh. // items[4].extension_data2 through items[7].extension_data2: // The timestamp when the character was last saved, in seconds since // January 1, 2000. Stored little-endian, so items[4] contains the LSB. // items[8].extension_data2 through items[12].extension_data2: // Number of power materials, mind materials, evade materials, def // materials, and luck materials (respectively) used by the player. // items[13].extension_data2 through items[15].extension_data2: // Unknown. These are not an array, but do appear to be related. struct PlayerInventoryItem { // 0x1C bytes le_uint16_t present; // See note above about these fields uint8_t extension_data1; uint8_t extension_data2; le_uint32_t flags; // 8 = equipped ItemData data; PlayerInventoryItem(); PlayerInventoryItem(const PlayerBankItem&); void clear(); } __attribute__((packed)); struct PlayerBankItem { // 0x18 bytes ItemData data; le_uint16_t amount; le_uint16_t show_flags; PlayerBankItem(); PlayerBankItem(const PlayerInventoryItem&); void clear(); } __attribute__((packed)); struct PlayerInventory { // 0x34C bytes uint8_t num_items; uint8_t hp_materials_used; uint8_t tp_materials_used; uint8_t language; PlayerInventoryItem items[30]; PlayerInventory(); size_t find_item(uint32_t item_id) const; size_t find_equipped_weapon() const; size_t find_equipped_armor() const; size_t find_equipped_mag() const; } __attribute__((packed)); struct PlayerBank { // 0x12C8 bytes le_uint32_t num_items; le_uint32_t meseta; PlayerBankItem items[200]; 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 PlayerBankItem& item); PlayerBankItem remove_item(uint32_t item_id, uint32_t amount); size_t find_item(uint32_t item_id); } __attribute__((packed)); struct PendingItemTrade { uint8_t other_client_id; bool confirmed; // true if client has sent a D2 command std::vector items; }; struct PendingCardTrade { uint8_t other_client_id; bool confirmed; // true if client has sent an EE D2 command std::vector> card_to_count; }; struct PlayerDispDataBB; struct PlayerStats { /* 00 */ CharacterStats char_stats; /* 0E */ le_uint16_t unknown_a1; /* 10 */ le_float unknown_a2; /* 14 */ le_float unknown_a3; /* 18 */ le_uint32_t level; /* 1C */ le_uint32_t experience; /* 20 */ le_uint32_t meseta; /* 24 */ PlayerStats() noexcept; } __attribute__((packed)); struct PlayerVisualConfig { /* 00 */ ptext name; /* 10 */ le_uint64_t unknown_a2; // Note: This is probably not actually a 64-bit int. /* 18 */ le_uint32_t name_color; // RGBA /* 1C */ uint8_t extra_model; /* 1D */ parray unused; /* 2C */ le_uint32_t unknown_a3; /* 30 */ uint8_t section_id; /* 31 */ uint8_t char_class; /* 32 */ uint8_t v2_flags; /* 33 */ uint8_t version; /* 34 */ le_uint32_t v1_flags; /* 38 */ le_uint16_t costume; /* 3A */ le_uint16_t skin; /* 3C */ le_uint16_t face; /* 3E */ le_uint16_t head; /* 40 */ le_uint16_t hair; /* 42 */ le_uint16_t hair_r; /* 44 */ le_uint16_t hair_g; /* 46 */ le_uint16_t hair_b; /* 48 */ le_float proportion_x; /* 4C */ le_float proportion_y; /* 50 */ PlayerVisualConfig() noexcept; } __attribute__((packed)); struct PlayerDispDataDCPCV3 { /* 00 */ PlayerStats stats; /* 24 */ PlayerVisualConfig visual; /* 74 */ parray config; /* BC */ parray v1_technique_levels; /* D0 */ // Note: This struct has a default constructor because it's used in a command // that has a fixed-size array. If we didn't define this constructor, the // trivial fields in that array's members would be uninitialized, and we could // send uninitialized memory to the client. PlayerDispDataDCPCV3() noexcept = default; void enforce_v2_limits(); PlayerDispDataBB to_bb() const; } __attribute__((packed)); struct PlayerDispDataBBPreview { /* 00 */ le_uint32_t experience; /* 04 */ le_uint32_t level; // The name field in this structure is used for the player's Guild Card // number, apparently (possibly because it's a char array and this is BB) /* 08 */ PlayerVisualConfig visual; /* 58 */ ptext name; /* 78 */ uint32_t play_time; /* 7C */ PlayerDispDataBBPreview() noexcept; } __attribute__((packed)); // BB player appearance and stats data struct PlayerDispDataBB { /* 0000 */ PlayerStats stats; /* 0024 */ PlayerVisualConfig visual; /* 0074 */ ptext name; /* 008C */ le_uint32_t play_time; /* 0090 */ uint32_t unknown_a3; /* 0094 */ parray config; /* 017C */ parray technique_levels; /* 0190 */ PlayerDispDataBB() noexcept; inline void enforce_v2_limits() {} PlayerDispDataDCPCV3 to_dcpcv3() const; PlayerDispDataBBPreview to_preview() const; void apply_preview(const PlayerDispDataBBPreview&); void apply_dressing_room(const PlayerDispDataBBPreview&); } __attribute__((packed)); // TODO: Is this the same for XB as it is for GC? (This struct is based on the // GC format) struct GuildCardV3 { /* 00 */ le_uint32_t player_tag; /* 04 */ le_uint32_t guild_card_number; /* 08 */ ptext name; /* 20 */ ptext description; /* 8C */ uint8_t present; // should be 1 /* 8D */ uint8_t language; /* 8E */ uint8_t section_id; /* 8F */ uint8_t char_class; /* 90 */ GuildCardV3() noexcept; } __attribute__((packed)); // BB guild card format struct GuildCardBB { /* 0000 */ le_uint32_t guild_card_number; /* 0004 */ ptext name; /* 0034 */ ptext team_name; /* 0054 */ ptext description; /* 0104 */ uint8_t present; // should be 1 if guild card entry exists /* 0105 */ uint8_t language; /* 0106 */ uint8_t section_id; /* 0107 */ uint8_t char_class; /* 0108 */ GuildCardBB() noexcept; void clear(); } __attribute__((packed)); // an entry in the BB guild card file struct GuildCardEntryBB { GuildCardBB data; ptext comment; parray unknown_a1; void clear(); } __attribute__((packed)); // the format of the BB guild card file struct GuildCardFileBB { parray unknown_a1; GuildCardBB blocked[0x1C]; parray unknown_a2; GuildCardEntryBB entries[0x69]; uint32_t checksum() const; } __attribute__((packed)); struct KeyAndTeamConfigBB { parray unknown_a1; // 0000 parray key_config; // 0114 parray joystick_config; // 0280 le_uint32_t guild_card_number; // 02B8 le_uint32_t team_id; // 02BC le_uint64_t team_info; // 02C0 le_uint16_t team_privilege_level; // 02C8 le_uint16_t reserved; // 02CA ptext team_name; // 02CC parray team_flag; // 02EC le_uint32_t team_rewards; // 0AEC } __attribute__((packed)); struct PlayerLobbyDataPC { le_uint32_t player_tag = 0; le_uint32_t guild_card = 0; // There's a strange behavior (bug? "feature"?) in Episode 3 where the start // button does nothing in the lobby (hence you can't "quit game") if the // client's IP address is zero. So, we fill it in with a fake nonzero value to // avoid this behavior, and to be consistent, we make IP addresses fake and // nonzero on all other versions too. be_uint32_t ip_address = 0x7F000001; le_uint32_t client_id = 0; ptext name; void clear(); } __attribute__((packed)); struct PlayerLobbyDataDCGC { le_uint32_t player_tag = 0; le_uint32_t guild_card = 0; be_uint32_t ip_address = 0x7F000001; le_uint32_t client_id = 0; ptext name; void clear(); } __attribute__((packed)); struct XBNetworkLocation { le_uint32_t internal_ipv4_address = 0x0A0A0A0A; le_uint32_t external_ipv4_address = 0x23232323; le_uint16_t port = 9100; parray mac_address = 0x77; parray unknown_a1; le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF; parray unknown_a2; void clear(); } __attribute__((packed)); struct PlayerLobbyDataXB { le_uint32_t player_tag = 0; le_uint32_t guild_card = 0; XBNetworkLocation netloc; le_uint32_t client_id = 0; ptext name; void clear(); } __attribute__((packed)); struct PlayerLobbyDataBB { le_uint32_t player_tag = 0; le_uint32_t guild_card = 0; // This field is a guess; the official builds didn't use this, but all other // versions have it be_uint32_t ip_address = 0x7F000001; parray unknown_a1; le_uint32_t client_id = 0; ptext name; le_uint32_t unknown_a2 = 0; void clear(); } __attribute__((packed)); template struct PlayerRecordsDCPC_Challenge { using CharT = typename std::conditional::type; /* 00 */ le_uint16_t title_color = 0x7FFF; /* 02 */ parray unknown_u0; /* 04 */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text /* 10 */ parray times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times /* 34 */ le_uint16_t unknown_g3 = 0; /* 36 */ le_uint16_t grave_deaths = 0; /* 38 */ parray grave_coords_time; /* 4C */ ptext grave_team; /* 60 */ ptext grave_message; /* 78 */ parray times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times /* 9C */ parray unknown_l4; /* A0 */ } __attribute__((packed)); struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge { } __attribute__((packed)); struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge { } __attribute__((packed)); template struct PlayerRecordsV3_Challenge { using U16T = typename std::conditional::type; using U32T = typename std::conditional::type; // Offsets are (1) relative to start of C5 entry, and (2) relative to start // of save file structure struct Stats { /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 /* 02:1E */ parray unknown_u0; /* 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; /* 64:80 */ U16T grave_deaths = 0; /* 66:82 */ parray unknown_u4; /* 68:84 */ parray grave_coords_time; /* 7C:98 */ ptext grave_team; /* 90:AC */ ptext grave_message; /* B0:CC */ parray unknown_m5; /* B4:D0 */ parray unknown_t6; /* D8:F4 */ } __attribute__((packed)); /* 0000:001C */ Stats stats; // On Episode 3, there are special cases that apply to this field - if the // text ends with certain strings (after decrypt_challenge_rank_text), the // player will have particle effects emanate from their character in the // lobby every 2 seconds. These effects are: // Ends with ":GOD" => blue circle // Ends with ":KING" => white particles // Ends with ":LORD" => rising yellow sparkles // Ends with ":CHAMP" => green circle /* 00D8:00F4 */ ptext rank_title; /* 00E4:0100 */ parray unknown_l7; /* 0100:011C */ } __attribute__((packed)); struct PlayerRecordsBB_Challenge { /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 /* 0002 */ parray unknown_u0; /* 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; /* 0064 */ le_uint16_t grave_deaths = 0; /* 0066 */ parray unknown_u4; /* 0068 */ parray grave_coords_time; /* 007C */ ptext grave_team; /* 00A4 */ ptext grave_message; /* 00E4 */ parray unknown_m5; /* 00E8 */ parray unknown_t6; /* 010C */ ptext rank_title; // Encrypted; see decrypt_challenge_rank_text /* 0124 */ parray unknown_l7; /* 0140 */ PlayerRecordsBB_Challenge() = default; PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default; PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default; PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec); PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec); PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec); operator PlayerRecordsDC_Challenge() const; operator PlayerRecordsPC_Challenge() const; operator PlayerRecordsV3_Challenge() const; } __attribute__((packed)); template struct PlayerRecords_Battle { using U16T = typename std::conditional::type; // On Episode 3, place_counts[0] is win count and [1] is loss count /* 00 */ parray place_counts; /* 08 */ U16T disconnect_count; /* 0A */ parray unknown_a1; /* 10 */ parray unknown_a2; /* 18 */ } __attribute__((packed)); template struct ChoiceSearchConfig { // 0 = enabled, 1 = disabled. Unused for command C3 le_uint32_t choice_search_disabled = 0; struct Entry { ItemIDT parent_category_id = 0; ItemIDT category_id = 0; } __attribute__((packed)); parray entries; } __attribute__((packed)); 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 */ ptext auto_reply; /* 0214 */ PlayerBank bank; /* 14DC */ PlayerRecordsBB_Challenge challenge_records; /* 161C */ PlayerDispDataBB disp; /* 17AC */ ptext guild_card_description; /* 185C */ ptext 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 PlayerInventoryItem& item); PlayerInventoryItem 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); void print_inventory(FILE* stream) const; } __attribute__((packed)); enum AccountFlag { IN_DRESSING_ROOM = 0x00000001, }; struct SavedAccountDataBB { // .nsa file format ptext signature; parray blocked_senders; GuildCardFileBB guild_cards; KeyAndTeamConfigBB key_config; le_uint32_t newserv_flags; le_uint32_t option_flags; parray shortcuts; parray symbol_chats; ptext team_name; } __attribute__((packed)); class ClientGameData { private: std::shared_ptr account_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 // 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; // Null unless the client is Episode 3 and has sent its config already std::shared_ptr ep3_config; // These are only used if the client is BB std::string bb_username; size_t bb_player_index; PlayerInventoryItem identify_result; std::array, 3> shop_contents; bool should_save; ClientGameData(); ~ClientGameData(); std::shared_ptr account(bool should_load = true); std::shared_ptr player(bool should_load = true); std::shared_ptr account() const; std::shared_ptr player() const; std::string account_data_filename() const; std::string player_data_filename() const; static std::string player_template_filename(uint8_t char_class); void create_player( const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); void load_account_data(); void save_account_data() const; void load_player_data(); // Note: This function is not const because it updates the player's play time. void save_player_data(); }; uint32_t compute_guild_card_checksum(const void* data, size_t size); template DestT convert_player_disp_data(const SrcT&) { static_assert(always_false::v, "unspecialized strcpy_t should never be called"); } template <> inline PlayerDispDataDCPCV3 convert_player_disp_data( const PlayerDispDataDCPCV3& src) { return src; } template <> inline PlayerDispDataDCPCV3 convert_player_disp_data( const PlayerDispDataBB& src) { return src.to_dcpcv3(); } template <> inline PlayerDispDataBB convert_player_disp_data( const PlayerDispDataDCPCV3& src) { return src.to_bb(); } template <> inline PlayerDispDataBB convert_player_disp_data( const PlayerDispDataBB& src) { return src; }