make quest categories configurable

This commit is contained in:
Martin Michelsen
2023-06-08 20:43:46 -07:00
parent d6eee92645
commit 25b6c594bd
114 changed files with 257 additions and 251 deletions
+90 -112
View File
@@ -20,6 +20,47 @@
using namespace std;
QuestCategoryIndex::Category::Category(uint32_t category_id, std::shared_ptr<const JSONObject> json)
: category_id(category_id) {
const auto& l = json->as_list();
this->flags = l.at(0)->as_int();
this->type = l.at(1)->as_string().at(0);
this->short_token = l.at(2)->as_string();
this->name = decode_sjis(l.at(3)->as_string());
this->description = decode_sjis(l.at(4)->as_string());
}
bool QuestCategoryIndex::Category::matches_flags(uint8_t request) const {
// If the request is for v1 or v2 (hence it has the HIDE_ON_PRE_V3 flag set)
// and the category also has that flag set, it never matches
if (request & this->flags & Flag::HIDE_ON_PRE_V3) {
return false;
}
return request & this->flags;
}
QuestCategoryIndex::QuestCategoryIndex(std::shared_ptr<const JSONObject> json) {
uint32_t next_category_id = 1;
for (const auto& it : json->as_list()) {
this->categories.emplace_back(next_category_id++, it);
}
}
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 {
return this->categories.at(category_id - 1);
}
// GCI decoding logic
template <bool IsBigEndian>
@@ -189,47 +230,6 @@ struct PSODownloadQuestHeader {
le_uint32_t encryption_seed;
} __attribute__((packed));
bool category_is_mode(QuestCategory category) {
return (category == QuestCategory::BATTLE) ||
(category == QuestCategory::CHALLENGE) ||
(category == QuestCategory::EPISODE_3);
}
const char* name_for_category(QuestCategory category) {
switch (category) {
case QuestCategory::RETRIEVAL:
return "Retrieval";
case QuestCategory::EXTERMINATION:
return "Extermination";
case QuestCategory::EVENT:
return "Event";
case QuestCategory::SHOP:
return "Shop";
case QuestCategory::VR:
return "VR";
case QuestCategory::TOWER:
return "Tower";
case QuestCategory::GOVERNMENT_EPISODE_1:
return "GovernmentEp1";
case QuestCategory::GOVERNMENT_EPISODE_2:
return "GovernmentEp2";
case QuestCategory::GOVERNMENT_EPISODE_4:
return "GovernmentEp4";
case QuestCategory::DOWNLOAD:
return "Download";
case QuestCategory::BATTLE:
return "Battle";
case QuestCategory::CHALLENGE:
return "Challenge";
case QuestCategory::SOLO:
return "Solo";
case QuestCategory::EPISODE_3:
return "Episode3";
default:
return "Unknown";
}
}
struct PSOQuestHeaderDC { // Same format for DC v1 and v2, thankfully
uint32_t start_offset;
uint32_t unknown_offset1;
@@ -288,10 +288,10 @@ struct PSOQuestHeaderBB {
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
Quest::Quest(const string& bin_filename)
Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> category_index)
: internal_id(-1),
menu_item_id(0),
category(QuestCategory::UNKNOWN),
category_id(0),
episode(Episode::NONE),
is_dcv1(false),
joinable(false),
@@ -345,49 +345,23 @@ Quest::Quest(const string& bin_filename)
throw invalid_argument("empty filename");
}
if (basename[0] == 'b') {
this->category = QuestCategory::BATTLE;
} else if (basename[0] == 'c') {
this->category = QuestCategory::CHALLENGE;
} else if (basename[0] == 'e') {
this->category = QuestCategory::EPISODE_3;
} else if (basename[0] != 'q') {
throw invalid_argument("filename does not indicate mode");
}
if (this->category != QuestCategory::EPISODE_3 && this->has_mnm_extension) {
throw invalid_argument("non-Ep3 quest has .mnm extension");
}
// If the quest category is still unknown, expect 3 tokens (one of them will
// tell us the category)
vector<string> tokens = split(basename, '-');
if (tokens.size() != (2 + (this->category == QuestCategory::UNKNOWN))) {
string category_token;
if (tokens.size() == 3) {
category_token = std::move(tokens[1]);
tokens.erase(tokens.begin() + 1);
} else if (tokens.size() != 2) {
throw invalid_argument("incorrect filename format");
}
auto& category = category_index->find(basename[0], category_token);
this->category_id = category.category_id;
// Parse the number out of the first token
this->internal_id = strtoull(tokens[0].c_str() + 1, nullptr, 10);
// Get the category from the second token if needed
if (this->category == QuestCategory::UNKNOWN) {
static const unordered_map<string, QuestCategory> name_to_category({
{"ret", QuestCategory::RETRIEVAL},
{"ext", QuestCategory::EXTERMINATION},
{"evt", QuestCategory::EVENT},
{"shp", QuestCategory::SHOP},
{"vr", QuestCategory::VR},
{"twr", QuestCategory::TOWER},
// Note: This will be overwritten later for Episode 2 & 4 quests - we
// haven't parsed the episode number from the quest script yet
{"gov", QuestCategory::GOVERNMENT_EPISODE_1},
{"dl", QuestCategory::DOWNLOAD},
{"1p", QuestCategory::SOLO},
});
this->category = name_to_category.at(tokens[1]);
tokens.erase(tokens.begin() + 1);
}
// 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},
@@ -439,7 +413,7 @@ Quest::Quest(const string& bin_filename)
case GameVersion::XB:
case GameVersion::GC: {
if (this->category == QuestCategory::EPISODE_3) {
if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) {
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
@@ -485,21 +459,16 @@ Quest::Quest(const string& bin_filename)
this->name = header->name;
this->short_description = header->short_description;
this->long_description = header->long_description;
if (this->category == QuestCategory::GOVERNMENT_EPISODE_1) {
if (this->episode == Episode::EP2) {
this->category = QuestCategory::GOVERNMENT_EPISODE_2;
} else if (this->episode == Episode::EP4) {
this->category = QuestCategory::GOVERNMENT_EPISODE_4;
} else if (this->episode != Episode::EP1) {
throw invalid_argument("government quest has invalid episode number");
}
}
break;
}
default:
throw logic_error("invalid quest game version");
}
if (this->has_mnm_extension && this->episode != Episode::EP3) {
throw runtime_error("non-Episode 3 quest has .mnm extension");
}
}
static string basename_for_filename(const string& filename) {
@@ -511,7 +480,7 @@ static string basename_for_filename(const string& filename) {
}
string Quest::bin_filename() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
return string_printf("m%06" PRId64 "p_e.bin", this->internal_id);
} else {
return basename_for_filename(this->file_basename + ".bin");
@@ -519,7 +488,7 @@ string Quest::bin_filename() const {
}
string Quest::dat_filename() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
} else {
return basename_for_filename(this->file_basename + ".dat");
@@ -563,7 +532,7 @@ shared_ptr<const string> Quest::bin_contents() const {
}
shared_ptr<const string> Quest::dat_contents() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
}
if (!this->dat_contents_ptr) {
@@ -916,12 +885,15 @@ void add_write_file_commands(
}
string Quest::export_qst(GameVersion version) const {
if (this->category == QuestCategory::EPISODE_3) {
throw runtime_error("Episode 3 quests cannot be encoded in QST format");
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");
}
StringWriter w;
// Some tools expect both open file commands at the beginning, hence this
// unfortunate abstraction-breaking.
switch (version) {
case GameVersion::DC:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, *this, true);
@@ -942,11 +914,15 @@ string Quest::export_qst(GameVersion version) const {
case GameVersion::GC:
case GameVersion::XB:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, true);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, false);
if (!is_ep3) {
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);
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_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);
}
break;
case GameVersion::BB:
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, *this, true);
@@ -963,12 +939,14 @@ string Quest::export_qst(GameVersion version) const {
return std::move(w.str());
}
QuestIndex::QuestIndex(const string& directory) : directory(directory) {
auto filename_set = list_directory(this->directory);
vector<string> filenames(filename_set.begin(), filename_set.end());
sort(filenames.begin(), filenames.end());
QuestIndex::QuestIndex(
const string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index)
: directory(directory),
category_index(category_index) {
uint32_t next_menu_item_id = 1;
for (const auto& filename : filenames) {
for (const auto& filename : list_directory_sorted(this->directory)) {
string full_path = this->directory + "/" + filename;
if (ends_with(filename, ".gba")) {
@@ -988,26 +966,26 @@ QuestIndex::QuestIndex(const string& directory) : directory(directory) {
ends_with(filename, ".mnm.dlq") ||
ends_with(filename, ".qst")) {
try {
shared_ptr<Quest> q(new Quest(full_path));
shared_ptr<Quest> q(new Quest(full_path, this->category_index));
q->menu_item_id = next_menu_item_id++;
string ascii_name = encode_sjis(q->name);
if (!this->version_menu_item_id_to_quest.emplace(
make_pair(q->version, q->menu_item_id), q)
.second) {
if (!this->version_menu_item_id_to_quest.emplace(make_pair(q->version, q->menu_item_id), q).second) {
throw logic_error("duplicate quest menu item id");
}
static_game_data_log.info("Indexed quest %s (%s => %s-%" PRId64 " (%" PRIu32 "), %s, %s, joinable=%s, dcv1=%s)",
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)",
ascii_name.c_str(),
filename.c_str(),
name_for_version(q->version),
q->internal_id,
q->menu_item_id,
name_for_category(q->category),
name_for_episode(q->episode),
category_name.c_str(),
q->category_id,
q->joinable ? "true" : "false",
q->is_dcv1 ? "true" : "false");
} catch (const exception& e) {
static_game_data_log.warning("Failed to parse quest file %s (%s)", filename.c_str(), e.what());
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
}
}
}
@@ -1023,14 +1001,14 @@ shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
}
vector<shared_ptr<const Quest>> QuestIndex::filter(
GameVersion version, bool is_dcv1, QuestCategory category) const {
GameVersion version, bool is_dcv1, 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 != category)) {
if ((q->is_dcv1 != is_dcv1) || (q->category_id != category_id)) {
continue;
}
ret.emplace_back(q);
@@ -1076,7 +1054,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->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests cannot be converted to download quests");
}
+36 -23
View File
@@ -10,26 +10,38 @@
#include "StaticGameData.hh"
#include "Version.hh"
enum class QuestCategory {
UNKNOWN = -1,
RETRIEVAL = 0,
EXTERMINATION,
EVENT,
SHOP,
VR,
TOWER,
GOVERNMENT_EPISODE_1,
GOVERNMENT_EPISODE_2,
GOVERNMENT_EPISODE_4,
DOWNLOAD,
BATTLE,
CHALLENGE,
SOLO,
EPISODE_3,
};
struct QuestCategoryIndex {
struct Category {
enum Flag {
NORMAL = 0x01,
BATTLE = 0x02,
CHALLENGE = 0x04,
SOLO = 0x08,
GOVERNMENT = 0x10,
DOWNLOAD = 0x20,
EP3_DOWNLOAD = 0x40,
HIDE_ON_PRE_V3 = 0x80,
};
bool category_is_mode(QuestCategory category);
const char* name_for_category(QuestCategory category);
uint32_t category_id;
uint8_t flags;
char type;
std::string short_token;
std::u16string name;
std::u16string description;
explicit Category(uint32_t category_id, std::shared_ptr<const JSONObject> json);
bool matches_flags(uint8_t request) const;
};
std::vector<Category> categories;
explicit QuestCategoryIndex(std::shared_ptr<const JSONObject> json);
const Category& find(char type, const std::string& short_token) const;
const Category& at(uint32_t category_id) const;
};
class Quest {
public:
@@ -43,7 +55,7 @@ public:
};
int64_t internal_id;
uint32_t menu_item_id;
QuestCategory category;
uint32_t category_id;
Episode episode;
bool is_dcv1;
bool joinable;
@@ -56,7 +68,7 @@ public:
std::u16string short_description;
std::u16string long_description;
Quest(const std::string& file_basename);
Quest(const std::string& file_basename, std::shared_ptr<const QuestCategoryIndex> category_index);
Quest(const Quest&) = default;
Quest(Quest&&) = default;
Quest& operator=(const Quest&) = default;
@@ -91,6 +103,7 @@ private:
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;
@@ -98,10 +111,10 @@ struct QuestIndex {
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
QuestIndex(const std::string& directory);
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 std::string> get_gba(const std::string& name) const;
std::vector<std::shared_ptr<const Quest>> filter(GameVersion version,
bool is_dcv1, QuestCategory category) const;
bool is_dcv1, uint32_t category_id) const;
};
+26 -51
View File
@@ -30,43 +30,6 @@ const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier";
const char* CARD_AUCTION_DISCONNECT_HOOK_NAME = "card_auction";
const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client";
vector<MenuItem> quest_categories_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
});
vector<MenuItem> quest_battle_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::BATTLE), u"Battle", u"$E$C6Battle mode rule\nsets", 0),
});
vector<MenuItem> quest_challenge_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::CHALLENGE), u"Challenge", u"$E$C6Challenge mode\nquests", 0),
});
vector<MenuItem> quest_solo_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::SOLO), u"Solo Quests", u"$E$C6Quests that require\na single player", 0),
});
vector<MenuItem> quest_government_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_1), u"Hero in Red", u"$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_2), u"The Military's Hero", u"$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_4), u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0),
});
vector<MenuItem> quest_download_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::DOWNLOAD), u"Download", u"$E$C6Quests to download\nto your Memory Card", 0),
});
static shared_ptr<const Menu> proxy_options_menu_for_client(
shared_ptr<ServerState> s, shared_ptr<const Client> c) {
shared_ptr<Menu> ret(new Menu(MenuID::PROXY_OPTIONS, u"Proxy options"));
@@ -1714,16 +1677,25 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
case MainMenuItemID::DOWNLOAD_QUESTS:
if (c->flags & Client::Flag::IS_EPISODE_3) {
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, QuestCategory::EPISODE_3);
// Episode 3 has only download quests, not online quests, so this is
// always the download quest menu. (Episode 3 does actually have
// online quests, but they're served via a server data request
// instead of the file download paradigm that other versions use.)
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);
break;
}
}
send_quest_menu(c, MenuID::QUEST, quests, true);
} else {
send_quest_menu(c, MenuID::QUEST_FILTER, quest_download_menu, true);
uint8_t flags = QuestCategoryIndex::Category::Flag::DOWNLOAD;
if (c->version() == GameVersion::DC || c->version() == GameVersion::PC) {
flags |= QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3;
}
send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags);
}
break;
@@ -1954,9 +1926,8 @@ 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,
static_cast<QuestCategory>(item_id & 0xFF));
auto quests = s->quest_index->filter(
c->version(), c->flags & Client::Flag::IS_DC_V1, item_id);
// Hack: Assume the menu to be sent is the download quest menu if the
// client is not in any lobby
@@ -2330,29 +2301,33 @@ static void on_A2(shared_ptr<ServerState> s, shared_ptr<Client> c,
send_lobby_message_box(c, u"$C6Episode 3 does not\nprovide online quests\nvia this interface.");
} else {
vector<MenuItem>* menu = nullptr;
uint8_t flags = (c->version() == GameVersion::DC || c->version() == GameVersion::PC)
? QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3
: 0;
if ((c->version() == GameVersion::BB) && flag) {
menu = &quest_government_menu;
flags |= QuestCategoryIndex::Category::Flag::GOVERNMENT;
} else {
switch (l->mode) {
case GameMode::NORMAL:
menu = &quest_categories_menu;
flags |= QuestCategoryIndex::Category::Flag::NORMAL;
break;
case GameMode::BATTLE:
menu = &quest_battle_menu;
flags |= QuestCategoryIndex::Category::Flag::BATTLE;
break;
case GameMode::CHALLENGE:
menu = &quest_challenge_menu;
flags |= QuestCategoryIndex::Category::Flag::CHALLENGE;
break;
case GameMode::SOLO:
menu = &quest_solo_menu;
flags |= QuestCategoryIndex::Category::Flag::SOLO;
break;
default:
throw logic_error("invalid game mode");
}
}
send_quest_menu(c, MenuID::QUEST_FILTER, *menu, false);
send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags);
}
}
+15 -11
View File
@@ -1254,15 +1254,19 @@ template <typename EntryT>
void send_quest_menu_t(
shared_ptr<Client> c,
uint32_t menu_id,
const vector<MenuItem>& items,
bool is_download_menu) {
shared_ptr<const QuestCategoryIndex> category_index,
uint8_t flags) {
bool is_download_menu = flags & (QuestCategoryIndex::Category::Flag::DOWNLOAD | QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD);
vector<EntryT> entries;
for (const auto& item : items) {
for (const auto& category : category_index->categories) {
if (!category.matches_flags(flags)) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.item_id = item.item_id;
e.name = item.name;
e.short_description = item.description;
e.item_id = category.category_id;
e.name = category.name;
e.short_description = category.description;
add_color_inplace(e.short_description);
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
@@ -1290,20 +1294,20 @@ void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
}
void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
const vector<MenuItem>& items, bool is_download_menu) {
shared_ptr<const QuestCategoryIndex> category_index, uint8_t flags) {
switch (c->version()) {
case GameVersion::PC:
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::DC:
case GameVersion::GC:
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::XB:
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::BB:
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, category_index, flags);
break;
default:
throw logic_error("unimplemented versioned command");
+1 -1
View File
@@ -258,7 +258,7 @@ void send_game_menu(
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
const std::vector<std::shared_ptr<const Quest>>& quests, bool is_download_menu);
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
const std::vector<MenuItem>& items, bool is_download_menu);
std::shared_ptr<const QuestCategoryIndex> category_index, uint8_t flags);
void send_lobby_list(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
void send_join_lobby(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
+8 -1
View File
@@ -751,6 +751,13 @@ void ServerState::parse_config(shared_ptr<const JSONObject> config_json) {
this->ep3_menu_song = d.at("Episode3MenuSong")->as_int();
} catch (const out_of_range&) {
}
try {
this->quest_category_index.reset(new QuestCategoryIndex(d.at("QuestCategories")));
} catch (const exception& e) {
throw runtime_error(string_printf(
"QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what()));
}
}
void ServerState::load_licenses() {
@@ -857,7 +864,7 @@ void ServerState::load_ep3_data() {
void ServerState::load_quest_index() {
config_log.info("Collecting quest metadata");
this->quest_index.reset(new QuestIndex("system/quests"));
this->quest_index.reset(new QuestIndex("system/quests", this->quest_category_index));
}
void ServerState::compile_functions() {
+1
View File
@@ -65,6 +65,7 @@ struct ServerState {
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
std::shared_ptr<const DOLFileIndex> dol_file_index;
std::shared_ptr<const Episode3::DataIndex> ep3_data_index;
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTable> level_table;
std::shared_ptr<const BattleParamsIndex> battle_params;