rewrite quest metadata indexing

- split ep3 download quests from quest index
- fix Ep3 NTE download quests
- automatically detect battle/challenge params and area remaps
This commit is contained in:
Martin Michelsen
2025-09-28 10:15:14 -07:00
parent 48c225366f
commit fdd0bfea08
248 changed files with 1944 additions and 1543 deletions
+15 -9
View File
@@ -2074,8 +2074,7 @@ ChatCommandDefinition cc_quest(
a.check_is_game(true);
auto s = a.c->require_server_state();
Version effective_version = is_ep3(a.c->version()) ? Version::GC_V3 : a.c->version();
auto q = s->quest_index(effective_version)->get(stoul(a.text));
auto q = s->quest_index->get(stoul(a.text));
if (!q) {
throw precondition_failed("$C6Quest not found");
}
@@ -2085,11 +2084,20 @@ ChatCommandDefinition cc_quest(
if (l->count_clients() > 1) {
throw precondition_failed("$C6This command can only\nbe used with no\nother players present");
}
if (!q->allow_start_from_chat_command) {
if (!q->meta.allow_start_from_chat_command) {
throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command");
}
}
for (size_t client_id = 0; client_id < l->max_clients; client_id++) {
auto lc = l->clients[client_id];
if (lc) {
if (!q->version(lc->version(), lc->language())) {
throw precondition_failed("$C6Quest does not exist\nfor all players\' game\nversions");
}
}
}
set_lobby_quest(a.c->require_lobby(), q, true);
co_return;
});
@@ -2365,8 +2373,9 @@ ChatCommandDefinition cc_sound(
bool echo_to_all = (!a.text.empty() && a.text[0] == '!');
uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16);
// TODO: Using floor is technically incorrect here; it should be area
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, static_cast<uint8_t>(a.c->floor), 0x00, a.c->lobby_client_id, sound_id};
auto l = a.c->require_lobby();
uint8_t area = l->area_for_floor(a.c->version(), a.c->floor);
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id};
if (!echo_to_all) {
send_command_t(a.c, 0x60, 0x00, cmd);
} else if (a.c->proxy_session) {
@@ -2812,13 +2821,10 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en
throw precondition_failed("$C4No map loaded");
}
// TODO: We should use the actual area if a loaded quest has reassigned
// them; it's likely that the variations will be wrong if we don't
uint8_t area, layout_var;
auto s = a.c->require_server_state();
if (l->episode != Episode::EP3) {
auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty);
area = sdt->default_area_for_floor(l->episode, a.c->floor);
area = l->area_for_floor(a.c->version(), a.c->floor);
layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00;
} else {
area = a.c->floor;
+5 -3
View File
@@ -410,7 +410,8 @@ bool Client::can_see_quest(
if (!q->has_version_any_language(this->version())) {
return false;
}
return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present);
return this->evaluate_quest_availability_expression(
q->meta.available_expression, game, event, difficulty, num_players, v1_present);
}
bool Client::can_play_quest(
@@ -423,10 +424,11 @@ bool Client::can_play_quest(
if (!q->has_version_any_language(this->version())) {
return false;
}
if (num_players > q->max_players) {
if (num_players > q->meta.max_players) {
return false;
}
return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present);
return this->evaluate_quest_availability_expression(
q->meta.enabled_expression, game, event, difficulty, num_players, v1_present);
}
bool Client::can_use_chat_commands() const {
+23 -28
View File
@@ -2626,8 +2626,8 @@ MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
: language(language),
compressed_data(std::move(compressed_data)) {
string decompressed = prs_decompress(this->compressed_data);
compressed_data(make_shared<string>(std::move(compressed_data))) {
string decompressed = prs_decompress(*this->compressed_data);
if (decompressed.size() == sizeof(MapDefinitionTrial)) {
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
} else if (decompressed.size() == sizeof(MapDefinition)) {
@@ -2646,21 +2646,30 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
return this->trial_map;
}
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const {
if (is_nte) {
if (this->compressed_trial_data.empty()) {
std::shared_ptr<const std::string> MapIndex::VersionedMap::compressed(bool trial) const {
if (trial) {
if (!this->compressed_data_trial) {
auto md = this->trial();
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
this->compressed_data_trial = make_shared<string>(prs_compress(md.get(), sizeof(*md)));
}
return this->compressed_trial_data;
return this->compressed_data_trial;
} else {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
if (!this->compressed_data) {
this->compressed_data = make_shared<string>(prs_compress(this->map.get(), sizeof(*this->map)));
}
return this->compressed_data;
}
}
std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() const {
if (!this->download_data_trial) {
MapDefinitionTrial trial_map = *this->map;
trial_map.tag = 0x96;
this->download_data_trial = make_shared<string>(prs_compress(&trial_map, sizeof(trial_map)));
}
return this->download_data_trial;
}
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
: map_number(initial_version->map->map_number),
initial_version(initial_version) {
@@ -2704,6 +2713,7 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
}
MapIndex::MapIndex(const string& directory) {
map<uint32_t, shared_ptr<Map>> mutable_maps;
for (const auto& item : std::filesystem::directory_iterator(directory)) {
string filename = item.path().filename().string();
try {
@@ -2756,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) {
}
string name = vm->map->name.decode(vm->language);
auto map_it = this->maps.find(vm->map->map_number);
if (map_it == this->maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
auto map_it = mutable_maps.find(vm->map->map_number);
if (map_it == mutable_maps.end()) {
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
this->maps.emplace(vm->map->map_number, map_it->second);
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
filename,
vm->map->map_number,
@@ -2866,22 +2877,6 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
return compressed_map_list;
}
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
return this->maps.at(id);
}
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
return this->maps_by_name.at(name);
}
set<uint32_t> MapIndex::all_numbers() const {
set<uint32_t> ret;
for (const auto& it : this->maps) {
ret.emplace(it.first);
}
return ret;
}
COMDeckIndex::COMDeckIndex(const string& filename) {
try {
auto json = phosg::JSON::parse(phosg::load_file(filename));
+17 -8
View File
@@ -1179,7 +1179,8 @@ struct OverlayState {
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
// If tag is not 0x00000100, the game considers the map to be corrupt in
// offline mode and will delete it (if it's a download quest). The tag field
// doesn't seem to have any other use.
// doesn't seem to have any other use. In Trial Edition, download quests are
// expected to have 0x96 here instead.
/* 0000 */ be_uint32_t tag;
/* 0004 */ be_uint32_t map_number; // Must be unique across all maps
@@ -1597,12 +1598,14 @@ public:
VersionedMap(std::string&& compressed_data, uint8_t language);
std::shared_ptr<const MapDefinitionTrial> trial() const;
const std::string& compressed(bool is_nte) const;
std::shared_ptr<const std::string> compressed(bool trial) const;
std::shared_ptr<const std::string> trial_download() const;
private:
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
mutable std::string compressed_data;
mutable std::string compressed_trial_data;
mutable std::shared_ptr<std::string> compressed_data;
mutable std::shared_ptr<std::string> compressed_data_trial;
mutable std::shared_ptr<std::string> download_data_trial;
};
class Map {
@@ -1624,14 +1627,20 @@ public:
};
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
std::shared_ptr<const Map> for_number(uint32_t id) const;
std::shared_ptr<const Map> for_name(const std::string& name) const;
std::set<uint32_t> all_numbers() const;
inline std::shared_ptr<const Map> get(uint32_t id) const {
return this->maps.at(id);
}
inline std::shared_ptr<const Map> get(const std::string& name) const {
return this->maps_by_name.at(name);
}
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all() const {
return this->maps;
}
private:
// The compressed map lists are generated on demand from the maps map below
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
std::map<uint32_t, std::shared_ptr<Map>> maps;
std::map<uint32_t, std::shared_ptr<const Map>> maps;
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
};
+4 -4
View File
@@ -287,9 +287,9 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> ma
const auto& compressed = vm->compressed(is_nte);
phosg::StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
w.write(compressed);
uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
w.write(*compressed);
return std::move(w.str());
}
@@ -2588,7 +2588,7 @@ void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
this->send_6xB6x41_to_all_clients();
}
}
+1 -1
View File
@@ -357,7 +357,7 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name");
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
this->map = this->map_index->get(this->source_json.get_int("map_number"));
this->rules = Rules(this->source_json.at("rules"));
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {
+3 -4
View File
@@ -717,10 +717,9 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_rare_table_js
}
}
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json(
std::shared_ptr<const QuestIndex> quest_index) {
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json() {
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(quest_index->json());
return make_shared<phosg::JSON>(this->state->quest_index->json());
});
}
@@ -817,7 +816,7 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
ret = co_await this->generate_rare_table_json(req.path.substr(20));
} else if (req.path == "/y/data/quests") {
this->require_GET(req);
ret = co_await this->generate_quest_list_json(this->state->quest_index(Version::GC_V3));
ret = co_await this->generate_quest_list_json();
} else if (req.path == "/y/data/config") {
this->require_GET(req);
ret = this->state->config_json;
+1 -1
View File
@@ -41,7 +41,7 @@ protected:
std::shared_ptr<phosg::JSON> generate_rare_table_list_json() const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_rare_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json(std::shared_ptr<const QuestIndex> q);
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json();
void require_GET(const HTTPRequest& req);
phosg::JSON require_POST(const HTTPRequest& req);
-1
View File
@@ -9,7 +9,6 @@
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
#include "TeamIndex.hh"
+9 -1
View File
@@ -186,6 +186,14 @@ void Lobby::reset_next_item_ids() {
this->next_game_item_id = 0xCC000000;
}
uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const {
if (this->quest) {
return this->quest->meta.area_for_floor.at(floor);
}
auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty);
return sdt->default_area_for_floor(this->episode, floor);
}
shared_ptr<ServerState> Lobby::require_server_state() const {
auto s = this->server_state.lock();
if (!s) {
@@ -234,7 +242,7 @@ void Lobby::create_item_creator(Version logic_version) {
this->difficulty,
this->effective_section_id(),
rand_crypt,
this->quest ? this->quest->battle_rules : nullptr);
this->quest ? this->quest->meta.battle_rules : nullptr);
}
uint8_t Lobby::effective_section_id() const {
+2
View File
@@ -201,6 +201,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
this->enabled_flags ^= static_cast<uint32_t>(flag);
}
uint8_t area_for_floor(Version version, uint8_t floor) const;
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
void create_item_creator(Version logic_version = Version::UNKNOWN);
+18 -6
View File
@@ -2801,10 +2801,9 @@ Action a_show_ep3_maps(
s->load_ep3_cards();
s->load_ep3_maps();
auto map_ids = s->ep3_map_index->all_numbers();
const auto& map_ids = s->ep3_map_index->all();
phosg::log_info_f("{} maps", map_ids.size());
for (uint32_t map_id : map_ids) {
auto map = s->ep3_map_index->for_number(map_id);
for (const auto& [map_number, map] : map_ids) {
const auto& vms = map->all_versions();
for (size_t language = 0; language < vms.size(); language++) {
if (!vms[language]) {
@@ -2928,7 +2927,7 @@ Action a_check_supermaps(
SuperMap::EfficiencyStats all_quests_eff;
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX);
for (const auto& it : s->default_quest_index->quests_by_number) {
for (const auto& it : s->quest_index->quests_by_number) {
auto supermap = it.second->get_supermap(random_seed);
if (!supermap) {
throw logic_error("quest does not have a supermap, even with a specified random seed");
@@ -2938,7 +2937,7 @@ Action a_check_supermaps(
if (save_disassembly) {
string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt");
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
supermap->print(f.get());
filename_token = " => " + filename;
}
@@ -2949,7 +2948,7 @@ Action a_check_supermaps(
}
string filename = std::format("supermap_quest_{}_{:08X}_enemy_counts.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt");
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name);
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n");
for (size_t type_ss = 0; type_ss < static_cast<size_t>(EnemyType::MAX_ENEMY_TYPE); type_ss++) {
EnemyType type = static_cast<EnemyType>(type_ss);
@@ -3005,6 +3004,19 @@ Action a_check_supermaps(
phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str);
});
Action a_check_quests(
"check-quests", nullptr,
+[](phosg::Arguments& args) {
auto s = make_shared<ServerState>(get_config_filename(args));
s->is_debug = true;
s->load_config_early();
s->clear_file_caches();
s->load_patch_indexes();
s->load_set_data_tables();
s->load_maps();
s->load_quest_index();
});
Action a_parse_object_graph(
"parse-object-graph", nullptr, +[](phosg::Arguments& args) {
uint32_t root_object_address = args.get<uint32_t>("root", phosg::Arguments::IntFormat::HEX);
+8 -4
View File
@@ -67,7 +67,7 @@ vector<string> SetDataTableBase::map_filenames_for_variations(
return ret;
}
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episode, uint8_t floor) {
// For some inscrutable reason, Pioneer 2's area number in Episode 4 is
// discontiguous with all the rest. Why, Sega??
static const array<uint8_t, 0x12> areas_ep1 = {
@@ -82,7 +82,7 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
case Episode::EP1:
return areas_ep1.at(floor);
case Episode::EP2: {
const auto& areas = ((this->version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
const auto& areas = ((version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2);
return areas.at(floor);
}
case Episode::EP4:
@@ -92,6 +92,10 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor)
}
}
uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const {
return this->default_area_for_floor(this->version, episode, floor);
}
SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) {
if (is_big_endian(this->version)) {
this->load_table_t<true>(data);
@@ -5833,7 +5837,7 @@ phosg::JSON MapState::RareEnemyRates::json() const {
});
}
uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const {
uint32_t MapState::RareEnemyRates::get(EnemyType type) const {
switch (type) {
case EnemyType::HILDEBEAR:
return this->hildeblue;
@@ -6071,7 +6075,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptr<RandomGenerator
auto rare_type = type_definition_for_enemy(type).rare_type(fc.super_map->get_episode(), this->event, ene->floor);
if ((type == EnemyType::MERICARAND) || (rare_type != type)) {
unordered_map<uint32_t, float> det_cache;
uint32_t bb_rare_rate = this->bb_rare_rates->for_enemy_type(type);
uint32_t bb_rare_rate = this->bb_rare_rates->get(type);
for (Version v : ALL_NON_PATCH_VERSIONS) {
// Skip this version if the enemy doesn't exist there
uint16_t relative_enemy_index = ene->version(v).relative_enemy_index;
+2 -1
View File
@@ -67,6 +67,7 @@ public:
std::vector<std::string> map_filenames_for_variations(
Episode episode, GameMode mode, const Variations& variations, FilenameType type) const;
static uint8_t default_area_for_floor(Version version, Episode episode, uint8_t floor);
uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
protected:
@@ -672,7 +673,7 @@ public:
RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate);
explicit RareEnemyRates(const phosg::JSON& json);
uint32_t for_enemy_type(EnemyType type) const;
uint32_t get(EnemyType type) const;
std::string str() const;
phosg::JSON json() const;
+1
View File
@@ -21,6 +21,7 @@ constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST_EP1 = 0x55010155;
constexpr uint32_t QUEST_EP2 = 0x55020255;
constexpr uint32_t QUEST_EP3 = 0x55030355;
// See the decsription of the A2 command in CommandFormats.hh for why these
// menu IDs don't fit the rest of the pattern.
constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001;
+157 -400
View File
@@ -194,10 +194,10 @@ struct PSODownloadQuestHeader {
} __packed_ws__(PSODownloadQuestHeader, 8);
void VersionedQuest::assert_valid() const {
if (this->category_id == 0xFFFFFFFF) {
if (this->meta.category_id == 0xFFFFFFFF) {
throw runtime_error("category ID is not set");
}
if (this->quest_number == 0xFFFFFFFF) {
if (this->meta.quest_number == 0xFFFFFFFF) {
throw runtime_error("quest number is not set");
}
if (this->version == Version::UNKNOWN) {
@@ -206,96 +206,107 @@ void VersionedQuest::assert_valid() const {
if (this->language == 0xFF) {
throw runtime_error("language is not set");
}
if (this->episode == Episode::NONE) {
throw runtime_error("episode is not set");
switch (this->meta.episode) {
case Episode::EP1:
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area >= 0x12) {
throw runtime_error("Episode 1 quest specifies invalid area");
}
}
break;
case Episode::EP2:
if (is_v1_or_v2(this->version)) {
throw runtime_error("v1 or v2 quest specifies Episode 2");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if ((area < 0x12) || (area >= 0x24)) {
throw runtime_error("Episode 2 quest specifies invalid area");
}
}
break;
case Episode::EP3:
if (!is_ep3(this->version)) {
throw runtime_error("non-Ep3 quest specifies Episode 3");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
if (this->meta.area_for_floor[floor] != 0xFF) {
throw runtime_error("Episode 3 quest specifies floor overrides");
}
}
break;
case Episode::EP4:
if (!is_v4(this->version)) {
throw runtime_error("non-v4 quest specifies Episode 4");
}
for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) {
uint8_t area = this->meta.area_for_floor[floor];
if (area != 0xFF && (area < 0x24 || area >= 0x2F)) {
throw runtime_error("Episode 4 quest specifies invalid floor");
}
}
break;
case Episode::NONE:
throw runtime_error("episode is not set");
default:
throw runtime_error("episode is not valid");
}
if (this->max_players == 0) {
if (this->meta.max_players == 0) {
throw runtime_error("max players is not set");
}
if (!this->bin_contents) {
throw runtime_error("bin file is missing");
}
if (!is_ep3(this->version) && !this->dat_contents) {
if (!this->dat_contents) {
throw runtime_error("dat file is missing");
}
if (!is_ep3(this->version) && !this->map_file) {
if (!this->map_file) {
throw runtime_error("parsed map file is missing");
}
if (this->common_item_set_name.empty() != !this->common_item_set) {
if (this->meta.common_item_set_name.empty() != !this->meta.common_item_set) {
throw runtime_error("common item set name/pointer mismatch");
}
if (this->rare_item_set_name.empty() != !this->rare_item_set) {
if (this->meta.rare_item_set_name.empty() != !this->meta.rare_item_set) {
throw runtime_error("rare item set name/pointer mismatch");
}
if (this->allowed_drop_modes && !(this->allowed_drop_modes & (1 << static_cast<size_t>(this->default_drop_mode)))) {
if (this->meta.allowed_drop_modes &&
!(this->meta.allowed_drop_modes & (1 << static_cast<size_t>(this->meta.default_drop_mode)))) {
throw runtime_error("default drop mode is not allowed");
}
}
string VersionedQuest::bin_filename() const {
if (this->episode == Episode::EP3) {
return std::format("m{:06}p_e.bin", this->quest_number);
} else {
return std::format("quest{}.bin", this->quest_number);
}
return std::format("quest{}.bin", this->meta.quest_number);
}
string VersionedQuest::dat_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
} else {
return std::format("quest{}.dat", this->quest_number);
}
return std::format("quest{}.dat", this->meta.quest_number);
}
string VersionedQuest::pvr_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .pvr files");
} else {
return std::format("quest{}.pvr", this->quest_number);
}
return std::format("quest{}.pvr", this->meta.quest_number);
}
string VersionedQuest::xb_filename() const {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have Xbox filenames");
} else {
return std::format("quest{}_{}.dat", this->quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
}
return std::format("quest{}_{}.dat",
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
}
string VersionedQuest::encode_qst() const {
unordered_map<string, shared_ptr<const string>> files;
files.emplace(std::format("quest{}.bin", this->quest_number), this->bin_contents);
files.emplace(std::format("quest{}.dat", this->quest_number), this->dat_contents);
files.emplace(std::format("quest{}.bin", this->meta.quest_number), this->bin_contents);
files.emplace(std::format("quest{}.dat", this->meta.quest_number), this->dat_contents);
if (this->pvr_contents) {
files.emplace(std::format("quest{}.pvr", this->quest_number), this->pvr_contents);
files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents);
}
string xb_filename = std::format("quest{}_{}.dat", quest_number, static_cast<char>(tolower(char_for_language_code(language))));
return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded);
string xb_filename = std::format("quest{}_{}.dat",
this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(language))));
return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded);
}
Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
: quest_number(initial_version->quest_number),
category_id(initial_version->category_id),
episode(initial_version->episode),
allow_start_from_chat_command(initial_version->allow_start_from_chat_command),
joinable(initial_version->joinable),
max_players(initial_version->max_players),
lock_status_register(initial_version->lock_status_register),
name(initial_version->name),
supermap(nullptr),
battle_rules(initial_version->battle_rules),
challenge_template_index(initial_version->challenge_template_index),
description_flag(initial_version->description_flag),
available_expression(initial_version->available_expression),
enabled_expression(initial_version->enabled_expression),
common_item_set_name(initial_version->common_item_set_name),
rare_item_set_name(initial_version->rare_item_set_name),
common_item_set(initial_version->common_item_set),
rare_item_set(initial_version->rare_item_set),
allowed_drop_modes(initial_version->allowed_drop_modes),
default_drop_mode(initial_version->default_drop_mode) {
: meta(initial_version->meta), supermap(nullptr) {
this->add_version(initial_version);
}
@@ -305,8 +316,9 @@ phosg::JSON Quest::json() const {
versions_json.emplace_back(phosg::JSON::dict({
{"Version", phosg::name_for_enum(vq->version)},
{"Language", name_for_language_code(vq->language)},
{"ShortDescription", vq->short_description},
{"LongDescription", vq->long_description},
{"Name", vq->meta.name},
{"ShortDescription", vq->meta.short_description},
{"LongDescription", vq->meta.long_description},
{"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)},
{"DATFileSize", vq->dat_contents ? vq->dat_contents->size() : phosg::JSON(nullptr)},
{"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)},
@@ -314,23 +326,7 @@ phosg::JSON Quest::json() const {
}
return phosg::JSON::dict({
{"Number", this->quest_number},
{"CategoryID", this->category_id},
{"Episode", name_for_episode(this->episode)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
{"Name", this->name},
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
{"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)},
{"DescriptionFlag", this->description_flag},
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name},
{"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name},
{"AllowedDropModes", this->allowed_drop_modes},
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
{"Metadata", this->meta.json()},
{"Versions", std::move(versions_json)},
});
}
@@ -340,112 +336,7 @@ uint32_t Quest::versions_key(Version v, uint8_t language) {
}
void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->quest_number != vq->quest_number) {
throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
this->quest_number, vq->quest_number));
}
if (this->category_id != vq->category_id) {
throw runtime_error(std::format(
"quest version is in a different category (existing: {:08X}, new: {:08X})",
this->category_id, vq->category_id));
}
if (this->episode != vq->episode) {
throw runtime_error(std::format(
"quest version is in a different episode (existing: {}, new: {})",
name_for_episode(this->episode), name_for_episode(vq->episode)));
}
if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) {
throw runtime_error(std::format(
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false"));
}
if (this->joinable != vq->joinable) {
throw runtime_error(std::format(
"quest version has a different joinability state (existing: {}, new: {})",
this->joinable ? "true" : "false", vq->joinable ? "true" : "false"));
}
if (this->max_players != vq->max_players) {
throw runtime_error(std::format(
"quest version has a different maximum player count (existing: {}, new: {})",
this->max_players, vq->max_players));
}
if (this->lock_status_register != vq->lock_status_register) {
throw runtime_error(std::format(
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
this->lock_status_register, vq->lock_status_register));
}
if (!this->battle_rules != !vq->battle_rules) {
throw runtime_error(std::format(
"quest version has a different battle rules presence state (existing: {}, new: {})",
this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent"));
}
if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) {
string existing_str = this->battle_rules->json().serialize();
string new_str = vq->battle_rules->json().serialize();
throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})",
existing_str, new_str));
}
if (this->challenge_template_index != vq->challenge_template_index) {
throw runtime_error(std::format(
"quest version has different challenge template index (existing: {}, new: {})",
this->challenge_template_index, vq->challenge_template_index));
}
if (this->description_flag != vq->description_flag) {
throw runtime_error(std::format(
"quest version has different description flag (existing: {:02X}, new: {:02X})",
this->description_flag, vq->description_flag));
}
if (!this->available_expression != !vq->available_expression) {
throw runtime_error(std::format(
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent"));
}
if (this->available_expression && *this->available_expression != *vq->available_expression) {
string existing_str = this->available_expression->str();
string new_str = vq->available_expression->str();
throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})",
existing_str, new_str));
}
if (!this->enabled_expression != !vq->enabled_expression) {
throw runtime_error(std::format(
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent"));
}
if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) {
string existing_str = this->enabled_expression->str();
string new_str = vq->enabled_expression->str();
throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})",
existing_str, new_str));
}
if (this->common_item_set_name != vq->common_item_set_name) {
throw runtime_error(std::format(
"quest version has different common table name (existing: {}, new: {})",
this->common_item_set_name, vq->common_item_set_name));
}
if (this->common_item_set != vq->common_item_set) {
throw runtime_error("quest version has different common table");
}
if (this->rare_item_set_name != vq->rare_item_set_name) {
throw runtime_error(std::format(
"quest version has different rare table name (existing: {}, new: {})",
this->rare_item_set_name, vq->rare_item_set_name));
}
if (this->rare_item_set != vq->rare_item_set) {
throw runtime_error("quest version has different rare table");
}
if (this->allowed_drop_modes != vq->allowed_drop_modes) {
throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})",
this->allowed_drop_modes, vq->allowed_drop_modes));
}
if (this->default_drop_mode != vq->default_drop_mode) {
throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})",
phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(vq->default_drop_mode)));
}
this->meta.assert_compatible(vq->meta);
this->versions.emplace(this->versions_key(vq->version, vq->language), vq);
}
@@ -477,12 +368,12 @@ std::shared_ptr<const SuperMap> Quest::get_supermap(int64_t random_seed) const {
return nullptr;
}
auto supermap = make_shared<SuperMap>(this->episode, map_files);
auto supermap = make_shared<SuperMap>(this->meta.episode, map_files);
if (save_to_cache) {
this->supermap = supermap;
}
static_game_data_log.info_f("Constructed {} supermap for quest {} ({})",
save_to_cache ? "cacheable" : "temporary", this->quest_number, this->name);
save_to_cache ? "cacheable" : "temporary", this->meta.quest_number, this->meta.name);
return supermap;
}
@@ -522,8 +413,7 @@ QuestIndex::QuestIndex(
const string& directory,
shared_ptr<const QuestCategoryIndex> category_index,
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets,
bool is_ep3)
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets)
: directory(directory),
category_index(category_index) {
@@ -533,7 +423,7 @@ QuestIndex::QuestIndex(
};
struct BINFileData {
string filename;
unique_ptr<QuestMetadata> metadata;
shared_ptr<const AssembledQuestScript> assembled;
shared_ptr<const string> data;
};
struct DATFileData {
@@ -547,12 +437,6 @@ QuestIndex::QuestIndex(
map<string, FileData> json_files;
map<string, uint32_t> categories;
for (const auto& cat : this->category_index->categories) {
// Don't index Ep3 download categories for non-Ep3 quest indexing, and vice
// versa
if (is_ep3 != cat->check_flag(QuestMenuType::EP3_DOWNLOAD)) {
continue;
}
auto add_file = [&](map<string, FileData>& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories");
@@ -569,7 +453,7 @@ QuestIndex::QuestIndex(
}
};
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) {
auto add_bin_file = [&](const string& basename, const string& filename, string&& data, shared_ptr<AssembledQuestScript> assembled) {
if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("bin file " + basename + " exists in multiple categories");
}
@@ -581,9 +465,7 @@ QuestIndex::QuestIndex(
auto& entry = emplace_ret.first->second;
entry.filename = filename;
entry.data = data_ptr;
if (metadata) {
entry.metadata = make_unique<QuestMetadata>(*metadata);
}
entry.assembled = assembled;
if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00);
}
@@ -614,7 +496,7 @@ QuestIndex::QuestIndex(
}
string file_path = cat_path + "/" + filename;
unique_ptr<AssembledQuestScript> assembled;
shared_ptr<AssembledQuestScript> assembled;
try {
string orig_filename = filename;
string file_data;
@@ -629,7 +511,7 @@ QuestIndex::QuestIndex(
filename.resize(filename.size() - 4);
} else if (filename.ends_with(".bin.txt")) {
string include_dir = phosg::dirname(file_path);
assembled = make_unique<AssembledQuestScript>(assemble_quest_script(
assembled = make_shared<AssembledQuestScript>(assemble_quest_script(
phosg::load_file(file_path),
{include_dir, "system/quests/includes"},
{include_dir, "system/quests/includes", "system/client-functions/System"}));
@@ -655,9 +537,9 @@ QuestIndex::QuestIndex(
if (extension == "json") {
add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
} else if (extension == "bin" || extension == "mnm") {
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr);
add_bin_file(file_basename, orig_filename, std::move(file_data), assembled);
} else if (extension == "bind" || extension == "mnmd") {
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr);
add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled);
} else if (extension == "dat") {
add_dat_file(file_basename, orig_filename, std::move(file_data));
} else if (extension == "datd") {
@@ -710,28 +592,18 @@ QuestIndex::QuestIndex(
version_token = std::move(filename_tokens[1]);
language_token = std::move(filename_tokens[2]);
}
vq->category_id = categories.at(basename);
// Find the quest's metadata. If the quest was assembled (that is, if it
// came from a .bin.txt file), use the metadata from the source file;
// otherwise, figure it out from the already-assembled code
if (entry.metadata) {
vq->quest_number = entry.metadata->quest_number;
vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version;
vq->language = entry.metadata->language;
vq->episode = entry.metadata->episode;
vq->joinable = entry.metadata->joinable;
vq->max_players = entry.metadata->max_players;
vq->name = entry.metadata->name;
vq->short_description = entry.metadata->short_description;
vq->long_description = entry.metadata->long_description;
vq->meta.category_id = categories.at(basename);
if (entry.assembled) {
vq->meta.quest_number = entry.assembled->quest_number;
vq->version = entry.assembled->version;
vq->language = entry.assembled->language;
} else {
// Get the number from the first token
if (quest_number_token.empty()) {
throw runtime_error("quest number token is missing");
}
vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
vq->meta.quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10);
// Get the version from the second token
static const unordered_map<string, Version> name_to_version({
@@ -755,147 +627,45 @@ QuestIndex::QuestIndex(
throw runtime_error("language token is not a single character");
}
vq->language = language_code_for_char(language_token[0]);
auto bin_decompressed = prs_decompress(*entry.data);
switch (vq->version) {
case Version::DC_NTE: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDCNTE*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
vq->name = header->name.decode(vq->language);
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = phosg::fnv1a64(vq->name);
}
break;
}
case Version::DC_11_2000:
case Version::DC_V1:
case Version::DC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderDC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::PC_NTE:
case Version::PC_V2: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderPC*>(bin_decompressed.data());
vq->episode = Episode::EP1;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::GC_EP3_NTE:
case Version::GC_EP3: {
// Note: This codepath handles Episode 3 download quests, which are not
// the same as Episode 3 quest scripts. The latter are only used offline
// in story mode, but can be disassembled with disassemble_quest_script.
// It's unfortunate that Version::GC_EP3 is used here for Episode 3
// download quests (maps) and there for offline story mode scripts, but
// it's probably not worth refactoring this logic, at least right now.
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
auto* map = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
vq->episode = Episode::EP3;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = map->map_number;
}
vq->name = map->name.decode(vq->language);
vq->short_description = map->quest_name.decode(vq->language);
vq->long_description = map->description.decode(vq->language);
break;
}
case Version::XB_V3:
case Version::GC_NTE:
case Version::GC_V3: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderGC*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
case Version::BB_V4: {
if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) {
throw invalid_argument("file is too small for header");
}
auto* header = reinterpret_cast<const PSOQuestHeaderBB*>(bin_decompressed.data());
vq->episode = find_quest_episode_from_script(
bin_decompressed.data(), bin_decompressed.size(), vq->version);
vq->joinable |= header->joinable;
vq->max_players = 4;
if (vq->quest_number == 0xFFFFFFFF) {
vq->quest_number = header->quest_number;
}
vq->name = header->name.decode(vq->language);
vq->short_description = header->short_description.decode(vq->language);
vq->long_description = header->long_description.decode(vq->language);
break;
}
default:
throw logic_error("invalid quest game version");
}
}
// Find the corresponding dat and pvr files
auto bin_decompressed = prs_decompress(*entry.data);
populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->version, vq->language);
// If the quest was assembled (that is, if it came from a .bin.txt file),
// the metadata from the source file overrides any automatically-detected
// values from above
if (entry.assembled) {
vq->meta.quest_number = entry.assembled->quest_number;
vq->meta.episode = entry.assembled->episode;
vq->meta.joinable = entry.assembled->joinable;
vq->meta.max_players = entry.assembled->max_players;
vq->meta.name = entry.assembled->name;
vq->meta.short_description = entry.assembled->short_description;
vq->meta.long_description = entry.assembled->long_description;
}
// Find the corresponding dat and pvr files with the same basename as the
// bin file; if not found, look for them without the language suffix
const DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr;
if (!::is_ep3(vq->version)) {
// Look for dat and pvr files with the same basename as the bin file; if
// not found, look for them without the language suffix
try {
dat_filedata = &dat_files.at(basename);
} catch (const out_of_range&) {
try {
dat_filedata = &dat_files.at(basename);
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
try {
dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
throw runtime_error("no dat file found for bin file " + basename);
}
throw runtime_error("no dat file found for bin file " + basename);
}
}
try {
pvr_filedata = &pvr_files.at(basename);
} catch (const out_of_range&) {
try {
pvr_filedata = &pvr_files.at(basename);
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) {
try {
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} 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
}
// pvr files aren't required (and most quests do not have them), so
// don't fail if it's missing
}
}
vq->bin_contents = entry.data;
@@ -924,64 +694,56 @@ QuestIndex::QuestIndex(
if (json_filedata) {
auto metadata_json = phosg::JSON::parse(*json_filedata->data);
try {
vq->battle_rules = make_shared<BattleRules>(metadata_json.at("BattleRules"));
vq->meta.description_flag = metadata_json.at("DescriptionFlag").as_int();
} catch (const out_of_range&) {
}
try {
vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int();
vq->meta.available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
} catch (const out_of_range&) {
}
try {
vq->description_flag = metadata_json.at("DescriptionFlag").as_int();
vq->meta.enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
} catch (const out_of_range&) {
}
try {
vq->available_expression = make_shared<IntegralExpression>(metadata_json.get_string("AvailableIf"));
vq->meta.allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
} catch (const out_of_range&) {
}
try {
vq->enabled_expression = make_shared<IntegralExpression>(metadata_json.get_string("EnabledIf"));
vq->meta.joinable = metadata_json.get_bool("Joinable");
} catch (const out_of_range&) {
}
try {
vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand");
vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister");
} catch (const out_of_range&) {
}
try {
vq->joinable = metadata_json.get_bool("Joinable");
vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->meta.common_item_set_name.empty()) {
vq->meta.common_item_set = common_item_sets.at(vq->meta.common_item_set_name);
}
try {
vq->meta.rare_item_set_name = metadata_json.at("RareItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->meta.rare_item_set_name.empty()) {
vq->meta.rare_item_set = rare_item_sets.at(vq->meta.rare_item_set_name);
}
try {
vq->meta.allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int();
} catch (const out_of_range&) {
}
try {
vq->lock_status_register = metadata_json.get_int("LockStatusRegister");
} catch (const out_of_range&) {
}
try {
vq->common_item_set_name = metadata_json.at("CommonItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->common_item_set_name.empty()) {
vq->common_item_set = common_item_sets.at(vq->common_item_set_name);
}
try {
vq->rare_item_set_name = metadata_json.at("RareItemSetName").as_string();
} catch (const out_of_range&) {
}
if (!vq->rare_item_set_name.empty()) {
vq->rare_item_set = rare_item_sets.at(vq->rare_item_set_name);
}
try {
vq->allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int();
} catch (const out_of_range&) {
}
try {
vq->default_drop_mode = phosg::enum_for_name<ServerDropMode>(metadata_json.at("DefaultDropMode").as_string());
vq->meta.default_drop_mode = phosg::enum_for_name<ServerDropMode>(metadata_json.at("DefaultDropMode").as_string());
} catch (const out_of_range&) {
}
}
vq->assert_valid();
auto category_name = this->category_index->at(vq->category_id)->name;
auto category_name = this->category_index->at(vq->meta.category_id)->name;
string filenames_str = entry.filename;
if (dat_filedata) {
filenames_str += std::format("/{}", dat_filedata->filename);
@@ -992,30 +754,32 @@ QuestIndex::QuestIndex(
if (json_filedata) {
filenames_str += std::format("/{}", json_filedata->filename);
}
auto q_it = this->quests_by_number.find(vq->quest_number);
auto q_it = this->quests_by_number.find(vq->meta.quest_number);
if (q_it != this->quests_by_number.end()) {
q_it->second->add_version(vq);
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({})",
static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({}) with floors {}",
filenames_str,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
vq->quest_number,
vq->name);
vq->meta.quest_number,
vq->meta.name,
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
} else {
auto q = make_shared<Quest>(vq);
this->quests_by_number.emplace(vq->quest_number, q);
this->quests_by_name.emplace(vq->name, q);
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q);
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})",
this->quests_by_number.emplace(vq->meta.quest_number, q);
this->quests_by_name.emplace(vq->meta.name, q);
this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q);
static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}",
filenames_str,
phosg::name_for_enum(vq->version),
char_for_language_code(vq->language),
vq->quest_number,
vq->name,
name_for_episode(vq->episode),
vq->meta.quest_number,
vq->meta.name,
name_for_episode(vq->meta.episode),
category_name,
vq->category_id,
vq->joinable ? "joinable" : "not joinable");
vq->meta.category_id,
vq->meta.joinable ? "joinable" : "not joinable",
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
}
} catch (const exception& e) {
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what());
@@ -1096,7 +860,7 @@ vector<pair<QuestIndex::IncludeState, shared_ptr<const Quest>>> QuestIndex::filt
return ret;
}
for (auto it : category_it->second) {
if ((effective_episode != Episode::NONE) && (it.second->episode != effective_episode)) {
if ((effective_episode != Episode::NONE) && (it.second->meta.episode != effective_episode)) {
continue;
}
bool all_required_versions_present = true;
@@ -1145,8 +909,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre
data.resize((data.size() + 3) & (~3));
PSOV2Encryption encr(encryption_seed);
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
data.size() - sizeof(PSODownloadQuestHeader));
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader));
data.resize(original_size);
return data;
@@ -1158,12 +921,6 @@ shared_ptr<VersionedQuest> VersionedQuest::create_download_quest(uint8_t overrid
// this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again.
// 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 || is_ep3(this->version)) {
throw logic_error("Episode 3 quests cannot be converted to download quests");
}
string decompressed_bin = prs_decompress(*this->bin_contents);
void* data_ptr = decompressed_bin.data();
+5 -44
View File
@@ -13,6 +13,7 @@
#include "ItemParameterTable.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "QuestMetadata.hh"
#include "QuestScript.hh"
#include "RareItemSet.hh"
#include "StaticGameData.hh"
@@ -34,7 +35,6 @@ enum class QuestMenuType {
SOLO = 3,
GOVERNMENT = 4,
DOWNLOAD = 5,
EP3_DOWNLOAD = 6,
// 7 can't be used as a menu type (it enables the per-episode filter)
};
@@ -67,35 +67,16 @@ struct QuestCategoryIndex {
};
struct VersionedQuest {
QuestMetadata meta;
// Most of these default values are intentionally invalid; we use these
// values to check if each field was parsed during quest indexing.
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
Version version = Version::UNKNOWN;
uint8_t language = 0xFF;
Episode episode = Episode::NONE;
bool joinable = false;
uint8_t max_players = 0x00;
std::string name;
std::string short_description;
std::string long_description;
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const MapFile> map_file;
std::shared_ptr<const std::string> pvr_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
bool allow_start_from_chat_command = false;
int16_t lock_status_register = -1;
std::string common_item_set_name;
std::string rare_item_set_name;
std::shared_ptr<const CommonItemSet> common_item_set;
std::shared_ptr<const RareItemSet> rare_item_set;
uint8_t allowed_drop_modes = 0x00; // 0 = use server default
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
bool is_dlq_encoded = false;
void assert_valid() const;
@@ -110,27 +91,8 @@ struct VersionedQuest {
};
struct Quest {
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool allow_start_from_chat_command;
bool joinable;
uint8_t max_players;
int16_t lock_status_register;
std::string name;
QuestMetadata meta;
mutable std::shared_ptr<const SuperMap> supermap;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
std::string common_item_set_name;
std::string rare_item_set_name;
std::shared_ptr<const CommonItemSet> common_item_set;
std::shared_ptr<const RareItemSet> rare_item_set;
uint8_t allowed_drop_modes = 0x00; // 0 = use server default
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
Quest() = delete;
@@ -171,8 +133,7 @@ struct QuestIndex {
const std::string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index,
const std::unordered_map<std::string, std::shared_ptr<const CommonItemSet>>& common_item_sets,
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets,
bool is_ep3);
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets);
phosg::JSON json() const;
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
+164
View File
@@ -0,0 +1,164 @@
#include "QuestMetadata.hh"
using namespace std;
void QuestMetadata::assign_default_areas(Version version, Episode episode) {
for (size_t z = 0; z < 0x12; z++) {
this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z);
}
}
void QuestMetadata::assert_compatible(const QuestMetadata& other) const {
if (this->quest_number != other.quest_number) {
throw logic_error(std::format(
"incorrect versioned quest number (existing: {:08X}, new: {:08X})",
this->quest_number, other.quest_number));
}
if (this->category_id != other.category_id) {
throw runtime_error(std::format(
"quest version is in a different category (existing: {:08X}, new: {:08X})",
this->category_id, other.category_id));
}
if (this->episode != other.episode) {
throw runtime_error(std::format(
"quest version is in a different episode (existing: {}, new: {})",
name_for_episode(this->episode), name_for_episode(other.episode)));
}
if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) {
throw runtime_error(std::format(
"quest version has a different allow_start_from_chat_command state (existing: {}, new: {})",
this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false"));
}
if (this->joinable != other.joinable) {
throw runtime_error(std::format(
"quest version has a different joinability state (existing: {}, new: {})",
this->joinable ? "true" : "false", other.joinable ? "true" : "false"));
}
if (this->max_players != other.max_players) {
throw runtime_error(std::format(
"quest version has a different maximum player count (existing: {}, new: {})",
this->max_players, other.max_players));
}
if (this->lock_status_register != other.lock_status_register) {
throw runtime_error(std::format(
"quest version has a different lock status register (existing: {:04X}, new: {:04X})",
this->lock_status_register, other.lock_status_register));
}
if (!this->battle_rules != !other.battle_rules) {
throw runtime_error(std::format(
"quest version has a different battle rules presence state (existing: {}, new: {})",
this->battle_rules ? "present" : "absent", other.battle_rules ? "present" : "absent"));
}
if (this->battle_rules && (*this->battle_rules != *other.battle_rules)) {
string existing_str = this->battle_rules->json().serialize();
string new_str = other.battle_rules->json().serialize();
throw runtime_error(std::format(
"quest version has different battle rules (existing: {}, new: {})",
existing_str, new_str));
}
if (this->challenge_template_index != other.challenge_template_index) {
throw runtime_error(std::format(
"quest version has different challenge template index (existing: {}, new: {})",
this->challenge_template_index, other.challenge_template_index));
}
if (this->challenge_exp_multiplier != other.challenge_exp_multiplier) {
throw runtime_error(std::format(
"quest version has different challenge EXP multiplier (existing: {}, new: {})",
this->challenge_exp_multiplier, other.challenge_exp_multiplier));
}
if (this->challenge_difficulty != other.challenge_difficulty) {
throw runtime_error(std::format(
"quest version has different challenge difficulty (existing: {}, new: {})",
this->challenge_difficulty, other.challenge_difficulty));
}
for (size_t z = 0; z < this->area_for_floor.size(); z++) {
const auto& this_fa = this->area_for_floor[z];
const auto& other_fa = other.area_for_floor[z];
if (this_fa != other_fa) {
throw runtime_error(std::format(
"quest version has different area on floor 0x{:02X} (existing: {}, new: {})",
z, phosg::format_data_string(this->area_for_floor.data(), 0x12), phosg::format_data_string(other.area_for_floor.data(), 0x12)));
}
}
if (this->description_flag != other.description_flag) {
throw runtime_error(std::format(
"quest version has different description flag (existing: {:02X}, new: {:02X})",
this->description_flag, other.description_flag));
}
if (!this->available_expression != !other.available_expression) {
throw runtime_error(std::format(
"quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})",
this->available_expression ? "present" : "absent", other.available_expression ? "present" : "absent"));
}
if (this->available_expression && *this->available_expression != *other.available_expression) {
string existing_str = this->available_expression->str();
string new_str = other.available_expression->str();
throw runtime_error(std::format(
"quest version has a different available expression (existing: {}, new: {})",
existing_str, new_str));
}
if (!this->enabled_expression != !other.enabled_expression) {
throw runtime_error(std::format(
"quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})",
this->enabled_expression ? "present" : "absent", other.enabled_expression ? "present" : "absent"));
}
if (this->enabled_expression && *this->enabled_expression != *other.enabled_expression) {
string existing_str = this->enabled_expression->str();
string new_str = other.enabled_expression->str();
throw runtime_error(std::format(
"quest version has a different enabled expression (existing: {}, new: {})",
existing_str, new_str));
}
if (this->common_item_set_name != other.common_item_set_name) {
throw runtime_error(std::format(
"quest version has different common table name (existing: {}, new: {})",
this->common_item_set_name, other.common_item_set_name));
}
if (this->common_item_set != other.common_item_set) {
throw runtime_error("quest version has different common table");
}
if (this->rare_item_set_name != other.rare_item_set_name) {
throw runtime_error(std::format(
"quest version has different rare table name (existing: {}, new: {})",
this->rare_item_set_name, other.rare_item_set_name));
}
if (this->rare_item_set != other.rare_item_set) {
throw runtime_error("quest version has different rare table");
}
if (this->allowed_drop_modes != other.allowed_drop_modes) {
throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})",
this->allowed_drop_modes, other.allowed_drop_modes));
}
if (this->default_drop_mode != other.default_drop_mode) {
throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})",
phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode)));
}
}
phosg::JSON QuestMetadata::json() const {
auto floors_json = phosg::JSON::list();
for (const auto& fa : this->area_for_floor) {
floors_json.emplace_back(fa);
}
return phosg::JSON::dict({
{"CategoryID", this->category_id},
{"Number", this->quest_number},
{"Episode", name_for_episode(this->episode)},
{"FloorAssignments", floors_json},
{"Joinable", this->joinable},
{"MaxPlayers", this->max_players},
{"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)},
{"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)},
{"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)},
{"ChallengeDifficulty", (this->challenge_difficulty >= 0) ? this->challenge_difficulty : phosg::JSON(nullptr)},
{"DescriptionFlag", this->description_flag},
{"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)},
{"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name},
{"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name},
{"AllowedDropModes", this->allowed_drop_modes},
{"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)},
{"AllowStartFromChatCommand", this->allow_start_from_chat_command},
{"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)},
});
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <stdint.h>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "CommonItemSet.hh"
#include "IntegralExpression.hh"
#include "Map.hh"
#include "PlayerSubordinates.hh"
#include "RareItemSet.hh"
struct QuestMetadata {
// This structure contains configuration that should be the same across all
// versions of the quest, except for the name and description strings. This
// is used in both the Quest and VersionedQuest structures; in Quest, the
// name and description are used only internally.
uint32_t category_id = 0xFFFFFFFF;
uint32_t quest_number = 0xFFFFFFFF;
Episode episode = Episode::NONE;
std::array<uint8_t, 0x12> area_for_floor;
bool joinable = false;
uint8_t max_players = 0x00;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index = -1;
float challenge_exp_multiplier = -1.0f;
int8_t challenge_difficulty = -1;
uint8_t description_flag = 0x00;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
std::string common_item_set_name; // blank = use default
std::string rare_item_set_name; // blank = use default
std::shared_ptr<const CommonItemSet> common_item_set;
std::shared_ptr<const RareItemSet> rare_item_set;
uint8_t allowed_drop_modes = 0x00; // 0 = use server default
ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0
bool allow_start_from_chat_command = false;
int16_t lock_status_register = -1;
std::string name;
std::string short_description;
std::string long_description;
void assign_default_areas(Version version, Episode episode);
void assert_compatible(const QuestMetadata& other) const;
phosg::JSON json() const;
std::string areas_str() const;
};
+1204 -601
View File
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -5,6 +5,7 @@
#include <phosg/Encoding.hh>
#include <phosg/Tools.hh>
#include "QuestMetadata.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
@@ -19,6 +20,18 @@ struct PSOQuestHeaderDCNTE {
/* 0020 */
} __packed_ws__(PSOQuestHeaderDCNTE, 0x20);
struct PSOQuestHeaderDC112000 {
/* 0000 */ le_uint32_t code_offset = 0;
/* 0004 */ le_uint32_t function_table_offset = 0;
/* 0008 */ le_uint32_t size = 0;
/* 000C */ le_uint16_t unknown_a1 = 0;
/* 000E */ le_uint16_t unknown_a2 = 0;
/* 0010 */ pstring<TextEncoding::MARKED, 0x20> name;
/* 0030 */ pstring<TextEncoding::MARKED, 0x80> short_description;
/* 00B0 */ pstring<TextEncoding::MARKED, 0x120> long_description;
/* 01D0 */
} __packed_ws__(PSOQuestHeaderDC112000, 0x1D0);
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
/* 0000 */ le_uint32_t code_offset = 0;
/* 0004 */ le_uint32_t function_table_offset = 0;
@@ -100,7 +113,8 @@ std::string disassemble_quest_script(
bool reassembly_mode = false,
bool use_qedit_names = false);
struct QuestMetadata {
struct AssembledQuestScript {
std::string data;
int64_t quest_number = -1;
Version version = Version::UNKNOWN;
uint8_t language = 0xFF;
@@ -111,13 +125,9 @@ struct QuestMetadata {
std::string short_description;
std::string long_description;
};
struct AssembledQuestScript {
std::string data;
QuestMetadata metadata;
};
AssembledQuestScript assemble_quest_script(
const std::string& text,
const std::vector<std::string>& script_include_directories,
const std::vector<std::string>& native_include_directories);
Episode find_quest_episode_from_script(const void* data, size_t size, Version version);
void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language);
+108 -77
View File
@@ -341,14 +341,13 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
!c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE))) {
shared_ptr<const Quest> q;
try {
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version);
q = s->default_quest_index->get(quest_num);
q = s->quest_index->get(s->enable_send_function_call_quest_numbers.at(c->specific_version));
} catch (const out_of_range&) {
}
if (!q) {
c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version);
} else if (q) {
auto vq = q->version(is_ep3(c->version()) ? Version::GC_V3 : c->version(), 1);
auto vq = q->version(c->version(), 1);
if (vq) {
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE);
@@ -364,12 +363,14 @@ static asio::awaitable<void> on_login_complete(shared_ptr<Client> c) {
lobby_data.guild_card_number = c->login->account->account_id;
send_command_t(c, 0x64, 0x01, cmd);
} else {
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name);
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_filename();
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
send_open_quest_file(
c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(
c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
if (!is_v1_or_v2(c->version())) {
send_command(c, 0xAC, 0x00);
@@ -2153,11 +2154,10 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
case MenuID::QUEST_EP1:
case MenuID::QUEST_EP2: {
bool is_download_quest = !c->lobby.lock();
auto quest_index = s->quest_index(c->version());
if (!quest_index) {
if (!s->quest_index) {
send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest);
} else {
auto q = quest_index->get(cmd.item_id);
auto q = s->quest_index->get(cmd.item_id);
if (!q) {
send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest);
} else {
@@ -2165,12 +2165,22 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
if (!vq) {
send_quest_info(c, "$C4Quest does not\nexist for this game\nversion.", 0x00, is_download_quest);
} else {
send_quest_info(c, vq->long_description, vq->description_flag, is_download_quest);
send_quest_info(c, vq->meta.long_description, vq->meta.description_flag, is_download_quest);
}
}
}
break;
}
case MenuID::QUEST_EP3: {
auto map = s->ep3_download_map_index->get(cmd.item_id);
if (!map) {
send_quest_info(c, "$C4Map does not exist.", 0x00, true);
} else {
auto vm = map->version(c->language());
send_quest_info(c, vm->map->description.decode(vm->language), 0x00, true);
}
break;
}
case MenuID::GAME: {
auto game = s->find_lobby(cmd.item_id);
@@ -2245,7 +2255,7 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
if (game->quest) {
info += (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) ? "$C6Quest: " : "$C4Quest: ";
info += remove_color(game->quest->name);
info += remove_color(game->quest->meta.name);
info += "\n";
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
info += "$C6Quest in progress\n";
@@ -2396,22 +2406,22 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
lc->delete_overlay();
if ((l->quest->challenge_template_index >= 0) && !is_v4(leader_c->version())) {
if ((l->quest->meta.challenge_template_index >= 0) && !is_v4(leader_c->version())) {
// If the leader is BB, they will send an 02DF command that will create
// the overlays later; on other versions, we do it at quest start time
// (now) instead, hence the version check above.
if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index);
}
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version()));
lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true);
} else if (l->quest->battle_rules) {
} else if (l->quest->meta.battle_rules) {
if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index);
}
lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version()));
lc->create_battle_overlay(l->quest->meta.battle_rules, s->level_table(lc->version()));
lc->log.info_f("Created battle overlay");
}
}
@@ -2426,16 +2436,16 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
}
// Only allow loading battle/challenge quests if the game mode is correct
if ((q->challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) {
if ((q->meta.challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) {
throw runtime_error("incorrect game mode");
}
if ((q->battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) {
if ((q->meta.battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) {
throw runtime_error("incorrect game mode");
}
auto s = l->require_server_state();
if (q->joinable) {
if (q->meta.joinable) {
l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS);
} else {
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS);
@@ -2445,11 +2455,14 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
l->quest = q;
if (l->episode != Episode::EP3) {
l->episode = q->episode;
l->episode = q->meta.episode;
}
if (l->quest->allowed_drop_modes) {
l->allowed_drop_modes = l->quest->allowed_drop_modes;
l->drop_mode = l->quest->default_drop_mode;
if (l->quest->meta.allowed_drop_modes) {
l->allowed_drop_modes = l->quest->meta.allowed_drop_modes;
l->drop_mode = l->quest->meta.default_drop_mode;
}
if (l->quest->meta.challenge_difficulty >= 0) {
l->difficulty = l->quest->meta.challenge_difficulty;
}
l->create_item_creator();
@@ -2468,13 +2481,15 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
lc->channel->disconnect();
break;
}
lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name);
lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_filename();
send_open_quest_file(lc, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(lc, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
send_open_quest_file(
lc, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(
lc, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
// There is no such thing as command AC (quest barrier) on PSO V1 and V2;
// quests just start immediately when they're done downloading. (This is
@@ -2532,24 +2547,11 @@ static asio::awaitable<void> on_10_main_menu(shared_ptr<Client> c, uint32_t item
break;
case MainMenuItemID::DOWNLOAD_QUESTS: {
QuestMenuType menu_type = QuestMenuType::DOWNLOAD;
if (is_ep3(c->version())) {
menu_type = QuestMenuType::EP3_DOWNLOAD;
// 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.)
auto quest_index = s->quest_index(c->version());
uint16_t version_flags = (1 << static_cast<size_t>(c->version()));
const auto& categories = quest_index->categories(menu_type, Episode::EP3, version_flags);
if (categories.size() == 1) {
auto quests = quest_index->filter(Episode::EP3, version_flags, categories[0]->category_id);
send_quest_menu(c, quests, true);
break;
}
send_ep3_download_quest_menu(c);
} else {
send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
}
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, Episode::NONE);
break;
}
@@ -2799,9 +2801,13 @@ static asio::awaitable<void> on_10_game_menu(shared_ptr<Client> c, uint32_t item
}
static asio::awaitable<void> on_10_quest_categories(shared_ptr<Client> c, uint32_t item_id) {
// Episode 3 doesn't have this menu
if (is_ep3(c->version())) {
throw runtime_error("Episode 3 client made selection on quest categories menu");
}
auto s = c->require_server_state();
auto quest_index = s->quest_index(c->version());
if (!quest_index) {
if (!s->quest_index) {
send_lobby_message_box(c, "$C7Quests are not available.");
co_return;
}
@@ -2814,18 +2820,21 @@ static asio::awaitable<void> on_10_quest_categories(shared_ptr<Client> c, uint32
include_condition = l->quest_include_condition();
}
const auto& quests = quest_index->filter(episode, version_flags, item_id, include_condition);
const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition);
send_quest_menu(c, quests, !l);
}
static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
if (is_ep3(c->version())) {
throw runtime_error("Episode 1/2/4 quests cannot be downloaded by Ep3 clients");
}
auto s = c->require_server_state();
auto quest_index = s->quest_index(c->version());
if (!quest_index) {
if (!s->quest_index) {
send_lobby_message_box(c, "$C7Quests are not\navailable.");
co_return;
}
auto q = quest_index->get(item_id);
auto q = s->quest_index->get(item_id);
if (!q) {
send_lobby_message_box(c, "$C7Quest does not exist.");
co_return;
@@ -2840,7 +2849,7 @@ static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t ite
}
if (l) {
if (q->episode == Episode::EP3) {
if (q->meta.episode == Episode::EP3) {
send_lobby_message_box(c, "$C7Episode 3 quests\ncannot be loaded\nvia this interface.");
co_return;
}
@@ -2860,26 +2869,34 @@ static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t ite
send_lobby_message_box(c, "$C7Quest does not exist\nfor this game version.");
co_return;
}
// Episode 3 uses the download quest commands (A6/A7) but does not
// expect the server to have already encrypted the quest files, unlike
// other versions.
// TODO: This is not true for Episode 3 Trial Edition. We also would
// have to convert the map to a MapDefinitionTrial, though.
if (is_ep3(vq->version)) {
send_open_quest_file(c, q->name, vq->bin_filename(), "", vq->quest_number, QuestFileType::EPISODE_3, vq->bin_contents);
} else {
vq = vq->create_download_quest(c->language());
string xb_filename = vq->xb_filename();
QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
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->pvr_filename(), xb_filename, vq->quest_number, type, vq->pvr_contents);
}
vq = vq->create_download_quest(c->language());
string xb_filename = vq->xb_filename();
QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
send_open_quest_file(c, q->meta.name, vq->bin_filename(), xb_filename, vq->meta.quest_number, type, vq->bin_contents);
send_open_quest_file(c, q->meta.name, vq->dat_filename(), xb_filename, vq->meta.quest_number, type, vq->dat_contents);
if (vq->pvr_contents) {
send_open_quest_file(c, q->meta.name, vq->pvr_filename(), xb_filename, vq->meta.quest_number, type, vq->pvr_contents);
}
}
}
static asio::awaitable<void> on_10_ep3_download_quest_menu(shared_ptr<Client> c, uint32_t item_id) {
auto s = c->require_server_state();
if (!is_ep3(c->version())) {
throw runtime_error("Episode 3 quests can only be downloaded by Ep3 clients");
}
if (c->lobby.lock()) {
throw runtime_error("Episode 3 quests can only be downloaded when client is not in a lobby");
}
auto map = s->ep3_download_map_index->get(item_id);
auto vm = map->version(c->language());
auto name = vm->map->name.decode(vm->language);
string filename = std::format("m{:06}p_{:c}.bin", map->map_number, tolower(char_for_language_code(vm->language)));
auto data = (c->version() == Version::GC_EP3_NTE) ? vm->trial_download() : vm->compressed(false);
send_open_quest_file(c, name, filename, "", map->map_number, QuestFileType::EPISODE_3, data);
co_return;
}
static asio::awaitable<void> on_10_patch_switches(shared_ptr<Client> c, uint32_t item_id) {
if (item_id == PatchesMenuItemID::GO_BACK) {
send_main_menu(c);
@@ -3036,6 +3053,9 @@ static asio::awaitable<void> on_10(shared_ptr<Client> c, Channel::Message& msg)
case MenuID::QUEST_EP2:
co_await on_10_quest_menu(c, base_cmd.item_id);
break;
case MenuID::QUEST_EP3:
co_await on_10_ep3_download_quest_menu(c, base_cmd.item_id);
break;
case MenuID::PATCH_SWITCHES:
co_await on_10_patch_switches(c, base_cmd.item_id);
break;
@@ -3207,7 +3227,7 @@ static asio::awaitable<void> on_A2(shared_ptr<Client> c, Channel::Message& msg)
}
}
send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, l->episode);
send_quest_categories_menu(c, menu_type, l->episode);
l->set_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS);
}
@@ -4075,7 +4095,7 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
throw runtime_error("non-leader sent 02DF command");
}
auto vq = l->quest->version(Version::BB_V4, c->language());
if (vq->challenge_template_index != static_cast<ssize_t>(cmd.template_index)) {
if (vq->meta.challenge_template_index != static_cast<ssize_t>(cmd.template_index)) {
throw runtime_error("challenge template index in quest metadata does not match index sent by client");
}
@@ -4091,7 +4111,7 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index);
}
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version()));
lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version()));
lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true);
}
@@ -4103,6 +4123,12 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
case 0x03DF: {
const auto& cmd = check_size_t<C_SetChallengeModeDifficulty_BB_03DF>(msg.data);
if (!l->quest) {
throw runtime_error("challenge mode difficulty config command sent in non-challenge game");
}
if (static_cast<uint32_t>(l->quest->meta.challenge_difficulty) != cmd.difficulty) {
throw runtime_error("incorrect difficulty level");
}
if (l->difficulty != cmd.difficulty) {
l->difficulty = cmd.difficulty;
l->create_item_creator();
@@ -4112,8 +4138,13 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
}
case 0x04DF: {
const auto& cmd = check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data);
l->challenge_exp_multiplier = cmd.exp_multiplier;
check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data);
if (!l->quest) {
throw runtime_error("challenge mode difficulty config command sent in non-challenge game");
}
l->challenge_exp_multiplier = (l->quest->meta.challenge_exp_multiplier < 0)
? 1.0
: l->quest->meta.challenge_exp_multiplier;
l->log.info_f("(Challenge mode) EXP multiplier set to {:g}", l->challenge_exp_multiplier);
break;
}
@@ -4907,7 +4938,7 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
shared_ptr<const Quest> q;
try {
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version);
q = s->default_quest_index->get(quest_num);
q = s->quest_index->get(quest_num);
} catch (const out_of_range&) {
throw std::logic_error("cannot find patch enable quest after it was previously found during login");
}
@@ -4915,12 +4946,12 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
if (!vq) {
throw std::logic_error("cannot find patch enable quest version after it was previously found during login");
}
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name);
c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name);
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_filename();
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
co_return;
}
// Now l is not null
@@ -4990,8 +5021,8 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set");
should_resume_game = false;
@@ -5058,8 +5089,8 @@ static asio::awaitable<void> on_99(shared_ptr<Client> c, Channel::Message& msg)
string bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename();
send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents);
send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents);
send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set");
+10 -11
View File
@@ -3105,7 +3105,8 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
if (l->drop_mode != ServerDropMode::DISABLED) {
EnemyType boss_enemy_type = EnemyType::NONE;
bool is_ep2 = (l->episode == Episode::EP2);
if ((l->episode == Episode::EP1) && (c->floor == 0x0E)) {
uint8_t area = l->area_for_floor(c->version(), c->floor);
if ((l->episode == Episode::EP1) && (area == 0x0E)) {
// On Normal, Dark Falz does not have a third phase, so send the drop
// request after the end of the second phase. On all other difficulty
// levels, send it after the third phase.
@@ -3114,7 +3115,7 @@ static asio::awaitable<void> on_set_quest_flag(shared_ptr<Client> c, SubcommandM
} else if ((difficulty != 0) && (flag_num == 0x0037)) {
boss_enemy_type = EnemyType::DARK_FALZ_3;
}
} else if (is_ep2 && (flag_num == 0x0057) && (c->floor == 0x0D)) {
} else if (is_ep2 && (flag_num == 0x0057) && (area == 0x0D)) {
boss_enemy_type = EnemyType::OLGA_FLOW_2;
}
@@ -3187,9 +3188,9 @@ static asio::awaitable<void> on_sync_quest_register(shared_ptr<Client> c, Subcom
// If the lock status register is being written, change the game's flags to
// allow or forbid joining
if (l->quest &&
l->quest->joinable &&
(l->quest->lock_status_register >= 0) &&
(cmd.register_number == l->quest->lock_status_register)) {
l->quest->meta.joinable &&
(l->quest->meta.lock_status_register >= 0) &&
(cmd.register_number == l->quest->meta.lock_status_register)) {
// Lock if value is nonzero; unlock if value is zero
if (cmd.value.as_int) {
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS);
@@ -3723,10 +3724,8 @@ static asio::awaitable<void> on_set_entity_pos_and_angle_6x17(shared_ptr<Client>
if (l->episode != Episode::EP1) {
throw runtime_error("client sent 6x17 command in non-Ep1 game");
}
// TODO: If a quest is loaded, we should use the quest's floor assignments
// here instead of a constant
if (c->floor != 0x0D) {
throw runtime_error("client sent 6x17 command on floor other than Vol Opt");
if (l->area_for_floor(c->version(), c->floor) != 0x0D) {
throw runtime_error("client sent 6x17 command in area other than Vol Opt");
}
if (cmd.header.entity_id != c->lobby_client_id) {
// If the target is on a different floor or does not exist, just drop the
@@ -4606,7 +4605,7 @@ static asio::awaitable<void> on_challenge_mode_retry_or_quit(shared_ptr<Client>
throw runtime_error("6x97 sent by non-leader");
}
if (l->is_game() && (cmd.is_retry == 1) && l->quest && (l->quest->challenge_template_index >= 0)) {
if (l->is_game() && (cmd.is_retry == 1) && l->quest && (l->quest->meta.challenge_template_index >= 0)) {
auto s = l->require_server_state();
for (auto& m : l->floor_item_managers) {
@@ -4621,7 +4620,7 @@ static asio::awaitable<void> on_challenge_mode_retry_or_quit(shared_ptr<Client>
if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index);
}
lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(c->version()));
lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(c->version()));
lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true);
}
+33 -27
View File
@@ -1645,10 +1645,10 @@ void send_quest_menu_t(
}
auto& e = entries.emplace_back();
e.menu_id = ((it.second->episode == Episode::EP1) || (it.second->episode == Episode::EP3)) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->meta.quest_number;
e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->meta.short_description), c->language());
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
@@ -1666,21 +1666,31 @@ void send_quest_menu_bb(
}
auto& e = entries.emplace_back();
e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2;
e.item_id = it.second->quest_number;
e.name.encode(vq->name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language());
e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->meta.quest_number;
e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->meta.short_description), c->language());
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
void send_ep3_download_quest_menu(shared_ptr<Client> c) {
auto s = c->require_server_state();
vector<S_QuestMenuEntry_DC_GC_A2_A4> entries;
for (const auto& it : s->ep3_download_map_index->all()) {
auto vm = it.second->version(c->language());
auto& e = entries.emplace_back();
e.menu_id = MenuID::QUEST_EP3;
e.item_id = it.first; // map_number
e.name.encode(vm->map->name.decode(vm->language), c->language());
e.short_description.encode(add_color(vm->map->location_name.decode(vm->language)), c->language());
}
send_command_vt(c, 0xA4, entries.size(), entries);
}
template <typename EntryT>
void send_quest_categories_menu_t(
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
QuestIndex::IncludeCondition include_condition = nullptr;
if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
auto l = c->lobby.lock();
@@ -1694,7 +1704,8 @@ void send_quest_categories_menu_t(
}
vector<EntryT> entries;
for (const auto& cat : quest_index->categories(menu_type, episode, version_flags, include_condition)) {
auto s = c->require_server_state();
for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) {
auto& e = entries.emplace_back();
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1;
e.item_id = cat->category_id;
@@ -1702,7 +1713,7 @@ void send_quest_categories_menu_t(
e.short_description.encode(add_color(cat->description), c->language());
}
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD) || (menu_type == QuestMenuType::EP3_DOWNLOAD);
bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD);
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
}
@@ -1736,15 +1747,11 @@ void send_quest_menu(
}
}
void send_quest_categories_menu(
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
switch (c->version()) {
case Version::PC_NTE:
case Version::PC_V2:
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_type, episode);
break;
case Version::DC_NTE:
case Version::DC_11_2000:
@@ -1754,13 +1761,13 @@ void send_quest_categories_menu(
case Version::GC_V3:
case Version::GC_EP3_NTE:
case Version::GC_EP3:
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_type, episode);
break;
case Version::XB_V3:
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_type, episode);
break;
case Version::BB_V4:
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, quest_index, menu_type, episode);
send_quest_categories_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_type, episode);
break;
default:
throw logic_error("unimplemented versioned command");
@@ -4023,12 +4030,11 @@ void send_open_quest_file(
if (chunk_bytes > 0x400) {
chunk_bytes = 0x400;
}
send_quest_file_chunk(c, filename, offset / 0x400,
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
}
// If there are still chunks to send, track the file so the chunk
// acknowledgement handler (13 or A7) cna know what to send next
// acknowledgement handler (13 or A7) can know what to send next
if (chunks_to_send < total_chunks) {
c->sending_files.emplace(filename, contents);
c->log.info_f("Opened file {}", filename);
+2 -5
View File
@@ -312,11 +312,8 @@ void send_quest_menu(
std::shared_ptr<Client> c,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
bool is_download_menu);
void send_quest_categories_menu(
std::shared_ptr<Client> c,
std::shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode);
void send_ep3_download_quest_menu(std::shared_ptr<Client> c);
void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
void send_lobby_list(std::shared_ptr<Client> c);
void send_player_records(
+8 -16
View File
@@ -394,10 +394,6 @@ shared_ptr<const vector<string>> ServerState::information_contents_for_client(sh
return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3;
}
shared_ptr<const QuestIndex> ServerState::quest_index(Version version) const {
return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index;
}
size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
const auto& min_levels = is_v4(version)
? this->min_levels_v4
@@ -512,8 +508,8 @@ ItemData ServerState::parse_item_description(Version version, const string& desc
}
shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->common_item_set) {
return q->common_item_set;
if (q && q->meta.common_item_set) {
return q->meta.common_item_set;
} else if (is_v1_or_v2(logic_version)) {
// TODO: We should probably have a v1 common item set at some point too
return this->common_item_sets.at("common-table-v1-v2");
@@ -525,8 +521,8 @@ shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_versi
}
shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->rare_item_set) {
return q->rare_item_set;
if (q && q->meta.rare_item_set) {
return q->meta.rare_item_set;
} else if (is_v1(logic_version)) {
return this->rare_item_sets.at("rare-table-v1");
} else if (is_v2(logic_version)) {
@@ -2157,6 +2153,8 @@ void ServerState::load_ep3_cards() {
void ServerState::load_ep3_maps() {
config_log.info_f("Collecting Episode 3 maps");
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps");
config_log.info_f("Collecting Episode 3 download maps");
this->ep3_download_map_index = make_shared<Episode3::MapIndex>("system/ep3/maps-download");
}
void ServerState::load_ep3_tournament_state() {
@@ -2169,14 +2167,8 @@ void ServerState::load_ep3_tournament_state() {
void ServerState::load_quest_index() {
config_log.info_f("Collecting quests");
this->default_quest_index = make_shared<QuestIndex>("system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets, false);
config_log.info_f("Collecting Episode 3 download quests");
this->ep3_download_quest_index = make_shared<QuestIndex>(
"system/ep3/maps-download",
this->quest_category_index,
unordered_map<string, shared_ptr<const CommonItemSet>>{},
unordered_map<string, shared_ptr<const RareItemSet>>{},
true);
this->quest_index = make_shared<QuestIndex>(
"system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets);
}
void ServerState::compile_functions() {
+2 -3
View File
@@ -183,13 +183,13 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const Episode3::CardIndex> ep3_card_index;
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial;
std::shared_ptr<const Episode3::MapIndex> ep3_map_index;
std::shared_ptr<const Episode3::MapIndex> ep3_download_map_index;
std::shared_ptr<const Episode3::COMDeckIndex> ep3_com_deck_index;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_default_ex_values;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_ex_values;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_final_round_ex_values;
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> default_quest_index;
std::shared_ptr<const QuestIndex> ep3_download_quest_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTableV2> level_table_v1_v2;
std::shared_ptr<const LevelTable> level_table_v3;
std::shared_ptr<const LevelTable> level_table_v4;
@@ -375,7 +375,6 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
}
std::shared_ptr<const std::vector<std::string>> information_contents_for_client(std::shared_ptr<const Client> c) const;
std::shared_ptr<const QuestIndex> quest_index(Version version) const;
size_t default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const;
+1 -1
View File
@@ -703,7 +703,7 @@ ShellCommand c_create_tournament(
+[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args);
string map_name = get_quoted_string(args.args);
auto map = args.s->ep3_map_index->for_name(map_name);
auto map = args.s->ep3_map_index->get(map_name);
uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
Episode3::Rules rules;
rules.set_defaults();