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);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More