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