refine quest header format; use metadata from .bin.txt file if present

This commit is contained in:
Martin Michelsen
2025-02-27 00:17:41 -08:00
parent 78fe4ebf98
commit 4d7a3395ba
16 changed files with 489 additions and 407 deletions
+300 -238
View File
@@ -192,148 +192,33 @@ struct PSODownloadQuestHeader {
le_uint32_t encryption_seed;
} __packed_ws__(PSODownloadQuestHeader, 8);
VersionedQuest::VersionedQuest(
uint32_t quest_number,
uint32_t category_id,
Version version,
uint8_t language,
std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const MapFile> map_file,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index,
uint8_t description_flag,
std::shared_ptr<const IntegralExpression> available_expression,
std::shared_ptr<const IntegralExpression> enabled_expression,
bool allow_start_from_chat_command,
bool force_joinable,
int16_t lock_status_register)
: quest_number(quest_number),
category_id(category_id),
episode(Episode::NONE),
allow_start_from_chat_command(allow_start_from_chat_command),
joinable(force_joinable),
lock_status_register(lock_status_register),
version(version),
language(language),
is_dlq_encoded(false),
bin_contents(bin_contents),
dat_contents(dat_contents),
map_file(map_file),
pvr_contents(pvr_contents),
battle_rules(battle_rules),
challenge_template_index(challenge_template_index),
description_flag(description_flag),
available_expression(available_expression),
enabled_expression(enabled_expression) {
auto bin_decompressed = prs_decompress(*this->bin_contents);
switch (this->version) {
case Version::DC_NTE: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = phosg::fnv1a32(header, sizeof(header)) & 0xFFFF;
}
this->name = header->name.decode(this->language);
break;
}
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
}
this->name = header->name.decode(this->language);
this->short_description = header->short_description.decode(this->language);
this->long_description = header->long_description.decode(this->language);
break;
}
case Version::PC_NTE:
case Version::PC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
this->episode = Episode::EP1;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
}
this->name = header->name.decode(this->language);
this->short_description = header->short_description.decode(this->language);
this->long_description = header->long_description.decode(this->language);
break;
}
case Version::GC_EP3_NTE:
case Version::GC_EP3: {
// Note: This codepath handles Episode 3 download quests, which are not
// the same as Episode 3 quest scripts. The latter are only used offline
// in story mode, but can be disassembled with disassemble_quest_script.
// It's unfortunate that Version::GC_EP3 is used here for Episode 3
// download quests (maps) and there for offline story mode scripts, but
// it's probably not worth refactoring this logic, at least right now.
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
this->episode = Episode::EP3;
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = map->map_number;
}
this->name = map->name.decode(this->language);
this->short_description = map->quest_name.decode(this->language);
this->long_description = map->description.decode(this->language);
break;
}
case Version::XB_V3:
case Version::GC_NTE:
case Version::GC_V3: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
}
this->name = header->name.decode(this->language);
this->short_description = header->short_description.decode(this->language);
this->long_description = header->long_description.decode(this->language);
break;
}
case Version::BB_V4: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
this->joinable |= header->joinable;
this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version);
if (this->quest_number == 0xFFFFFFFF) {
this->quest_number = header->quest_number;
}
this->name = header->name.decode(this->language);
this->short_description = header->short_description.decode(this->language);
this->long_description = header->long_description.decode(this->language);
break;
}
default:
throw logic_error("invalid quest game version");
void VersionedQuest::assert_valid() const {
if (this->category_id == 0xFFFFFFFF) {
throw runtime_error("category ID is not set");
}
if (this->quest_number == 0xFFFFFFFF) {
throw runtime_error("quest number is not set");
}
if (this->version == Version::UNKNOWN) {
throw runtime_error("version is not set");
}
if (this->language == 0xFF) {
throw runtime_error("language is not set");
}
if (this->episode == Episode::NONE) {
throw runtime_error("episode is not set");
}
if (this->max_players == 0) {
throw runtime_error("max players is not set");
}
if (!this->bin_contents) {
throw runtime_error("bin file is missing");
}
if (!is_ep3(this->version) && !this->dat_contents) {
throw runtime_error("dat file is missing");
}
if (!is_ep3(this->version) && !this->map_file) {
throw runtime_error("parsed map file is missing");
}
}
@@ -386,6 +271,7 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
episode(initial_version->episode),
allow_start_from_chat_command(initial_version->allow_start_from_chat_command),
joinable(initial_version->joinable),
max_players(initial_version->max_players),
lock_status_register(initial_version->lock_status_register),
name(initial_version->name),
supermap(nullptr),
@@ -421,6 +307,7 @@ phosg::JSON Quest::json() const {
{"Episode", name_for_episode(this->episode)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
{"Name", this->name},
{"BattleRules", std::move(battle_rules_json)},
@@ -438,46 +325,85 @@ uint32_t Quest::versions_key(Version v, uint8_t language) {
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->quest_number != vq->quest_number) {
throw logic_error("incorrect versioned quest number");
throw logic_error(phosg::string_printf(
"incorrect versioned quest number (existing: %08" PRIX32 ", new: %08" PRIX32 ")",
this->quest_number, vq->quest_number));
}
if (this->category_id != vq->category_id) {
throw runtime_error("quest version is in a different category");
throw runtime_error(phosg::string_printf(
"quest version is in a different category (existing: %08" PRIX32 ", new: %08" PRIX32 ")",
this->category_id, vq->category_id));
}
if (this->episode != vq->episode) {
throw runtime_error("quest version is in a different episode");
throw runtime_error(phosg::string_printf(
"quest version is in a different episode (existing: %s, new: %s)",
name_for_episode(this->episode), name_for_episode(vq->episode)));
}
if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) {
throw runtime_error("quest version has a different allow_start_from_chat_command state");
throw runtime_error(phosg::string_printf(
"quest version has a different allow_start_from_chat_command state (existing: %s, new: %s)",
this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false"));
}
if (this->joinable != vq->joinable) {
throw runtime_error("quest version has a different joinability state");
throw runtime_error(phosg::string_printf(
"quest version has a different joinability state (existing: %s, new: %s)",
this->joinable ? "true" : "false", vq->joinable ? "true" : "false"));
}
if (this->max_players != vq->max_players) {
throw runtime_error(phosg::string_printf(
"quest version has a different maximum player count (existing: %hhu, new: %hhu)",
this->max_players, vq->max_players));
}
if (this->lock_status_register != vq->lock_status_register) {
throw runtime_error("quest version has a different lock status register");
throw runtime_error(phosg::string_printf(
"quest version has a different lock status register (existing: %04hX, new: %04hX)",
this->lock_status_register, vq->lock_status_register));
}
if (!this->battle_rules != !vq->battle_rules) {
throw runtime_error("quest version has a different battle rules presence state");
throw runtime_error(phosg::string_printf(
"quest version has a different battle rules presence state (existing: %s, new: %s)",
this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent"));
}
if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) {
throw runtime_error("quest version has different battle rules");
string existing_str = this->battle_rules->json().serialize();
string new_str = vq->battle_rules->json().serialize();
throw runtime_error(phosg::string_printf(
"quest version has different battle rules (existing: %s, new: %s)",
existing_str.c_str(), new_str.c_str()));
}
if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error("quest version has different challenge template index");
throw runtime_error(phosg::string_printf(
"quest version has different challenge template index (existing: %zd, new: %zd)",
this->challenge_template_index, vq->challenge_template_index));
}
if (this->description_flag != vq->description_flag) {
throw runtime_error("quest version has different description flag");
throw runtime_error(phosg::string_printf(
"quest version has different description flag (existing: %02hhX, new: %02hhX)",
this->description_flag, vq->description_flag));
}
if (!this->available_expression != !vq->available_expression) {
throw runtime_error("quest version has available expression but root quest does not, or vice versa");
throw runtime_error(phosg::string_printf(
"quest version has available expression but root quest does not, or vice versa (existing: %s, new: %s)",
this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent"));
}
if (this->available_expression && *this->available_expression != *vq->available_expression) {
throw runtime_error("quest version has a different available expression");
string existing_str = this->available_expression->str();
string new_str = vq->available_expression->str();
throw runtime_error(phosg::string_printf(
"quest version has a different available expression (existing: %s, new: %s)",
existing_str.c_str(), new_str.c_str()));
}
if (!this->enabled_expression != !vq->enabled_expression) {
throw runtime_error("quest version has enabled expression but root quest does not, or vice versa");
throw runtime_error(phosg::string_printf(
"quest version has enabled expression but root quest does not, or vice versa (existing: %s, new: %s)",
this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent"));
}
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
throw runtime_error("quest version has a different enabled expression");
string existing_str = this->enabled_expression->str();
string new_str = vq->enabled_expression->str();
throw runtime_error(phosg::string_printf(
"quest version has a different enabled expression (existing: %s, new: %s)",
existing_str.c_str(), new_str.c_str()));
}
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
@@ -563,12 +489,17 @@ QuestIndex::QuestIndex(
string filename;
shared_ptr<const string> data;
};
struct BINFileData {
string filename;
unique_ptr<QuestMetadata> metadata;
shared_ptr<const string> data;
};
struct DATFileData {
string filename;
shared_ptr<const string> data;
shared_ptr<const MapFile> map_file;
};
map<string, FileData> bin_files;
map<string, BINFileData> bin_files;
map<string, DATFileData> dat_files;
map<string, FileData> pvr_files;
map<string, FileData> json_files;
@@ -596,18 +527,34 @@ QuestIndex::QuestIndex(
}
};
auto add_dat_file = [&](const string& basename, const string& filename, string&& value) {
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories");
throw runtime_error("bin file " + basename + " exists in multiple categories");
}
auto data_ptr = make_shared<string>(std::move(value));
auto data_ptr = make_shared<string>(std::move(data));
auto emplace_ret = bin_files.emplace(basename, BINFileData{});
if (!emplace_ret.second) {
throw runtime_error("bin file " + basename + " already exists");
}
auto& entry = emplace_ret.first->second;
entry.filename = filename;
entry.data = data_ptr;
if (metadata) {
entry.metadata = make_unique<QuestMetadata>(*metadata);
}
if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
};
auto add_dat_file = [&](const string& basename, const string& filename, string&& data) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("dat file " + basename + " exists in multiple categories");
}
auto data_ptr = make_shared<string>(std::move(data));
auto map_file = make_shared<MapFile>(make_shared<string>(prs_decompress(*data_ptr)));
if (!dat_files.emplace(basename, DATFileData{filename, data_ptr, map_file}).second) {
throw runtime_error("file " + basename + " already exists");
throw runtime_error("dat file " + basename + " already exists");
}
// There is a bug in the client that prevents quests from loading properly
// if any file's size is a multiple of 0x400. See the comments on the 13
// command in CommandFormats.hh for more details.
if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
@@ -624,6 +571,7 @@ QuestIndex::QuestIndex(
}
string file_path = cat_path + "/" + filename;
unique_ptr<AssembledQuestScript> assembled;
try {
string orig_filename = filename;
string file_data;
@@ -638,10 +586,11 @@ QuestIndex::QuestIndex(
filename.resize(filename.size() - 4);
} else if (phosg::ends_with(filename, ".bin.txt")) {
string include_dir = phosg::dirname(file_path);
file_data = assemble_quest_script(
assembled = make_unique<AssembledQuestScript>(assemble_quest_script(
phosg::load_file(file_path),
{include_dir, "system/quests/includes"},
{include_dir, "system/quests/includes", "system/client-functions/System"});
{include_dir, "system/quests/includes", "system/client-functions/System"}));
file_data = std::move(assembled->data);
filename.resize(filename.size() - 4);
if (phosg::ends_with(filename, ".bin")) {
filename.push_back('d');
@@ -663,9 +612,9 @@ QuestIndex::QuestIndex(
if (extension == "json") {
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
} else if (extension == "bin" || extension == "mnm") {
add_file(bin_files, file_basename, orig_filename, std::move(file_data), true);
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr);
} else if (extension == "bind" || extension == "mnmd") {
add_file(bin_files, file_basename, orig_filename, prs_compress_optimal(file_data), true);
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr);
} else if (extension == "dat") {
add_dat_file(file_basename, orig_filename, std::move(file_data));
} else if (extension == "datd") {
@@ -676,7 +625,7 @@ QuestIndex::QuestIndex(
auto files = decode_qst_data(file_data);
for (auto& it : files) {
if (phosg::ends_with(it.first, ".bin")) {
add_file(bin_files, file_basename, orig_filename, std::move(it.second), true);
add_bin_file(file_basename, orig_filename, std::move(it.second), nullptr);
} else if (phosg::ends_with(it.first, ".dat")) {
add_dat_file(file_basename, orig_filename, std::move(it.second));
} else if (phosg::ends_with(it.first, ".pvr")) {
@@ -696,11 +645,10 @@ QuestIndex::QuestIndex(
// All quests have a bin file (even in Episode 3, though its format is
// different), so we use bin_files as the primary list of all quests that
// should be indexed
for (auto& bin_it : bin_files) {
const string& basename = bin_it.first;
const auto* bin_filedata = &bin_it.second;
for (auto& [basename, entry] : bin_files) {
try {
auto vq = make_shared<VersionedQuest>();
// Quest .bin filenames are like K###-VERS-LANG.EXT, where:
// K can be any character (usually it's q)
// # = quest number (does not have to match the internal quest number)
@@ -719,42 +667,172 @@ QuestIndex::QuestIndex(
version_token = std::move(filename_tokens[1]);
language_token = std::move(filename_tokens[2]);
}
vq->category_id = categories.at(basename);
uint32_t category_id = categories.at(basename);
// Find the quest's metadata. If the quest was assembled (that is, if it
// came from a .bin.txt file), use the metadata from the source file;
// otherwise, figure it out from the already-assembled code
if (entry.metadata) {
vq->quest_number = entry.metadata->quest_number;
vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version;
vq->language = entry.metadata->language;
vq->episode = entry.metadata->episode;
vq->joinable = entry.metadata->joinable;
vq->max_players = entry.metadata->max_players;
vq->name = entry.metadata->name;
vq->short_description = entry.metadata->short_description;
vq->long_description = entry.metadata->long_description;
// Get the number from the first token
if (quest_number_token.empty()) {
throw runtime_error("quest number token is missing");
} else {
// Get the number from the first token
if (quest_number_token.empty()) {
throw runtime_error("quest number token is missing");
}
vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
// Get the version from the second token
static const unordered_map<string, Version> name_to_version({
{"dn", Version::DC_NTE},
{"dp", Version::DC_11_2000},
{"d1", Version::DC_V1},
{"dc", Version::DC_V2},
{"pcn", Version::PC_NTE},
{"pc", Version::PC_V2},
{"gcn", Version::GC_NTE},
{"gc", Version::GC_V3},
{"gc3t", Version::GC_EP3_NTE},
{"gc3", Version::GC_EP3},
{"xb", Version::XB_V3},
{"bb", Version::BB_V4},
});
vq->version = name_to_version.at(version_token);
// Get the language from the last token
if (language_token.size() != 1) {
throw runtime_error("language token is not a single character");
}
vq->language = language_code_for_char(language_token[0]);
auto bin_decompressed = prs_decompress(*entry.data);
switch (vq->version) {
case Version::DC_NTE: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
vq->name = header->name.decode(vq->language);
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = phosg::fnv1a64(vq->name);
}
break;
}
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::PC_NTE:
case Version::PC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::GC_EP3_NTE:
case Version::GC_EP3: {
// Note: This codepath handles Episode 3 download quests, which are not
// the same as Episode 3 quest scripts. The latter are only used offline
// in story mode, but can be disassembled with disassemble_quest_script.
// It's unfortunate that Version::GC_EP3 is used here for Episode 3
// download quests (maps) and there for offline story mode scripts, but
// it's probably not worth refactoring this logic, at least right now.
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
vq->episode = Episode::EP3;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = map->map_number;
}
vq->name = map->name.decode(vq->language);
vq->short_description = map->quest_name.decode(vq->language);
vq->long_description = map->description.decode(vq->language);
break;
}
case Version::XB_V3:
case Version::GC_NTE:
case Version::GC_V3: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::BB_V4: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->joinable |= header->joinable;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
default:
throw logic_error("invalid quest game version");
}
}
uint32_t quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
// Get the version from the second token
static const unordered_map<string, Version> name_to_version({
{"dn", Version::DC_NTE},
{"dp", Version::DC_11_2000},
{"d1", Version::DC_V1},
{"dc", Version::DC_V2},
{"pcn", Version::PC_NTE},
{"pc", Version::PC_V2},
{"gcn", Version::GC_NTE},
{"gc", Version::GC_V3},
{"gc3t", Version::GC_EP3_NTE},
{"gc3", Version::GC_EP3},
{"xb", Version::XB_V3},
{"bb", Version::BB_V4},
});
auto version = name_to_version.at(version_token);
// Get the language from the last token
if (language_token.size() != 1) {
throw runtime_error("language token is not a single character");
}
uint8_t language = language_code_for_char(language_token[0]);
// Find the corresponding dat and pvr files
const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr;
if (!::is_ep3(version)) {
if (!::is_ep3(vq->version)) {
// Look for dat and pvr files with the same basename as the bin file; if
// not found, look for them without the language suffix
try {
@@ -777,17 +855,17 @@ QuestIndex::QuestIndex(
}
}
}
vq->bin_contents = entry.data;
if (dat_filedata) {
vq->dat_contents = dat_filedata->data;
vq->map_file = dat_filedata->map_file;
}
if (pvr_filedata) {
vq->pvr_contents = pvr_filedata->data;
}
// Load the quest's metadata phosg::JSON file, if it exists
// Load the quest's metadata JSON file, if it exists
const FileData* json_filedata = nullptr;
shared_ptr<BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0;
shared_ptr<const IntegralExpression> available_expression;
shared_ptr<const IntegralExpression> enabled_expression;
bool allow_start_from_chat_command = false;
bool force_joinable = false;
int16_t lock_status_register = -1;
try {
json_filedata = &json_files.at(basename);
} catch (const out_of_range&) {
@@ -803,59 +881,43 @@ QuestIndex::QuestIndex(
if (json_filedata) {
auto metadata_json = phosg::JSON::parse(*json_filedata->data);
try {
battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
vq->battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
} catch (const out_of_range&) {
}
try {
challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
} catch (const out_of_range&) {
}
try {
description_flag = metadata_json.at("DescriptionFlag").as_int();
vq->description_flag = metadata_json.at("DescriptionFlag").as_int();
} catch (const out_of_range&) {
}
try {
available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
vq->available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
vq->enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) {
}
try {
allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
} catch (const out_of_range&) {
}
try {
force_joinable = metadata_json.get_bool("Joinable");
vq->joinable = metadata_json.get_bool("Joinable");
} catch (const out_of_range&) {
}
try {
lock_status_register = metadata_json.get_int("LockStatusRegister");
vq->lock_status_register = metadata_json.get_int("LockStatusRegister");
} catch (const out_of_range&) {
}
}
auto vq = make_shared<VersionedQuest>(
quest_number,
category_id,
version,
language,
bin_filedata->data,
dat_filedata ? dat_filedata->data : nullptr,
dat_filedata ? dat_filedata->map_file : nullptr,
pvr_filedata ? pvr_filedata->data : nullptr,
battle_rules,
challenge_template_index,
description_flag,
available_expression,
enabled_expression,
allow_start_from_chat_command,
force_joinable,
lock_status_register);
vq->assert_valid();
auto category_name = this->category_index->at(vq->category_id)->name;
string filenames_str = bin_filedata->filename;
string filenames_str = entry.filename;
if (dat_filedata) {
filenames_str += phosg::string_printf("/%s", dat_filedata->filename.c_str());
}
@@ -891,7 +953,7 @@ QuestIndex::QuestIndex(
vq->joinable ? "joinable" : "not joinable");
}
} catch (const exception& e) {
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", basename.c_str(), e.what());
static_game_data_log.warning("(%s) Failed to index quest file: %s", basename.c_str(), e.what());
}
}
}