index quests by number, then by version
This commit is contained in:
@@ -2210,9 +2210,9 @@ MapIndex::MapIndex(const string& directory) {
|
||||
} else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) {
|
||||
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
|
||||
} else if (ends_with(filename, ".gci")) {
|
||||
entry.reset(new MapEntry(Quest::decode_gci_file(directory + "/" + filename)));
|
||||
entry.reset(new MapEntry(decode_gci_file(directory + "/" + filename)));
|
||||
} else if (ends_with(filename, ".dlq")) {
|
||||
entry.reset(new MapEntry(Quest::decode_dlq_file(directory + "/" + filename)));
|
||||
entry.reset(new MapEntry(decode_dlq_file(directory + "/" + filename)));
|
||||
}
|
||||
|
||||
if (entry.get()) {
|
||||
|
||||
+16
-16
@@ -399,7 +399,7 @@ int main(int argc, char** argv) {
|
||||
Behavior behavior = Behavior::RUN_SERVER;
|
||||
GameVersion cli_version = GameVersion::GC;
|
||||
QuestScriptVersion cli_quest_version = QuestScriptVersion::GC_V3;
|
||||
Quest::FileFormat quest_file_type = Quest::FileFormat::BIN_DAT_GCI;
|
||||
QuestFileFormat quest_file_type = QuestFileFormat::BIN_DAT_GCI;
|
||||
string seed;
|
||||
string key_file_name;
|
||||
const char* config_filename = "system/config.json";
|
||||
@@ -582,16 +582,16 @@ int main(int argc, char** argv) {
|
||||
behavior = Behavior::DECODE_SJIS;
|
||||
} else if (!strcmp(argv[x], "decode-gci")) {
|
||||
behavior = Behavior::DECODE_QUEST_FILE;
|
||||
quest_file_type = Quest::FileFormat::BIN_DAT_GCI;
|
||||
quest_file_type = QuestFileFormat::BIN_DAT_GCI;
|
||||
} else if (!strcmp(argv[x], "decode-vms")) {
|
||||
behavior = Behavior::DECODE_QUEST_FILE;
|
||||
quest_file_type = Quest::FileFormat::BIN_DAT_VMS;
|
||||
quest_file_type = QuestFileFormat::BIN_DAT_VMS;
|
||||
} else if (!strcmp(argv[x], "decode-dlq")) {
|
||||
behavior = Behavior::DECODE_QUEST_FILE;
|
||||
quest_file_type = Quest::FileFormat::BIN_DAT_DLQ;
|
||||
quest_file_type = QuestFileFormat::BIN_DAT_DLQ;
|
||||
} else if (!strcmp(argv[x], "decode-qst")) {
|
||||
behavior = Behavior::DECODE_QUEST_FILE;
|
||||
quest_file_type = Quest::FileFormat::QST;
|
||||
quest_file_type = QuestFileFormat::QST;
|
||||
} else if (!strcmp(argv[x], "encode-qst")) {
|
||||
behavior = Behavior::ENCODE_QST;
|
||||
} else if (!strcmp(argv[x], "disassemble-quest-script")) {
|
||||
@@ -1337,19 +1337,19 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
string output_filename_base = input_filename;
|
||||
if (quest_file_type == Quest::FileFormat::BIN_DAT_GCI) {
|
||||
if (quest_file_type == QuestFileFormat::BIN_DAT_GCI) {
|
||||
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
||||
auto decoded = Quest::decode_gci_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
auto decoded = decode_gci_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_VMS) {
|
||||
} else if (quest_file_type == QuestFileFormat::BIN_DAT_VMS) {
|
||||
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
|
||||
auto decoded = Quest::decode_vms_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
auto decoded = decode_vms_file(input_filename, num_threads, dec_seed, skip_checksum);
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == Quest::FileFormat::BIN_DAT_DLQ) {
|
||||
auto decoded = Quest::decode_dlq_file(input_filename);
|
||||
} else if (quest_file_type == QuestFileFormat::BIN_DAT_DLQ) {
|
||||
auto decoded = decode_dlq_file(input_filename);
|
||||
save_file(output_filename_base + ".dec", decoded);
|
||||
} else if (quest_file_type == Quest::FileFormat::QST) {
|
||||
auto data = Quest::decode_qst_file(input_filename);
|
||||
} else if (quest_file_type == QuestFileFormat::QST) {
|
||||
auto data = decode_qst_file(input_filename);
|
||||
save_file(output_filename_base + ".bin", data.first);
|
||||
save_file(output_filename_base + ".dat", data.second);
|
||||
} else {
|
||||
@@ -1363,11 +1363,11 @@ int main(int argc, char** argv) {
|
||||
throw invalid_argument("an input filename is required");
|
||||
}
|
||||
|
||||
shared_ptr<Quest> q(new Quest(input_filename, cli_quest_version, nullptr));
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(input_filename, cli_quest_version, nullptr));
|
||||
if (download) {
|
||||
q = q->create_download_quest();
|
||||
vq = vq->create_download_quest();
|
||||
}
|
||||
string qst_data = q->encode_qst();
|
||||
string qst_data = vq->encode_qst();
|
||||
|
||||
write_output_data(qst_data.data(), qst_data.size());
|
||||
break;
|
||||
|
||||
+1
-1
@@ -488,7 +488,7 @@ void Map::add_enemies_from_quest_data(
|
||||
size_t size) {
|
||||
StringReader r(data, size);
|
||||
while (!r.eof()) {
|
||||
const auto& header = r.get<Quest::DATSectionHeader>();
|
||||
const auto& header = r.get<VersionedQuest::DATSectionHeader>();
|
||||
if (header.type == 0 && header.section_size == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
+1
-1
@@ -626,7 +626,7 @@ ProxyServer::LinkedSession::SavingFile::SavingFile(
|
||||
void ProxyServer::LinkedSession::SavingFile::write() const {
|
||||
string data = join(this->blocks);
|
||||
if (is_download && (ends_with(this->basename, ".bin") || ends_with(this->basename, ".dat"))) {
|
||||
data = Quest::decode_dlq_data(data);
|
||||
data = decode_dlq_data(data);
|
||||
}
|
||||
save_file(this->output_filename, data);
|
||||
}
|
||||
|
||||
+309
-237
@@ -216,37 +216,39 @@ struct PSODownloadQuestHeader {
|
||||
le_uint32_t encryption_seed;
|
||||
} __attribute__((packed));
|
||||
|
||||
Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<const QuestCategoryIndex> category_index)
|
||||
VersionedQuest::VersionedQuest(
|
||||
const string& bin_filename,
|
||||
QuestScriptVersion version,
|
||||
shared_ptr<const QuestCategoryIndex> category_index)
|
||||
: quest_number(0xFFFFFFFF),
|
||||
menu_item_id(0),
|
||||
category_id(0),
|
||||
category_id(0xFFFFFFFF),
|
||||
episode(Episode::NONE),
|
||||
joinable(false),
|
||||
version(version),
|
||||
file_format(FileFormat::BIN_DAT),
|
||||
file_format(QuestFileFormat::BIN_DAT),
|
||||
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;
|
||||
this->file_format = QuestFileFormat::BIN_DAT_GCI;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm.gci");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.vms")) {
|
||||
this->file_format = FileFormat::BIN_DAT_VMS;
|
||||
this->file_format = QuestFileFormat::BIN_DAT_VMS;
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".bin.dlq") || ends_with(bin_filename, ".mnm.dlq")) {
|
||||
this->file_format = FileFormat::BIN_DAT_DLQ;
|
||||
this->file_format = QuestFileFormat::BIN_DAT_DLQ;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm.dlq");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
|
||||
} else if (ends_with(bin_filename, ".qst")) {
|
||||
this->file_format = FileFormat::QST;
|
||||
this->file_format = QuestFileFormat::QST;
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bin") || ends_with(bin_filename, ".mnm")) {
|
||||
this->file_format = FileFormat::BIN_DAT;
|
||||
this->file_format = QuestFileFormat::BIN_DAT;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnm");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
|
||||
} else if (ends_with(bin_filename, ".bind") || ends_with(bin_filename, ".mnmd")) {
|
||||
this->file_format = FileFormat::BIN_DAT_UNCOMPRESSED;
|
||||
this->file_format = QuestFileFormat::BIN_DAT_UNCOMPRESSED;
|
||||
this->has_mnm_extension = ends_with(bin_filename, ".mnmd");
|
||||
this->file_basename = bin_filename.substr(0, bin_filename.size() - 5);
|
||||
} else {
|
||||
@@ -285,6 +287,9 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
this->category_id = 0;
|
||||
}
|
||||
|
||||
// Parse the number out of the first token
|
||||
this->quest_number = strtoull(tokens[0].c_str() + 1, nullptr, 10);
|
||||
|
||||
// Get the version from the second (or previously third) token
|
||||
static const unordered_map<string, QuestScriptVersion> name_to_version({
|
||||
{"dn", QuestScriptVersion::DC_NTE},
|
||||
@@ -316,7 +321,9 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = Episode::EP1;
|
||||
this->quest_number = header->quest_number;
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
this->quest_number = header->quest_number;
|
||||
}
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->short_description);
|
||||
this->long_description = decode_sjis(header->long_description);
|
||||
@@ -330,7 +337,9 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = Episode::EP1;
|
||||
this->quest_number = header->quest_number;
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
this->quest_number = header->quest_number;
|
||||
}
|
||||
this->name = header->name;
|
||||
this->short_description = header->short_description;
|
||||
this->long_description = header->long_description;
|
||||
@@ -348,13 +357,15 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
|
||||
throw invalid_argument("file is incorrect size");
|
||||
}
|
||||
auto* header = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
|
||||
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = Episode::EP3;
|
||||
this->quest_number = header->map_number;
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->quest_name);
|
||||
this->long_description = decode_sjis(header->description);
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
this->quest_number = map->map_number;
|
||||
}
|
||||
this->name = decode_sjis(map->name);
|
||||
this->short_description = decode_sjis(map->quest_name);
|
||||
this->long_description = decode_sjis(map->description);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -367,7 +378,9 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = (header->episode == 1) ? Episode::EP2 : Episode::EP1;
|
||||
this->quest_number = header->quest_number;
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
this->quest_number = header->quest_number;
|
||||
}
|
||||
this->name = decode_sjis(header->name);
|
||||
this->short_description = decode_sjis(header->short_description);
|
||||
this->long_description = decode_sjis(header->long_description);
|
||||
@@ -393,7 +406,9 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
default:
|
||||
throw runtime_error("invalid episode number");
|
||||
}
|
||||
this->quest_number = header->quest_number;
|
||||
if (this->quest_number == 0xFFFFFFFF) {
|
||||
this->quest_number = header->quest_number;
|
||||
}
|
||||
this->name = header->name;
|
||||
this->short_description = header->short_description;
|
||||
this->long_description = header->long_description;
|
||||
@@ -409,7 +424,7 @@ Quest::Quest(const string& bin_filename, QuestScriptVersion version, shared_ptr<
|
||||
}
|
||||
}
|
||||
|
||||
string Quest::bin_filename() const {
|
||||
string VersionedQuest::bin_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
return string_printf("m%06" PRIu32 "p_e.bin", this->quest_number);
|
||||
} else {
|
||||
@@ -417,7 +432,7 @@ string Quest::bin_filename() const {
|
||||
}
|
||||
}
|
||||
|
||||
string Quest::dat_filename() const {
|
||||
string VersionedQuest::dat_filename() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .dat files");
|
||||
} else {
|
||||
@@ -425,31 +440,31 @@ string Quest::dat_filename() const {
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const string> Quest::bin_contents() const {
|
||||
shared_ptr<const string> VersionedQuest::bin_contents() const {
|
||||
if (!this->bin_contents_ptr) {
|
||||
switch (this->file_format) {
|
||||
case FileFormat::BIN_DAT:
|
||||
case QuestFileFormat::BIN_DAT:
|
||||
this->bin_contents_ptr.reset(new string(load_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm" : ".bin"))));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
this->bin_contents_ptr.reset(new string(prs_compress(load_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnmd" : ".bind")))));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_GCI:
|
||||
this->bin_contents_ptr.reset(new string(this->decode_gci_file(
|
||||
case QuestFileFormat::BIN_DAT_GCI:
|
||||
this->bin_contents_ptr.reset(new string(decode_gci_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.gci" : ".bin.gci"))));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_VMS:
|
||||
this->bin_contents_ptr.reset(new string(this->decode_vms_file(
|
||||
case QuestFileFormat::BIN_DAT_VMS:
|
||||
this->bin_contents_ptr.reset(new string(decode_vms_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.vms" : ".bin.vms"))));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_DLQ:
|
||||
this->bin_contents_ptr.reset(new string(this->decode_dlq_file(
|
||||
case QuestFileFormat::BIN_DAT_DLQ:
|
||||
this->bin_contents_ptr.reset(new string(decode_dlq_file(
|
||||
this->file_basename + (this->has_mnm_extension ? ".mnm.dlq" : ".bin.dlq"))));
|
||||
break;
|
||||
case FileFormat::QST: {
|
||||
auto result = this->decode_qst_file(this->file_basename + ".qst");
|
||||
case QuestFileFormat::QST: {
|
||||
auto result = decode_qst_file(this->file_basename + ".qst");
|
||||
this->bin_contents_ptr.reset(new string(std::move(result.first)));
|
||||
this->dat_contents_ptr.reset(new string(std::move(result.second)));
|
||||
break;
|
||||
@@ -461,29 +476,29 @@ shared_ptr<const string> Quest::bin_contents() const {
|
||||
return this->bin_contents_ptr;
|
||||
}
|
||||
|
||||
shared_ptr<const string> Quest::dat_contents() const {
|
||||
shared_ptr<const string> VersionedQuest::dat_contents() const {
|
||||
if (this->episode == Episode::EP3) {
|
||||
throw logic_error("Episode 3 quests do not have .dat files");
|
||||
}
|
||||
if (!this->dat_contents_ptr) {
|
||||
switch (this->file_format) {
|
||||
case FileFormat::BIN_DAT:
|
||||
case QuestFileFormat::BIN_DAT:
|
||||
this->dat_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
||||
this->dat_contents_ptr.reset(new string(prs_compress(load_file(this->file_basename + ".datd"))));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_GCI:
|
||||
this->dat_contents_ptr.reset(new string(this->decode_gci_file(this->file_basename + ".dat.gci")));
|
||||
case QuestFileFormat::BIN_DAT_GCI:
|
||||
this->dat_contents_ptr.reset(new string(decode_gci_file(this->file_basename + ".dat.gci")));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_VMS:
|
||||
this->dat_contents_ptr.reset(new string(this->decode_vms_file(this->file_basename + ".dat.vms")));
|
||||
case QuestFileFormat::BIN_DAT_VMS:
|
||||
this->dat_contents_ptr.reset(new string(decode_vms_file(this->file_basename + ".dat.vms")));
|
||||
break;
|
||||
case FileFormat::BIN_DAT_DLQ:
|
||||
this->dat_contents_ptr.reset(new string(this->decode_dlq_file(this->file_basename + ".dat.dlq")));
|
||||
case QuestFileFormat::BIN_DAT_DLQ:
|
||||
this->dat_contents_ptr.reset(new string(decode_dlq_file(this->file_basename + ".dat.dlq")));
|
||||
break;
|
||||
case FileFormat::QST: {
|
||||
auto result = this->decode_qst_file(this->file_basename + ".qst");
|
||||
case QuestFileFormat::QST: {
|
||||
auto result = decode_qst_file(this->file_basename + ".qst");
|
||||
this->bin_contents_ptr.reset(new string(std::move(result.first)));
|
||||
this->dat_contents_ptr.reset(new string(std::move(result.second)));
|
||||
break;
|
||||
@@ -495,8 +510,237 @@ shared_ptr<const string> Quest::dat_contents() const {
|
||||
return this->dat_contents_ptr;
|
||||
}
|
||||
|
||||
string Quest::decode_gci_file(
|
||||
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
|
||||
string VersionedQuest::encode_qst() const {
|
||||
return encode_qst_file(
|
||||
*this->bin_contents(),
|
||||
*this->dat_contents(),
|
||||
this->name,
|
||||
this->quest_number,
|
||||
this->version,
|
||||
this->is_dlq_encoded);
|
||||
}
|
||||
|
||||
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
|
||||
: quest_number(initial_version->quest_number),
|
||||
category_id(initial_version->category_id),
|
||||
episode(initial_version->episode),
|
||||
joinable(initial_version->joinable),
|
||||
name(initial_version->name),
|
||||
versions_present(1 << static_cast<size_t>(initial_version->version)) {
|
||||
versions.emplace(initial_version->version, initial_version);
|
||||
}
|
||||
|
||||
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
|
||||
if (this->quest_number != vq->quest_number) {
|
||||
throw logic_error("incorrect versioned quest number");
|
||||
}
|
||||
if (this->category_id != vq->category_id) {
|
||||
throw runtime_error("quest version is in a different category");
|
||||
}
|
||||
if (this->episode != vq->episode) {
|
||||
throw runtime_error("quest version is in a different episode");
|
||||
}
|
||||
if (this->joinable != vq->joinable) {
|
||||
throw runtime_error("quest version has a different joinability state");
|
||||
}
|
||||
|
||||
uint16_t presence_mask = 1 << static_cast<size_t>(vq->version);
|
||||
if (this->versions_present & presence_mask) {
|
||||
throw runtime_error("quest version is already present");
|
||||
}
|
||||
this->versions_present |= presence_mask;
|
||||
this->versions.emplace(vq->version, vq);
|
||||
}
|
||||
|
||||
bool Quest::has_version(QuestScriptVersion v) const {
|
||||
return !!(this->versions_present & (1 << static_cast<size_t>(v)));
|
||||
}
|
||||
|
||||
shared_ptr<const VersionedQuest> Quest::version(QuestScriptVersion v) const {
|
||||
try {
|
||||
return this->versions.at(v);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QuestIndex::QuestIndex(
|
||||
const string& directory,
|
||||
std::shared_ptr<const QuestCategoryIndex> category_index)
|
||||
: directory(directory),
|
||||
category_index(category_index) {
|
||||
|
||||
for (const auto& filename : list_directory_sorted(this->directory)) {
|
||||
string full_path = this->directory + "/" + filename;
|
||||
|
||||
if (ends_with(filename, ".gba")) {
|
||||
shared_ptr<string> contents(new string(load_file(full_path)));
|
||||
this->gba_file_contents.emplace(make_pair(filename, contents));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ends_with(filename, ".bin") ||
|
||||
ends_with(filename, ".bind") ||
|
||||
ends_with(filename, ".bin.gci") ||
|
||||
ends_with(filename, ".bin.vms") ||
|
||||
ends_with(filename, ".bin.dlq") ||
|
||||
ends_with(filename, ".mnm") ||
|
||||
ends_with(filename, ".mnmd") ||
|
||||
ends_with(filename, ".mnm.gci") ||
|
||||
ends_with(filename, ".mnm.dlq") ||
|
||||
ends_with(filename, ".qst")) {
|
||||
try {
|
||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(full_path, QuestScriptVersion::UNKNOWN, this->category_index));
|
||||
|
||||
string ascii_name = encode_sjis(vq->name);
|
||||
auto category_name = encode_sjis(this->category_index->at(vq->category_id).name);
|
||||
|
||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||
if (q_it != this->quests_by_number.end()) {
|
||||
q_it->second->add_version(vq);
|
||||
static_game_data_log.info("(%s) Added %s version of quest %" PRIu32 " \"%s\"",
|
||||
filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str());
|
||||
} else {
|
||||
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
|
||||
static_game_data_log.info("(%s) Created %s quest %" PRIu32 " \"%s\" (%s, %s (%" PRIu32 "), %s)",
|
||||
filename.c_str(),
|
||||
name_for_enum(vq->version),
|
||||
vq->quest_number,
|
||||
ascii_name.c_str(),
|
||||
name_for_episode(vq->episode),
|
||||
category_name.c_str(),
|
||||
vq->category_id,
|
||||
vq->joinable ? "joinable" : "not joinable");
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
||||
try {
|
||||
return this->quests_by_number.at(quest_number);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
|
||||
try {
|
||||
return this->gba_file_contents.at(name);
|
||||
} catch (const out_of_range&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
vector<shared_ptr<const Quest>> QuestIndex::filter(uint32_t category_id, QuestScriptVersion version) const {
|
||||
vector<shared_ptr<const Quest>> ret;
|
||||
for (auto it : this->quests_by_number) {
|
||||
if (it.second->category_id == category_id && it.second->has_version(version)) {
|
||||
ret.emplace_back(it.second);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
string encode_download_quest_file(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
|
||||
// Download quest files are like normal (PRS-compressed) quest files, but they
|
||||
// are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small
|
||||
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
|
||||
|
||||
if (encryption_seed == 0) {
|
||||
encryption_seed = random_object<uint32_t>();
|
||||
}
|
||||
if (decompressed_size == 0) {
|
||||
decompressed_size = prs_decompress_size(compressed_data);
|
||||
}
|
||||
|
||||
string data(8, '\0');
|
||||
auto* header = reinterpret_cast<PSODownloadQuestHeader*>(data.data());
|
||||
header->size = decompressed_size;
|
||||
header->encryption_seed = encryption_seed;
|
||||
data += compressed_data;
|
||||
|
||||
// Add temporary extra bytes if necessary so encryption won't fail - the data
|
||||
// size must be a multiple of 4 for PSO V2 encryption.
|
||||
size_t original_size = data.size();
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
PSOV2Encryption encr(encryption_seed);
|
||||
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
|
||||
data.size() - sizeof(PSODownloadQuestHeader));
|
||||
data.resize(original_size);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
shared_ptr<VersionedQuest> VersionedQuest::create_download_quest() const {
|
||||
// The download flag needs to be set in the bin header, or else the client
|
||||
// will ignore it when scanning for download quests in an offline game. To set
|
||||
// this flag, we need to decompress the quest's .bin file, set the flag, then
|
||||
// recompress it again.
|
||||
|
||||
// This function should not be used for Episode 3 quests (they should be sent
|
||||
// to the client as-is, without any encryption or other preprocessing)
|
||||
if (this->episode == Episode::EP3 || this->version == QuestScriptVersion::GC_EP3) {
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
}
|
||||
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents());
|
||||
|
||||
void* data_ptr = decompressed_bin.data();
|
||||
switch (this->version) {
|
||||
case QuestScriptVersion::DC_NTE:
|
||||
case QuestScriptVersion::DC_V1:
|
||||
case QuestScriptVersion::DC_V2:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::PC_V2:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderPC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::GC_NTE:
|
||||
case QuestScriptVersion::GC_V3:
|
||||
case QuestScriptVersion::XB_V3:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderGC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::BB_V4:
|
||||
throw invalid_argument("PSOBB does not support download quests");
|
||||
case QuestScriptVersion::GC_EP3:
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
default:
|
||||
throw invalid_argument("unknown game version");
|
||||
}
|
||||
|
||||
string compressed_bin = prs_compress(decompressed_bin);
|
||||
|
||||
// Return a new VersionedQuest object with appropriately-processed .bin and
|
||||
// .dat file contents
|
||||
shared_ptr<VersionedQuest> dlq(new VersionedQuest(*this));
|
||||
dlq->bin_contents_ptr.reset(new string(encode_download_quest_file(compressed_bin, decompressed_bin.size())));
|
||||
dlq->dat_contents_ptr.reset(new string(encode_download_quest_file(*this->dat_contents())));
|
||||
dlq->is_dlq_encoded = true;
|
||||
return dlq;
|
||||
}
|
||||
|
||||
string decode_gci_file(
|
||||
const string& filename,
|
||||
ssize_t find_seed_num_threads,
|
||||
int64_t known_seed,
|
||||
bool skip_checksum) {
|
||||
string data = load_file(filename);
|
||||
|
||||
StringReader r(data);
|
||||
@@ -596,8 +840,11 @@ string Quest::decode_gci_file(
|
||||
}
|
||||
}
|
||||
|
||||
string Quest::decode_vms_file(
|
||||
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) {
|
||||
string decode_vms_file(
|
||||
const string& filename,
|
||||
ssize_t find_seed_num_threads,
|
||||
int64_t known_seed,
|
||||
bool skip_checksum) {
|
||||
string data = load_file(filename);
|
||||
|
||||
StringReader r(data);
|
||||
@@ -629,7 +876,7 @@ string Quest::decode_vms_file(
|
||||
}
|
||||
}
|
||||
|
||||
string Quest::decode_dlq_data(const string& data) {
|
||||
string decode_dlq_data(const string& data) {
|
||||
StringReader r(data);
|
||||
uint32_t decompressed_size = r.get_u32l();
|
||||
uint32_t key = r.get_u32l();
|
||||
@@ -652,9 +899,9 @@ string Quest::decode_dlq_data(const string& data) {
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
string Quest::decode_dlq_file(const string& filename) {
|
||||
string decode_dlq_file(const string& filename) {
|
||||
auto f = fopen_unique(filename, "rb");
|
||||
return Quest::decode_dlq_data(read_all(f.get()));
|
||||
return decode_dlq_data(read_all(f.get()));
|
||||
}
|
||||
|
||||
template <typename HeaderT, typename OpenFileT>
|
||||
@@ -668,7 +915,7 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
string internal_dat_filename;
|
||||
uint32_t bin_file_size = 0;
|
||||
uint32_t dat_file_size = 0;
|
||||
Quest::FileFormat subformat = Quest::FileFormat::QST; // Stand-in for unknown
|
||||
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
|
||||
while (!r.eof()) {
|
||||
// Handle BB's implicit 8-byte command alignment
|
||||
static constexpr size_t alignment = sizeof(HeaderT);
|
||||
@@ -681,15 +928,15 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
const auto& header = r.get<HeaderT>();
|
||||
|
||||
if (header.command == 0x44 || header.command == 0x13) {
|
||||
if (subformat == Quest::FileFormat::QST) {
|
||||
subformat = Quest::FileFormat::BIN_DAT;
|
||||
} else if (subformat != Quest::FileFormat::BIN_DAT) {
|
||||
if (subformat == QuestFileFormat::QST) {
|
||||
subformat = QuestFileFormat::BIN_DAT;
|
||||
} else if (subformat != QuestFileFormat::BIN_DAT) {
|
||||
throw runtime_error("QST file contains mixed download and non-download commands");
|
||||
}
|
||||
} else if (header.command == 0xA6 || header.command == 0xA7) {
|
||||
if (subformat == Quest::FileFormat::QST) {
|
||||
subformat = Quest::FileFormat::BIN_DAT_DLQ;
|
||||
} else if (subformat != Quest::FileFormat::BIN_DAT_DLQ) {
|
||||
if (subformat == QuestFileFormat::QST) {
|
||||
subformat = QuestFileFormat::BIN_DAT_DLQ;
|
||||
} else if (subformat != QuestFileFormat::BIN_DAT_DLQ) {
|
||||
throw runtime_error("QST file contains mixed download and non-download commands");
|
||||
}
|
||||
}
|
||||
@@ -763,15 +1010,15 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
throw runtime_error("dat file does not match expected size");
|
||||
}
|
||||
|
||||
if (subformat == Quest::FileFormat::BIN_DAT_DLQ) {
|
||||
bin_contents = Quest::decode_dlq_file(bin_contents);
|
||||
dat_contents = Quest::decode_dlq_file(dat_contents);
|
||||
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
|
||||
bin_contents = decode_dlq_file(bin_contents);
|
||||
dat_contents = decode_dlq_file(dat_contents);
|
||||
}
|
||||
|
||||
return make_pair(bin_contents, dat_contents);
|
||||
}
|
||||
|
||||
pair<string, string> Quest::decode_qst_file(const string& filename) {
|
||||
pair<string, string> decode_qst_file(const string& filename) {
|
||||
auto f = fopen_unique(filename, "rb");
|
||||
|
||||
// QST files start with an open file command, but the format differs depending
|
||||
@@ -842,7 +1089,7 @@ void add_write_file_commands(
|
||||
}
|
||||
}
|
||||
|
||||
string Quest::encode_qst(
|
||||
string encode_qst_file(
|
||||
const string& bin_data,
|
||||
const string& dat_data,
|
||||
const u16string& name,
|
||||
@@ -900,178 +1147,3 @@ string Quest::encode_qst(
|
||||
|
||||
return std::move(w.str());
|
||||
}
|
||||
|
||||
string Quest::encode_qst() const {
|
||||
return this->encode_qst(
|
||||
*this->bin_contents(),
|
||||
*this->dat_contents(),
|
||||
this->name,
|
||||
this->quest_number,
|
||||
this->version,
|
||||
this->is_dlq_encoded);
|
||||
}
|
||||
|
||||
QuestIndex::QuestIndex(
|
||||
const string& directory,
|
||||
std::shared_ptr<const QuestCategoryIndex> category_index)
|
||||
: directory(directory),
|
||||
category_index(category_index) {
|
||||
|
||||
uint32_t next_menu_item_id = 1;
|
||||
for (const auto& filename : list_directory_sorted(this->directory)) {
|
||||
string full_path = this->directory + "/" + filename;
|
||||
|
||||
if (ends_with(filename, ".gba")) {
|
||||
shared_ptr<string> contents(new string(load_file(full_path)));
|
||||
this->gba_file_contents.emplace(make_pair(filename, contents));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ends_with(filename, ".bin") ||
|
||||
ends_with(filename, ".bind") ||
|
||||
ends_with(filename, ".bin.gci") ||
|
||||
ends_with(filename, ".bin.vms") ||
|
||||
ends_with(filename, ".bin.dlq") ||
|
||||
ends_with(filename, ".mnm") ||
|
||||
ends_with(filename, ".mnmd") ||
|
||||
ends_with(filename, ".mnm.gci") ||
|
||||
ends_with(filename, ".mnm.dlq") ||
|
||||
ends_with(filename, ".qst")) {
|
||||
try {
|
||||
shared_ptr<Quest> q(new Quest(full_path, QuestScriptVersion::UNKNOWN, this->category_index));
|
||||
q->menu_item_id = next_menu_item_id++;
|
||||
string ascii_name = encode_sjis(q->name);
|
||||
if (!this->version_menu_item_id_to_quest.emplace(make_pair(q->version, q->menu_item_id), q).second) {
|
||||
throw logic_error("duplicate quest menu item id");
|
||||
}
|
||||
auto category_name = encode_sjis(this->category_index->at(q->category_id).name);
|
||||
static_game_data_log.info("Indexed quest %s (%s => %s-%" PRIu32 " (%" PRIu32 "), %s, %s (%" PRIu32 "), joinable=%s)",
|
||||
ascii_name.c_str(),
|
||||
filename.c_str(),
|
||||
name_for_enum(q->version),
|
||||
q->quest_number,
|
||||
q->menu_item_id,
|
||||
name_for_episode(q->episode),
|
||||
category_name.c_str(),
|
||||
q->category_id,
|
||||
q->joinable ? "true" : "false");
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shared_ptr<const Quest> QuestIndex::get(
|
||||
QuestScriptVersion version, uint32_t menu_item_id) const {
|
||||
return this->version_menu_item_id_to_quest.at(make_pair(version, menu_item_id));
|
||||
}
|
||||
|
||||
shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
|
||||
return this->gba_file_contents.at(name);
|
||||
}
|
||||
|
||||
vector<shared_ptr<const Quest>> QuestIndex::filter(
|
||||
QuestScriptVersion version, uint32_t category_id) const {
|
||||
auto it = this->version_menu_item_id_to_quest.lower_bound(make_pair(version, 0));
|
||||
auto end_it = this->version_menu_item_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF));
|
||||
|
||||
vector<shared_ptr<const Quest>> ret;
|
||||
for (; it != end_it; it++) {
|
||||
if (it->second->category_id == category_id) {
|
||||
ret.emplace_back(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
string Quest::encode_download_quest_file(
|
||||
const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
|
||||
// Download quest files are like normal (PRS-compressed) quest files, but they
|
||||
// are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small
|
||||
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
|
||||
|
||||
if (encryption_seed == 0) {
|
||||
encryption_seed = random_object<uint32_t>();
|
||||
}
|
||||
if (decompressed_size == 0) {
|
||||
decompressed_size = prs_decompress_size(compressed_data);
|
||||
}
|
||||
|
||||
string data(8, '\0');
|
||||
auto* header = reinterpret_cast<PSODownloadQuestHeader*>(data.data());
|
||||
header->size = decompressed_size;
|
||||
header->encryption_seed = encryption_seed;
|
||||
data += compressed_data;
|
||||
|
||||
// Add temporary extra bytes if necessary so encryption won't fail - the data
|
||||
// size must be a multiple of 4 for PSO V2 encryption.
|
||||
size_t original_size = data.size();
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
PSOV2Encryption encr(encryption_seed);
|
||||
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
|
||||
data.size() - sizeof(PSODownloadQuestHeader));
|
||||
data.resize(original_size);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
shared_ptr<Quest> Quest::create_download_quest() const {
|
||||
// The download flag needs to be set in the bin header, or else the client
|
||||
// will ignore it when scanning for download quests in an offline game. To set
|
||||
// this flag, we need to decompress the quest's .bin file, set the flag, then
|
||||
// recompress it again.
|
||||
|
||||
// This function should not be used for Episode 3 quests (they should be sent
|
||||
// to the client as-is, without any encryption or other preprocessing)
|
||||
if (this->episode == Episode::EP3 || this->version == QuestScriptVersion::GC_EP3) {
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
}
|
||||
|
||||
string decompressed_bin = prs_decompress(*this->bin_contents());
|
||||
|
||||
void* data_ptr = decompressed_bin.data();
|
||||
switch (this->version) {
|
||||
case QuestScriptVersion::DC_NTE:
|
||||
case QuestScriptVersion::DC_V1:
|
||||
case QuestScriptVersion::DC_V2:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::PC_V2:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderPC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::GC_NTE:
|
||||
case QuestScriptVersion::GC_V3:
|
||||
case QuestScriptVersion::XB_V3:
|
||||
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
|
||||
throw runtime_error("bin file is too small for header");
|
||||
}
|
||||
reinterpret_cast<PSOQuestHeaderGC*>(data_ptr)->is_download = 0x01;
|
||||
break;
|
||||
case QuestScriptVersion::BB_V4:
|
||||
throw invalid_argument("PSOBB does not support download quests");
|
||||
case QuestScriptVersion::GC_EP3:
|
||||
throw logic_error("Episode 3 quests cannot be converted to download quests");
|
||||
default:
|
||||
throw invalid_argument("unknown game version");
|
||||
}
|
||||
|
||||
string compressed_bin = prs_compress(decompressed_bin);
|
||||
|
||||
// Return a new Quest object with appropriately-processed .bin and .dat file
|
||||
// contents
|
||||
shared_ptr<Quest> dlq(new Quest(*this));
|
||||
dlq->bin_contents_ptr.reset(new string(this->encode_download_quest_file(
|
||||
compressed_bin, decompressed_bin.size())));
|
||||
dlq->dat_contents_ptr.reset(new string(this->encode_download_quest_file(*this->dat_contents())));
|
||||
dlq->is_dlq_encoded = true;
|
||||
return dlq;
|
||||
}
|
||||
|
||||
+71
-47
@@ -10,6 +10,15 @@
|
||||
#include "QuestScript.hh"
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
enum class QuestFileFormat {
|
||||
BIN_DAT = 0,
|
||||
BIN_DAT_UNCOMPRESSED,
|
||||
BIN_DAT_GCI,
|
||||
BIN_DAT_VMS,
|
||||
BIN_DAT_DLQ,
|
||||
QST,
|
||||
};
|
||||
|
||||
struct QuestCategoryIndex {
|
||||
struct Category {
|
||||
enum Flag {
|
||||
@@ -43,7 +52,7 @@ struct QuestCategoryIndex {
|
||||
const Category& at(uint32_t category_id) const;
|
||||
};
|
||||
|
||||
class Quest {
|
||||
class VersionedQuest {
|
||||
public:
|
||||
struct DATSectionHeader {
|
||||
le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too
|
||||
@@ -52,33 +61,24 @@ public:
|
||||
le_uint32_t data_size;
|
||||
} __attribute__((packed));
|
||||
|
||||
enum class FileFormat {
|
||||
BIN_DAT = 0,
|
||||
BIN_DAT_UNCOMPRESSED,
|
||||
BIN_DAT_GCI,
|
||||
BIN_DAT_VMS,
|
||||
BIN_DAT_DLQ,
|
||||
QST,
|
||||
};
|
||||
uint32_t quest_number;
|
||||
uint32_t menu_item_id;
|
||||
uint32_t category_id;
|
||||
Episode episode;
|
||||
bool joinable;
|
||||
QuestScriptVersion version;
|
||||
std::string file_basename; // we append -<version>.<bin/dat> when reading
|
||||
FileFormat file_format;
|
||||
QuestFileFormat file_format;
|
||||
bool has_mnm_extension;
|
||||
bool is_dlq_encoded;
|
||||
std::u16string name;
|
||||
std::u16string short_description;
|
||||
std::u16string long_description;
|
||||
|
||||
Quest(const std::string& file_basename, QuestScriptVersion version, std::shared_ptr<const QuestCategoryIndex> category_index);
|
||||
Quest(const Quest&) = default;
|
||||
Quest(Quest&&) = default;
|
||||
Quest& operator=(const Quest&) = default;
|
||||
Quest& operator=(Quest&&) = default;
|
||||
VersionedQuest(const std::string& file_basename, QuestScriptVersion version, std::shared_ptr<const QuestCategoryIndex> category_index);
|
||||
VersionedQuest(const VersionedQuest&) = default;
|
||||
VersionedQuest(VersionedQuest&&) = default;
|
||||
VersionedQuest& operator=(const VersionedQuest&) = default;
|
||||
VersionedQuest& operator=(VersionedQuest&&) = default;
|
||||
|
||||
std::string bin_filename() const;
|
||||
std::string dat_filename() const;
|
||||
@@ -86,31 +86,7 @@ public:
|
||||
std::shared_ptr<const std::string> bin_contents() const;
|
||||
std::shared_ptr<const std::string> dat_contents() const;
|
||||
|
||||
static std::string encode_download_quest_file(
|
||||
const std::string& compressed_data, size_t decompressed_size = 0, uint32_t encryption_seed = 0);
|
||||
std::shared_ptr<Quest> create_download_quest() const;
|
||||
|
||||
static std::string decode_gci_file(
|
||||
const std::string& filename,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
static std::string decode_vms_file(
|
||||
const std::string& filename,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
static std::string decode_dlq_file(const std::string& filename);
|
||||
static std::string decode_dlq_data(const std::string& filename);
|
||||
static std::pair<std::string, std::string> decode_qst_file(const std::string& filename);
|
||||
|
||||
static std::string encode_qst(
|
||||
const std::string& bin_data,
|
||||
const std::string& dat_data,
|
||||
const std::u16string& name,
|
||||
uint32_t quest_number,
|
||||
QuestScriptVersion version,
|
||||
bool is_dlq_encoded);
|
||||
std::shared_ptr<VersionedQuest> create_download_quest() const;
|
||||
std::string encode_qst() const;
|
||||
|
||||
private:
|
||||
@@ -119,20 +95,68 @@ private:
|
||||
mutable std::shared_ptr<std::string> dat_contents_ptr;
|
||||
};
|
||||
|
||||
class Quest {
|
||||
public:
|
||||
Quest() = delete;
|
||||
explicit Quest(std::shared_ptr<const VersionedQuest> initial_version);
|
||||
Quest(const Quest&) = default;
|
||||
Quest(Quest&&) = default;
|
||||
Quest& operator=(const Quest&) = default;
|
||||
Quest& operator=(Quest&&) = default;
|
||||
|
||||
void add_version(shared_ptr<const VersionedQuest> vq);
|
||||
bool has_version(QuestScriptVersion v) const;
|
||||
shared_ptr<const VersionedQuest> version(QuestScriptVersion v) const;
|
||||
|
||||
uint32_t quest_number;
|
||||
uint32_t category_id;
|
||||
Episode episode;
|
||||
bool joinable;
|
||||
std::u16string name;
|
||||
|
||||
uint16_t versions_present;
|
||||
std::unordered_map<QuestScriptVersion, std::shared_ptr<const VersionedQuest>> versions;
|
||||
};
|
||||
|
||||
struct QuestIndex {
|
||||
std::string directory;
|
||||
std::shared_ptr<const QuestCategoryIndex> category_index;
|
||||
|
||||
std::map<std::pair<QuestScriptVersion, uint64_t>, std::shared_ptr<Quest>> version_menu_item_id_to_quest;
|
||||
|
||||
std::map<std::string, std::vector<std::shared_ptr<Quest>>> category_to_quests;
|
||||
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
||||
|
||||
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
|
||||
|
||||
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index);
|
||||
|
||||
std::shared_ptr<const Quest> get(QuestScriptVersion version, uint32_t id) const;
|
||||
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
|
||||
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
|
||||
std::vector<std::shared_ptr<const Quest>> filter(
|
||||
QuestScriptVersion version, uint32_t category_id) const;
|
||||
std::vector<std::shared_ptr<const Quest>> filter(uint32_t category_id, QuestScriptVersion version) const;
|
||||
};
|
||||
|
||||
std::string encode_download_quest_file(
|
||||
const std::string& compressed_data,
|
||||
size_t decompressed_size = 0,
|
||||
uint32_t encryption_seed = 0);
|
||||
|
||||
std::string decode_gci_file(
|
||||
const std::string& filename,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
std::string decode_vms_file(
|
||||
const std::string& filename,
|
||||
ssize_t find_seed_num_threads = -1,
|
||||
int64_t known_seed = -1,
|
||||
bool skip_checksum = false);
|
||||
|
||||
std::string decode_dlq_file(const std::string& filename);
|
||||
std::string decode_dlq_data(const std::string& data);
|
||||
|
||||
std::pair<std::string, std::string> decode_qst_file(const std::string& filename);
|
||||
std::string encode_qst_file(
|
||||
const std::string& bin_data,
|
||||
const std::string& dat_data,
|
||||
const std::u16string& name,
|
||||
uint32_t quest_number,
|
||||
QuestScriptVersion version,
|
||||
bool is_dlq_encoded);
|
||||
|
||||
+48
-44
@@ -25,61 +25,65 @@ template <>
|
||||
const char* name_for_enum<QuestScriptVersion>(QuestScriptVersion v);
|
||||
|
||||
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
|
||||
le_uint32_t code_offset;
|
||||
le_uint32_t function_table_offset;
|
||||
le_uint32_t size;
|
||||
le_uint32_t unused;
|
||||
uint8_t is_download;
|
||||
uint8_t unknown1;
|
||||
le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
ptext<char, 0x20> name;
|
||||
ptext<char, 0x80> short_description;
|
||||
ptext<char, 0x120> long_description;
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
/* 0004 */ le_uint32_t function_table_offset;
|
||||
/* 0008 */ le_uint32_t size;
|
||||
/* 000C */ le_uint32_t unused;
|
||||
/* 0010 */ uint8_t is_download;
|
||||
/* 0011 */ uint8_t unknown1;
|
||||
/* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
/* 0014 */ ptext<char, 0x20> name;
|
||||
/* 0034 */ ptext<char, 0x80> short_description;
|
||||
/* 00B4 */ ptext<char, 0x120> long_description;
|
||||
/* 01D4 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOQuestHeaderPC {
|
||||
le_uint32_t code_offset;
|
||||
le_uint32_t function_table_offset;
|
||||
le_uint32_t size;
|
||||
le_uint32_t unused;
|
||||
uint8_t is_download;
|
||||
uint8_t unknown1;
|
||||
le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
ptext<char16_t, 0x20> name;
|
||||
ptext<char16_t, 0x80> short_description;
|
||||
ptext<char16_t, 0x120> long_description;
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
/* 0004 */ le_uint32_t function_table_offset;
|
||||
/* 0008 */ le_uint32_t size;
|
||||
/* 000C */ le_uint32_t unused;
|
||||
/* 0010 */ uint8_t is_download;
|
||||
/* 0011 */ uint8_t unknown1;
|
||||
/* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
/* 0014 */ ptext<char16_t, 0x20> name;
|
||||
/* 0054 */ ptext<char16_t, 0x80> short_description;
|
||||
/* 0154 */ ptext<char16_t, 0x120> long_description;
|
||||
/* 0394 */
|
||||
} __attribute__((packed));
|
||||
|
||||
// TODO: Is the XB quest header format the same as on GC? If not, make a
|
||||
// separate struct; if so, rename this struct to V3.
|
||||
struct PSOQuestHeaderGC {
|
||||
le_uint32_t code_offset;
|
||||
le_uint32_t function_table_offset;
|
||||
le_uint32_t size;
|
||||
le_uint32_t unused;
|
||||
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 (?)
|
||||
ptext<char, 0x20> name;
|
||||
ptext<char, 0x80> short_description;
|
||||
ptext<char, 0x120> long_description;
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
/* 0004 */ le_uint32_t function_table_offset;
|
||||
/* 0008 */ le_uint32_t size;
|
||||
/* 000C */ le_uint32_t unused;
|
||||
/* 0010 */ uint8_t is_download;
|
||||
/* 0011 */ uint8_t unknown1;
|
||||
/* 0012 */ uint8_t quest_number;
|
||||
/* 0013 */ uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?)
|
||||
/* 0014 */ ptext<char, 0x20> name;
|
||||
/* 0034 */ ptext<char, 0x80> short_description;
|
||||
/* 00B4 */ ptext<char, 0x120> long_description;
|
||||
/* 01D4 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOQuestHeaderBB {
|
||||
le_uint32_t code_offset;
|
||||
le_uint32_t function_table_offset;
|
||||
le_uint32_t size;
|
||||
le_uint32_t unused;
|
||||
le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
le_uint16_t unused2;
|
||||
uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
|
||||
uint8_t max_players;
|
||||
uint8_t joinable_in_progress;
|
||||
uint8_t unknown;
|
||||
ptext<char16_t, 0x20> name;
|
||||
ptext<char16_t, 0x80> short_description;
|
||||
ptext<char16_t, 0x120> long_description;
|
||||
/* 0000 */ le_uint32_t code_offset;
|
||||
/* 0004 */ le_uint32_t function_table_offset;
|
||||
/* 0008 */ le_uint32_t size;
|
||||
/* 000C */ le_uint32_t unused;
|
||||
/* 0010 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
|
||||
/* 0012 */ le_uint16_t unused2;
|
||||
/* 0014 */ uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
|
||||
/* 0015 */ uint8_t max_players;
|
||||
/* 0016 */ uint8_t joinable_in_progress;
|
||||
/* 0017 */ uint8_t unknown;
|
||||
/* 0018 */ ptext<char16_t, 0x20> name;
|
||||
/* 0058 */ ptext<char16_t, 0x80> short_description;
|
||||
/* 0158 */ ptext<char16_t, 0x120> long_description;
|
||||
/* 0398 */
|
||||
} __attribute__((packed));
|
||||
|
||||
std::string disassemble_quest_script(const void* data, size_t size, QuestScriptVersion version);
|
||||
|
||||
+41
-26
@@ -1477,11 +1477,16 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
if (!s->quest_index) {
|
||||
send_quest_info(c, u"$C6Quests are not available.", is_download_quest);
|
||||
} else {
|
||||
auto q = s->quest_index->get(c->quest_version(), cmd.item_id);
|
||||
auto q = s->quest_index->get(cmd.item_id);
|
||||
if (!q) {
|
||||
send_quest_info(c, u"$C4Quest does not\nexist.", is_download_quest);
|
||||
} else {
|
||||
send_quest_info(c, q->long_description.c_str(), is_download_quest);
|
||||
auto vq = q->version(c->quest_version());
|
||||
if (!vq) {
|
||||
send_quest_info(c, u"$C4Quest does not\nexist for this game\nversion.", is_download_quest);
|
||||
} else {
|
||||
send_quest_info(c, vq->long_description, is_download_quest);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1537,23 +1542,22 @@ static void on_09(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
}
|
||||
|
||||
if (game->quest) {
|
||||
if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) {
|
||||
info += "$C6Quest: " + encode_sjis(game->quest->name);
|
||||
} else {
|
||||
info += "$C4Quest: " + encode_sjis(game->quest->name);
|
||||
}
|
||||
info += (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) ? "$C6Quest: " : "$C4Quest: ";
|
||||
info += encode_sjis(game->quest->name);
|
||||
info += "\n";
|
||||
} else if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) {
|
||||
info += "$C6Quest in progress";
|
||||
info += "$C6Quest in progress\n";
|
||||
} else if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) {
|
||||
info += "$C4Quest in progress";
|
||||
info += "$C4Quest in progress\n";
|
||||
} else if (game->flags & Lobby::Flag::BATTLE_IN_PROGRESS) {
|
||||
info += "$C4Battle in progress";
|
||||
info += "$C4Battle in progress\n";
|
||||
}
|
||||
|
||||
if (game->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) {
|
||||
info += "$C4View Battle forbidden";
|
||||
info += "$C4View Battle forbidden\n";
|
||||
}
|
||||
|
||||
strip_trailing_whitespace(info);
|
||||
send_ship_info(c, decode_sjis(info));
|
||||
}
|
||||
break;
|
||||
@@ -1718,7 +1722,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
vector<shared_ptr<const Quest>> quests;
|
||||
for (const auto& category : s->quest_category_index->categories) {
|
||||
if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) {
|
||||
quests = s->quest_index->filter(c->quest_version(), category.category_id);
|
||||
quests = s->quest_index->filter(category.category_id, c->quest_version());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1963,7 +1967,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
break;
|
||||
}
|
||||
shared_ptr<Lobby> l = c->lobby.lock();
|
||||
auto quests = s->quest_index->filter(c->quest_version(), item_id);
|
||||
auto quests = s->quest_index->filter(item_id, c->quest_version());
|
||||
|
||||
// Hack: Assume the menu to be sent is the download quest menu if the
|
||||
// client is not in any lobby
|
||||
@@ -1977,11 +1981,16 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
send_lobby_message_box(c, u"$C6Quests are not available.");
|
||||
break;
|
||||
}
|
||||
auto q = s->quest_index->get(c->quest_version(), item_id);
|
||||
auto q = s->quest_index->get(item_id);
|
||||
if (!q) {
|
||||
send_lobby_message_box(c, u"$C6Quest does not exist.");
|
||||
break;
|
||||
}
|
||||
auto vq = q->version(c->quest_version());
|
||||
if (!vq) {
|
||||
send_lobby_message_box(c, u"$C6Quest does not exist\nfor this game version.");
|
||||
break;
|
||||
}
|
||||
|
||||
// If the client is not in a lobby, send the quest as a download quest.
|
||||
// Otherwise, they must be in a game to load a quest.
|
||||
@@ -1992,13 +2001,13 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
}
|
||||
|
||||
bool is_ep3 = (q->episode == Episode::EP3);
|
||||
string bin_basename = q->bin_filename();
|
||||
shared_ptr<const string> bin_contents = q->bin_contents();
|
||||
string bin_basename = vq->bin_filename();
|
||||
shared_ptr<const string> bin_contents = vq->bin_contents();
|
||||
string dat_basename;
|
||||
shared_ptr<const string> dat_contents;
|
||||
if (!is_ep3) {
|
||||
dat_basename = q->dat_filename();
|
||||
dat_contents = q->dat_contents();
|
||||
dat_basename = vq->dat_filename();
|
||||
dat_contents = vq->dat_contents();
|
||||
}
|
||||
|
||||
if (l) {
|
||||
@@ -2043,13 +2052,15 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
// Episode 3 uses the download quest commands (A6/A7) but does not
|
||||
// expect the server to have already encrypted the quest files, unlike
|
||||
// other versions.
|
||||
// TODO: This is not true for Episode 3 Trial Edition. We also would
|
||||
// have to convert the map to MapDefinitionTrial, though.
|
||||
if (!is_ep3) {
|
||||
q = q->create_download_quest();
|
||||
vq = vq->create_download_quest();
|
||||
}
|
||||
send_open_quest_file(c, quest_name, bin_basename, q->bin_contents(),
|
||||
send_open_quest_file(c, quest_name, bin_basename, vq->bin_contents(),
|
||||
is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD);
|
||||
if (dat_contents) {
|
||||
send_open_quest_file(c, quest_name, dat_basename, q->dat_contents(),
|
||||
send_open_quest_file(c, quest_name, dat_basename, vq->dat_contents(),
|
||||
is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD);
|
||||
}
|
||||
}
|
||||
@@ -2394,7 +2405,7 @@ static void on_AC_V3_BB(shared_ptr<Client> c, uint16_t, uint32_t, const string&
|
||||
(l->base_version == GameVersion::BB) &&
|
||||
l->map &&
|
||||
l->quest) {
|
||||
auto dat_contents = prs_decompress(*l->quest->dat_contents());
|
||||
auto dat_contents = prs_decompress(*l->quest->version(QuestScriptVersion::BB_V4)->dat_contents());
|
||||
l->map->clear();
|
||||
l->map->add_enemies_from_quest_data(l->episode, l->difficulty, l->event, dat_contents.data(), dat_contents.size());
|
||||
c->log.info("Replaced enemies list with quest layout (%zu entries)",
|
||||
@@ -3587,10 +3598,14 @@ static void on_6F(shared_ptr<Client> c, uint16_t, uint32_t, const string& data)
|
||||
if (!l->quest) {
|
||||
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest");
|
||||
}
|
||||
string bin_basename = l->quest->bin_filename();
|
||||
shared_ptr<const string> bin_contents = l->quest->bin_contents();
|
||||
string dat_basename = l->quest->dat_filename();
|
||||
shared_ptr<const string> dat_contents = l->quest->dat_contents();
|
||||
auto vq = l->quest->version(c->quest_version());
|
||||
if (!vq) {
|
||||
throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version");
|
||||
}
|
||||
string bin_basename = vq->bin_filename();
|
||||
shared_ptr<const string> bin_contents = vq->bin_contents();
|
||||
string dat_basename = vq->dat_filename();
|
||||
shared_ptr<const string> dat_contents = vq->dat_contents();
|
||||
|
||||
send_open_quest_file(c, bin_basename + ".bin",
|
||||
bin_basename, bin_contents, QuestFileType::ONLINE);
|
||||
|
||||
+9
-3
@@ -1272,13 +1272,19 @@ void send_quest_menu_t(
|
||||
uint32_t menu_id,
|
||||
const vector<shared_ptr<const Quest>>& quests,
|
||||
bool is_download_menu) {
|
||||
auto v = c->quest_version();
|
||||
vector<EntryT> entries;
|
||||
for (const auto& quest : quests) {
|
||||
auto vq = quest->version(v);
|
||||
if (!vq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& e = entries.emplace_back();
|
||||
e.menu_id = menu_id;
|
||||
e.item_id = quest->menu_item_id;
|
||||
e.name = quest->name;
|
||||
e.short_description = quest->short_description;
|
||||
e.item_id = quest->quest_number;
|
||||
e.name = vq->name;
|
||||
e.short_description = vq->short_description;
|
||||
add_color_inplace(e.short_description);
|
||||
}
|
||||
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
|
||||
|
||||
Reference in New Issue
Block a user