handle missing block indexes in qst decoder
This commit is contained in:
+7
-4
@@ -1564,10 +1564,13 @@ Action a_decode_qst(
|
|||||||
Action a_encode_qst(
|
Action a_encode_qst(
|
||||||
"encode-qst", "\
|
"encode-qst", "\
|
||||||
encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\
|
encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\
|
||||||
Encode the input quest file (in .bin/.dat format) into a .qst file. If\n\
|
Encode the input quest files (in .bin format) into a .qst file. There must\n\
|
||||||
--download is given, generates a download .qst instead of an online .qst.\n\
|
be a .dat file with the same name as the .bin file, which will be included\n\
|
||||||
Specify the quest\'s game version with one of the --dc-nte, --dc-v1,\n\
|
in the resulting .qst file. If there is a .pvr file with the same name as\n\
|
||||||
--dc-v2, --pc, --gc-nte, --gc, --gc-ep3, --xb, or --bb options.\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) {
|
+[](phosg::Arguments& args) {
|
||||||
std::string input_filename = args.get<std::string>(1, false);
|
std::string input_filename = args.get<std::string>(1, false);
|
||||||
if (input_filename.empty() || (input_filename == "-")) {
|
if (input_filename.empty() || (input_filename == "-")) {
|
||||||
|
|||||||
+50
-26
@@ -1064,9 +1064,19 @@ template <typename HeaderT, typename OpenFileT>
|
|||||||
static std::unordered_map<std::string, std::string> decode_qst_data_t(const std::string& data) {
|
static std::unordered_map<std::string, std::string> decode_qst_data_t(const std::string& data) {
|
||||||
phosg::StringReader r(data);
|
phosg::StringReader r(data);
|
||||||
|
|
||||||
std::unordered_map<std::string, std::string> files;
|
struct File {
|
||||||
std::unordered_map<std::string, size_t> file_remaining_bytes;
|
struct Block {
|
||||||
|
uint32_t index = 0;
|
||||||
|
const void* data = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
};
|
||||||
|
std::deque<Block> blocks;
|
||||||
|
size_t total_size = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, File> files;
|
||||||
|
|
||||||
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
|
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
|
||||||
|
bool assemble_in_order = true;
|
||||||
while (!r.eof()) {
|
while (!r.eof()) {
|
||||||
// Handle BB's implicit 8-byte command alignment
|
// Handle BB's implicit 8-byte command alignment
|
||||||
static constexpr size_t alignment = sizeof(HeaderT);
|
static constexpr size_t alignment = sizeof(HeaderT);
|
||||||
@@ -1098,11 +1108,7 @@ static std::unordered_map<std::string, std::string> decode_qst_data_t(const std:
|
|||||||
}
|
}
|
||||||
const auto& cmd = r.get<OpenFileT>();
|
const auto& cmd = r.get<OpenFileT>();
|
||||||
std::string internal_filename = cmd.filename.decode();
|
std::string internal_filename = cmd.filename.decode();
|
||||||
|
if (!files.emplace(internal_filename, File{{}, cmd.file_size}).second) {
|
||||||
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) {
|
|
||||||
throw std::runtime_error("qst opens the same file multiple times: " + internal_filename);
|
throw std::runtime_error("qst opens the same file multiple times: " + internal_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,41 +1119,59 @@ static std::unordered_map<std::string, std::string> decode_qst_data_t(const std:
|
|||||||
throw std::runtime_error("qst write file command has incorrect size");
|
throw std::runtime_error("qst write file command has incorrect size");
|
||||||
}
|
}
|
||||||
const auto& cmd = r.get<S_WriteFile_13_A7>();
|
const auto& cmd = r.get<S_WriteFile_13_A7>();
|
||||||
if (cmd.data_size > 0x400) {
|
|
||||||
throw std::runtime_error("qst contains invalid write command");
|
|
||||||
}
|
|
||||||
std::string filename = cmd.filename.decode();
|
std::string filename = cmd.filename.decode();
|
||||||
|
if (cmd.data_size > 0x400) {
|
||||||
std::string& file_data = files.at(filename);
|
throw std::runtime_error(std::format(
|
||||||
size_t& remaining_bytes = file_remaining_bytes.at(filename);
|
"qst chunk {}:{:02X} has invalid size {:08X}", filename, header.flag, cmd.data_size));
|
||||||
|
|
||||||
if (file_data.size() & 0x3FF) {
|
|
||||||
throw std::runtime_error("qst contains uneven chunks out of order");
|
|
||||||
}
|
}
|
||||||
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<const char*>(cmd.data.data()), cmd.data_size);
|
|
||||||
remaining_bytes -= cmd.data_size;
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error("invalid command in qst file");
|
throw std::runtime_error("invalid command in qst file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& it : file_remaining_bytes) {
|
// QST is an unfortunately underspecified format, and there are technically many versions of it out there (though I'm
|
||||||
if (it.second) {
|
// sure no one has ever thought much about this). Here we handle one of the format's quirks, in which some QST files
|
||||||
throw std::runtime_error(std::format("expected {} (0x{:X}) more bytes for file {}", it.second, it.second, it.first));
|
// 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<std::string, std::string> 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<const char*>(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) {
|
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
|
||||||
for (auto& it : files) {
|
for (auto& [_, data] : ret) {
|
||||||
it.second = decode_dlq_data(it.second);
|
data = decode_dlq_data(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data) {
|
std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct QuestMetadata {
|
|||||||
std::string str() const;
|
std::string str() const;
|
||||||
};
|
};
|
||||||
uint32_t category_id = 0xFFFFFFFF;
|
uint32_t category_id = 0xFFFFFFFF;
|
||||||
uint32_t quest_number = 0xFFFFFFFF;
|
uint32_t quest_number = 0;
|
||||||
Episode episode = Episode::NONE;
|
Episode episode = Episode::NONE;
|
||||||
std::array<FloorAssignment, 0x12> floor_assignments;
|
std::array<FloorAssignment, 0x12> floor_assignments;
|
||||||
bool joinable = false;
|
bool joinable = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user