index quests by number, then by version

This commit is contained in:
Martin Michelsen
2023-10-08 11:14:12 -07:00
parent e723e80171
commit 8df36ea3c2
715 changed files with 498 additions and 377 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);