organize quests directory by category
This commit is contained in:
+5
-2
@@ -217,8 +217,7 @@ struct PRSPathNode {
|
|||||||
size_t to_offset = 0;
|
size_t to_offset = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
string prs_compress_optimal(
|
string prs_compress_optimal(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) {
|
||||||
const void* in_data_v, size_t in_size, ProgressCallback progress_fn) {
|
|
||||||
const uint8_t* in_data = reinterpret_cast<const uint8_t*>(in_data_v);
|
const uint8_t* in_data = reinterpret_cast<const uint8_t*>(in_data_v);
|
||||||
|
|
||||||
vector<PRSPathNode> nodes;
|
vector<PRSPathNode> nodes;
|
||||||
@@ -435,6 +434,10 @@ string prs_compress_optimal(
|
|||||||
return std::move(w.close());
|
return std::move(w.close());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string prs_compress_optimal(const string& data, ProgressCallback progress_fn) {
|
||||||
|
return prs_compress_optimal(data.data(), data.size(), progress_fn);
|
||||||
|
}
|
||||||
|
|
||||||
PRSCompressor::PRSCompressor(
|
PRSCompressor::PRSCompressor(
|
||||||
ssize_t compression_level, ProgressCallback progress_fn)
|
ssize_t compression_level, ProgressCallback progress_fn)
|
||||||
: compression_level(compression_level),
|
: compression_level(compression_level),
|
||||||
|
|||||||
+2
-4
@@ -174,10 +174,8 @@ std::string prs_compress_indexed(
|
|||||||
// Compresses data using PRS to the smallest possible output size. This function
|
// Compresses data using PRS to the smallest possible output size. This function
|
||||||
// is slow, but produces results significantly smaller than even Sega's original
|
// is slow, but produces results significantly smaller than even Sega's original
|
||||||
// compressor.
|
// compressor.
|
||||||
std::string prs_compress_optimal(
|
std::string prs_compress_optimal(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr);
|
||||||
const void* vdata,
|
std::string prs_compress_optimal(const std::string& data, ProgressCallback progress_fn = nullptr);
|
||||||
size_t size,
|
|
||||||
ProgressCallback progress_fn = nullptr);
|
|
||||||
|
|
||||||
// Decompresses PRS-compressed data.
|
// Decompresses PRS-compressed data.
|
||||||
struct PRSDecompressResult {
|
struct PRSDecompressResult {
|
||||||
|
|||||||
+13
-4
@@ -1349,9 +1349,10 @@ int main(int argc, char** argv) {
|
|||||||
auto decoded = decode_dlq_data(read_input_data());
|
auto decoded = decode_dlq_data(read_input_data());
|
||||||
save_file(output_filename_base + ".dec", decoded);
|
save_file(output_filename_base + ".dec", decoded);
|
||||||
} else if (quest_file_type == QuestFileFormat::QST) {
|
} else if (quest_file_type == QuestFileFormat::QST) {
|
||||||
auto data = decode_qst_data(read_input_data());
|
auto files = decode_qst_data(read_input_data());
|
||||||
save_file(output_filename_base + ".bin", data.first);
|
for (const auto& it : files) {
|
||||||
save_file(output_filename_base + ".dat", data.second);
|
save_file(output_filename_base + "-" + it.first, it.second);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw logic_error("invalid quest file format");
|
throw logic_error("invalid quest file format");
|
||||||
}
|
}
|
||||||
@@ -1367,9 +1368,17 @@ int main(int argc, char** argv) {
|
|||||||
string dat_filename = ends_with(bin_filename, ".bin")
|
string dat_filename = ends_with(bin_filename, ".bin")
|
||||||
? (bin_filename.substr(0, bin_filename.size() - 3) + "dat")
|
? (bin_filename.substr(0, bin_filename.size() - 3) + "dat")
|
||||||
: (bin_filename + ".dat");
|
: (bin_filename + ".dat");
|
||||||
|
string pvr_filename = ends_with(bin_filename, ".bin")
|
||||||
|
? (bin_filename.substr(0, bin_filename.size() - 3) + "pvr")
|
||||||
|
: (bin_filename + ".pvr");
|
||||||
shared_ptr<string> bin_data(new string(load_file(bin_filename)));
|
shared_ptr<string> bin_data(new string(load_file(bin_filename)));
|
||||||
shared_ptr<string> dat_data(new string(load_file(dat_filename)));
|
shared_ptr<string> dat_data(new string(load_file(dat_filename)));
|
||||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(0, 0, cli_quest_version, 0, bin_data, dat_data));
|
shared_ptr<string> pvr_data;
|
||||||
|
try {
|
||||||
|
shared_ptr<string> dat_data(new string(load_file(pvr_filename)));
|
||||||
|
} catch (const cannot_open_file&) {
|
||||||
|
}
|
||||||
|
shared_ptr<VersionedQuest> vq(new VersionedQuest(0, 0, cli_quest_version, 0, bin_data, dat_data, pvr_data));
|
||||||
if (download) {
|
if (download) {
|
||||||
vq = vq->create_download_quest();
|
vq = vq->create_download_quest();
|
||||||
}
|
}
|
||||||
|
|||||||
+247
-299
@@ -24,10 +24,9 @@ using namespace std;
|
|||||||
QuestCategoryIndex::Category::Category(uint32_t category_id, const JSON& json)
|
QuestCategoryIndex::Category::Category(uint32_t category_id, const JSON& json)
|
||||||
: category_id(category_id) {
|
: category_id(category_id) {
|
||||||
this->flags = json.get_int(0);
|
this->flags = json.get_int(0);
|
||||||
this->type = json.get_string(1).at(0);
|
this->directory_name = json.get_string(1);
|
||||||
this->short_token = json.get_string(2);
|
this->name = json.get_string(2);
|
||||||
this->name = json.get_string(3);
|
this->description = json.get_string(3);
|
||||||
this->description = json.get_string(4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuestCategoryIndex::Category::matches_flags(uint8_t request) const {
|
bool QuestCategoryIndex::Category::matches_flags(uint8_t request) const {
|
||||||
@@ -46,17 +45,6 @@ QuestCategoryIndex::QuestCategoryIndex(const JSON& json) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestCategoryIndex::Category& QuestCategoryIndex::find(char type, const std::string& short_token) const {
|
|
||||||
// Technically we should index these and do a map lookup, but there will
|
|
||||||
// probably always only be a small constant number of them
|
|
||||||
for (const auto& it : this->categories) {
|
|
||||||
if (it.type == type && it.short_token == short_token) {
|
|
||||||
return it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw out_of_range(string_printf("no category with type %c and short_token %s", type, short_token.c_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const QuestCategoryIndex::Category& QuestCategoryIndex::at(uint32_t category_id) const {
|
const QuestCategoryIndex::Category& QuestCategoryIndex::at(uint32_t category_id) const {
|
||||||
return this->categories.at(category_id - 1);
|
return this->categories.at(category_id - 1);
|
||||||
}
|
}
|
||||||
@@ -223,6 +211,7 @@ VersionedQuest::VersionedQuest(
|
|||||||
uint8_t language,
|
uint8_t language,
|
||||||
std::shared_ptr<const std::string> bin_contents,
|
std::shared_ptr<const std::string> bin_contents,
|
||||||
std::shared_ptr<const std::string> dat_contents,
|
std::shared_ptr<const std::string> dat_contents,
|
||||||
|
std::shared_ptr<const std::string> pvr_contents,
|
||||||
std::shared_ptr<const BattleRules> battle_rules,
|
std::shared_ptr<const BattleRules> battle_rules,
|
||||||
ssize_t challenge_template_index)
|
ssize_t challenge_template_index)
|
||||||
: quest_number(quest_number),
|
: quest_number(quest_number),
|
||||||
@@ -234,6 +223,7 @@ VersionedQuest::VersionedQuest(
|
|||||||
is_dlq_encoded(false),
|
is_dlq_encoded(false),
|
||||||
bin_contents(bin_contents),
|
bin_contents(bin_contents),
|
||||||
dat_contents(dat_contents),
|
dat_contents(dat_contents),
|
||||||
|
pvr_contents(pvr_contents),
|
||||||
battle_rules(battle_rules),
|
battle_rules(battle_rules),
|
||||||
challenge_template_index(challenge_template_index) {
|
challenge_template_index(challenge_template_index) {
|
||||||
|
|
||||||
@@ -361,14 +351,14 @@ string VersionedQuest::xb_filename() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
string VersionedQuest::encode_qst() const {
|
string VersionedQuest::encode_qst() const {
|
||||||
return encode_qst_file(
|
unordered_map<string, shared_ptr<const string>> files;
|
||||||
*this->bin_contents,
|
files.emplace(string_printf("quest%" PRIu32 ".bin", this->quest_number), this->bin_contents);
|
||||||
*this->dat_contents,
|
files.emplace(string_printf("quest%" PRIu32 ".dat", this->quest_number), this->dat_contents);
|
||||||
this->name,
|
if (this->pvr_contents) {
|
||||||
this->quest_number,
|
files.emplace(string_printf("quest%" PRIu32 ".pvr", this->quest_number), this->pvr_contents);
|
||||||
this->language,
|
}
|
||||||
this->version,
|
string xb_filename = string_printf("quest%" PRIu32 "_%c.dat", quest_number, tolower(char_for_language_code(language)));
|
||||||
this->is_dlq_encoded);
|
return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
|
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
|
||||||
@@ -447,76 +437,128 @@ QuestIndex::QuestIndex(
|
|||||||
: directory(directory),
|
: directory(directory),
|
||||||
category_index(category_index) {
|
category_index(category_index) {
|
||||||
|
|
||||||
unordered_map<string, shared_ptr<const string>> dat_cache;
|
map<string, shared_ptr<const string>> bin_files;
|
||||||
unordered_map<string, shared_ptr<const JSON>> metadata_json_cache;
|
map<string, shared_ptr<const string>> dat_files;
|
||||||
|
map<string, shared_ptr<const string>> pvr_files;
|
||||||
|
map<string, shared_ptr<const string>> json_files;
|
||||||
|
map<string, uint32_t> categories;
|
||||||
|
for (const auto& cat : this->category_index->categories) {
|
||||||
|
|
||||||
for (const auto& bin_filename : list_directory_sorted(directory)) {
|
auto add_file = [&](map<string, shared_ptr<const string>>& files, const string& name, string&& value) {
|
||||||
string bin_path = this->directory + "/" + bin_filename;
|
if (categories.emplace(name, cat.category_id).first->second != cat.category_id) {
|
||||||
|
throw runtime_error("file " + name + " exists in multiple categories");
|
||||||
|
}
|
||||||
|
shared_ptr<const string> data_ptr(new string(std::move(value)));
|
||||||
|
if (!files.emplace(name, data_ptr).second) {
|
||||||
|
throw runtime_error("file " + name + " already exists");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (ends_with(bin_filename, ".gba")) {
|
string cat_path = directory + "/" + cat.directory_name;
|
||||||
shared_ptr<string> contents(new string(load_file(bin_path)));
|
if (!isdir(cat_path)) {
|
||||||
this->gba_file_contents.emplace(make_pair(bin_filename, contents));
|
static_game_data_log.warning("Quest category directory %s is missing; skipping it", cat_path.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
for (string filename : list_directory_sorted(cat_path)) {
|
||||||
|
string file_path = cat_path + "/" + filename;
|
||||||
|
try {
|
||||||
|
string file_data;
|
||||||
|
if (ends_with(filename, ".gci")) {
|
||||||
|
file_data = decode_gci_data(load_file(file_path));
|
||||||
|
filename.resize(filename.size() - 4);
|
||||||
|
} else if (ends_with(filename, ".vms")) {
|
||||||
|
file_data = decode_vms_data(load_file(file_path));
|
||||||
|
filename.resize(filename.size() - 4);
|
||||||
|
} else if (ends_with(filename, ".dlq")) {
|
||||||
|
file_data = decode_dlq_data(load_file(file_path));
|
||||||
|
filename.resize(filename.size() - 4);
|
||||||
|
} else {
|
||||||
|
file_data = load_file(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t dot_pos = filename.rfind('.');
|
||||||
|
string file_basename;
|
||||||
|
string extension;
|
||||||
|
if (dot_pos != string::npos) {
|
||||||
|
file_basename = tolower(filename.substr(0, dot_pos));
|
||||||
|
extension = tolower(filename.substr(dot_pos + 1));
|
||||||
|
} else {
|
||||||
|
file_basename = tolower(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension == "json") {
|
||||||
|
add_file(json_files, file_basename, std::move(file_data));
|
||||||
|
} else if (extension == "bin" || extension == "mnm") {
|
||||||
|
add_file(bin_files, file_basename, std::move(file_data));
|
||||||
|
} else if (extension == "bind" || extension == "mnmd") {
|
||||||
|
add_file(bin_files, file_basename, prs_compress_optimal(file_data));
|
||||||
|
} else if (extension == "dat") {
|
||||||
|
add_file(dat_files, file_basename, std::move(file_data));
|
||||||
|
} else if (extension == "datd") {
|
||||||
|
add_file(dat_files, file_basename, prs_compress_optimal(file_data));
|
||||||
|
} else if (extension == "pvr") {
|
||||||
|
add_file(pvr_files, file_basename, std::move(file_data));
|
||||||
|
} else if (extension == "pvrd") {
|
||||||
|
add_file(pvr_files, file_basename, prs_compress_optimal(file_data));
|
||||||
|
} else if (extension == "qst") {
|
||||||
|
auto files = decode_qst_data(file_data);
|
||||||
|
for (auto& it : files) {
|
||||||
|
if (ends_with(it.first, ".bin")) {
|
||||||
|
add_file(bin_files, file_basename, std::move(it.second));
|
||||||
|
} else if (ends_with(it.first, ".dat")) {
|
||||||
|
add_file(dat_files, file_basename, std::move(it.second));
|
||||||
|
} else if (ends_with(it.first, ".pvr")) {
|
||||||
|
add_file(pvr_files, file_basename, std::move(it.second));
|
||||||
|
} else {
|
||||||
|
throw runtime_error("qst file contains unsupported file type: " + it.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
static_game_data_log.warning("(%s) Skipping file (unsupported format)", filename.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const exception& e) {
|
||||||
|
static_game_data_log.warning("(%s) Failed to load quest file: (%s)", filename.c_str(), e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
shared_ptr<const string> bin_contents = bin_it.second;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QuestFileFormat format;
|
// Quest .bin filenames are like K###-VERS-LANG.EXT, where:
|
||||||
string basename;
|
// K can be any character (usually it's q)
|
||||||
if (ends_with(bin_filename, ".bin.gci") || ends_with(bin_filename, ".mnm.gci")) {
|
|
||||||
format = QuestFileFormat::BIN_DAT_GCI;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
|
||||||
} else if (ends_with(bin_filename, ".bin.vms")) {
|
|
||||||
format = QuestFileFormat::BIN_DAT_VMS;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
|
||||||
} else if (ends_with(bin_filename, ".bin.dlq") || ends_with(bin_filename, ".mnm.dlq")) {
|
|
||||||
format = QuestFileFormat::BIN_DAT_DLQ;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 8);
|
|
||||||
} else if (ends_with(bin_filename, ".qst")) {
|
|
||||||
format = QuestFileFormat::QST;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 4);
|
|
||||||
} else if (ends_with(bin_filename, ".bin") || ends_with(bin_filename, ".mnm")) {
|
|
||||||
format = QuestFileFormat::BIN_DAT;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 4);
|
|
||||||
} else if (ends_with(bin_filename, ".bind") || ends_with(bin_filename, ".mnmd")) {
|
|
||||||
format = QuestFileFormat::BIN_DAT_UNCOMPRESSED;
|
|
||||||
basename = bin_filename.substr(0, bin_filename.size() - 5);
|
|
||||||
} else {
|
|
||||||
continue; // Silently skip file
|
|
||||||
}
|
|
||||||
if (basename.empty()) {
|
|
||||||
throw invalid_argument("empty filename");
|
|
||||||
}
|
|
||||||
if (basename.size() < 2) {
|
|
||||||
throw logic_error("basename too short for language trim");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quest .bin filenames are like K###-CAT-VERS-LANG.EXT, where:
|
|
||||||
// K = class (quest, battle, challenge, etc.)
|
|
||||||
// # = quest number (does not have to match the internal quest number)
|
// # = quest number (does not have to match the internal quest number)
|
||||||
// CAT = menu category in which quest should appear (optional)
|
// VERS = PSO version that the quest is for (dc, pc, gc, etc.)
|
||||||
// VERS = PSO version that the quest is for
|
|
||||||
// LANG = client language (j, e, g, f, s)
|
// LANG = client language (j, e, g, f, s)
|
||||||
// EXT = file type (bin, bind, bin.dlq, qst, etc.)
|
// EXT = file type (bin, bind, bin.dlq, qst, etc.)
|
||||||
// Quest .dat filenames are like K###-CAT-VERS.EXT (same as for .bin except
|
// EXT has already been stripped off by the time we get here, so we just
|
||||||
// the LANG token is omitted)
|
// parse the remaining fields.
|
||||||
vector<string> filename_tokens = split(basename, '-');
|
string quest_number_token, version_token, language_token;
|
||||||
|
{
|
||||||
string category_token;
|
vector<string> filename_tokens = split(basename, '-');
|
||||||
if (filename_tokens.size() == 4) {
|
if (filename_tokens.size() != 3) {
|
||||||
category_token = std::move(filename_tokens[1]);
|
throw invalid_argument("incorrect filename format");
|
||||||
filename_tokens.erase(filename_tokens.begin() + 1);
|
}
|
||||||
} else if (filename_tokens.size() != 3) {
|
quest_number_token = std::move(filename_tokens[0]);
|
||||||
throw invalid_argument("incorrect filename format");
|
version_token = std::move(filename_tokens[1]);
|
||||||
|
language_token = std::move(filename_tokens[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t category_id = this->category_index
|
uint32_t category_id = categories.at(basename);
|
||||||
? this->category_index->find(basename[0], category_token).category_id
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Parse the number out of the first token
|
// Get the number from the first token
|
||||||
uint32_t quest_number = strtoull(filename_tokens[0].c_str() + 1, nullptr, 10);
|
if (quest_number_token.empty()) {
|
||||||
|
throw runtime_error("quest number token is missing");
|
||||||
|
}
|
||||||
|
uint32_t quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
|
||||||
|
|
||||||
// Get the version from the second (or previously third) token
|
// Get the version from the second token
|
||||||
static const unordered_map<string, QuestScriptVersion> name_to_version({
|
static const unordered_map<string, QuestScriptVersion> name_to_version({
|
||||||
{"dn", QuestScriptVersion::DC_NTE},
|
{"dn", QuestScriptVersion::DC_NTE},
|
||||||
{"d1", QuestScriptVersion::DC_V1},
|
{"d1", QuestScriptVersion::DC_V1},
|
||||||
@@ -528,166 +570,98 @@ QuestIndex::QuestIndex(
|
|||||||
{"xb", QuestScriptVersion::XB_V3},
|
{"xb", QuestScriptVersion::XB_V3},
|
||||||
{"bb", QuestScriptVersion::BB_V4},
|
{"bb", QuestScriptVersion::BB_V4},
|
||||||
});
|
});
|
||||||
auto version = name_to_version.at(filename_tokens[1]);
|
auto version = name_to_version.at(version_token);
|
||||||
|
|
||||||
// Get the language from the last token
|
// Get the language from the last token
|
||||||
if (filename_tokens[2].size() != 1) {
|
if (language_token.size() != 1) {
|
||||||
throw runtime_error("language token is not a single character");
|
throw runtime_error("language token is not a single character");
|
||||||
}
|
}
|
||||||
uint8_t language = language_code_for_char(filename_tokens[2][0]);
|
uint8_t language = language_code_for_char(language_token[0]);
|
||||||
|
|
||||||
shared_ptr<const string> bin_contents;
|
|
||||||
shared_ptr<const string> dat_contents;
|
|
||||||
string bin_data = load_file(bin_path);
|
|
||||||
switch (format) {
|
|
||||||
case QuestFileFormat::BIN_DAT:
|
|
||||||
bin_contents.reset(new string(std::move(bin_data)));
|
|
||||||
break;
|
|
||||||
case QuestFileFormat::BIN_DAT_UNCOMPRESSED:
|
|
||||||
bin_contents.reset(new string(prs_compress(bin_data)));
|
|
||||||
break;
|
|
||||||
case QuestFileFormat::BIN_DAT_GCI:
|
|
||||||
bin_contents.reset(new string(decode_gci_data(bin_data)));
|
|
||||||
break;
|
|
||||||
case QuestFileFormat::BIN_DAT_VMS:
|
|
||||||
bin_contents.reset(new string(decode_vms_data(bin_data)));
|
|
||||||
break;
|
|
||||||
case QuestFileFormat::BIN_DAT_DLQ:
|
|
||||||
bin_contents.reset(new string(decode_dlq_data(bin_data)));
|
|
||||||
break;
|
|
||||||
case QuestFileFormat::QST: {
|
|
||||||
auto result = decode_qst_data(bin_data);
|
|
||||||
bin_contents.reset(new string(std::move(result.first)));
|
|
||||||
dat_contents.reset(new string(std::move(result.second)));
|
|
||||||
dat_cache.emplace(basename, dat_contents);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw logic_error("invalid quest file format");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Find the corresponding dat and pvr files
|
||||||
string dat_filename;
|
string dat_filename;
|
||||||
if (!dat_contents && (version != QuestScriptVersion::GC_EP3)) {
|
string pvr_filename;
|
||||||
if (basename.size() < 2) {
|
shared_ptr<const string> dat_contents;
|
||||||
throw logic_error("basename too short for language trim");
|
shared_ptr<const string> pvr_contents;
|
||||||
}
|
if (version != QuestScriptVersion::GC_EP3) {
|
||||||
|
// Look for dat and pvr files with the same basename as the bin file; if
|
||||||
// Look for dat file with the same basename as the bin file; if not
|
// not found, look for them without the language suffix
|
||||||
// found, look for a dat file without the language suffix
|
|
||||||
string dat_basename;
|
|
||||||
for (size_t z = 0; z < 2; z++) {
|
|
||||||
dat_basename = z ? basename.substr(0, basename.size() - 2) : basename;
|
|
||||||
dat_filename = dat_basename;
|
|
||||||
try {
|
|
||||||
dat_contents = dat_cache.at(dat_basename);
|
|
||||||
break;
|
|
||||||
} catch (const out_of_range&) {
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".dat";
|
|
||||||
string dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_path)) {
|
|
||||||
dat_contents.reset(new string(load_file(dat_path)));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".datd";
|
|
||||||
dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_path)) {
|
|
||||||
string decompressed = load_file(dat_path);
|
|
||||||
dat_contents.reset(new string(prs_compress_optimal(decompressed.data(), decompressed.size())));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".dat.gci";
|
|
||||||
dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_path)) {
|
|
||||||
dat_contents.reset(new string(decode_gci_data(load_file(dat_path))));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".dat.vms";
|
|
||||||
dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_path)) {
|
|
||||||
dat_contents.reset(new string(decode_vms_data(load_file(dat_path))));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".dat.dlq";
|
|
||||||
dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_path)) {
|
|
||||||
dat_contents.reset(new string(decode_dlq_data(load_file(dat_path))));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dat_filename = dat_basename + ".qst";
|
|
||||||
dat_path = this->directory + "/" + dat_filename;
|
|
||||||
if (isfile(dat_basename + ".qst")) {
|
|
||||||
dat_contents.reset(new string(decode_dlq_data(load_file(dat_basename + ".dat.dlq"))));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dat_contents) {
|
|
||||||
dat_cache.emplace(dat_basename, dat_contents);
|
|
||||||
} else {
|
|
||||||
throw runtime_error("no dat file found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for a JSON file with the same basename as the bin file; if not
|
|
||||||
// found, look for a JSON file without the language suffix
|
|
||||||
shared_ptr<const JSON> metadata_json;
|
|
||||||
string json_filename;
|
|
||||||
for (size_t z = 0; z < 3; z++) {
|
|
||||||
string json_basename;
|
|
||||||
if (z == 0) {
|
|
||||||
json_filename = basename + ".json";
|
|
||||||
} else if (z == 1) {
|
|
||||||
json_filename = basename.substr(0, basename.size() - 2) + ".json"; // Strip off language prefix
|
|
||||||
} else if (z == 2) {
|
|
||||||
json_filename = basename.substr(0, basename.find('-')) + ".json"; // Look only at base token (e.g. "b88001")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
metadata_json = metadata_json_cache.at(json_filename);
|
dat_filename = basename;
|
||||||
break;
|
dat_contents = dat_files.at(dat_filename);
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
|
try {
|
||||||
|
dat_filename = quest_number_token + "-" + version_token;
|
||||||
|
dat_contents = dat_files.at(dat_filename);
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
throw runtime_error("no dat file found for bin file " + basename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
string json_path = this->directory + "/" + json_filename;
|
pvr_filename = basename;
|
||||||
if (isfile(json_path)) {
|
pvr_contents = pvr_files.at(pvr_filename);
|
||||||
metadata_json.reset(new JSON(JSON::parse(load_file(json_path))));
|
} catch (const out_of_range&) {
|
||||||
break;
|
try {
|
||||||
|
pvr_filename = quest_number_token + "-" + version_token;
|
||||||
|
pvr_contents = pvr_files.at(pvr_filename);
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
// pvr files aren't required (and most quests do not have them), so
|
||||||
|
// don't fail if it's missing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata_json_cache.emplace(json_filename, metadata_json);
|
|
||||||
|
|
||||||
|
// Load the quest's metadata JSON file, if it exists
|
||||||
|
string json_filename;
|
||||||
|
JSON metadata_json = nullptr;
|
||||||
shared_ptr<BattleRules> battle_rules;
|
shared_ptr<BattleRules> battle_rules;
|
||||||
ssize_t challenge_template_index = -1;
|
ssize_t challenge_template_index = -1;
|
||||||
if (metadata_json) {
|
try {
|
||||||
|
json_filename = basename;
|
||||||
|
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||||
|
} catch (const out_of_range&) {
|
||||||
try {
|
try {
|
||||||
battle_rules.reset(new BattleRules(metadata_json->at("battle_rules")));
|
json_filename = quest_number_token + "-" + version_token;
|
||||||
|
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
try {
|
||||||
|
json_filename = quest_number_token;
|
||||||
|
metadata_json = JSON::parse(*json_files.at(json_filename));
|
||||||
|
} catch (const out_of_range&) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!metadata_json.is_null()) {
|
||||||
|
try {
|
||||||
|
battle_rules.reset(new BattleRules(metadata_json.at("battle_rules")));
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
challenge_template_index = metadata_json->at("challenge_template_index").as_int();
|
challenge_template_index = metadata_json.at("challenge_template_index").as_int();
|
||||||
} catch (const out_of_range&) {
|
} catch (const out_of_range&) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<VersionedQuest> vq(new VersionedQuest(
|
shared_ptr<VersionedQuest> vq(new VersionedQuest(
|
||||||
quest_number, category_id, version, language, bin_contents, dat_contents, battle_rules, challenge_template_index));
|
quest_number,
|
||||||
|
category_id,
|
||||||
|
version,
|
||||||
|
language,
|
||||||
|
bin_contents,
|
||||||
|
dat_contents,
|
||||||
|
pvr_contents,
|
||||||
|
battle_rules,
|
||||||
|
challenge_template_index));
|
||||||
|
|
||||||
auto category_name = this->category_index->at(vq->category_id).name;
|
auto category_name = this->category_index->at(vq->category_id).name;
|
||||||
|
string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat");
|
||||||
string dat_str = dat_filename.empty() ? "" : (" with layout " + dat_filename);
|
string battle_rules_str = battle_rules ? (" with battle rules from " + json_filename + ".json") : "";
|
||||||
string battle_rules_str = battle_rules ? (" with battle rules from " + json_filename) : "";
|
|
||||||
string challenge_template_str = (challenge_template_index >= 0) ? string_printf(" with challenge template index %zd", vq->challenge_template_index) : "";
|
string challenge_template_str = (challenge_template_index >= 0) ? string_printf(" with challenge template index %zd", vq->challenge_template_index) : "";
|
||||||
auto q_it = this->quests_by_number.find(vq->quest_number);
|
auto q_it = this->quests_by_number.find(vq->quest_number);
|
||||||
if (q_it != this->quests_by_number.end()) {
|
if (q_it != this->quests_by_number.end()) {
|
||||||
q_it->second->add_version(vq);
|
q_it->second->add_version(vq);
|
||||||
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " (%s)%s%s%s",
|
static_game_data_log.info("(%s) Added %s %c version of quest %" PRIu32 " (%s)%s%s%s",
|
||||||
bin_filename.c_str(),
|
basename.c_str(),
|
||||||
name_for_enum(vq->version),
|
name_for_enum(vq->version),
|
||||||
char_for_language_code(vq->language),
|
char_for_language_code(vq->language),
|
||||||
vq->quest_number,
|
vq->quest_number,
|
||||||
@@ -698,7 +672,7 @@ QuestIndex::QuestIndex(
|
|||||||
} else {
|
} else {
|
||||||
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
|
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
|
||||||
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)%s%s%s",
|
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)%s%s%s",
|
||||||
bin_filename.c_str(),
|
basename.c_str(),
|
||||||
name_for_enum(vq->version),
|
name_for_enum(vq->version),
|
||||||
char_for_language_code(vq->language),
|
char_for_language_code(vq->language),
|
||||||
vq->quest_number,
|
vq->quest_number,
|
||||||
@@ -712,7 +686,7 @@ QuestIndex::QuestIndex(
|
|||||||
challenge_template_str.c_str());
|
challenge_template_str.c_str());
|
||||||
}
|
}
|
||||||
} catch (const exception& e) {
|
} catch (const exception& e) {
|
||||||
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", bin_filename.c_str(), e.what());
|
static_game_data_log.warning("(%s) Failed to index quest file: (%s)", basename.c_str(), e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,14 +699,6 @@ shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>> QuestIndex::filter(uint32_t category_id, QuestScriptVersion version) const {
|
||||||
vector<shared_ptr<const Quest>> ret;
|
vector<shared_ptr<const Quest>> ret;
|
||||||
for (auto it : this->quests_by_number) {
|
for (auto it : this->quests_by_number) {
|
||||||
@@ -833,6 +799,9 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
|
|||||||
shared_ptr<VersionedQuest> dlq(new VersionedQuest(*this));
|
shared_ptr<VersionedQuest> dlq(new VersionedQuest(*this));
|
||||||
dlq->bin_contents.reset(new string(encode_download_quest_data(compressed_bin, decompressed_bin.size())));
|
dlq->bin_contents.reset(new string(encode_download_quest_data(compressed_bin, decompressed_bin.size())));
|
||||||
dlq->dat_contents.reset(new string(encode_download_quest_data(*this->dat_contents)));
|
dlq->dat_contents.reset(new string(encode_download_quest_data(*this->dat_contents)));
|
||||||
|
if (this->pvr_contents) {
|
||||||
|
dlq->pvr_contents.reset(new string(encode_download_quest_data(*this->pvr_contents)));
|
||||||
|
}
|
||||||
dlq->is_dlq_encoded = true;
|
dlq->is_dlq_encoded = true;
|
||||||
return dlq;
|
return dlq;
|
||||||
}
|
}
|
||||||
@@ -997,15 +966,11 @@ string decode_dlq_data(const string& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename HeaderT, typename OpenFileT>
|
template <typename HeaderT, typename OpenFileT>
|
||||||
static pair<string, string> decode_qst_data_t(const string& data) {
|
static unordered_map<string, string> decode_qst_data_t(const string& data) {
|
||||||
StringReader r(data);
|
StringReader r(data);
|
||||||
|
|
||||||
string bin_contents;
|
unordered_map<string, string> files;
|
||||||
string dat_contents;
|
unordered_map<string, size_t> file_remaining_bytes;
|
||||||
string internal_bin_filename;
|
|
||||||
string internal_dat_filename;
|
|
||||||
uint32_t bin_file_size = 0;
|
|
||||||
uint32_t dat_file_size = 0;
|
|
||||||
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
|
QuestFileFormat subformat = QuestFileFormat::QST; // Stand-in for unknown
|
||||||
while (!r.eof()) {
|
while (!r.eof()) {
|
||||||
// Handle BB's implicit 8-byte command alignment
|
// Handle BB's implicit 8-byte command alignment
|
||||||
@@ -1039,24 +1004,11 @@ static pair<string, string> decode_qst_data_t(const string& data) {
|
|||||||
const auto& cmd = r.get<OpenFileT>();
|
const auto& cmd = r.get<OpenFileT>();
|
||||||
string internal_filename = cmd.filename.decode();
|
string internal_filename = cmd.filename.decode();
|
||||||
|
|
||||||
if (ends_with(internal_filename, ".bin")) {
|
if (!files.emplace(internal_filename, "").second) {
|
||||||
if (internal_bin_filename.empty()) {
|
throw runtime_error("qst opens the same file multiple times: " + internal_filename);
|
||||||
internal_bin_filename = internal_filename;
|
}
|
||||||
} else {
|
if (!file_remaining_bytes.emplace(internal_filename, cmd.file_size).second) {
|
||||||
throw runtime_error("qst contains multiple bin files");
|
throw runtime_error("qst opens the same file multiple times: " + internal_filename);
|
||||||
}
|
|
||||||
bin_file_size = cmd.file_size;
|
|
||||||
|
|
||||||
} else if (ends_with(internal_filename, ".dat")) {
|
|
||||||
if (internal_dat_filename.empty()) {
|
|
||||||
internal_dat_filename = internal_filename;
|
|
||||||
} else {
|
|
||||||
throw runtime_error("qst contains multiple dat files");
|
|
||||||
}
|
|
||||||
dat_file_size = cmd.file_size;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw runtime_error("qst contains non-bin, non-dat file");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (header.command == 0x13 || header.command == 0xA7) {
|
} else if (header.command == 0x13 || header.command == 0xA7) {
|
||||||
@@ -1067,50 +1019,44 @@ static pair<string, string> decode_qst_data_t(const string& data) {
|
|||||||
throw runtime_error("qst write file command has incorrect size");
|
throw runtime_error("qst write file command has incorrect size");
|
||||||
}
|
}
|
||||||
const auto& cmd = r.get<S_WriteFile_13_A7>();
|
const auto& cmd = r.get<S_WriteFile_13_A7>();
|
||||||
string filename = cmd.filename.decode();
|
|
||||||
|
|
||||||
string* dest = nullptr;
|
|
||||||
if (filename == internal_bin_filename) {
|
|
||||||
dest = &bin_contents;
|
|
||||||
} else if (filename == internal_dat_filename) {
|
|
||||||
dest = &dat_contents;
|
|
||||||
} else {
|
|
||||||
throw runtime_error(string_printf("qst contains write command for non-open file \"%s\" (open files are \"%s\" and \"%s\")",
|
|
||||||
filename.c_str(), internal_bin_filename.c_str(), internal_dat_filename.c_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd.data_size > 0x400) {
|
if (cmd.data_size > 0x400) {
|
||||||
throw runtime_error("qst contains invalid write command");
|
throw runtime_error("qst contains invalid write command");
|
||||||
}
|
}
|
||||||
if (dest->size() & 0x3FF) {
|
string filename = cmd.filename.decode();
|
||||||
|
|
||||||
|
string& file_data = files.at(filename);
|
||||||
|
size_t& remaining_bytes = file_remaining_bytes.at(filename);
|
||||||
|
|
||||||
|
if (file_data.size() & 0x3FF) {
|
||||||
throw runtime_error("qst contains uneven chunks out of order");
|
throw runtime_error("qst contains uneven chunks out of order");
|
||||||
}
|
}
|
||||||
if (header.flag != dest->size() / 0x400) {
|
if (header.flag != file_data.size() / 0x400) {
|
||||||
throw runtime_error("qst contains chunks out of order");
|
throw runtime_error("qst contains chunks out of order");
|
||||||
}
|
}
|
||||||
dest->append(reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
|
file_data.append(reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
|
||||||
|
remaining_bytes -= cmd.data_size;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw runtime_error("invalid command in qst file");
|
throw runtime_error("invalid command in qst file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bin_contents.size() != bin_file_size) {
|
for (const auto& it : file_remaining_bytes) {
|
||||||
throw runtime_error("bin file does not match expected size");
|
if (it.second) {
|
||||||
}
|
throw runtime_error(string_printf("expected %zu (0x%zX) more bytes for file %s", it.second, it.second, it.first.c_str()));
|
||||||
if (dat_contents.size() != dat_file_size) {
|
}
|
||||||
throw runtime_error("dat file does not match expected size");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
|
if (subformat == QuestFileFormat::BIN_DAT_DLQ) {
|
||||||
bin_contents = decode_dlq_data(bin_contents);
|
for (auto& it : files) {
|
||||||
dat_contents = decode_dlq_data(dat_contents);
|
it.second = decode_dlq_data(it.second);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return make_pair(bin_contents, dat_contents);
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
pair<string, string> decode_qst_data(const string& data) {
|
unordered_map<string, string> decode_qst_data(const string& data) {
|
||||||
// QST files start with an open file command, but the format differs depending
|
// QST files start with an open file command, but the format differs depending
|
||||||
// on the PSO version that the qst file is for. We can detect the format from
|
// on the PSO version that the qst file is for. We can detect the format from
|
||||||
// the first 4 bytes in the file:
|
// the first 4 bytes in the file:
|
||||||
@@ -1216,58 +1162,60 @@ void add_write_file_commands_t(
|
|||||||
}
|
}
|
||||||
|
|
||||||
string encode_qst_file(
|
string encode_qst_file(
|
||||||
const string& bin_data,
|
const unordered_map<string, shared_ptr<const string>>& files,
|
||||||
const string& dat_data,
|
|
||||||
const string& name,
|
const string& name,
|
||||||
uint32_t quest_number,
|
uint32_t quest_number,
|
||||||
uint8_t language,
|
const string& xb_filename,
|
||||||
QuestScriptVersion version,
|
QuestScriptVersion version,
|
||||||
bool is_dlq_encoded) {
|
bool is_dlq_encoded) {
|
||||||
StringWriter w;
|
StringWriter w;
|
||||||
|
|
||||||
string bin_filename = string_printf("quest%" PRIu32 ".bin", quest_number);
|
|
||||||
string dat_filename = string_printf("quest%" PRIu32 ".dat", quest_number);
|
|
||||||
string xb_filename = string_printf("quest%" PRIu32 "_%c.dat", quest_number, tolower(char_for_language_code(language)));
|
|
||||||
|
|
||||||
// Some tools expect both open file commands at the beginning, hence this
|
// Some tools expect both open file commands at the beginning, hence this
|
||||||
// unfortunate abstraction-breaking.
|
// unfortunate abstraction-breaking.
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case QuestScriptVersion::DC_NTE:
|
case QuestScriptVersion::DC_NTE:
|
||||||
case QuestScriptVersion::DC_V1:
|
case QuestScriptVersion::DC_V1:
|
||||||
case QuestScriptVersion::DC_V2:
|
case QuestScriptVersion::DC_V2:
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
for (const auto& it : files) {
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
|
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
|
}
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
|
for (const auto& it : files) {
|
||||||
|
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case QuestScriptVersion::PC_V2:
|
case QuestScriptVersion::PC_V2:
|
||||||
add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
for (const auto& it : files) {
|
||||||
add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
|
add_open_file_command_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||||
add_write_file_commands_t<PSOCommandHeaderPC>(w, bin_filename, bin_data, is_dlq_encoded, false);
|
}
|
||||||
add_write_file_commands_t<PSOCommandHeaderPC>(w, dat_filename, dat_data, is_dlq_encoded, false);
|
for (const auto& it : files) {
|
||||||
|
add_write_file_commands_t<PSOCommandHeaderPC>(w, it.first, *it.second, is_dlq_encoded, false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case QuestScriptVersion::GC_NTE:
|
case QuestScriptVersion::GC_NTE:
|
||||||
case QuestScriptVersion::GC_V3:
|
case QuestScriptVersion::GC_V3:
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
|
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
|
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
|
|
||||||
break;
|
|
||||||
case QuestScriptVersion::GC_EP3:
|
case QuestScriptVersion::GC_EP3:
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
for (const auto& it : files) {
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
|
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_PC_GC_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||||
|
}
|
||||||
|
for (const auto& it : files) {
|
||||||
|
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case QuestScriptVersion::XB_V3:
|
case QuestScriptVersion::XB_V3:
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
for (const auto& it : files) {
|
||||||
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
|
add_open_file_command_t<PSOCommandHeaderDCV3, S_OpenFile_XB_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, bin_filename, bin_data, is_dlq_encoded, false);
|
}
|
||||||
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, dat_filename, dat_data, is_dlq_encoded, false);
|
for (const auto& it : files) {
|
||||||
|
add_write_file_commands_t<PSOCommandHeaderDCV3>(w, it.first, *it.second, is_dlq_encoded, false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case QuestScriptVersion::BB_V4:
|
case QuestScriptVersion::BB_V4:
|
||||||
add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, bin_filename, xb_filename, quest_number, bin_data.size(), is_dlq_encoded);
|
for (const auto& it : files) {
|
||||||
add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, dat_filename, xb_filename, quest_number, dat_data.size(), is_dlq_encoded);
|
add_open_file_command_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded);
|
||||||
add_write_file_commands_t<PSOCommandHeaderBB>(w, bin_filename, bin_data, is_dlq_encoded, true);
|
}
|
||||||
add_write_file_commands_t<PSOCommandHeaderBB>(w, dat_filename, dat_data, is_dlq_encoded, true);
|
for (const auto& it : files) {
|
||||||
|
add_write_file_commands_t<PSOCommandHeaderBB>(w, it.first, *it.second, is_dlq_encoded, true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw logic_error("invalid game version");
|
throw logic_error("invalid game version");
|
||||||
|
|||||||
+7
-10
@@ -5,6 +5,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "PlayerSubordinates.hh"
|
#include "PlayerSubordinates.hh"
|
||||||
@@ -35,8 +36,7 @@ struct QuestCategoryIndex {
|
|||||||
|
|
||||||
uint32_t category_id;
|
uint32_t category_id;
|
||||||
uint8_t flags;
|
uint8_t flags;
|
||||||
char type;
|
std::string directory_name;
|
||||||
std::string short_token;
|
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string description;
|
std::string description;
|
||||||
|
|
||||||
@@ -49,7 +49,6 @@ struct QuestCategoryIndex {
|
|||||||
|
|
||||||
explicit QuestCategoryIndex(const JSON& json);
|
explicit QuestCategoryIndex(const JSON& json);
|
||||||
|
|
||||||
const Category& find(char type, const std::string& short_token) const;
|
|
||||||
const Category& at(uint32_t category_id) const;
|
const Category& at(uint32_t category_id) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,6 +65,7 @@ struct VersionedQuest {
|
|||||||
std::string long_description;
|
std::string long_description;
|
||||||
std::shared_ptr<const std::string> bin_contents;
|
std::shared_ptr<const std::string> bin_contents;
|
||||||
std::shared_ptr<const std::string> dat_contents;
|
std::shared_ptr<const std::string> dat_contents;
|
||||||
|
std::shared_ptr<const std::string> pvr_contents;
|
||||||
std::shared_ptr<const BattleRules> battle_rules;
|
std::shared_ptr<const BattleRules> battle_rules;
|
||||||
ssize_t challenge_template_index;
|
ssize_t challenge_template_index;
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ struct VersionedQuest {
|
|||||||
uint8_t language,
|
uint8_t language,
|
||||||
std::shared_ptr<const std::string> bin_contents,
|
std::shared_ptr<const std::string> bin_contents,
|
||||||
std::shared_ptr<const std::string> dat_contents,
|
std::shared_ptr<const std::string> dat_contents,
|
||||||
|
std::shared_ptr<const std::string> pvr_contents,
|
||||||
std::shared_ptr<const BattleRules> battle_rules = nullptr,
|
std::shared_ptr<const BattleRules> battle_rules = nullptr,
|
||||||
ssize_t challenge_template_index = -1);
|
ssize_t challenge_template_index = -1);
|
||||||
|
|
||||||
@@ -119,12 +120,9 @@ struct QuestIndex {
|
|||||||
|
|
||||||
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
|
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);
|
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index);
|
||||||
|
|
||||||
std::shared_ptr<const Quest> get(uint32_t quest_number) 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(uint32_t category_id, QuestScriptVersion version) const;
|
std::vector<std::shared_ptr<const Quest>> filter(uint32_t category_id, QuestScriptVersion version) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,13 +142,12 @@ std::string decode_vms_data(
|
|||||||
int64_t known_seed = -1,
|
int64_t known_seed = -1,
|
||||||
bool skip_checksum = false);
|
bool skip_checksum = false);
|
||||||
std::string decode_dlq_data(const std::string& data);
|
std::string decode_dlq_data(const std::string& data);
|
||||||
std::pair<std::string, std::string> decode_qst_data(const std::string& data);
|
std::unordered_map<std::string, std::string> decode_qst_data(const std::string& data);
|
||||||
|
|
||||||
std::string encode_qst_file(
|
std::string encode_qst_file(
|
||||||
const std::string& bin_data,
|
const std::unordered_map<std::string, std::shared_ptr<const std::string>>& files,
|
||||||
const std::string& dat_data,
|
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
uint32_t quest_number,
|
uint32_t quest_number,
|
||||||
uint8_t language,
|
const std::string& xb_filename,
|
||||||
QuestScriptVersion version,
|
QuestScriptVersion version,
|
||||||
bool is_dlq_encoded);
|
bool is_dlq_encoded);
|
||||||
|
|||||||
@@ -2289,8 +2289,12 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
|
|||||||
} else {
|
} else {
|
||||||
vq = vq->create_download_quest(c->language());
|
vq = vq->create_download_quest(c->language());
|
||||||
string xb_filename = vq->xb_filename();
|
string xb_filename = vq->xb_filename();
|
||||||
send_open_quest_file(c, q->name, vq->bin_filename(), xb_filename, vq->quest_number, QuestFileType::DOWNLOAD, vq->bin_contents);
|
QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
|
||||||
send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, QuestFileType::DOWNLOAD, vq->dat_contents);
|
send_open_quest_file(c, q->name, vq->bin_filename(), xb_filename, vq->quest_number, type, vq->bin_contents);
|
||||||
|
send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, type, vq->dat_contents);
|
||||||
|
if (vq->pvr_contents) {
|
||||||
|
send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, type, vq->pvr_contents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
+4
-3
@@ -2939,10 +2939,11 @@ void send_open_quest_file_t(
|
|||||||
cmd.name.encode("GBA Demo");
|
cmd.name.encode("GBA Demo");
|
||||||
cmd.type = 2;
|
cmd.type = 2;
|
||||||
break;
|
break;
|
||||||
case QuestFileType::DOWNLOAD:
|
case QuestFileType::DOWNLOAD_WITHOUT_PVR:
|
||||||
|
case QuestFileType::DOWNLOAD_WITH_PVR:
|
||||||
command_num = 0xA6;
|
command_num = 0xA6;
|
||||||
cmd.name.encode("PSO/" + quest_name);
|
cmd.name.encode("PSO/" + quest_name);
|
||||||
cmd.type = 0;
|
cmd.type = (type == QuestFileType::DOWNLOAD_WITH_PVR) ? 1 : 0;
|
||||||
break;
|
break;
|
||||||
case QuestFileType::EPISODE_3:
|
case QuestFileType::EPISODE_3:
|
||||||
command_num = 0xA6;
|
command_num = 0xA6;
|
||||||
@@ -2968,7 +2969,7 @@ void send_open_quest_file_t<S_OpenFile_XB_44_A6>(
|
|||||||
QuestFileType type) {
|
QuestFileType type) {
|
||||||
S_OpenFile_XB_44_A6 cmd;
|
S_OpenFile_XB_44_A6 cmd;
|
||||||
cmd.name.encode("PSO/" + quest_name);
|
cmd.name.encode("PSO/" + quest_name);
|
||||||
cmd.type = 0;
|
cmd.type = (type == QuestFileType::DOWNLOAD_WITH_PVR) ? 1 : 0;
|
||||||
cmd.file_size = file_size;
|
cmd.file_size = file_size;
|
||||||
cmd.filename.encode(filename);
|
cmd.filename.encode(filename);
|
||||||
cmd.xb_filename.encode(xb_filename);
|
cmd.xb_filename.encode(xb_filename);
|
||||||
|
|||||||
+2
-1
@@ -355,7 +355,8 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);
|
|||||||
|
|
||||||
enum class QuestFileType {
|
enum class QuestFileType {
|
||||||
ONLINE = 0,
|
ONLINE = 0,
|
||||||
DOWNLOAD,
|
DOWNLOAD_WITHOUT_PVR,
|
||||||
|
DOWNLOAD_WITH_PVR,
|
||||||
EPISODE_3,
|
EPISODE_3,
|
||||||
GBA_DEMO,
|
GBA_DEMO,
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-22
@@ -472,7 +472,7 @@
|
|||||||
// descriptions. (We don't use a map here because the category order
|
// descriptions. (We don't use a map here because the category order
|
||||||
// specified here is the order that appears in the quest menu.)
|
// specified here is the order that appears in the quest menu.)
|
||||||
"QuestCategories": [
|
"QuestCategories": [
|
||||||
// Each entry is [flags, type, token, category_name, description].
|
// Each entry is [flags, directory_name, category_name, description].
|
||||||
// These fields are:
|
// These fields are:
|
||||||
// flags: a bit field containing the following:
|
// flags: a bit field containing the following:
|
||||||
// 0x01 - appears in normal mode
|
// 0x01 - appears in normal mode
|
||||||
@@ -483,30 +483,28 @@
|
|||||||
// 0x20 - appears in download quest menu
|
// 0x20 - appears in download quest menu
|
||||||
// 0x40 - appears in Episode 3 download quest menu
|
// 0x40 - appears in Episode 3 download quest menu
|
||||||
// 0x80 - hidden on pre-V3 versions (DC, PC)
|
// 0x80 - hidden on pre-V3 versions (DC, PC)
|
||||||
// type: the character that newserv expects at the beginning of the quest
|
// directory_name: the directory inside system/quests that contains quests
|
||||||
// filename, generally one of b, c, d, e, or q.
|
// for this category.
|
||||||
// short_token: the token newserv expects to see in quest filenames after
|
|
||||||
// the quest number.
|
|
||||||
// category_name: what appears in the quest menu on the client.
|
// category_name: what appears in the quest menu on the client.
|
||||||
// description: what appears in the category description window (may
|
// description: what appears in the category description window (may
|
||||||
// contain color escape codes like $C6).
|
// contain color escape codes like $C6).
|
||||||
[0x21, "q", "ret", "Retrieval", "$E$C6Quests that involve\nretrieving an object"],
|
[0x21, "retrieval", "Retrieval", "$E$C6Quests that involve\nretrieving an object"],
|
||||||
[0x21, "q", "ext", "Extermination", "$E$C6Quests that involve\ndestroying all\nmonsters"],
|
[0x21, "extermination", "Extermination", "$E$C6Quests that involve\ndestroying all\nmonsters"],
|
||||||
[0x21, "q", "evt", "Events", "$E$C6Quests that are part\nof an event"],
|
[0x21, "events", "Events", "$E$C6Quests that are part\nof an event"],
|
||||||
[0x21, "q", "shp", "Shops", "$E$C6Quests that contain\nshops"],
|
[0x21, "shops", "Shops", "$E$C6Quests that contain\nshops"],
|
||||||
[0x21, "q", "vr", "Virtual Reality", "$E$C6Quests that are\ndone in a simulator"],
|
[0x21, "vr", "Virtual Reality", "$E$C6Quests that are\ndone in a simulator"],
|
||||||
[0xA1, "q", "twr", "Control Tower", "$E$C6Quests that take\nplace at the Control\nTower"],
|
[0xA1, "tower", "Control Tower", "$E$C6Quests that take\nplace at the Control\nTower"],
|
||||||
[0xA1, "q", "tm", "Team", "$E$C6Quests for you\nand your team\nmembers."],
|
[0xA1, "team", "Team", "$E$C6Quests for you\nand your team\nmembers."],
|
||||||
[0x02, "b", "", "Battle", "$E$C6Battle mode rule\nsets"],
|
[0x02, "battle", "Battle", "$E$C6Battle mode rule\nsets"],
|
||||||
[0x04, "c", "", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"],
|
[0x04, "challenge-ep1", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"],
|
||||||
[0x84, "d", "", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],
|
[0x84, "challenge-ep2", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],
|
||||||
[0x08, "q", "1p", "Solo", "$E$C6Quests that require\na single player"],
|
[0x08, "solo", "Solo", "$E$C6Quests that require\na single player"],
|
||||||
[0x10, "q", "gv1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"],
|
[0x10, "government-ep1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"],
|
||||||
[0x10, "q", "gv2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
|
[0x10, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
|
||||||
[0x10, "q", "gv4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
|
[0x10, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
|
||||||
[0x20, "q", "dl", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
[0x20, "download", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
||||||
[0x40, "e", "dlt", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"],
|
[0x40, "download-ep3-trial", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"],
|
||||||
[0x40, "e", "dl", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
[0x40, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"],
|
||||||
],
|
],
|
||||||
|
|
||||||
// Cheat mode behavior. There are three values:
|
// Cheat mode behavior. There are three values:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e765-dlt-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3-trial/e765-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e901-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e901-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e901-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e901-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e903-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e903-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e903-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e903-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e904-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e904-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e904-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e904-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e905-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e905-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e905-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e905-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e906-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e906-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e906-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e906-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e907-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e907-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e907-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e907-gc3-j.mnm
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e908-dl-gc3-e.mnm
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../maps-download/e908-dl-gc3-j.mnm
|
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e908-gc3-e.mnm
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../maps-download/download-ep3/e908-gc3-j.mnm
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user