use QuestScriptVersion to index quests

This commit is contained in:
Martin Michelsen
2023-07-15 22:29:18 -07:00
parent 64e637dbfb
commit 4858ccd812
8 changed files with 156 additions and 91 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ newserv automatically finds quests in the system/quests/ directory. To install y
Standard quest files should be named like `q###-CATEGORY-VERSION.EXT`, battle quests should be named like `b###-VERSION.EXT`, challenge quests should be named like `c###-VERSION.EXT` for Episode 1 or `d###-VERSION.EXT` for Episode 2, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gv1/gv2/gv4 = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `VERSION`: d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, bb = Blue Burst
- `VERSION`: dn = Dreamcast NTE, d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gcn = GameCube Trial Edition, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, xb = Xbox, bb = Blue Burst
- `EXT`: file extension (see table below)
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, and it puts them in the Retrieval category because the filenames contain `-ret`.
+29
View File
@@ -98,6 +98,35 @@ Client::~Client() {
this->log.info("Deleted");
}
QuestScriptVersion Client::quest_version() const {
switch (this->version()) {
case GameVersion::DC:
if (this->flags & Flag::IS_TRIAL_EDITION) {
return QuestScriptVersion::DC_NTE;
} else if (this->flags & Flag::IS_DC_V1) {
return QuestScriptVersion::DC_V1;
} else {
return QuestScriptVersion::DC_V2;
}
case GameVersion::PC:
return QuestScriptVersion::PC_V2;
case GameVersion::GC:
if (this->flags & Flag::IS_TRIAL_EDITION) {
return QuestScriptVersion::GC_NTE;
} else if (this->flags & Flag::IS_EPISODE_3) {
return QuestScriptVersion::GC_EP3;
} else {
return QuestScriptVersion::GC_V3;
}
case GameVersion::XB:
return QuestScriptVersion::XB_V3;
case GameVersion::BB:
return QuestScriptVersion::BB_V4;
default:
throw logic_error("client\'s game version does not have a quest version");
}
}
void Client::set_license(shared_ptr<const License> l) {
this->license = l;
this->game_data.guild_card_number = this->license->serial_number;
+2
View File
@@ -15,6 +15,7 @@
#include "PSOProtocol.hh"
#include "PatchFileIndex.hh"
#include "Player.hh"
#include "QuestScript.hh"
#include "Text.hh"
extern const uint64_t CLIENT_CONFIG_MAGIC;
@@ -171,6 +172,7 @@ struct Client {
inline GameVersion version() const {
return this->channel.version;
}
QuestScriptVersion quest_version() const;
void set_license(std::shared_ptr<const License> l);
+83 -76
View File
@@ -236,7 +236,6 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
menu_item_id(0),
category_id(0),
episode(Episode::NONE),
is_dcv1(false),
joinable(false),
file_format(FileFormat::BIN_DAT),
has_mnm_extension(false),
@@ -278,12 +277,6 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
}
}
// Quest filenames are like:
// b###-VV.bin for battle mode
// c###-VV.bin for challenge mode
// e###-gc3.mnm (or .bin) for episode 3
// q###-CAT-VV.bin for normal quests
if (basename.empty()) {
throw invalid_argument("empty filename");
}
@@ -305,14 +298,16 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
this->internal_id = strtoull(tokens[0].c_str() + 1, nullptr, 10);
// Get the version from the second (or previously third) token
static const unordered_map<string, GameVersion> name_to_version({
{"d1", GameVersion::DC},
{"dc", GameVersion::DC},
{"pc", GameVersion::PC},
{"gc", GameVersion::GC},
{"gc3", GameVersion::GC},
{"xb", GameVersion::XB},
{"bb", GameVersion::BB},
static const unordered_map<string, QuestScriptVersion> name_to_version({
{"dn", QuestScriptVersion::DC_NTE},
{"d1", QuestScriptVersion::DC_V1},
{"dc", QuestScriptVersion::DC_V2},
{"pc", QuestScriptVersion::PC_V2},
{"gcn", QuestScriptVersion::GC_NTE},
{"gc", QuestScriptVersion::GC_V3},
{"gc3", QuestScriptVersion::GC_EP3},
{"xb", QuestScriptVersion::XB_V3},
{"bb", QuestScriptVersion::BB_V4},
});
this->version = name_to_version.at(tokens[1]);
@@ -323,11 +318,9 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
auto bin_decompressed = prs_decompress(*bin_compressed);
switch (this->version) {
case GameVersion::PATCH:
throw invalid_argument("patch server quests are not valid");
break;
case GameVersion::DC: {
case QuestScriptVersion::DC_NTE:
case QuestScriptVersion::DC_V1:
case QuestScriptVersion::DC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
throw invalid_argument("file is too small for header");
}
@@ -337,11 +330,10 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
this->name = decode_sjis(header->name);
this->short_description = decode_sjis(header->short_description);
this->long_description = decode_sjis(header->long_description);
this->is_dcv1 = (tokens[1] == "d1");
break;
}
case GameVersion::PC: {
case QuestScriptVersion::PC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
throw invalid_argument("file is too small for header");
}
@@ -354,33 +346,42 @@ Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> ca
break;
}
case GameVersion::XB:
case GameVersion::GC: {
if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) {
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
auto* header = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
this->joinable = false;
this->episode = Episode::EP3;
this->name = decode_sjis(header->name);
this->short_description = decode_sjis(header->quest_name);
this->long_description = decode_sjis(header->description);
} else {
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->joinable = false;
this->episode = (header->episode == 1) ? Episode::EP2 : Episode::EP1;
this->name = decode_sjis(header->name);
this->short_description = decode_sjis(header->short_description);
this->long_description = decode_sjis(header->long_description);
case QuestScriptVersion::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 the QuestScriptVersion::GC_EP3 value 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* header = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
this->joinable = false;
this->episode = Episode::EP3;
this->name = decode_sjis(header->name);
this->short_description = decode_sjis(header->quest_name);
this->long_description = decode_sjis(header->description);
break;
}
case GameVersion::BB: {
case QuestScriptVersion::XB_V3:
case QuestScriptVersion::GC_NTE:
case QuestScriptVersion::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->joinable = false;
this->episode = (header->episode == 1) ? Episode::EP2 : Episode::EP1;
this->name = decode_sjis(header->name);
this->short_description = decode_sjis(header->short_description);
this->long_description = decode_sjis(header->long_description);
break;
}
case QuestScriptVersion::BB_V4: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
throw invalid_argument("file is too small for header");
}
@@ -827,7 +828,7 @@ void add_write_file_commands(
}
}
string Quest::export_qst(GameVersion version) const {
string Quest::export_qst() const {
bool is_ep3 = this->episode == Episode::EP3;
if (is_ep3 && !this->is_dlq_encoded) {
throw runtime_error("Episode 3 quests can only be encoded in download QST format");
@@ -837,8 +838,10 @@ string Quest::export_qst(GameVersion version) const {
// Some tools expect both open file commands at the beginning, hence this
// unfortunate abstraction-breaking.
switch (version) {
case GameVersion::DC:
switch (this->version) {
case QuestScriptVersion::DC_NTE:
case QuestScriptVersion::DC_V1:
case QuestScriptVersion::DC_V2:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, *this, true);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, *this, false);
add_write_file_commands<PSOCommandHeaderDCV3>(
@@ -846,7 +849,7 @@ string Quest::export_qst(GameVersion version) const {
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
break;
case GameVersion::PC:
case QuestScriptVersion::PC_V2:
add_open_file_command<PSOCommandHeaderPC, S_OpenFile_PC_V3_44_A6>(w, *this, true);
add_open_file_command<PSOCommandHeaderPC, S_OpenFile_PC_V3_44_A6>(w, *this, false);
add_write_file_commands<PSOCommandHeaderPC>(
@@ -854,20 +857,22 @@ string Quest::export_qst(GameVersion version) const {
add_write_file_commands<PSOCommandHeaderPC>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
break;
case GameVersion::GC:
case GameVersion::XB:
case QuestScriptVersion::GC_NTE:
case QuestScriptVersion::GC_V3:
case QuestScriptVersion::XB_V3:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, true);
if (!is_ep3) {
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, false);
}
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, false);
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded);
if (!is_ep3) {
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
}
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
break;
case GameVersion::BB:
case QuestScriptVersion::GC_EP3:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, true);
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded);
break;
case QuestScriptVersion::BB_V4:
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, *this, true);
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, *this, false);
add_write_file_commands<PSOCommandHeaderBB>(
@@ -916,17 +921,16 @@ QuestIndex::QuestIndex(
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-%" PRId64 " (%" PRIu32 "), %s, %s (%" PRIu32 "), joinable=%s, dcv1=%s)",
static_game_data_log.info("Indexed quest %s (%s => %s-%" PRId64 " (%" PRIu32 "), %s, %s (%" PRIu32 "), joinable=%s)",
ascii_name.c_str(),
filename.c_str(),
name_for_version(q->version),
name_for_enum(q->version),
q->internal_id,
q->menu_item_id,
name_for_episode(q->episode),
category_name.c_str(),
q->category_id,
q->joinable ? "true" : "false",
q->is_dcv1 ? "true" : "false");
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());
}
@@ -934,8 +938,8 @@ QuestIndex::QuestIndex(
}
}
shared_ptr<const Quest> QuestIndex::get(GameVersion version,
uint32_t menu_item_id) const {
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));
}
@@ -944,17 +948,15 @@ shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
}
vector<shared_ptr<const Quest>> QuestIndex::filter(
GameVersion version, bool is_dcv1, uint32_t category_id) const {
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++) {
shared_ptr<const Quest> q = it->second;
if ((q->is_dcv1 != is_dcv1) || (q->category_id != category_id)) {
continue;
if (it->second->category_id == category_id) {
ret.emplace_back(it->second);
}
ret.emplace_back(q);
}
return ret;
@@ -997,7 +999,7 @@ shared_ptr<Quest> Quest::create_download_quest() const {
// 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) {
if (this->episode == Episode::EP3 || this->version == QuestScriptVersion::GC_EP3) {
throw logic_error("Episode 3 quests cannot be converted to download quests");
}
@@ -1005,27 +1007,32 @@ shared_ptr<Quest> Quest::create_download_quest() const {
void* data_ptr = decompressed_bin.data();
switch (this->version) {
case GameVersion::DC:
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 GameVersion::PC:
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 GameVersion::XB:
case GameVersion::GC:
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 GameVersion::BB:
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");
}
+7 -8
View File
@@ -7,8 +7,8 @@
#include <string>
#include <vector>
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "Version.hh"
struct QuestCategoryIndex {
struct Category {
@@ -64,9 +64,8 @@ public:
uint32_t menu_item_id;
uint32_t category_id;
Episode episode;
bool is_dcv1;
bool joinable;
GameVersion version;
QuestScriptVersion version;
std::string file_basename; // we append -<version>.<bin/dat> when reading
FileFormat file_format;
bool has_mnm_extension;
@@ -101,7 +100,7 @@ public:
static std::string decode_dlq_data(const std::string& filename);
static std::pair<std::string, std::string> decode_qst_file(const std::string& filename);
std::string export_qst(GameVersion version) const;
std::string export_qst() const;
private:
// these are populated when requested
@@ -113,7 +112,7 @@ struct QuestIndex {
std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index;
std::map<std::pair<GameVersion, uint64_t>, std::shared_ptr<Quest>> version_menu_item_id_to_quest;
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;
@@ -121,8 +120,8 @@ struct QuestIndex {
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index);
std::shared_ptr<const Quest> get(GameVersion version, uint32_t id) const;
std::shared_ptr<const Quest> get(QuestScriptVersion version, uint32_t id) const;
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
std::vector<std::shared_ptr<const Quest>> filter(GameVersion version,
bool is_dcv1, uint32_t category_id) const;
std::vector<std::shared_ptr<const Quest>> filter(
QuestScriptVersion version, uint32_t category_id) const;
};
+26
View File
@@ -18,6 +18,32 @@
using namespace std;
template <>
const char* name_for_enum<QuestScriptVersion>(QuestScriptVersion v) {
switch (v) {
case QuestScriptVersion::DC_NTE:
return "DC_NTE";
case QuestScriptVersion::DC_V1:
return "DC_V1";
case QuestScriptVersion::DC_V2:
return "DC_V2";
case QuestScriptVersion::PC_V2:
return "PC_V2";
case QuestScriptVersion::GC_NTE:
return "GC_NTE";
case QuestScriptVersion::GC_V3:
return "GC_V3";
case QuestScriptVersion::XB_V3:
return "XB_V3";
case QuestScriptVersion::GC_EP3:
return "GC_EP3";
case QuestScriptVersion::BB_V4:
return "BB_V4";
default:
return "__UNKNOWN__";
}
}
// bit_cast isn't in the standard place on macOS (it is apparently implicitly
// included by resource_dasm, but newserv can be built without resource_dasm)
// and I'm too lazy to go find the right header to include
+4
View File
@@ -3,6 +3,7 @@
#include <stdint.h>
#include <phosg/Encoding.hh>
#include <phosg/Tools.hh>
#include "Text.hh"
#include "Version.hh"
@@ -19,6 +20,9 @@ enum class QuestScriptVersion {
BB_V4 = 8,
};
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;
+4 -6
View File
@@ -1444,7 +1444,7 @@ static void on_09(shared_ptr<ServerState> s, shared_ptr<Client> c,
if (!s->quest_index) {
send_quest_info(c, u"$C6Quests are not available.", !c->lobby_id);
} else {
auto q = s->quest_index->get(c->version(), cmd.item_id);
auto q = s->quest_index->get(c->quest_version(), cmd.item_id);
if (!q) {
send_quest_info(c, u"$C4Quest does not\nexist.", !c->lobby_id);
} else {
@@ -1674,8 +1674,7 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
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->version(), c->flags & Client::Flag::IS_DC_V1, category.category_id);
quests = s->quest_index->filter(c->quest_version(), category.category_id);
break;
}
}
@@ -1916,8 +1915,7 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
break;
}
shared_ptr<Lobby> l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr;
auto quests = s->quest_index->filter(
c->version(), c->flags & Client::Flag::IS_DC_V1, item_id);
auto quests = s->quest_index->filter(c->quest_version(), item_id);
// Hack: Assume the menu to be sent is the download quest menu if the
// client is not in any lobby
@@ -1930,7 +1928,7 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
send_lobby_message_box(c, u"$C6Quests are not available.");
break;
}
auto q = s->quest_index->get(c->version(), item_id);
auto q = s->quest_index->get(c->quest_version(), item_id);
if (!q) {
send_lobby_message_box(c, u"$C6Quest does not exist.");
break;