diff --git a/README.md b/README.md index c679e1f7..d9aa05a2 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ Current known issues / missing features / things to do: - Implement all remaining player_use_item cases (there are many!) - Handle mag feeding and evolution properly - Implement trade window -- Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on other versions). + - Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on other versions). +- There is a function that encodes QST files, but there's no corresponding CLI option. - PSOX is not tested at all. - Memory patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly. - Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed. diff --git a/src/Quest.cc b/src/Quest.cc index d7be5cec..05da74b2 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -393,7 +393,8 @@ Quest::Quest(const string& bin_filename) is_dcv1(false), joinable(false), file_format(FileFormat::BIN_DAT), - has_mnm_extension(false) { + has_mnm_extension(false), + is_dlq_encoded(false) { if (ends_with(bin_filename, ".bin.gci") || ends_with(bin_filename, ".mnm.gci")) { this->file_format = FileFormat::BIN_DAT_GCI; @@ -981,6 +982,95 @@ pair Quest::decode_qst(const string& filename) { } } +template +void add_command_header( + StringWriter& w, uint8_t command, uint8_t flag, uint16_t size) { + HeaderT header; + header.command = command; + header.flag = flag; + header.size = sizeof(HeaderT) + size; + w.put(header); +} + +template +void add_open_file_command(StringWriter& w, const Quest& q, bool is_bin) { + add_command_header( + w, q.is_dlq_encoded ? 0xA6 : 0x44, q.internal_id, + sizeof(S_OpenFile_DC_44_A6)); + CmdT cmd; + cmd.name = "PSO/" + encode_sjis(q.name); + cmd.filename = q.file_basename + (is_bin ? ".bin" : ".dat"); + cmd.flags = q.is_dlq_encoded ? 0 : 2; + // TODO: It'd be nice to have something like w.emplace(...) to avoid copying + // the command structs into the StringWriter. + w.put(cmd); +} + +template +void add_write_file_commands( + StringWriter& w, + const string& filename, + const string& data, + bool is_dlq_encoded) { + for (size_t z = 0; z < data.size(); z += 0x400) { + size_t chunk_size = min(data.size() - z, 0x400); + add_command_header(w, is_dlq_encoded ? 0xA7 : 0x13, z >> 10, sizeof(S_WriteFile_13_A7)); + S_WriteFile_13_A7 cmd; + cmd.filename = filename; + memcpy(cmd.data.data(), &data[z], chunk_size); + cmd.data_size = chunk_size; + w.put(cmd); + } +} + +string Quest::export_qst(GameVersion version) const { + if (this->category == QuestCategory::EPISODE_3) { + throw runtime_error("Episode 3 quests cannot be encoded in QST format"); + } + + StringWriter w; + + switch (version) { + case GameVersion::DC: + add_open_file_command(w, *this, true); + add_open_file_command(w, *this, false); + add_write_file_commands( + w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded); + add_write_file_commands( + w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + break; + case GameVersion::PC: + add_open_file_command(w, *this, true); + add_open_file_command(w, *this, false); + add_write_file_commands( + w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded); + add_write_file_commands( + w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + break; + case GameVersion::GC: + case GameVersion::XB: + add_open_file_command(w, *this, true); + add_open_file_command(w, *this, false); + add_write_file_commands( + w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded); + add_write_file_commands( + w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + break; + case GameVersion::BB: + add_open_file_command(w, *this, true); + add_open_file_command(w, *this, false); + add_write_file_commands( + w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded); + add_write_file_commands( + w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + break; + default: + throw logic_error("invalid game version"); + } + + return move(w.str()); +} + QuestIndex::QuestIndex(const string& directory) : directory(directory) { @@ -1139,5 +1229,6 @@ shared_ptr Quest::create_download_quest() const { compressed_bin, decompressed_bin.size()))); dlq->dat_contents_ptr.reset(new string(create_download_quest_file( *this->dat_contents(), prs_decompress_size(*this->dat_contents())))); + dlq->is_dlq_encoded = true; return dlq; } diff --git a/src/Quest.hh b/src/Quest.hh index ed9c82ea..8249f42c 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -55,6 +55,7 @@ public: std::string file_basename; // we append -. when reading FileFormat file_format; bool has_mnm_extension; + bool is_dlq_encoded; std::u16string name; std::u16string short_description; std::u16string long_description; @@ -84,6 +85,8 @@ public: static std::string decode_dlq(const std::string& filename); static std::pair decode_qst(const std::string& filename); + std::string export_qst(GameVersion version) const; + private: // these are populated when requested mutable std::shared_ptr bin_contents_ptr;