diff --git a/src/Main.cc b/src/Main.cc index e413a0b9..8416c5cd 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1564,10 +1564,13 @@ Action a_decode_qst( Action a_encode_qst( "encode-qst", "\ encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\ - Encode the input quest file (in .bin/.dat format) into a .qst file. If\n\ - --download is given, generates a download .qst instead of an online .qst.\n\ - Specify the quest\'s game version with one of the --dc-nte, --dc-v1,\n\ - --dc-v2, --pc, --gc-nte, --gc, --gc-ep3, --xb, or --bb options.\n", + Encode the input quest files (in .bin format) into a .qst file. There must\n\ + be a .dat file with the same name as the .bin file, which will be included\n\ + in the resulting .qst file. If there is a .pvr file with the same name as\n\ + the .bin file, it will be included as well. If --download is given,\n\ + generates a download .qst instead of an online .qst. Specify the quest\'s\n\ + game version with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte,\n\ + --gc, --gc-ep3, --xb, or --bb options.\n", +[](phosg::Arguments& args) { std::string input_filename = args.get(1, false); if (input_filename.empty() || (input_filename == "-")) { diff --git a/src/Quest.cc b/src/Quest.cc index f2848e38..aedeb644 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -1064,9 +1064,19 @@ template static std::unordered_map decode_qst_data_t(const std::string& data) { phosg::StringReader r(data); - std::unordered_map files; - std::unordered_map file_remaining_bytes; + struct File { + struct Block { + uint32_t index = 0; + const void* data = nullptr; + size_t size = 0; + }; + std::deque blocks; + size_t total_size = 0; + }; + std::unordered_map files; + QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown + bool assemble_in_order = true; while (!r.eof()) { // Handle BB's implicit 8-byte command alignment static constexpr size_t alignment = sizeof(HeaderT); @@ -1098,11 +1108,7 @@ static std::unordered_map decode_qst_data_t(const std: } const auto& cmd = r.get(); std::string internal_filename = cmd.filename.decode(); - - if (!files.emplace(internal_filename, "").second) { - throw std::runtime_error("qst opens the same file multiple times: " + internal_filename); - } - if (!file_remaining_bytes.emplace(internal_filename, cmd.file_size).second) { + if (!files.emplace(internal_filename, File{{}, cmd.file_size}).second) { throw std::runtime_error("qst opens the same file multiple times: " + internal_filename); } @@ -1113,41 +1119,59 @@ static std::unordered_map decode_qst_data_t(const std: throw std::runtime_error("qst write file command has incorrect size"); } const auto& cmd = r.get(); - if (cmd.data_size > 0x400) { - throw std::runtime_error("qst contains invalid write command"); - } std::string filename = cmd.filename.decode(); - - std::string& file_data = files.at(filename); - size_t& remaining_bytes = file_remaining_bytes.at(filename); - - if (file_data.size() & 0x3FF) { - throw std::runtime_error("qst contains uneven chunks out of order"); + if (cmd.data_size > 0x400) { + throw std::runtime_error(std::format( + "qst chunk {}:{:02X} has invalid size {:08X}", filename, header.flag, cmd.data_size)); } - if (header.flag != file_data.size() / 0x400) { - throw std::runtime_error("qst contains chunks out of order"); + + auto& file = files.at(filename); + size_t offset = header.flag * 0x400; + if (offset + cmd.data_size > file.total_size) { + throw std::runtime_error(std::format("qst chunk {}:{:02X} extends beyond end of file", filename, header.flag)); + } + auto& block = file.blocks.emplace_back(); + block.index = header.flag; + block.data = cmd.data.data(); + block.size = cmd.data_size; + + if (header.flag) { + assemble_in_order = false; } - file_data.append(reinterpret_cast(cmd.data.data()), cmd.data_size); - remaining_bytes -= cmd.data_size; } else { throw std::runtime_error("invalid command in qst file"); } } - for (const auto& it : file_remaining_bytes) { - if (it.second) { - throw std::runtime_error(std::format("expected {} (0x{:X}) more bytes for file {}", it.second, it.second, it.first)); + // QST is an unfortunately underspecified format, and there are technically many versions of it out there (though I'm + // sure no one has ever thought much about this). Here we handle one of the format's quirks, in which some QST files + // have all zeroes for chunk indexes, so we just assume in that case that the chunks are in the correct order in the + // source file. + std::unordered_map ret; + for (const auto& [name, file] : files) { + std::string& assembled_file = ret.emplace(name, "").first->second; + if (assemble_in_order) { + static_game_data_log.warning_f( + "QST file entry {} is missing block indexes; assuming blocks are assembled in order", name); + for (const auto& block : file.blocks) { + assembled_file.append(reinterpret_cast(block.data), block.size); + } + } else { + assembled_file.resize(file.total_size, 0x00); + for (const auto& block : file.blocks) { + memcpy(assembled_file.data() + block.index * 0x400, block.data, block.size); + } } } if (subformat == QuestFileFormat::BIN_DAT_DLQ) { - for (auto& it : files) { - it.second = decode_dlq_data(it.second); + for (auto& [_, data] : ret) { + data = decode_dlq_data(data); } } - return files; + return ret; } std::unordered_map decode_qst_data(const std::string& data) { diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh index df1635d4..940ee96d 100644 --- a/src/QuestMetadata.hh +++ b/src/QuestMetadata.hh @@ -39,7 +39,7 @@ struct QuestMetadata { std::string str() const; }; uint32_t category_id = 0xFFFFFFFF; - uint32_t quest_number = 0xFFFFFFFF; + uint32_t quest_number = 0; Episode episode = Episode::NONE; std::array floor_assignments; bool joinable = false;