diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 77d98357..e368da9a 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1531,9 +1531,13 @@ struct C_Login_BB_93 { // 96 (C->S): Character save information struct C_CharSaveInfo_V3_BB_96 { - // This field appears to be a checksum or random stamp of some sort; it seems - // to be unique and constant per character. - le_uint32_t unknown_a1 = 0; + // The creation timestamp is the number of seconds since 12:00AM on 1 January + // 2000. Instead of computing this directly from the TBR (on PSO GC), the game + // uses localtime(), then converts that to the desired timestamp. The leap + // year correction in the latter phase of this computation seems incorrect; it + // adds a day in 2002, 2006, etc. instead of 2004, 2008, etc. See + // compute_psogc_timestamp in SaveFileFormats.cc for details. + le_uint32_t creation_timestamp = 0; // This field counts certain events on a per-character basis. One of the // relevant events is the act of sending a 96 command; another is the act of // receiving a 97 command (to which the client responds with a B1 command). diff --git a/src/Main.cc b/src/Main.cc index 2d21afdc..643dad0a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -620,7 +620,7 @@ int main(int argc, char** argv) { const auto& header = r.get(); header.check(); const auto& system = r.get(); - round1_seed = system.creation_internet_time; + round1_seed = system.creation_timestamp; } else if (!seed.empty()) { round1_seed = stoul(seed, nullptr, 16); } else { diff --git a/src/RareItemSet.hh b/src/RareItemSet.hh index d7acf1c3..166de568 100644 --- a/src/RareItemSet.hh +++ b/src/RareItemSet.hh @@ -15,10 +15,9 @@ class RareItemSet { public: struct Table { // 0x280 in size; describes one difficulty, section ID, and episode - // TODO: It looks like this structure can actually vary. We see the offsets - // 0194 and 01B2 in the unused section, along with the value 1E (number of - // box rares). In PSOGC, these all appear to be the same size/format, but - // that's probably not strictly required to be the case. + // TODO: It looks like this structure can actually vary. In PSOGC, these all + // appear to be the same size/format, but that's probably not strictly + // required to be the case. struct Drop { uint8_t probability; uint8_t item_code[3]; diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 45b0620f..8a7e4ca0 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -87,3 +87,23 @@ bool PSOGCIFileHeader::is_ep12() const { bool PSOGCIFileHeader::is_ep3() const { return (this->game_id[2] == 'S'); } + + + +uint32_t compute_psogc_timestamp( + uint16_t year, + uint8_t month, + uint8_t day, + uint8_t hour, + uint8_t minute, + uint8_t second) { + static uint16_t month_start_day[12] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + + uint32_t year_start_day = ((year - 1998) >> 2) + (year - 2000) * 365; + if ((((year - 1998) & 3) == 0) && (month < 3)) { + year_start_day--; + } + uint32_t res_day = (day - 1) + year_start_day + month_start_day[month - 1]; + return second + (minute + (hour + (res_day * 24)) * 60) * 60; +} diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 2a606d51..7b0d6753 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -71,7 +71,10 @@ struct PSOGCSystemFile { /* 000E */ be_uint16_t surround_sound_enabled; /* 0010 */ parray unknown_a6; /* 0110 */ parray unknown_a7; - /* 0118 */ be_uint32_t creation_internet_time; // Character file round1 seed + // This timestamp is the number of seconds since 12:00AM on 1 January 2000. + // This field is also used as the round1 seed for encrypting the character and + // Guild Card files. + /* 0118 */ be_uint32_t creation_timestamp; /* 011C */ } __attribute__((packed)); @@ -116,9 +119,9 @@ struct PSOGCCharacterFile { /* 0000 */ PlayerInventory inventory; /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ be_uint32_t unknown_a1; - /* 0420 */ be_uint32_t save_token; // Sent in 96 command + /* 0420 */ be_uint32_t creation_timestamp; /* 0424 */ parray unknown_a2; - /* 0430 */ be_uint32_t save_count; // Sent in 96 command + /* 0430 */ be_uint32_t save_count; /* 0434 */ parray unknown_a3; /* 0664 */ PlayerBank bank; /* 192C */ GuildCardV3 guild_card; @@ -148,19 +151,25 @@ struct PSOGCEp3CharacterFile { /* 034C */ PlayerDispDataDCPCV3 disp; /* 041C */ be_uint32_t unknown_a1; /* 0420 */ be_uint32_t save_token; // Sent in 96 command - /* 0424 */ parray unknown_a2; + // The signature field holds the value 0xA205B064, which is 2718281828 in + // decimal - approximately e * 10^9. It's unknown why Sega chose this value. + /* 0424 */ be_uint32_t signature; + /* 0428 */ be_uint32_t unknown_a2; + /* 042C */ be_uint32_t unknown_a3; /* 0430 */ be_uint32_t save_count; // Sent in 96 command - /* 0434 */ parray unknown_a3; + /* 0434 */ parray unknown_a4; + /* 0450 */ parray unknown_a5; + /* 0460 */ parray unknown_a6; /* 08CC */ GuildCardV3 guild_card; /* 095C */ parray symbol_chats; /* 0D7C */ parray chat_shortcuts; - /* 140C */ parray unknown_a4; + /* 140C */ parray unknown_a7; /* 14B8 */ ptext info_board; - /* 1564 */ parray unknown_a5; + /* 1564 */ parray unknown_a8; /* 1658 */ Episode3::PlayerConfig ep3_config; - /* 39A8 */ be_uint32_t unknown_a7; - /* 39AC */ be_uint32_t unknown_a8; - /* 39B0 */ be_uint32_t unknown_a9; + /* 39A8 */ be_uint32_t unknown_a9; + /* 39AC */ be_uint32_t unknown_a10; + /* 39B0 */ be_uint32_t unknown_a11; /* 39B4 */ } __attribute__((packed)); /* 00004 */ parray characters; @@ -201,7 +210,7 @@ struct PSOGCGuildCardFile { } __attribute__((packed)); /* 00C4 */ parray entries; /* D2C4 */ parray blocked_senders; - /* E284 */ be_uint32_t unknown_a3; + /* E284 */ be_uint32_t creation_timestamp; /* E288 */ be_uint32_t round2_seed; /* E28C */ } __attribute__((packed)); @@ -285,3 +294,13 @@ std::string encrypt_gci_fixed_size_file_data_section( return encrypt_gci_or_vms_v2_data_section( &encrypted, sizeof(StructT), round1_seed); } + + + +uint32_t compute_psogc_timestamp( + uint16_t year, + uint8_t month, + uint8_t day, + uint8_t hour, + uint8_t minute, + uint8_t second);