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
+1
View File
@@ -111,6 +111,7 @@ set(SOURCES
src/PSOGCObjectGraph.cc src/PSOGCObjectGraph.cc
src/PSOProtocol.cc src/PSOProtocol.cc
src/Quest.cc src/Quest.cc
src/QuestMetadata.cc
src/QuestScript.cc src/QuestScript.cc
src/RareItemSet.cc src/RareItemSet.cc
src/ReceiveCommands.cc src/ReceiveCommands.cc
+1 -1
View File
@@ -318,7 +318,7 @@ For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/. For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/.
Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/battle/b88001.json for all of the available options and how to use them. Some of the options are: Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are:
- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met - Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met
- Enable the quest to be joined while in progress - Enable the quest to be joined while in progress
- Override the common and/or rare item tables and set the allowed drop modes - Override the common and/or rare item tables and set the allowed drop modes
+15 -9
View File
@@ -2074,8 +2074,7 @@ ChatCommandDefinition cc_quest(
a.check_is_game(true); a.check_is_game(true);
auto s = a.c->require_server_state(); 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->get(stoul(a.text));
auto q = s->quest_index(effective_version)->get(stoul(a.text));
if (!q) { if (!q) {
throw precondition_failed("$C6Quest not found"); throw precondition_failed("$C6Quest not found");
} }
@@ -2085,11 +2084,20 @@ ChatCommandDefinition cc_quest(
if (l->count_clients() > 1) { if (l->count_clients() > 1) {
throw precondition_failed("$C6This command can only\nbe used with no\nother players present"); 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"); 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); set_lobby_quest(a.c->require_lobby(), q, true);
co_return; co_return;
}); });
@@ -2365,8 +2373,9 @@ ChatCommandDefinition cc_sound(
bool echo_to_all = (!a.text.empty() && a.text[0] == '!'); 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); 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 auto l = a.c->require_lobby();
G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, static_cast<uint8_t>(a.c->floor), 0x00, a.c->lobby_client_id, sound_id}; 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) { if (!echo_to_all) {
send_command_t(a.c, 0x60, 0x00, cmd); send_command_t(a.c, 0x60, 0x00, cmd);
} else if (a.c->proxy_session) { } 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"); 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; uint8_t area, layout_var;
auto s = a.c->require_server_state(); auto s = a.c->require_server_state();
if (l->episode != Episode::EP3) { if (l->episode != Episode::EP3) {
auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty); area = l->area_for_floor(a.c->version(), a.c->floor);
area = sdt->default_area_for_floor(l->episode, a.c->floor);
layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00; layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00;
} else { } else {
area = a.c->floor; 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())) { if (!q->has_version_any_language(this->version())) {
return false; 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( bool Client::can_play_quest(
@@ -423,10 +424,11 @@ bool Client::can_play_quest(
if (!q->has_version_any_language(this->version())) { if (!q->has_version_any_language(this->version())) {
return false; return false;
} }
if (num_players > q->max_players) { if (num_players > q->meta.max_players) {
return false; 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 { 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) MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
: language(language), : language(language),
compressed_data(std::move(compressed_data)) { compressed_data(make_shared<string>(std::move(compressed_data))) {
string decompressed = prs_decompress(this->compressed_data); string decompressed = prs_decompress(*this->compressed_data);
if (decompressed.size() == sizeof(MapDefinitionTrial)) { if (decompressed.size() == sizeof(MapDefinitionTrial)) {
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data())); this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
} else if (decompressed.size() == sizeof(MapDefinition)) { } else if (decompressed.size() == sizeof(MapDefinition)) {
@@ -2646,21 +2646,30 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
return this->trial_map; return this->trial_map;
} }
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const { std::shared_ptr<const std::string> MapIndex::VersionedMap::compressed(bool trial) const {
if (is_nte) { if (trial) {
if (this->compressed_trial_data.empty()) { if (!this->compressed_data_trial) {
auto md = this->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 { } else {
if (this->compressed_data.empty()) { if (!this->compressed_data) {
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map)); this->compressed_data = make_shared<string>(prs_compress(this->map.get(), sizeof(*this->map)));
} }
return this->compressed_data; 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) MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
: map_number(initial_version->map->map_number), : map_number(initial_version->map->map_number),
initial_version(initial_version) { initial_version(initial_version) {
@@ -2704,6 +2713,7 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
} }
MapIndex::MapIndex(const string& directory) { MapIndex::MapIndex(const string& directory) {
map<uint32_t, shared_ptr<Map>> mutable_maps;
for (const auto& item : std::filesystem::directory_iterator(directory)) { for (const auto& item : std::filesystem::directory_iterator(directory)) {
string filename = item.path().filename().string(); string filename = item.path().filename().string();
try { try {
@@ -2756,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) {
} }
string name = vm->map->name.decode(vm->language); string name = vm->map->name.decode(vm->language);
auto map_it = this->maps.find(vm->map->map_number); auto map_it = mutable_maps.find(vm->map->map_number);
if (map_it == this->maps.end()) { if (map_it == mutable_maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first; 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} {} ({}; {})", static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
filename, filename,
vm->map->map_number, 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; 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) { COMDeckIndex::COMDeckIndex(const string& filename) {
try { try {
auto json = phosg::JSON::parse(phosg::load_file(filename)); 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 struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
// If tag is not 0x00000100, the game considers the map to be corrupt in // 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 // 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; /* 0000 */ be_uint32_t tag;
/* 0004 */ be_uint32_t map_number; // Must be unique across all maps /* 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); VersionedMap(std::string&& compressed_data, uint8_t language);
std::shared_ptr<const MapDefinitionTrial> trial() const; 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: private:
mutable std::shared_ptr<const MapDefinitionTrial> trial_map; mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
mutable std::string compressed_data; mutable std::shared_ptr<std::string> compressed_data;
mutable std::string compressed_trial_data; mutable std::shared_ptr<std::string> compressed_data_trial;
mutable std::shared_ptr<std::string> download_data_trial;
}; };
class Map { class Map {
@@ -1624,14 +1627,20 @@ public:
}; };
const std::string& get_compressed_list(size_t num_players, uint8_t language) const; 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; inline std::shared_ptr<const Map> get(uint32_t id) const {
std::shared_ptr<const Map> for_name(const std::string& name) const; return this->maps.at(id);
std::set<uint32_t> all_numbers() const; }
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: private:
// The compressed map lists are generated on demand from the maps map below // The compressed map lists are generated on demand from the maps map below
mutable std::vector<std::array<std::string, 4>> compressed_map_lists; 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; 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); const auto& compressed = vm->compressed(is_nte);
phosg::StringWriter w; phosg::StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3); 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.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
w.write(compressed); w.write(*compressed);
return std::move(w.str()); 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); const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP 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)) { 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(); this->send_6xB6x41_to_all_clients();
} }
} }
+1 -1
View File
@@ -357,7 +357,7 @@ void Tournament::init() {
bool is_registration_complete; bool is_registration_complete;
if (!this->source_json.is_null()) { if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name"); 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->rules = Rules(this->source_json.at("rules"));
this->flags = this->source_json.get_int("flags", 0x02); this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) { 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( asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json() {
std::shared_ptr<const QuestIndex> quest_index) {
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::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)); ret = co_await this->generate_rare_table_json(req.path.substr(20));
} else if (req.path == "/y/data/quests") { } else if (req.path == "/y/data/quests") {
this->require_GET(req); 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") { } else if (req.path == "/y/data/config") {
this->require_GET(req); this->require_GET(req);
ret = this->state->config_json; 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; 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_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_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); void require_GET(const HTTPRequest& req);
phosg::JSON require_POST(const HTTPRequest& req); phosg::JSON require_POST(const HTTPRequest& req);
-1
View File
@@ -9,7 +9,6 @@
#include <vector> #include <vector>
#include "PlayerSubordinates.hh" #include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
#include "TeamIndex.hh" #include "TeamIndex.hh"
+9 -1
View File
@@ -186,6 +186,14 @@ void Lobby::reset_next_item_ids() {
this->next_game_item_id = 0xCC000000; 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 { shared_ptr<ServerState> Lobby::require_server_state() const {
auto s = this->server_state.lock(); auto s = this->server_state.lock();
if (!s) { if (!s) {
@@ -234,7 +242,7 @@ void Lobby::create_item_creator(Version logic_version) {
this->difficulty, this->difficulty,
this->effective_section_id(), this->effective_section_id(),
rand_crypt, rand_crypt,
this->quest ? this->quest->battle_rules : nullptr); this->quest ? this->quest->meta.battle_rules : nullptr);
} }
uint8_t Lobby::effective_section_id() const { 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); 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<ServerState> require_server_state() const;
std::shared_ptr<ChallengeParameters> require_challenge_params() const; std::shared_ptr<ChallengeParameters> require_challenge_params() const;
void create_item_creator(Version logic_version = Version::UNKNOWN); 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_cards();
s->load_ep3_maps(); 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()); phosg::log_info_f("{} maps", map_ids.size());
for (uint32_t map_id : map_ids) { for (const auto& [map_number, map] : map_ids) {
auto map = s->ep3_map_index->for_number(map_id);
const auto& vms = map->all_versions(); const auto& vms = map->all_versions();
for (size_t language = 0; language < vms.size(); language++) { for (size_t language = 0; language < vms.size(); language++) {
if (!vms[language]) { if (!vms[language]) {
@@ -2928,7 +2927,7 @@ Action a_check_supermaps(
SuperMap::EfficiencyStats all_quests_eff; SuperMap::EfficiencyStats all_quests_eff;
uint32_t random_seed = args.get<uint32_t>("random-seed", 0, phosg::Arguments::IntFormat::HEX); 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); auto supermap = it.second->get_supermap(random_seed);
if (!supermap) { if (!supermap) {
throw logic_error("quest does not have a supermap, even with a specified random seed"); 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) { if (save_disassembly) {
string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed); string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt"); 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()); supermap->print(f.get());
filename_token = " => " + filename; 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); string filename = std::format("supermap_quest_{}_{:08X}_enemy_counts.txt", it.first, random_seed);
auto f = phosg::fopen_unique(filename, "wt"); 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"); 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++) { 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); 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); 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( Action a_parse_object_graph(
"parse-object-graph", nullptr, +[](phosg::Arguments& args) { "parse-object-graph", nullptr, +[](phosg::Arguments& args) {
uint32_t root_object_address = args.get<uint32_t>("root", phosg::Arguments::IntFormat::HEX); 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; 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 // For some inscrutable reason, Pioneer 2's area number in Episode 4 is
// discontiguous with all the rest. Why, Sega?? // discontiguous with all the rest. Why, Sega??
static const array<uint8_t, 0x12> areas_ep1 = { 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: case Episode::EP1:
return areas_ep1.at(floor); return areas_ep1.at(floor);
case Episode::EP2: { 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); return areas.at(floor);
} }
case Episode::EP4: 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) { SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) {
if (is_big_endian(this->version)) { if (is_big_endian(this->version)) {
this->load_table_t<true>(data); 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) { switch (type) {
case EnemyType::HILDEBEAR: case EnemyType::HILDEBEAR:
return this->hildeblue; 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); 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)) { if ((type == EnemyType::MERICARAND) || (rare_type != type)) {
unordered_map<uint32_t, float> det_cache; 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) { for (Version v : ALL_NON_PATCH_VERSIONS) {
// Skip this version if the enemy doesn't exist there // Skip this version if the enemy doesn't exist there
uint16_t relative_enemy_index = ene->version(v).relative_enemy_index; 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( std::vector<std::string> map_filenames_for_variations(
Episode episode, GameMode mode, const Variations& variations, FilenameType type) const; 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; uint8_t default_area_for_floor(Episode episode, uint8_t floor) const;
protected: protected:
@@ -672,7 +673,7 @@ public:
RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate); RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate);
explicit RareEnemyRates(const phosg::JSON& json); explicit RareEnemyRates(const phosg::JSON& json);
uint32_t for_enemy_type(EnemyType type) const; uint32_t get(EnemyType type) const;
std::string str() const; std::string str() const;
phosg::JSON json() 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 GAME = 0x44000044;
constexpr uint32_t QUEST_EP1 = 0x55010155; constexpr uint32_t QUEST_EP1 = 0x55010155;
constexpr uint32_t QUEST_EP2 = 0x55020255; 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 // See the decsription of the A2 command in CommandFormats.hh for why these
// menu IDs don't fit the rest of the pattern. // menu IDs don't fit the rest of the pattern.
constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001; constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001;
+157 -400
View File
@@ -194,10 +194,10 @@ struct PSODownloadQuestHeader {
} __packed_ws__(PSODownloadQuestHeader, 8); } __packed_ws__(PSODownloadQuestHeader, 8);
void VersionedQuest::assert_valid() const { void VersionedQuest::assert_valid() const {
if (this->category_id == 0xFFFFFFFF) { if (this->meta.category_id == 0xFFFFFFFF) {
throw runtime_error("category ID is not set"); 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"); throw runtime_error("quest number is not set");
} }
if (this->version == Version::UNKNOWN) { if (this->version == Version::UNKNOWN) {
@@ -206,96 +206,107 @@ void VersionedQuest::assert_valid() const {
if (this->language == 0xFF) { if (this->language == 0xFF) {
throw runtime_error("language is not set"); throw runtime_error("language is not set");
} }
if (this->episode == Episode::NONE) { switch (this->meta.episode) {
throw runtime_error("episode is not set"); 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"); throw runtime_error("max players is not set");
} }
if (!this->bin_contents) { if (!this->bin_contents) {
throw runtime_error("bin file is missing"); 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"); 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"); 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"); 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"); 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"); throw runtime_error("default drop mode is not allowed");
} }
} }
string VersionedQuest::bin_filename() const { string VersionedQuest::bin_filename() const {
if (this->episode == Episode::EP3) { return std::format("quest{}.bin", this->meta.quest_number);
return std::format("m{:06}p_e.bin", this->quest_number);
} else {
return std::format("quest{}.bin", this->quest_number);
}
} }
string VersionedQuest::dat_filename() const { string VersionedQuest::dat_filename() const {
if (this->episode == Episode::EP3) { return std::format("quest{}.dat", this->meta.quest_number);
throw logic_error("Episode 3 quests do not have .dat files");
} else {
return std::format("quest{}.dat", this->quest_number);
}
} }
string VersionedQuest::pvr_filename() const { string VersionedQuest::pvr_filename() const {
if (this->episode == Episode::EP3) { return std::format("quest{}.pvr", this->meta.quest_number);
throw logic_error("Episode 3 quests do not have .pvr files");
} else {
return std::format("quest{}.pvr", this->quest_number);
}
} }
string VersionedQuest::xb_filename() const { string VersionedQuest::xb_filename() const {
if (this->episode == Episode::EP3) { return std::format("quest{}_{}.dat",
throw logic_error("Episode 3 quests do not have Xbox filenames"); this->meta.quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
} else {
return std::format("quest{}_{}.dat", this->quest_number, static_cast<char>(tolower(char_for_language_code(this->language))));
}
} }
string VersionedQuest::encode_qst() const { string VersionedQuest::encode_qst() const {
unordered_map<string, shared_ptr<const string>> files; 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{}.bin", this->meta.quest_number), this->bin_contents);
files.emplace(std::format("quest{}.dat", this->quest_number), this->dat_contents); files.emplace(std::format("quest{}.dat", this->meta.quest_number), this->dat_contents);
if (this->pvr_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)))); string xb_filename = std::format("quest{}_{}.dat",
return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded); 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::Quest(shared_ptr<const VersionedQuest> initial_version)
: quest_number(initial_version->quest_number), : meta(initial_version->meta), supermap(nullptr) {
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) {
this->add_version(initial_version); this->add_version(initial_version);
} }
@@ -305,8 +316,9 @@ phosg::JSON Quest::json() const {
versions_json.emplace_back(phosg::JSON::dict({ versions_json.emplace_back(phosg::JSON::dict({
{"Version", phosg::name_for_enum(vq->version)}, {"Version", phosg::name_for_enum(vq->version)},
{"Language", name_for_language_code(vq->language)}, {"Language", name_for_language_code(vq->language)},
{"ShortDescription", vq->short_description}, {"Name", vq->meta.name},
{"LongDescription", vq->long_description}, {"ShortDescription", vq->meta.short_description},
{"LongDescription", vq->meta.long_description},
{"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)}, {"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)},
{"DATFileSize", vq->dat_contents ? vq->dat_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)}, {"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)},
@@ -314,23 +326,7 @@ phosg::JSON Quest::json() const {
} }
return phosg::JSON::dict({ return phosg::JSON::dict({
{"Number", this->quest_number}, {"Metadata", this->meta.json()},
{"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)},
{"Versions", std::move(versions_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) { void Quest::add_version(shared_ptr<const VersionedQuest> vq) {
if (this->quest_number != vq->quest_number) { this->meta.assert_compatible(vq->meta);
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->versions.emplace(this->versions_key(vq->version, vq->language), vq); 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; 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) { if (save_to_cache) {
this->supermap = supermap; this->supermap = supermap;
} }
static_game_data_log.info_f("Constructed {} supermap for quest {} ({})", 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; return supermap;
} }
@@ -522,8 +413,7 @@ QuestIndex::QuestIndex(
const string& directory, const string& directory,
shared_ptr<const QuestCategoryIndex> category_index, shared_ptr<const QuestCategoryIndex> category_index,
const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets, const unordered_map<string, shared_ptr<const CommonItemSet>>& common_item_sets,
const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets, const unordered_map<string, shared_ptr<const RareItemSet>>& rare_item_sets)
bool is_ep3)
: directory(directory), : directory(directory),
category_index(category_index) { category_index(category_index) {
@@ -533,7 +423,7 @@ QuestIndex::QuestIndex(
}; };
struct BINFileData { struct BINFileData {
string filename; string filename;
unique_ptr<QuestMetadata> metadata; shared_ptr<const AssembledQuestScript> assembled;
shared_ptr<const string> data; shared_ptr<const string> data;
}; };
struct DATFileData { struct DATFileData {
@@ -547,12 +437,6 @@ QuestIndex::QuestIndex(
map<string, FileData> json_files; map<string, FileData> json_files;
map<string, uint32_t> categories; map<string, uint32_t> categories;
for (const auto& cat : this->category_index->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) { 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) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("file " + basename + " exists in multiple categories"); 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) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) {
throw runtime_error("bin file " + basename + " exists in multiple categories"); throw runtime_error("bin file " + basename + " exists in multiple categories");
} }
@@ -581,9 +465,7 @@ QuestIndex::QuestIndex(
auto& entry = emplace_ret.first->second; auto& entry = emplace_ret.first->second;
entry.filename = filename; entry.filename = filename;
entry.data = data_ptr; entry.data = data_ptr;
if (metadata) { entry.assembled = assembled;
entry.metadata = make_unique<QuestMetadata>(*metadata);
}
if (!(data_ptr->size() & 0x3FF)) { if (!(data_ptr->size() & 0x3FF)) {
data_ptr->push_back(0x00); data_ptr->push_back(0x00);
} }
@@ -614,7 +496,7 @@ QuestIndex::QuestIndex(
} }
string file_path = cat_path + "/" + filename; string file_path = cat_path + "/" + filename;
unique_ptr<AssembledQuestScript> assembled; shared_ptr<AssembledQuestScript> assembled;
try { try {
string orig_filename = filename; string orig_filename = filename;
string file_data; string file_data;
@@ -629,7 +511,7 @@ QuestIndex::QuestIndex(
filename.resize(filename.size() - 4); filename.resize(filename.size() - 4);
} else if (filename.ends_with(".bin.txt")) { } else if (filename.ends_with(".bin.txt")) {
string include_dir = phosg::dirname(file_path); 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), phosg::load_file(file_path),
{include_dir, "system/quests/includes"}, {include_dir, "system/quests/includes"},
{include_dir, "system/quests/includes", "system/client-functions/System"})); {include_dir, "system/quests/includes", "system/client-functions/System"}));
@@ -655,9 +537,9 @@ QuestIndex::QuestIndex(
if (extension == "json") { if (extension == "json") {
add_file(json_files, file_basename, orig_filename, std::move(file_data), false); add_file(json_files, file_basename, orig_filename, std::move(file_data), false);
} else if (extension == "bin" || extension == "mnm") { } 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") { } 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") { } else if (extension == "dat") {
add_dat_file(file_basename, orig_filename, std::move(file_data)); add_dat_file(file_basename, orig_filename, std::move(file_data));
} else if (extension == "datd") { } else if (extension == "datd") {
@@ -710,28 +592,18 @@ QuestIndex::QuestIndex(
version_token = std::move(filename_tokens[1]); version_token = std::move(filename_tokens[1]);
language_token = std::move(filename_tokens[2]); language_token = std::move(filename_tokens[2]);
} }
vq->category_id = categories.at(basename); vq->meta.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;
if (entry.assembled) {
vq->meta.quest_number = entry.assembled->quest_number;
vq->version = entry.assembled->version;
vq->language = entry.assembled->language;
} else { } else {
// Get the number from the first token // Get the number from the first token
if (quest_number_token.empty()) { if (quest_number_token.empty()) {
throw runtime_error("quest number token is missing"); 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 // Get the version from the second token
static const unordered_map<string, Version> name_to_version({ 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"); throw runtime_error("language token is not a single character");
} }
vq->language = language_code_for_char(language_token[0]); 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 DATFileData* dat_filedata = nullptr;
const FileData* pvr_filedata = nullptr; const FileData* pvr_filedata = nullptr;
if (!::is_ep3(vq->version)) { try {
// Look for dat and pvr files with the same basename as the bin file; if dat_filedata = &dat_files.at(basename);
// not found, look for them without the language suffix } catch (const out_of_range&) {
try { try {
dat_filedata = &dat_files.at(basename); dat_filedata = &dat_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) { } catch (const out_of_range&) {
try { throw runtime_error("no dat file found for bin file " + basename);
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);
}
} }
}
try {
pvr_filedata = &pvr_files.at(basename);
} catch (const out_of_range&) {
try { try {
pvr_filedata = &pvr_files.at(basename); pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token);
} catch (const out_of_range&) { } catch (const out_of_range&) {
try { // pvr files aren't required (and most quests do not have them), so
pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token); // don't fail if it's missing
} 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
}
} }
} }
vq->bin_contents = entry.data; vq->bin_contents = entry.data;
@@ -924,64 +694,56 @@ QuestIndex::QuestIndex(
if (json_filedata) { if (json_filedata) {
auto metadata_json = phosg::JSON::parse(*json_filedata->data); auto metadata_json = phosg::JSON::parse(*json_filedata->data);
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { 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&) { } catch (const out_of_range&) {
} }
try { try {
vq->lock_status_register = metadata_json.get_int("LockStatusRegister"); vq->meta.default_drop_mode = phosg::enum_for_name<ServerDropMode>(metadata_json.at("DefaultDropMode").as_string());
} 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());
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
} }
vq->assert_valid(); 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; string filenames_str = entry.filename;
if (dat_filedata) { if (dat_filedata) {
filenames_str += std::format("/{}", dat_filedata->filename); filenames_str += std::format("/{}", dat_filedata->filename);
@@ -992,30 +754,32 @@ QuestIndex::QuestIndex(
if (json_filedata) { if (json_filedata) {
filenames_str += std::format("/{}", json_filedata->filename); 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()) { if (q_it != this->quests_by_number.end()) {
q_it->second->add_version(vq); 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, filenames_str,
phosg::name_for_enum(vq->version), phosg::name_for_enum(vq->version),
char_for_language_code(vq->language), char_for_language_code(vq->language),
vq->quest_number, vq->meta.quest_number,
vq->name); vq->meta.name,
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
} else { } else {
auto q = make_shared<Quest>(vq); auto q = make_shared<Quest>(vq);
this->quests_by_number.emplace(vq->quest_number, q); this->quests_by_number.emplace(vq->meta.quest_number, q);
this->quests_by_name.emplace(vq->name, q); this->quests_by_name.emplace(vq->meta.name, q);
this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, 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 {} ({}) ({}, {} ({}), {})", static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}",
filenames_str, filenames_str,
phosg::name_for_enum(vq->version), phosg::name_for_enum(vq->version),
char_for_language_code(vq->language), char_for_language_code(vq->language),
vq->quest_number, vq->meta.quest_number,
vq->name, vq->meta.name,
name_for_episode(vq->episode), name_for_episode(vq->meta.episode),
category_name, category_name,
vq->category_id, vq->meta.category_id,
vq->joinable ? "joinable" : "not joinable"); vq->meta.joinable ? "joinable" : "not joinable",
phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12));
} }
} catch (const exception& e) { } catch (const exception& e) {
static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what()); 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; return ret;
} }
for (auto it : category_it->second) { 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; continue;
} }
bool all_required_versions_present = true; 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)); data.resize((data.size() + 3) & (~3));
PSOV2Encryption encr(encryption_seed); PSOV2Encryption encr(encryption_seed);
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader));
data.size() - sizeof(PSODownloadQuestHeader));
data.resize(original_size); data.resize(original_size);
return data; 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 // this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again. // 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); string decompressed_bin = prs_decompress(*this->bin_contents);
void* data_ptr = decompressed_bin.data(); void* data_ptr = decompressed_bin.data();
+5 -44
View File
@@ -13,6 +13,7 @@
#include "ItemParameterTable.hh" #include "ItemParameterTable.hh"
#include "Map.hh" #include "Map.hh"
#include "PlayerSubordinates.hh" #include "PlayerSubordinates.hh"
#include "QuestMetadata.hh"
#include "QuestScript.hh" #include "QuestScript.hh"
#include "RareItemSet.hh" #include "RareItemSet.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
@@ -34,7 +35,6 @@ enum class QuestMenuType {
SOLO = 3, SOLO = 3,
GOVERNMENT = 4, GOVERNMENT = 4,
DOWNLOAD = 5, DOWNLOAD = 5,
EP3_DOWNLOAD = 6,
// 7 can't be used as a menu type (it enables the per-episode filter) // 7 can't be used as a menu type (it enables the per-episode filter)
}; };
@@ -67,35 +67,16 @@ struct QuestCategoryIndex {
}; };
struct VersionedQuest { struct VersionedQuest {
QuestMetadata meta;
// Most of these default values are intentionally invalid; we use these // Most of these default values are intentionally invalid; we use these
// values to check if each field was parsed during quest indexing. // 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; Version version = Version::UNKNOWN;
uint8_t language = 0xFF; 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> bin_contents;
std::shared_ptr<const std::string> dat_contents; std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const MapFile> map_file; std::shared_ptr<const MapFile> map_file;
std::shared_ptr<const std::string> pvr_contents; 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; bool is_dlq_encoded = false;
void assert_valid() const; void assert_valid() const;
@@ -110,27 +91,8 @@ struct VersionedQuest {
}; };
struct Quest { struct Quest {
uint32_t quest_number; QuestMetadata meta;
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;
mutable std::shared_ptr<const SuperMap> supermap; 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; std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
Quest() = delete; Quest() = delete;
@@ -171,8 +133,7 @@ struct QuestIndex {
const std::string& directory, const std::string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index, 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 CommonItemSet>>& common_item_sets,
const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets, const std::unordered_map<std::string, std::shared_ptr<const RareItemSet>>& rare_item_sets);
bool is_ep3);
phosg::JSON json() const; phosg::JSON json() const;
std::shared_ptr<const Quest> get(uint32_t quest_number) 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/Encoding.hh>
#include <phosg/Tools.hh> #include <phosg/Tools.hh>
#include "QuestMetadata.hh"
#include "StaticGameData.hh" #include "StaticGameData.hh"
#include "Text.hh" #include "Text.hh"
#include "Version.hh" #include "Version.hh"
@@ -19,6 +20,18 @@ struct PSOQuestHeaderDCNTE {
/* 0020 */ /* 0020 */
} __packed_ws__(PSOQuestHeaderDCNTE, 0x20); } __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 struct PSOQuestHeaderDC { // Same format for DC v1 and v2
/* 0000 */ le_uint32_t code_offset = 0; /* 0000 */ le_uint32_t code_offset = 0;
/* 0004 */ le_uint32_t function_table_offset = 0; /* 0004 */ le_uint32_t function_table_offset = 0;
@@ -100,7 +113,8 @@ std::string disassemble_quest_script(
bool reassembly_mode = false, bool reassembly_mode = false,
bool use_qedit_names = false); bool use_qedit_names = false);
struct QuestMetadata { struct AssembledQuestScript {
std::string data;
int64_t quest_number = -1; int64_t quest_number = -1;
Version version = Version::UNKNOWN; Version version = Version::UNKNOWN;
uint8_t language = 0xFF; uint8_t language = 0xFF;
@@ -111,13 +125,9 @@ struct QuestMetadata {
std::string short_description; std::string short_description;
std::string long_description; std::string long_description;
}; };
struct AssembledQuestScript {
std::string data;
QuestMetadata metadata;
};
AssembledQuestScript assemble_quest_script( AssembledQuestScript assemble_quest_script(
const std::string& text, const std::string& text,
const std::vector<std::string>& script_include_directories, const std::vector<std::string>& script_include_directories,
const std::vector<std::string>& native_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))) { !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE))) {
shared_ptr<const Quest> q; shared_ptr<const Quest> q;
try { try {
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version); q = s->quest_index->get(s->enable_send_function_call_quest_numbers.at(c->specific_version));
q = s->default_quest_index->get(quest_num);
} catch (const out_of_range&) { } catch (const out_of_range&) {
} }
if (!q) { if (!q) {
c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version); c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version);
} else if (q) { } 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) { if (vq) {
c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL); c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL);
c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE); 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; lobby_data.guild_card_number = c->login->account->account_id;
send_command_t(c, 0x64, 0x01, cmd); send_command_t(c, 0x64, 0x01, cmd);
} else { } 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 bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename(); string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_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(
send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); 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())) { if (!is_v1_or_v2(c->version())) {
send_command(c, 0xAC, 0x00); 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_EP1:
case MenuID::QUEST_EP2: { case MenuID::QUEST_EP2: {
bool is_download_quest = !c->lobby.lock(); bool is_download_quest = !c->lobby.lock();
auto quest_index = s->quest_index(c->version()); if (!s->quest_index) {
if (!quest_index) {
send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest); send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest);
} else { } else {
auto q = quest_index->get(cmd.item_id); auto q = s->quest_index->get(cmd.item_id);
if (!q) { if (!q) {
send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest); send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest);
} else { } else {
@@ -2165,12 +2165,22 @@ static asio::awaitable<void> on_09(shared_ptr<Client> c, Channel::Message& msg)
if (!vq) { if (!vq) {
send_quest_info(c, "$C4Quest does not\nexist for this game\nversion.", 0x00, is_download_quest); send_quest_info(c, "$C4Quest does not\nexist for this game\nversion.", 0x00, is_download_quest);
} else { } 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; 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: { case MenuID::GAME: {
auto game = s->find_lobby(cmd.item_id); 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) { if (game->quest) {
info += (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) ? "$C6Quest: " : "$C4Quest: "; 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"; info += "\n";
} else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { } else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
info += "$C6Quest in progress\n"; info += "$C6Quest in progress\n";
@@ -2396,22 +2406,22 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
lc->delete_overlay(); 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 // 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 // the overlays later; on other versions, we do it at quest start time
// (now) instead, hence the version check above. // (now) instead, hence the version check above.
if (is_v4(lc->version())) { if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index); 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"); lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true); 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())) { if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index); 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"); 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 // 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"); 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"); throw runtime_error("incorrect game mode");
} }
auto s = l->require_server_state(); auto s = l->require_server_state();
if (q->joinable) { if (q->meta.joinable) {
l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS); l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS);
} else { } else {
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS); 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; l->quest = q;
if (l->episode != Episode::EP3) { if (l->episode != Episode::EP3) {
l->episode = q->episode; l->episode = q->meta.episode;
} }
if (l->quest->allowed_drop_modes) { if (l->quest->meta.allowed_drop_modes) {
l->allowed_drop_modes = l->quest->allowed_drop_modes; l->allowed_drop_modes = l->quest->meta.allowed_drop_modes;
l->drop_mode = l->quest->default_drop_mode; 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(); 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(); lc->channel->disconnect();
break; 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 bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename(); string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_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(
send_open_quest_file(lc, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); 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; // 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 // 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; break;
case MainMenuItemID::DOWNLOAD_QUESTS: { case MainMenuItemID::DOWNLOAD_QUESTS: {
QuestMenuType menu_type = QuestMenuType::DOWNLOAD;
if (is_ep3(c->version())) { if (is_ep3(c->version())) {
menu_type = QuestMenuType::EP3_DOWNLOAD; send_ep3_download_quest_menu(c);
// Episode 3 has only download quests, not online quests, so this is } else {
// always the download quest menu. (Episode 3 does actually have send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE);
// 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_quest_categories_menu(c, s->quest_index(c->version()), menu_type, Episode::NONE);
break; 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) { 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 s = c->require_server_state();
auto quest_index = s->quest_index(c->version()); if (!s->quest_index) {
if (!quest_index) {
send_lobby_message_box(c, "$C7Quests are not available."); send_lobby_message_box(c, "$C7Quests are not available.");
co_return; 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(); 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); send_quest_menu(c, quests, !l);
} }
static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t item_id) { 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 s = c->require_server_state();
auto quest_index = s->quest_index(c->version()); if (!s->quest_index) {
if (!quest_index) {
send_lobby_message_box(c, "$C7Quests are not\navailable."); send_lobby_message_box(c, "$C7Quests are not\navailable.");
co_return; co_return;
} }
auto q = quest_index->get(item_id); auto q = s->quest_index->get(item_id);
if (!q) { if (!q) {
send_lobby_message_box(c, "$C7Quest does not exist."); send_lobby_message_box(c, "$C7Quest does not exist.");
co_return; co_return;
@@ -2840,7 +2849,7 @@ static asio::awaitable<void> on_10_quest_menu(shared_ptr<Client> c, uint32_t ite
} }
if (l) { 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."); send_lobby_message_box(c, "$C7Episode 3 quests\ncannot be loaded\nvia this interface.");
co_return; 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."); send_lobby_message_box(c, "$C7Quest does not exist\nfor this game version.");
co_return; co_return;
} }
// Episode 3 uses the download quest commands (A6/A7) but does not vq = vq->create_download_quest(c->language());
// expect the server to have already encrypted the quest files, unlike string xb_filename = vq->xb_filename();
// other versions. QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR;
// TODO: This is not true for Episode 3 Trial Edition. We also would send_open_quest_file(c, q->meta.name, vq->bin_filename(), xb_filename, vq->meta.quest_number, type, vq->bin_contents);
// have to convert the map to a MapDefinitionTrial, though. send_open_quest_file(c, q->meta.name, vq->dat_filename(), xb_filename, vq->meta.quest_number, type, vq->dat_contents);
if (is_ep3(vq->version)) { if (vq->pvr_contents) {
send_open_quest_file(c, q->name, vq->bin_filename(), "", vq->quest_number, QuestFileType::EPISODE_3, vq->bin_contents); send_open_quest_file(c, q->meta.name, vq->pvr_filename(), xb_filename, vq->meta.quest_number, type, vq->pvr_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);
}
} }
} }
} }
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) { static asio::awaitable<void> on_10_patch_switches(shared_ptr<Client> c, uint32_t item_id) {
if (item_id == PatchesMenuItemID::GO_BACK) { if (item_id == PatchesMenuItemID::GO_BACK) {
send_main_menu(c); 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: case MenuID::QUEST_EP2:
co_await on_10_quest_menu(c, base_cmd.item_id); co_await on_10_quest_menu(c, base_cmd.item_id);
break; break;
case MenuID::QUEST_EP3:
co_await on_10_ep3_download_quest_menu(c, base_cmd.item_id);
break;
case MenuID::PATCH_SWITCHES: case MenuID::PATCH_SWITCHES:
co_await on_10_patch_switches(c, base_cmd.item_id); co_await on_10_patch_switches(c, base_cmd.item_id);
break; 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); 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"); throw runtime_error("non-leader sent 02DF command");
} }
auto vq = l->quest->version(Version::BB_V4, c->language()); 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"); 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())) { if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index); 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"); lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true); 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: { case 0x03DF: {
const auto& cmd = check_size_t<C_SetChallengeModeDifficulty_BB_03DF>(msg.data); 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) { if (l->difficulty != cmd.difficulty) {
l->difficulty = cmd.difficulty; l->difficulty = cmd.difficulty;
l->create_item_creator(); l->create_item_creator();
@@ -4112,8 +4138,13 @@ static asio::awaitable<void> on_DF_BB(shared_ptr<Client> c, Channel::Message& ms
} }
case 0x04DF: { case 0x04DF: {
const auto& cmd = check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data); check_size_t<C_SetChallengeModeEXPMultiplier_BB_04DF>(msg.data);
l->challenge_exp_multiplier = cmd.exp_multiplier; 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); l->log.info_f("(Challenge mode) EXP multiplier set to {:g}", l->challenge_exp_multiplier);
break; break;
} }
@@ -4907,7 +4938,7 @@ static asio::awaitable<void> on_6F(shared_ptr<Client> c, Channel::Message& msg)
shared_ptr<const Quest> q; shared_ptr<const Quest> q;
try { try {
int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version); 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&) { } catch (const out_of_range&) {
throw std::logic_error("cannot find patch enable quest after it was previously found during login"); 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) { if (!vq) {
throw std::logic_error("cannot find patch enable quest version after it was previously found during login"); 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 bin_filename = vq->bin_filename();
string dat_filename = vq->dat_filename(); string dat_filename = vq->dat_filename();
string xb_filename = vq->xb_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, 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->quest_number, QuestFileType::ONLINE, vq->dat_contents); send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
co_return; co_return;
} }
// Now l is not null // 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 bin_filename = vq->bin_filename();
string dat_filename = vq->dat_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, bin_filename, bin_filename, "", vq->meta.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, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set"); c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set");
should_resume_game = false; 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 bin_filename = vq->bin_filename();
string dat_filename = vq->dat_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, bin_filename, bin_filename, "", vq->meta.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, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents);
c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST);
c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set"); 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) { if (l->drop_mode != ServerDropMode::DISABLED) {
EnemyType boss_enemy_type = EnemyType::NONE; EnemyType boss_enemy_type = EnemyType::NONE;
bool is_ep2 = (l->episode == Episode::EP2); 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 // 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 // request after the end of the second phase. On all other difficulty
// levels, send it after the third phase. // 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)) { } else if ((difficulty != 0) && (flag_num == 0x0037)) {
boss_enemy_type = EnemyType::DARK_FALZ_3; 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; 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 // If the lock status register is being written, change the game's flags to
// allow or forbid joining // allow or forbid joining
if (l->quest && if (l->quest &&
l->quest->joinable && l->quest->meta.joinable &&
(l->quest->lock_status_register >= 0) && (l->quest->meta.lock_status_register >= 0) &&
(cmd.register_number == l->quest->lock_status_register)) { (cmd.register_number == l->quest->meta.lock_status_register)) {
// Lock if value is nonzero; unlock if value is zero // Lock if value is nonzero; unlock if value is zero
if (cmd.value.as_int) { if (cmd.value.as_int) {
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS); 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) { if (l->episode != Episode::EP1) {
throw runtime_error("client sent 6x17 command in non-Ep1 game"); 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 if (l->area_for_floor(c->version(), c->floor) != 0x0D) {
// here instead of a constant throw runtime_error("client sent 6x17 command in area other than Vol Opt");
if (c->floor != 0x0D) {
throw runtime_error("client sent 6x17 command on floor other than Vol Opt");
} }
if (cmd.header.entity_id != c->lobby_client_id) { if (cmd.header.entity_id != c->lobby_client_id) {
// If the target is on a different floor or does not exist, just drop the // 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"); 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(); auto s = l->require_server_state();
for (auto& m : l->floor_item_managers) { 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())) { if (is_v4(lc->version())) {
lc->change_bank(lc->bb_character_index); 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"); lc->log.info_f("Created challenge overlay");
l->assign_inventory_and_bank_item_ids(lc, true); 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(); 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.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->quest_number; e.item_id = it.second->meta.quest_number;
e.name.encode(vq->name, c->language()); e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->short_description), 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); 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(); auto& e = entries.emplace_back();
e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2; e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1;
e.item_id = it.second->quest_number; e.item_id = it.second->meta.quest_number;
e.name.encode(vq->name, c->language()); e.name.encode(vq->meta.name, c->language());
e.short_description.encode(add_color(vq->short_description), c->language()); e.short_description.encode(add_color(vq->meta.short_description), c->language());
e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0; e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0;
} }
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); 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> template <typename EntryT>
void send_quest_categories_menu_t( void send_quest_categories_menu_t(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
QuestIndex::IncludeCondition include_condition = nullptr; QuestIndex::IncludeCondition include_condition = nullptr;
if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) { if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) {
auto l = c->lobby.lock(); auto l = c->lobby.lock();
@@ -1694,7 +1704,8 @@ void send_quest_categories_menu_t(
} }
vector<EntryT> entries; 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(); auto& e = entries.emplace_back();
e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1; e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1;
e.item_id = cat->category_id; 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()); 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); 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( void send_quest_categories_menu(shared_ptr<Client> c, QuestMenuType menu_type, Episode episode) {
shared_ptr<Client> c,
shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode) {
switch (c->version()) { switch (c->version()) {
case Version::PC_NTE: case Version::PC_NTE:
case Version::PC_V2: 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; break;
case Version::DC_NTE: case Version::DC_NTE:
case Version::DC_11_2000: case Version::DC_11_2000:
@@ -1754,13 +1761,13 @@ void send_quest_categories_menu(
case Version::GC_V3: case Version::GC_V3:
case Version::GC_EP3_NTE: case Version::GC_EP3_NTE:
case Version::GC_EP3: 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; break;
case Version::XB_V3: 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; break;
case Version::BB_V4: 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; break;
default: default:
throw logic_error("unimplemented versioned command"); throw logic_error("unimplemented versioned command");
@@ -4023,12 +4030,11 @@ void send_open_quest_file(
if (chunk_bytes > 0x400) { if (chunk_bytes > 0x400) {
chunk_bytes = 0x400; chunk_bytes = 0x400;
} }
send_quest_file_chunk(c, filename, offset / 0x400, send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
} }
// If there are still chunks to send, track the file so the chunk // 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) { if (chunks_to_send < total_chunks) {
c->sending_files.emplace(filename, contents); c->sending_files.emplace(filename, contents);
c->log.info_f("Opened file {}", filename); c->log.info_f("Opened file {}", filename);
+2 -5
View File
@@ -312,11 +312,8 @@ void send_quest_menu(
std::shared_ptr<Client> c, std::shared_ptr<Client> c,
const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests, const std::vector<std::pair<QuestIndex::IncludeState, std::shared_ptr<const Quest>>>& quests,
bool is_download_menu); bool is_download_menu);
void send_quest_categories_menu( void send_ep3_download_quest_menu(std::shared_ptr<Client> c);
std::shared_ptr<Client> c, void send_quest_categories_menu(std::shared_ptr<Client> c, QuestMenuType menu_type, Episode episode);
std::shared_ptr<const QuestIndex> quest_index,
QuestMenuType menu_type,
Episode episode);
void send_lobby_list(std::shared_ptr<Client> c); void send_lobby_list(std::shared_ptr<Client> c);
void send_player_records( 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; 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 { size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const {
const auto& min_levels = is_v4(version) const auto& min_levels = is_v4(version)
? this->min_levels_v4 ? 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 { shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->common_item_set) { if (q && q->meta.common_item_set) {
return q->common_item_set; return q->meta.common_item_set;
} else if (is_v1_or_v2(logic_version)) { } else if (is_v1_or_v2(logic_version)) {
// TODO: We should probably have a v1 common item set at some point too // TODO: We should probably have a v1 common item set at some point too
return this->common_item_sets.at("common-table-v1-v2"); 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 { shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version, shared_ptr<const Quest> q) const {
if (q && q->rare_item_set) { if (q && q->meta.rare_item_set) {
return q->rare_item_set; return q->meta.rare_item_set;
} else if (is_v1(logic_version)) { } else if (is_v1(logic_version)) {
return this->rare_item_sets.at("rare-table-v1"); return this->rare_item_sets.at("rare-table-v1");
} else if (is_v2(logic_version)) { } else if (is_v2(logic_version)) {
@@ -2157,6 +2153,8 @@ void ServerState::load_ep3_cards() {
void ServerState::load_ep3_maps() { void ServerState::load_ep3_maps() {
config_log.info_f("Collecting Episode 3 maps"); config_log.info_f("Collecting Episode 3 maps");
this->ep3_map_index = make_shared<Episode3::MapIndex>("system/ep3/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() { void ServerState::load_ep3_tournament_state() {
@@ -2169,14 +2167,8 @@ void ServerState::load_ep3_tournament_state() {
void ServerState::load_quest_index() { void ServerState::load_quest_index() {
config_log.info_f("Collecting quests"); 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); this->quest_index = make_shared<QuestIndex>(
config_log.info_f("Collecting Episode 3 download quests"); "system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets);
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);
} }
void ServerState::compile_functions() { 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;
std::shared_ptr<const Episode3::CardIndex> ep3_card_index_trial; 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_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 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_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_ex_values;
std::shared_ptr<const G_SetEXResultValues_Ep3_6xB4x4B> ep3_tournament_final_round_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 QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> default_quest_index; std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const QuestIndex> ep3_download_quest_index;
std::shared_ptr<const LevelTableV2> level_table_v1_v2; std::shared_ptr<const LevelTableV2> level_table_v1_v2;
std::shared_ptr<const LevelTable> level_table_v3; std::shared_ptr<const LevelTable> level_table_v3;
std::shared_ptr<const LevelTable> level_table_v4; 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 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; 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>> { +[](ShellCommand::Args& args) -> asio::awaitable<deque<string>> {
string name = get_quoted_string(args.args); string name = get_quoted_string(args.args);
string map_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); uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0);
Episode3::Rules rules; Episode3::Rules rules;
rules.set_defaults(); rules.set_defaults();
+3 -6
View File
@@ -639,7 +639,6 @@
// 0x008 - appears in solo mode (BB) // 0x008 - appears in solo mode (BB)
// 0x010 - appears at government counter (BB) // 0x010 - appears at government counter (BB)
// 0x020 - appears in download quest menu // 0x020 - appears in download quest menu
// 0x040 - appears in Episode 3 download quest menu
// 0x080 - hide quests that don't match the game's episode // 0x080 - hide quests that don't match the game's episode
// 0x100 - is Episode 2 Challenge category // 0x100 - is Episode 2 Challenge category
// directory_name: the directory inside system/quests that contains quests // directory_name: the directory inside system/quests that contains quests
@@ -668,8 +667,6 @@
[0x010, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"], [0x010, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
[0x010, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"], [0x010, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
[0x020, "download", "Download", "$E$C6Quests to download\nto your Memory Card"], [0x020, "download", "Download", "$E$C6Quests to download\nto your Memory Card"],
[0x040, "download-ep3-trial", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"],
[0x040, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"],
], ],
// BB bank size. If you change either of these values, you must also add // BB bank size. If you change either of these values, you must also add
@@ -1231,9 +1228,9 @@
// true and false here, since the server doesn't have direct access to the // true and false here, since the server doesn't have direct access to the
// client's quest flags from their save file. // client's quest flags from their save file.
// If you use an expression, the format is the same as the AvailableIf and // If you use an expression, the format is the same as the AvailableIf and
// EnabledIf fields in quest JSON files (see system/quests/battle/b88001.json // EnabledIf fields in quest JSONs (see system/quests/retrieval/q058.json for
// for details). Note that the expression is only evaluated at the time the // details). Note that the expression is only evaluated at the time the game
// game is created, and the player-specific tokens like C_EpX_YY refer to the // is created, and the player-specific tokens like C_EpX_YY refer to the
// player who created the game. // player who created the game.
// The UnlockAllAreas option is now gone; if you want the same behavior as if // The UnlockAllAreas option is now gone; if you want the same behavior as if
// it were enabled, uncomment all the "area unlocks" lines below. Note that // it were enabled, uncomment all the "area unlocks" lines below. Note that
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3-trial/e765-gc3-e.mnm ../maps-download/e765-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3-trial/e765-gc3-j.mnm ../maps-download/e765-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e901-gc3-e.mnm ../maps-download/e901-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e901-gc3-j.mnm ../maps-download/e901-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e903-gc3-e.mnm ../maps-download/e903-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e903-gc3-j.mnm ../maps-download/e903-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e904-gc3-e.mnm ../maps-download/e904-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e904-gc3-j.mnm ../maps-download/e904-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e905-gc3-e.mnm ../maps-download/e905-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e905-gc3-j.mnm ../maps-download/e905-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e906-gc3-e.mnm ../maps-download/e906-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e906-gc3-j.mnm ../maps-download/e906-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e907-gc3-e.mnm ../maps-download/e907-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e907-gc3-j.mnm ../maps-download/e907-gc3-j.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e908-gc3-e.mnm ../maps-download/e908-gc3-e.mnm
+1 -1
View File
@@ -1 +1 @@
../maps-download/download-ep3/e908-gc3-j.mnm ../maps-download/e908-gc3-j.mnm
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

Some files were not shown because too many files have changed in this diff Show More