diff --git a/Compression.cc b/Compression.cc index 9bedd4c0..92f56f8f 100644 --- a/Compression.cc +++ b/Compression.cc @@ -184,7 +184,7 @@ string prs_decompress(const string& data, size_t max_size) { } output += static_cast(ch); if (max_size && (output.size() > max_size)) { - throw runtime_error("maximumoutput size exceeded"); + throw runtime_error("maximum output size exceeded"); } continue; } @@ -258,3 +258,109 @@ string prs_decompress(const string& data, size_t max_size) { } } } + +size_t prs_decompress_size(const string& data, size_t max_size) { + size_t output_size = 0; + StringReader r(data.data(), data.size()); + + int32_t r3, r5; + int bitpos = 9; + int16_t currentbyte; // int16_t because it can be -1 when EOF occurs + int flag; + int offset; + unsigned long x; + + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output_size; + } + + for (;;) { + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output_size; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + if (flag) { + int ch = get_u8_or_eof(r); + if (ch == EOF) { + return output_size; + } + output_size++; + if (max_size && (output_size > max_size)) { + throw runtime_error("maximum output size exceeded"); + } + continue; + } + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output_size; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + if (flag) { + r3 = get_u8_or_eof(r); + if (r3 == EOF) { + return output_size; + } + int high_byte = get_u8_or_eof(r); + if (high_byte == EOF) { + return output_size; + } + offset = ((high_byte & 0xFF) << 8) | (r3 & 0xFF); + if (offset == 0) { + return output_size; + } + r3 = r3 & 0x00000007; + r5 = (offset >> 3) | 0xFFFFE000; + if (r3 == 0) { + flag = 0; + r3 = get_u8_or_eof(r); + if (r3 == EOF) { + return output_size; + } + r3 = (r3 & 0xFF) + 1; + } else { + r3 += 2; + } + } else { + r3 = 0; + for (x = 0; x < 2; x++) { + bitpos--; + if (bitpos == 0) { + currentbyte = get_u8_or_eof(r); + if (currentbyte == EOF) { + return output_size; + } + bitpos = 8; + } + flag = currentbyte & 1; + currentbyte = currentbyte >> 1; + offset = r3 << 1; + r3 = offset | flag; + } + offset = get_u8_or_eof(r); + if (offset == EOF) { + return output_size; + } + r3 += 2; + r5 = offset | 0xFFFFFF00; + } + if (r3 == 0) { + continue; + } + output_size += r3; + if (max_size && (output_size > max_size)) { + throw runtime_error("maximum output size exceeded"); + } + } +} diff --git a/Compression.hh b/Compression.hh index 790e86ca..7b59bcf1 100644 --- a/Compression.hh +++ b/Compression.hh @@ -8,3 +8,4 @@ std::string prs_compress(const std::string& data); std::string prs_decompress(const std::string& data, size_t max_size = 0); +size_t prs_decompress_size(const std::string& data, size_t max_size = 0); diff --git a/Quest.cc b/Quest.cc index 474e7aa8..f7cccc78 100644 --- a/Quest.cc +++ b/Quest.cc @@ -6,6 +6,7 @@ #include #include "Compression.hh" +#include "PSOEncryption.hh" #include "Text.hh" using namespace std; @@ -56,8 +57,8 @@ struct PSOQuestHeaderDC { // same for dc v1 and v2, thankfully uint32_t unknown_offset1; uint32_t size; uint32_t unused; + uint8_t is_download; uint8_t unknown1; - uint8_t unknown2; uint16_t quest_number; // 0xFFFF for challenge quests char name[0x20]; char short_description[0x80]; @@ -69,8 +70,8 @@ struct PSOQuestHeaderPC { uint32_t unknown_offset1; uint32_t size; uint32_t unused; + uint8_t is_download; uint8_t unknown1; - uint8_t unknown2; uint16_t quest_number; // 0xFFFF for challenge quests char16_t name[0x20]; char16_t short_description[0x80]; @@ -82,7 +83,8 @@ struct PSOQuestHeaderGC { uint32_t unknown_offset1; uint32_t size; uint32_t unused; - uint16_t unknown1; + uint8_t is_download; + uint8_t unknown1; uint8_t quest_number; uint8_t episode; // 1 = ep2. apparently some quests have 0xFF here, which means ep1 (?) char name[0x20]; @@ -362,3 +364,76 @@ vector> QuestIndex::filter(GameVersion version, return ret; } + + + +static string create_download_quest_file(const string& compressed_data, + size_t decompressed_size) { + struct PSODownloadQuestHeader { + uint32_t decompressed_size; + uint32_t encryption_seed; // note: use PC encryption, even for GC quests + }; + + string data(8, '\0'); + auto* header = reinterpret_cast(const_cast( + compressed_data.data())); + header->decompressed_size = decompressed_size + sizeof(PSODownloadQuestHeader); + header->encryption_seed = random_object(); + data += compressed_data; + + // add extra bytes if necessary so encryption won't fail + data.resize((data.size() + 3) & (~3)); + + // TODO: for DC quests, do we use DC encryption? + PSOPCEncryption encr(header->encryption_seed); + encr.encrypt(const_cast(data.data() + sizeof(PSODownloadQuestHeader)), + data.size() - sizeof(PSODownloadQuestHeader)); + return data; +} + +shared_ptr Quest::create_download_quest(const string& file_basename) const { + if (this->category == QuestCategory::Download) { + throw invalid_argument("quest is already a download quest"); + } + + string decompressed_bin = prs_decompress(*this->bin_contents()); + void* data_ptr = const_cast(decompressed_bin.data()); + switch (this->version) { + case GameVersion::DC: + reinterpret_cast(data_ptr)->is_download = 0x01; + break; + case GameVersion::PC: + reinterpret_cast(data_ptr)->is_download = 0x01; + break; + case GameVersion::GC: + reinterpret_cast(data_ptr)->is_download = 0x01; + break; + case GameVersion::BB: + throw invalid_argument("PSOBB does not support download quests"); + default: + throw invalid_argument("unknown game version"); + } + + shared_ptr dlq(new Quest(file_basename)); + dlq->quest_id = this->quest_id; + dlq->category = QuestCategory::Download; + dlq->episode = this->episode; + dlq->is_dcv1 = this->is_dcv1; + dlq->joinable = this->joinable; + dlq->version = this->version; + dlq->name = this->name; + dlq->short_description = this->short_description; + dlq->long_description = this->long_description; + + dlq->bin_contents_ptr.reset(new string(create_download_quest_file( + prs_compress(decompressed_bin), decompressed_bin.size()))); + + auto dat_contents = this->dat_contents(); + dlq->dat_contents_ptr.reset(new string(create_download_quest_file( + *dat_contents, prs_decompress_size(*dat_contents)))); + + save_file(dlq->bin_filename(), *dlq->bin_contents_ptr); + save_file(dlq->dat_filename(), *dlq->dat_contents_ptr); + + return dlq; +} diff --git a/Quest.hh b/Quest.hh index 04934eba..73c649ac 100644 --- a/Quest.hh +++ b/Quest.hh @@ -53,6 +53,9 @@ struct Quest { std::shared_ptr bin_contents() const; std::shared_ptr dat_contents() const; + + std::shared_ptr create_download_quest( + const std::string& file_basename) const; }; struct QuestIndex {