implement quest unlock flags

This commit is contained in:
Martin Michelsen
2023-11-29 22:22:19 -08:00
parent 3d2d96eb7e
commit 3743d0a156
75 changed files with 564 additions and 306 deletions
+1 -1
View File
@@ -4626,7 +4626,7 @@ struct G_SyncFlagState_6x6E_Decompressed {
struct G_SetQuestFlags_6x6F {
G_UnusedHeader header;
parray<parray<uint8_t, 0x80>, 4> quest_flags_by_difficulty;
QuestFlags quest_flags;
} __packed__;
// 6x70: Sync player disp data and inventory (used while loading into game)
+15 -14
View File
@@ -13,21 +13,22 @@ class LicenseIndex;
struct License {
enum Flag : uint32_t {
// clang-format off
KICK_USER = 0x00000001,
BAN_USER = 0x00000002,
SILENCE_USER = 0x00000004,
CHANGE_LOBBY_INFO = 0x00000008,
CHANGE_EVENT = 0x00000010,
ANNOUNCE = 0x00000020,
FREE_JOIN_GAMES = 0x00000040,
UNLOCK_GAMES = 0x00000080,
DEBUG = 0x01000000,
CHEAT_ANYWHERE = 0x02000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x000000FF,
ROOT = 0x7FFFFFFF,
KICK_USER = 0x00000001,
BAN_USER = 0x00000002,
SILENCE_USER = 0x00000004,
CHANGE_LOBBY_INFO = 0x00000008,
CHANGE_EVENT = 0x00000010,
ANNOUNCE = 0x00000020,
FREE_JOIN_GAMES = 0x00000040,
UNLOCK_GAMES = 0x00000080,
DEBUG = 0x01000000,
CHEAT_ANYWHERE = 0x02000000,
DISABLE_QUEST_REQUIREMENTS = 0x04000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x000000FF,
ROOT = 0x7FFFFFFF,
UNUSED_BITS = 0x7CFFFF00,
UNUSED_BITS = 0x78FFFF00,
// clang-format on
};
+27
View File
@@ -401,3 +401,30 @@ unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() co
}
return ret;
}
QuestIndex::IncludeCondition Lobby::quest_include_condition() const {
return [this](shared_ptr<const Quest> q) -> bool {
if (q->require_flag >= 0) {
for (const auto& lc : this->clients) {
if (lc && !lc->game_data.character()->quest_flags.get(this->difficulty, q->require_flag)) {
return false;
}
}
}
if (!q->require_team_reward_key.empty()) {
// TODO: Technically we should check that all clients are on the SAME
// team, but I'm lazy (for now), so we allow clients on different teams
// to access the quest as long as all their teams have the quest reward.
for (const auto& lc : this->clients) {
if (!lc) {
continue;
}
auto team = lc->team();
if (!team || !team->has_reward(q->require_team_reward_key)) {
return false;
}
}
}
return true;
};
}
+2
View File
@@ -192,6 +192,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
void on_item_id_generated_externally(uint32_t item_id);
void assign_inventory_item_ids(std::shared_ptr<Client> c);
QuestIndex::IncludeCondition quest_include_condition() const;
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
std::unordered_map<uint32_t, std::shared_ptr<Client>> clients_by_serial_number() const;
+46 -46
View File
@@ -686,59 +686,59 @@ size_t PlayerBank::find_item(uint32_t item_id) {
BattleRules::BattleRules(const JSON& json) {
static const JSON empty_list = JSON::list();
this->tech_disk_mode = json.get_enum("tech_disk_mode", this->tech_disk_mode);
this->weapon_and_armor_mode = json.get_enum("weapon_and_armor_mode", this->weapon_and_armor_mode);
this->mag_mode = json.get_enum("mag_mode", this->mag_mode);
this->tool_mode = json.get_enum("tool_mode", this->tool_mode);
this->trap_mode = json.get_enum("trap_mode", this->trap_mode);
this->unused_F817 = json.get_int("unused_F817", this->unused_F817);
this->respawn_mode = json.get_int("respawn_mode", this->respawn_mode);
this->replace_char = json.get_int("replace_char", this->replace_char);
this->drop_weapon = json.get_int("drop_weapon", this->drop_weapon);
this->is_teams = json.get_int("is_teams", this->is_teams);
this->hide_target_reticle = json.get_int("hide_target_reticle", this->hide_target_reticle);
this->meseta_mode = json.get_enum("meseta_mode", this->meseta_mode);
this->death_level_up = json.get_int("death_level_up", this->death_level_up);
const JSON& trap_counts_json = json.get("trap_counts", empty_list);
this->tech_disk_mode = json.get_enum("TechDiskMode", this->tech_disk_mode);
this->weapon_and_armor_mode = json.get_enum("WeaponAndArmorMode", this->weapon_and_armor_mode);
this->mag_mode = json.get_enum("MagMode", this->mag_mode);
this->tool_mode = json.get_enum("ToolMode", this->tool_mode);
this->trap_mode = json.get_enum("TrapMode", this->trap_mode);
this->unused_F817 = json.get_int("UnusedF817", this->unused_F817);
this->respawn_mode = json.get_int("RespawnMode", this->respawn_mode);
this->replace_char = json.get_int("ReplaceChar", this->replace_char);
this->drop_weapon = json.get_int("DropWeapon", this->drop_weapon);
this->is_teams = json.get_int("IsTeams", this->is_teams);
this->hide_target_reticle = json.get_int("HideTargetReticle", this->hide_target_reticle);
this->meseta_mode = json.get_enum("MesetaMode", this->meseta_mode);
this->death_level_up = json.get_int("DeathLevelUp", this->death_level_up);
const JSON& trap_counts_json = json.get("TrapCounts", empty_list);
for (size_t z = 0; z < trap_counts_json.size(); z++) {
this->trap_counts[z] = trap_counts_json.at(z).as_int();
}
this->enable_sonar = json.get_int("enable_sonar", this->enable_sonar);
this->sonar_count = json.get_int("sonar_count", this->sonar_count);
this->forbid_scape_dolls = json.get_int("forbid_scape_dolls", this->forbid_scape_dolls);
this->lives = json.get_int("lives", this->lives);
this->max_tech_level = json.get_int("max_tech_level", this->max_tech_level);
this->char_level = json.get_int("char_level", this->char_level);
this->time_limit = json.get_int("time_limit", this->time_limit);
this->death_tech_level_up = json.get_int("death_tech_level_up", this->death_tech_level_up);
this->box_drop_area = json.get_int("box_drop_area", this->box_drop_area);
this->enable_sonar = json.get_int("EnableSonar", this->enable_sonar);
this->sonar_count = json.get_int("SonarCount", this->sonar_count);
this->forbid_scape_dolls = json.get_int("ForbidScapeDolls", this->forbid_scape_dolls);
this->lives = json.get_int("Lives", this->lives);
this->max_tech_level = json.get_int("MaxTechLevel", this->max_tech_level);
this->char_level = json.get_int("CharLevel", this->char_level);
this->time_limit = json.get_int("TimeLimit", this->time_limit);
this->death_tech_level_up = json.get_int("DeathTechLevelUp", this->death_tech_level_up);
this->box_drop_area = json.get_int("BoxDropArea", this->box_drop_area);
}
JSON BattleRules::json() const {
return JSON::dict({
{"tech_disk_mode", this->tech_disk_mode},
{"weapon_and_armor_mode", this->weapon_and_armor_mode},
{"mag_mode", this->mag_mode},
{"tool_mode", this->tool_mode},
{"trap_mode", this->trap_mode},
{"unused_F817", this->unused_F817},
{"respawn_mode", this->respawn_mode},
{"replace_char", this->replace_char},
{"drop_weapon", this->drop_weapon},
{"is_teams", this->is_teams},
{"hide_target_reticle", this->hide_target_reticle},
{"meseta_mode", this->meseta_mode},
{"death_level_up", this->death_level_up},
{"trap_counts", JSON::list({this->trap_counts[0], this->trap_counts[1], this->trap_counts[2], this->trap_counts[3]})},
{"enable_sonar", this->enable_sonar},
{"sonar_count", this->sonar_count},
{"forbid_scape_dolls", this->forbid_scape_dolls},
{"lives", this->lives.load()},
{"max_tech_level", this->max_tech_level.load()},
{"char_level", this->char_level.load()},
{"time_limit", this->time_limit.load()},
{"death_tech_level_up", this->death_tech_level_up.load()},
{"box_drop_area", this->box_drop_area.load()},
{"TechDiskMode", this->tech_disk_mode},
{"WeaponAndArmorMode", this->weapon_and_armor_mode},
{"MagMode", this->mag_mode},
{"ToolMode", this->tool_mode},
{"TrapMode", this->trap_mode},
{"UnusedF817", this->unused_F817},
{"RespawnMode", this->respawn_mode},
{"ReplaceChar", this->replace_char},
{"DropWeapon", this->drop_weapon},
{"IsTeams", this->is_teams},
{"HideTargetReticle", this->hide_target_reticle},
{"MesetaMode", this->meseta_mode},
{"DeathLevelUp", this->death_level_up},
{"TrapCounts", JSON::list({this->trap_counts[0], this->trap_counts[1], this->trap_counts[2], this->trap_counts[3]})},
{"EnableSonar", this->enable_sonar},
{"SonarCount", this->sonar_count},
{"ForbidScapeDolls", this->forbid_scape_dolls},
{"Lives", this->lives.load()},
{"MaxTechLevel", this->max_tech_level.load()},
{"CharLevel", this->char_level.load()},
{"TimeLimit", this->time_limit.load()},
{"DeathTechLevelUp", this->death_tech_level_up.load()},
{"BoxDropArea", this->box_drop_area.load()},
});
}
+34
View File
@@ -483,6 +483,40 @@ inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
return src;
}
struct QuestFlagsForDifficulty {
parray<uint8_t, 0x80> data;
inline bool get(uint16_t flag_index) const {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
return !!(this->data[byte_index] & mask);
}
inline void set(uint16_t flag_index) {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
this->data[byte_index] |= mask;
}
inline void clear(uint16_t flag_index) {
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
this->data[byte_index] &= (~mask);
}
} __attribute__((packed));
struct QuestFlags {
parray<QuestFlagsForDifficulty, 4> data;
inline bool get(uint8_t difficulty, uint16_t flag_index) const {
return this->data[difficulty].get(flag_index);
}
inline void set(uint8_t difficulty, uint16_t flag_index) {
this->data[difficulty].set(flag_index);
}
inline void clear(uint8_t difficulty, uint16_t flag_index) {
this->data[difficulty].clear(flag_index);
}
} __attribute__((packed));
struct BattleRules {
enum class TechDiskMode : uint8_t {
ALLOW = 0,
+56 -32
View File
@@ -204,7 +204,9 @@ VersionedQuest::VersionedQuest(
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules,
ssize_t challenge_template_index)
ssize_t challenge_template_index,
int16_t require_flag,
const string& require_team_reward_key)
: quest_number(quest_number),
category_id(category_id),
episode(Episode::NONE),
@@ -216,7 +218,9 @@ VersionedQuest::VersionedQuest(
dat_contents(dat_contents),
pvr_contents(pvr_contents),
battle_rules(battle_rules),
challenge_template_index(challenge_template_index) {
challenge_template_index(challenge_template_index),
require_flag(require_flag),
require_team_reward_key(require_team_reward_key) {
auto bin_decompressed = prs_decompress(*this->bin_contents);
@@ -373,7 +377,9 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
joinable(initial_version->joinable),
name(initial_version->name),
battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index) {
challenge_template_index(initial_version->challenge_template_index),
require_flag(initial_version->require_flag),
require_team_reward_key(initial_version->require_team_reward_key) {
this->versions.emplace(this->versions_key(initial_version->version, initial_version->language), initial_version);
}
@@ -403,6 +409,12 @@ void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error("quest version has different challenge template index");
}
if (this->require_flag != vq->require_flag) {
throw runtime_error("quest version has different required flag");
}
if (this->require_team_reward_key != vq->require_team_reward_key) {
throw runtime_error("quest version has different required team reward key");
}
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
}
@@ -627,6 +639,8 @@ QuestIndex::QuestIndex(
JSON metadata_json = nullptr;
shared_ptr<BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
int16_t require_flag = -1;
string require_team_reward_key;
try {
json_filename = basename;
metadata_json = JSON::parse(*json_files.at(json_filename));
@@ -644,11 +658,16 @@ QuestIndex::QuestIndex(
}
if (!metadata_json.is_null()) {
try {
battle_rules.reset(new BattleRules(metadata_json.at("battle_rules")));
battle_rules.reset(new BattleRules(metadata_json.at("BattleRules")));
} catch (const out_of_range&) {
}
try {
challenge_template_index = metadata_json.at("challenge_template_index").as_int();
challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
} catch (const out_of_range&) {
}
require_flag = metadata_json.get_int("RequireFlag", -1);
try {
require_team_reward_key = metadata_json.get_int("RequireTeamRewardKey", -1);
} catch (const out_of_range&) {
}
}
@@ -662,7 +681,9 @@ QuestIndex::QuestIndex(
dat_contents,
pvr_contents,
battle_rules,
challenge_template_index));
challenge_template_index,
require_flag,
require_team_reward_key));
auto category_name = this->category_index->at(vq->category_id)->name;
string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat");
@@ -681,7 +702,9 @@ QuestIndex::QuestIndex(
battle_rules_str.c_str(),
challenge_template_str.c_str());
} else {
this->quests_by_number.emplace(vq->quest_number, new Quest(vq));
shared_ptr<Quest> q(new Quest(vq));
this->quests_by_number.emplace(vq->quest_number, q);
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
static_game_data_log.info("(%s) Created %s %c quest %" PRIu32 " (%s) (%s, %s (%" PRIu32 "), %s)%s%s%s",
basename.c_str(),
name_for_enum(vq->version),
@@ -710,47 +733,48 @@ shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
}
}
const vector<shared_ptr<const QuestCategoryIndex::Category>>& QuestIndex::categories(
QuestMenuType menu_type, Episode episode, Version version) const {
vector<shared_ptr<const QuestCategoryIndex::Category>> QuestIndex::categories(
QuestMenuType menu_type,
Episode episode,
Version version,
function<bool(std::shared_ptr<const Quest>)> include_condition) const {
// The episode filter should apply in normal or solo mode
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE;
}
uint64_t key = (static_cast<uint32_t>(menu_type) << 20) | (static_cast<uint32_t>(episode) << 16) | static_cast<uint32_t>(version);
try {
return this->category_filter_results_cache.at(key);
} catch (const out_of_range&) {
auto& ret = this->category_filter_results_cache[key];
for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(menu_type, episode, version, cat->category_id).empty()) {
ret.emplace_back(cat);
}
vector<shared_ptr<const QuestCategoryIndex::Category>> ret;
for (const auto& cat : this->category_index->categories) {
if (cat->check_flag(menu_type) && !this->filter(menu_type, episode, version, cat->category_id, include_condition, 1).empty()) {
ret.emplace_back(cat);
}
return ret;
}
return ret;
}
const vector<shared_ptr<const Quest>>& QuestIndex::filter(
QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const {
vector<shared_ptr<const Quest>> QuestIndex::filter(
QuestMenuType menu_type,
Episode episode,
Version version,
uint32_t category_id,
function<bool(std::shared_ptr<const Quest>)> include_condition,
size_t limit) const {
if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) {
episode = Episode::NONE;
}
uint64_t key = (static_cast<uint64_t>(episode) << 48) | (static_cast<uint64_t>(version) << 32) | category_id;
try {
return this->quest_filter_results_cache.at(key);
} catch (const out_of_range&) {
vector<shared_ptr<const Quest>>& ret = this->quest_filter_results_cache[key];
for (auto it : this->quests_by_number) {
if (((episode == Episode::NONE) || (it.second->episode == episode)) &&
(it.second->category_id == category_id) &&
it.second->has_version_any_language(version)) {
ret.emplace_back(it.second);
vector<shared_ptr<const Quest>> ret;
for (auto it : this->quests_by_category_id_and_number.at(category_id)) {
if (((episode == Episode::NONE) || (it.second->episode == episode)) &&
it.second->has_version_any_language(version) &&
(!include_condition || include_condition(it.second))) {
ret.emplace_back(it.second);
if (limit && (ret.size() >= limit)) {
break;
}
}
return ret;
}
return ret;
}
string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) {
+23 -8
View File
@@ -11,6 +11,7 @@
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
enum class QuestFileFormat {
BIN_DAT = 0,
@@ -69,6 +70,8 @@ struct VersionedQuest {
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
int16_t require_flag; // <0 = none
std::string require_team_reward_key;
VersionedQuest(
uint32_t quest_number,
@@ -79,7 +82,9 @@ struct VersionedQuest {
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const std::string> pvr_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1);
ssize_t challenge_template_index = -1,
int16_t require_flag = -1,
const std::string& require_team_reward_key = "");
std::string bin_filename() const;
std::string dat_filename() const;
@@ -112,26 +117,36 @@ public:
std::string name;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
int16_t require_flag;
std::string require_team_reward_key;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
};
struct QuestIndex {
using IncludeCondition = std::function<bool(std::shared_ptr<const Quest>)>;
std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index;
std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
mutable std::unordered_map<uint32_t, std::vector<std::shared_ptr<const QuestCategoryIndex::Category>>> category_filter_results_cache;
mutable std::unordered_map<uint64_t, std::vector<std::shared_ptr<const Quest>>> quest_filter_results_cache;
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
const std::vector<std::shared_ptr<const QuestCategoryIndex::Category>>& categories(
QuestMenuType menu_type, Episode episode, Version version) const;
const std::vector<std::shared_ptr<const Quest>>& filter(
QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const;
std::vector<std::shared_ptr<const QuestCategoryIndex::Category>> categories(
QuestMenuType menu_type,
Episode episode,
Version version,
IncludeCondition include_condition = nullptr) const;
std::vector<std::shared_ptr<const Quest>> filter(
QuestMenuType menu_type,
Episode episode,
Version version,
uint32_t category_id,
IncludeCondition include_condition = nullptr,
size_t limit = 0) const;
};
std::string encode_download_quest_data(
+9 -2
View File
@@ -2306,6 +2306,7 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
shared_ptr<Lobby> l = c->lobby.lock();
Episode episode = l ? l->episode : Episode::NONE;
QuestMenuType menu_type = QuestMenuType::NORMAL;
QuestIndex::IncludeCondition include_condition = nullptr;
if (!l) {
// Assume the menu to be sent is the download quest menu if the client
// is not in any lobby
@@ -2324,9 +2325,12 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
break;
}
}
if (!(c->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
include_condition = l->quest_include_condition();
}
}
const auto& quests = quest_index->filter(menu_type, episode, c->version(), item_id);
const auto& quests = quest_index->filter(menu_type, episode, c->version(), item_id, include_condition);
send_quest_menu(c, MenuID::QUEST, quests, !l);
break;
}
@@ -4145,10 +4149,13 @@ static void on_6F(shared_ptr<Client> c, uint16_t command, uint32_t, string& data
}
c->config.clear_flag(Client::Flag::LOADING);
send_server_time(c);
if (l->base_version == Version::BB_V4) {
send_set_exp_multiplier(l);
}
send_server_time(c);
if (c->version() == Version::BB_V4) {
send_all_nearby_team_metadatas_to_client(c, false);
}
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
string variations_str;
+5 -7
View File
@@ -691,9 +691,9 @@ static void on_set_player_visible(shared_ptr<Client> c, uint8_t command, uint8_t
send_message_box(c, "$C6All lobbies are full.\n\n$C7You are in a private lobby. You can use the\nteleporter to join other lobbies if there is space\navailable.");
send_lobby_message_box(c, "");
}
}
if (c->version() == Version::BB_V4) {
send_all_nearby_team_metadatas_to_client(c, false);
if (c->version() == Version::BB_V4) {
send_all_nearby_team_metadatas_to_client(c, false);
}
}
}
}
@@ -1741,12 +1741,10 @@ static void on_set_quest_flag(shared_ptr<Client> c, uint8_t command, uint8_t fla
// The client explicitly checks for both 0 and 1 - any other value means no
// operation is performed.
size_t byte_index = flag_index >> 3;
uint8_t mask = 0x80 >> (flag_index & 7);
if (action == 0) {
p->quest_flags[difficulty][byte_index] |= mask;
p->quest_flags.set(difficulty, flag_index);
} else if (action == 1) {
p->quest_flags[difficulty][byte_index] &= (~mask);
p->quest_flags.clear(difficulty, flag_index);
}
forward_subcommand(c, command, flag, data, size);
+3 -4
View File
@@ -195,7 +195,7 @@ struct PSOBBCharacterFile {
/* 04E8 */ le_uint32_t play_time_seconds = 0;
/* 04EC */ le_uint32_t option_flags = 0x00040058;
/* 04F0 */ parray<uint8_t, 4> unknown_a2;
/* 04F4 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 04F4 */ QuestFlags quest_flags;
/* 06F4 */ le_uint32_t death_count = 0;
/* 06F8 */ PlayerBank bank;
/* 19C0 */ GuildCardBB guild_card;
@@ -343,8 +343,7 @@ struct PSOGCCharacterFile {
/* 0430:0014 */ be_uint32_t save_count;
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a4;
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a5;
// 1024 bits (flags) per difficulty
/* 0460:0044 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ be_uint32_t death_count;
/* 0664:0248 */ PlayerBank bank;
/* 192C:1510 */ GuildCardGC guild_card;
@@ -761,7 +760,7 @@ struct LegacySavedPlayerDataBB { // .nsc file format
/* 185C */ pstring<TextEncoding::UTF16, 0x00AC> info_board;
/* 19B4 */ PlayerInventory inventory;
/* 1D00 */ parray<uint8_t, 4> unknown_a2;
/* 1D04 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 1D04 */ QuestFlags quest_flags;
/* 1F04 */ le_uint32_t death_count;
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_global_flags;
/* 1F60 */ parray<le_uint16_t, 0x0014> tech_menu_config;
+7 -1
View File
@@ -1440,8 +1440,14 @@ void send_quest_categories_menu_t(
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
QuestIndex::IncludeCondition include_condition = nullptr;
if (!(c->license->flags & License::Flag::DISABLE_QUEST_REQUIREMENTS)) {
auto l = c->lobby.lock();
include_condition = l ? l->quest_include_condition() : nullptr;
}
vector<EntryT> entries;
for (const auto& cat : quest_index->categories(menu_type, episode, c->version())) {
for (const auto& cat : quest_index->categories(menu_type, episode, c->version(), include_condition)) {
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.item_id = cat->category_id;