rewrite quest metadata indexing

- split ep3 download quests from quest index
- fix Ep3 NTE download quests
- automatically detect battle/challenge params and area remaps
This commit is contained in:
Martin Michelsen
2025-09-28 10:15:14 -07:00
parent 48c225366f
commit fdd0bfea08
248 changed files with 1944 additions and 1543 deletions
+23 -28
View File
@@ -2626,8 +2626,8 @@ MapIndex::VersionedMap::VersionedMap(shared_ptr<const MapDefinition> map, uint8_
MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language)
: language(language),
compressed_data(std::move(compressed_data)) {
string decompressed = prs_decompress(this->compressed_data);
compressed_data(make_shared<string>(std::move(compressed_data))) {
string decompressed = prs_decompress(*this->compressed_data);
if (decompressed.size() == sizeof(MapDefinitionTrial)) {
this->map = make_shared<MapDefinition>(*reinterpret_cast<const MapDefinitionTrial*>(decompressed.data()));
} else if (decompressed.size() == sizeof(MapDefinition)) {
@@ -2646,21 +2646,30 @@ shared_ptr<const MapDefinitionTrial> MapIndex::VersionedMap::trial() const {
return this->trial_map;
}
const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const {
if (is_nte) {
if (this->compressed_trial_data.empty()) {
std::shared_ptr<const std::string> MapIndex::VersionedMap::compressed(bool trial) const {
if (trial) {
if (!this->compressed_data_trial) {
auto md = this->trial();
this->compressed_trial_data = prs_compress(md.get(), sizeof(*md));
this->compressed_data_trial = make_shared<string>(prs_compress(md.get(), sizeof(*md)));
}
return this->compressed_trial_data;
return this->compressed_data_trial;
} else {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map));
if (!this->compressed_data) {
this->compressed_data = make_shared<string>(prs_compress(this->map.get(), sizeof(*this->map)));
}
return this->compressed_data;
}
}
std::shared_ptr<const std::string> MapIndex::VersionedMap::trial_download() const {
if (!this->download_data_trial) {
MapDefinitionTrial trial_map = *this->map;
trial_map.tag = 0x96;
this->download_data_trial = make_shared<string>(prs_compress(&trial_map, sizeof(trial_map)));
}
return this->download_data_trial;
}
MapIndex::Map::Map(shared_ptr<const VersionedMap> initial_version)
: map_number(initial_version->map->map_number),
initial_version(initial_version) {
@@ -2704,6 +2713,7 @@ shared_ptr<const MapIndex::VersionedMap> MapIndex::Map::version(uint8_t language
}
MapIndex::MapIndex(const string& directory) {
map<uint32_t, shared_ptr<Map>> mutable_maps;
for (const auto& item : std::filesystem::directory_iterator(directory)) {
string filename = item.path().filename().string();
try {
@@ -2756,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) {
}
string name = vm->map->name.decode(vm->language);
auto map_it = this->maps.find(vm->map->map_number);
if (map_it == this->maps.end()) {
map_it = this->maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
auto map_it = mutable_maps.find(vm->map->map_number);
if (map_it == mutable_maps.end()) {
map_it = mutable_maps.emplace(vm->map->map_number, make_shared<Map>(vm)).first;
this->maps.emplace(vm->map->map_number, map_it->second);
static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})",
filename,
vm->map->map_number,
@@ -2866,22 +2877,6 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language
return compressed_map_list;
}
shared_ptr<const MapIndex::Map> MapIndex::for_number(uint32_t id) const {
return this->maps.at(id);
}
shared_ptr<const MapIndex::Map> MapIndex::for_name(const string& name) const {
return this->maps_by_name.at(name);
}
set<uint32_t> MapIndex::all_numbers() const {
set<uint32_t> ret;
for (const auto& it : this->maps) {
ret.emplace(it.first);
}
return ret;
}
COMDeckIndex::COMDeckIndex(const string& filename) {
try {
auto json = phosg::JSON::parse(phosg::load_file(filename));
+17 -8
View File
@@ -1179,7 +1179,8 @@ struct OverlayState {
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
// If tag is not 0x00000100, the game considers the map to be corrupt in
// offline mode and will delete it (if it's a download quest). The tag field
// doesn't seem to have any other use.
// doesn't seem to have any other use. In Trial Edition, download quests are
// expected to have 0x96 here instead.
/* 0000 */ be_uint32_t tag;
/* 0004 */ be_uint32_t map_number; // Must be unique across all maps
@@ -1597,12 +1598,14 @@ public:
VersionedMap(std::string&& compressed_data, uint8_t language);
std::shared_ptr<const MapDefinitionTrial> trial() const;
const std::string& compressed(bool is_nte) const;
std::shared_ptr<const std::string> compressed(bool trial) const;
std::shared_ptr<const std::string> trial_download() const;
private:
mutable std::shared_ptr<const MapDefinitionTrial> trial_map;
mutable std::string compressed_data;
mutable std::string compressed_trial_data;
mutable std::shared_ptr<std::string> compressed_data;
mutable std::shared_ptr<std::string> compressed_data_trial;
mutable std::shared_ptr<std::string> download_data_trial;
};
class Map {
@@ -1624,14 +1627,20 @@ public:
};
const std::string& get_compressed_list(size_t num_players, uint8_t language) const;
std::shared_ptr<const Map> for_number(uint32_t id) const;
std::shared_ptr<const Map> for_name(const std::string& name) const;
std::set<uint32_t> all_numbers() const;
inline std::shared_ptr<const Map> get(uint32_t id) const {
return this->maps.at(id);
}
inline std::shared_ptr<const Map> get(const std::string& name) const {
return this->maps_by_name.at(name);
}
inline const std::map<uint32_t, std::shared_ptr<const Map>>& all() const {
return this->maps;
}
private:
// The compressed map lists are generated on demand from the maps map below
mutable std::vector<std::array<std::string, 4>> compressed_map_lists;
std::map<uint32_t, std::shared_ptr<Map>> maps;
std::map<uint32_t, std::shared_ptr<const Map>> maps;
std::unordered_map<std::string, std::shared_ptr<Map>> maps_by_name;
};
+4 -4
View File
@@ -287,9 +287,9 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr<const MapIndex::Map> ma
const auto& compressed = vm->compressed(is_nte);
phosg::StringWriter w;
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0});
w.write(compressed);
uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3);
w.put<G_MapData_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0});
w.write(*compressed);
return std::move(w.str());
}
@@ -2588,7 +2588,7 @@ void Server::handle_CAx41_map_request(shared_ptr<Client>, const string& data) {
const auto& cmd = check_size_t<G_MapDataRequest_Ep3_CAx41>(data);
this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA");
if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) {
this->last_chosen_map = this->options.map_index->for_number(cmd.map_number);
this->last_chosen_map = this->options.map_index->get(cmd.map_number);
this->send_6xB6x41_to_all_clients();
}
}
+1 -1
View File
@@ -357,7 +357,7 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name");
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
this->map = this->map_index->get(this->source_json.get_int("map_number"));
this->rules = Rules(this->source_json.at("rules"));
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {